SWHarden.com

The personal website of Scott W Harden

Drawing with Maui.Graphics

How to use Maui.Graphics to draw and create animations in a Windows Forms and WPF applications

.NET MAUI (Multi-Platform Application User Interface) is a new framework for creating cross-platform apps using C#. MAUI will be released as part of .NET 6 in November 2021 and it is expected to come with Maui.Graphics, a cross-platform drawing library superior to System.Drawing in many ways. Although System.Drawing.Common currently supports rendering in Linux and MacOS, cross-platform support for System.Drawing will sunset over the next few releases and begin throwing a PlatformNotSupportedException in .NET 6.

By creating a graphics model using only Maui.Graphics dependencies, users can share drawing code across multiple rendering technologies (GDI, Skia, SharpDX, etc.), operating systems (Windows, Linux, MacOS, etc.), and application frameworks (WinForms, WPF, Maui, WinUI, etc.). This page demonstrates how to create a platform-agnostic graphics model and render it using Windows Forms and WPF. Resources in the Maui.Graphics namespace can be used by any modern .NET application (not just Maui apps).

⚠️ WARNING: Maui.Graphics is still a pre-release experimental library (as noted on their GitHub page). Although the code examples on this page work presently, the API may change between now and the official release.

1. Create a new Project

For this example I will start by creating a a .NET 5.0 WinForms project from scratch. Later we will extend the solution to include a WPF project that uses the same graphics model.

2. Add References to Maui.Graphics

We need to get the windows forms MAUI control and all its dependencies. At the time of writing (September, 2021) these packages are not yet available on NuGet, but they can be downloaded from the Microsoft.Maui.Graphics GitHub page.

💡 Tip: If you’re developing a desktop application you can improve the “rebuild all” time by editing the csproj files of your dependencies so TargetFrameworks only includes .NET Standard targets.

3. Create a Drawable Object

A drawable is a class that implements IDrawable, has a Draw() method, and can be rendered anywhere Maui.Graphics is supported. By only depending on Maui.Graphics it’s easy to create a graphics model that can be used on any operating system using any supported graphical framework.

using Microsoft.Maui.Graphics;

This drawable fills the image blue and renders 1,000 randomly-placed anti-aliased semi-transparent white lines on it. Note that the location of the lines depends on the size of the render field (passed-in as an argument).

public class RandomLines : IDrawable
{
    public void Draw(ICanvas canvas, RectangleF dirtyRect)
    {
        canvas.FillColor = Color.FromArgb("#003366");
        canvas.FillRectangle(dirtyRect);

        canvas.StrokeSize = 1;
        canvas.StrokeColor = Color.FromRgba(255, 255, 255, 100);
        Random Rand = new();
        for (int i = 0; i < 1000; i++)
        {
            canvas.DrawLine(
                x1: (float)Rand.NextDouble() * dirtyRect.Width,
                y1: (float)Rand.NextDouble() * dirtyRect.Height,
                x2: (float)Rand.NextDouble() * dirtyRect.Width,
                y2: (float)Rand.NextDouble() * dirtyRect.Height);
        }
    }
}

4. Add a GraphicsView Control

Drag/drop a GraphicsView control from the Toolbox onto your application and assign your graphics model to its Drawable field.

For animations you can use a timer to invalidate the control (forcing a redraw) automatically every 20 ms.

Windows Forms (Rendering with GDI)

Windows Forms applications may also want to intercept SizeChanged events to force redraws as the window is resized.

public partial class Form1 : Form
{
    public Form1()
    {
        InitializeComponent();
        gdiGraphicsView1.Drawable = new RandomLines();
    }

    private void GdiGraphicsView1_SizeChanged(object? sender, EventArgs e) =>
        gdiGraphicsView1.Invalidate();

    private void timer1_Tick(object sender, EventArgs e) =>
        gdiGraphicsView1.Invalidate();
}

