Skip to content

Instantly share code, notes, and snippets.

@mrange
Last active April 17, 2022 16:11
Show Gist options
  • Save mrange/e847db0548ffa694fc27386e1cd007ae to your computer and use it in GitHub Desktop.
Save mrange/e847db0548ffa694fc27386e1cd007ae to your computer and use it in GitHub Desktop.
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