Drawing with Maui.Graphics
.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.
Microsoft.Maui.Graphics
was still in preview. See Drawing with Maui Graphics (blog post) and C# Data Visualization (website) for updated code examples and information about using this library.
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.
-
Add the
Microsoft.Maui.Graphics
project to your solution and add a reference to it from your project. -
Windows Forms: Add the
Microsoft.Maui.Graphics.GDI.Winforms
project to your solution and add a reference to it in your WinForms project. AGDIGraphicsView
control should appear in the toolbox. -
WPF: Add the
Microsoft.Maui.Graphics.Skia.WPF
project to your solution and add a reference to it in your WPF project. AWDSkiaGraphicsView
control should appear in the toolbox.
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
-
Source Code: Balls.zip (10 kB)
-
WinForms (GDI) Demo: Balls-WinForms.exe (237 kB)
-
WPF (Skia) Demo: Balls-WPF.exe (13 MB)
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