I found performance to be quite adequate. On my system 1,000 lines rendered on an 800x600 window at ~60 fps. Like System.Drawing this system slows down as a function of image size, so full-screen 1920x1080 animation was much slower (~10 fps).

WPF (Rendering with SkiaSharp)

<Skia:WDSkiaGraphicsView Name="MyGraphicsView" />
public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        MyGraphicsView.Drawable = new RandomLines();

        DispatcherTimer timer = new();
        timer.Interval = TimeSpan.FromMilliseconds(20);
        timer.Tick += Timer_Tick; ;
        timer.Start();
    }

    private void Timer_Tick(object sender, EventArgs e) =>
        MyGraphicsView.Invalidate();
}

Extend the Graphics Model

Since our project is configured to display the same graphics model with both WinForms and WPF, it’s easy to edit the model in one place and it’s updated everywhere. We can replace the random lines model with one that manages randomly colored and sized balls that bounce off the edges of the window as the model advances.

BallField.cs

public class BallField : IDrawable
{
    private readonly Ball[] Balls;

    public BallField(int ballCount)
    {
        Balls = new Ball[ballCount];
    }

    public void Draw(ICanvas canvas, RectangleF dirtyRect)
    {
        canvas.FillColor = Colors.Navy;
        canvas.FillRectangle(dirtyRect);

        foreach (Ball ball in Balls)
        {
            ball?.Draw(canvas);
        }
    }

    public void Randomize(double width, double height)
    {
        Random rand = new();
        for (int i = 0; i < Balls.Length; i++)
        {
            Balls[i] = new()
            {
                X = rand.NextDouble() * width,
                Y = rand.NextDouble() * height,
                Radius = rand.NextDouble() * 5 + 5,
                XVel = rand.NextDouble() - .5,
                YVel = rand.NextDouble() - .5,
                R = (byte)rand.Next(50, 255),
                G = (byte)rand.Next(50, 255),
                B = (byte)rand.Next(50, 255),
            };
        }
    }

    public void Advance(double timeDelta, double width, double height)
    {
        foreach (Ball ball in Balls)
        {
            ball?.Advance(timeDelta, width, height);
        }
    }
}

Ball.cs

public class Ball
{
    public double X;
    public double Y;
    public double Radius = 5;
    public double XVel;
    public double YVel;
    public byte R, G, B;

    public void Draw(ICanvas canvas)
    {
        canvas.FillColor = Color.FromRgb(R, G, B);
        canvas.FillCircle((float)X, (float)Y, (float)Radius);
    }

    public void Advance(double timeDelta, double width, double height)
    {
        MoveForward(timeDelta);
        Bounce(width, height);
    }

    private void MoveForward(double timeDelta)
    {
        X += XVel * timeDelta;
        Y += YVel * timeDelta;
    }

    private void Bounce(double width, double height)
    {
        double minX = Radius;
        double minY = Radius;
        double maxX = width - Radius;
        double maxY = height - Radius;

        if (X < minX)
        {
            X = minX + (minX - X);
            XVel = -XVel;
        }
        else if (X > maxX)
        {
            X = maxX - (X - maxX);
            XVel = -XVel;
        }

        if (Y < minY)
        {
            Y = minY + (minY - Y);
            YVel = -YVel;
        }
        else if (Y > maxY)
        {
            Y = maxY - (Y - maxY);
            YVel = -YVel;
        }
    }
}

Download This Project

To build this project from source code you currently have to download Maui.Graphics source from GitHub and edit the solution file to point to the correct directory containing these projects. This will get a lot easier after Microsoft puts their WinForms and WPF controls on NuGet.

Coding Challenge

Can you recreate this classic screensaver using Maui.Graphics? Bonus points if the user can customize the number of shapes, the number of corners each shape has, and the number of lines drawn in each shape’s history. It’s a fun problem and I encourage you to give it a go! Here’s how I did it: mystify-maui.zip

Resources