4

Simple 2D graphics in F# and WPF

 2 years ago
source link: https://gist.github.com/mrange/e847db0548ffa694fc27386e1cd007ae
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client
Simple 2D graphics in F# and WPF

Simple 2D graphics in F# and WPF

This is an example on how to use WPF and F# to draw simple graphics intended to help devs interested in F# to experiment with a simple yet not trivial app.

I tried to annotate the source code to help guide a developer familiar with languages like C#. If you have suggestions for how to improve it please leave a comment below.

How to run

  1. WPF requires a Windows box, maybe I get around to do a Avalonia demo later.
  2. Install dotnet: https://dotnet.microsoft.com/en-us/download
  3. Create a folder named for example: FsWpfBootstrap
  4. Create file in the folder named: FsWpfBootstrap.fsproj and copy the content of 1_FsWpfBootstrap.fsproj below into that file
  5. Create file in the folder named: Program.fs and copy the content of 2_Program.fs below into that file
  6. Launch the application in Visual Studio or through the command line dotnet run from the folder FsWpfBootstrap
  7. Should look a bit like the image in the tweet
  8. Tinker with the OnRender function

<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup> <OutputType>WinExe</OutputType> <TargetFramework>net6.0-windows</TargetFramework> <UseWPF>true</UseWPF> </PropertyGroup>

<ItemGroup> <Compile Include="Program.fs" /> </ItemGroup>

</Project>

// Hi. For the "good stuff" scroll down to line 74. // The rest is just bootstrapping

// `open` are F# version of C# `using` open System open System.Globalization open System.Windows open System.Windows.Media open System.Windows.Media.Animation

// Creates a CanvasElement class that will act like a canvas for us // We override the OnRender method to draw graphics. In order to make the graphics // animate we have a time animation that invalidates the element which forces a redraw type CanvasElement () = class // This is how in F# we inherit, this is typically not done as much // as in C# but in order to be part of WPF Visual tree we need to // inherit UIElement inherit UIElement ()

// Declaring a DependencyProperty member for Time // This is WPF magic but it's created so that we can create // an "animation" of the time value. // This will help use do smooth updates. // Nothing like web requestAnimationFrame in WPF AFAIK static let timeProperty = let pc = PropertyChangedCallback CanvasElement.TimePropertyChanged let md = PropertyMetadata (0., pc) DependencyProperty.Register ("Time", typeof<float>, typeof<CanvasElement>, md)

// Freezing resources prevents updates of WPF Resources // Can help WPF optimize rendering // #Freezable is like C# constraint : where T : Freezable let freeze (f : #Freezable) = f.Freeze () f

// Helper function to create pens let makePen thickness brush = Pen (Thickness = thickness, Brush = brush) |> freeze

let treePen = makePen 2. Brushes.GreenYellow

// More WPF dependency property magic // Not very interesting but this becomes member function in the class static member TimePropertyChanged (d : DependencyObject) (e : DependencyPropertyChangedEventArgs) = let g = d :?> CanvasElement // Whenever time change we invalidate the entire canvas element g.InvalidateVisual ()

// Idiomatically WPF Dependency properties should be readonly // static fields. However, F# don't allow us to declare that // Luckily it seems static readonly properties works fine static member TimeProperty = timeProperty

// Gets the Time dependency property member x.Time = x.GetValue CanvasElement.TimeProperty :?> float

// Create an animation that animates a floating point from 0 to 1E9 // over 1E9 seconds thus the time. This animation is then hooked onto the Time property // Basically more WPF magic member x.Start () = // Initial time value let b = 0.0 // End time, application animation stops after approx 30 years let e = 1E9 let dur = Duration (TimeSpan.FromSeconds (e - b)) let ani = DoubleAnimation (b, e, dur) |> freeze // Animating Time property x.BeginAnimation (CanvasElement.TimeProperty, ani);

// Finally we get to the good stuff! // dc is a DeviceContext, basically a canvas we can draw on override x.OnRender dc = // Get the current time, will change over time (hohoh) let time = x.Time // This is the size of the canvas in pixels let rs = x.RenderSize

// Let's draw some dancing circles // The angle is a function of time, this will animate the circles let a = 2.0*time let ay = sqrt 0.5 for i = 0 to 9 do // In F# shadowing a previous variable with the same name is // not consider a problem // The previous `a` still exists but locally in this loop // `a` now has a new value (with potentially a new type) let a = a+float i let x = 100.0*sin a + 0.5*rs.Width let y = 100.0*sin (ay*a) + 150.0 dc.DrawEllipse (Brushes.DarkGreen, treePen, Point (x, y), 10.0, 10.0)

// Function to render a tree // This function is declared inside the OnRender function // something that is now commonly used in C# as well let tree dc time p = let rec recurse n (dc : DrawingContext) (lt : Matrix) (rt : Matrix) d (p : Point) = if n > 0 then let np = p + d

dc.DrawLine (treePen, p, np)

// Shortens the next branch let d = 0.75*d let ld = lt.Transform d let rd = rt.Transform d

// Draw left branch recurse (n - 1) dc lt rt ld np // Draw right branch recurse (n - 1) dc lt rt rd np

// Which direction the root branch has let up = Vector (0., -200.)

// Transform for left branches let lt = Matrix.Identity // Transform for right branches let rt = Matrix.Identity

// The angle between new branch and preceeding branch let a = 30. // Animate the angles using time let b = 10.*sin time

// Apply rotation to transforms lt.Rotate (a + b) rt.Rotate -(a - b)

// Draw the tree 9 levels deep recurse 9 dc lt rt up p

// Bottom of the screen in the center X-wise let rootPos = Point (0.5*rs.Width, rs.Height)

// Renders the tree tree tree dc time rootPos

end

// Tells F# that this method is the main entry point [<EntryPoint>] // More 1990s magic! Basically in Windows there's a requirement that // UI controls runs in something called a Single Threaded Apartment. // So we tell .NET that the thread that calls main should be in a // Single Threaded Apartment. // Basically MS idea in 1990s on how to solve the problem of writing // multi threaded applications. // The .NET equivalent to apartments could be SynchronizationContext [<STAThread>] let main argv = // Sets up the main window let window = Window (Title = "FsWpfBootstrap", Background = Brushes.Black) // Creates our canvas let element = CanvasElement () // Makes our canvas the content of the Window window.Content <- element // Starts the time animation element.Start () // Shows the Window window.ShowDialog () |> ignore 0

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK