In one of my projects, I used Win2D for graphics. This time, I decided to try SkiaSharp.
The task is to draw white text with a black outline. It sounds quite simple. But there is no easy solution. Or maybe I didn’t search well enough. In any case, I need to use Win2D to create geometry from the text and then draw it. Maybe there are ways to do it without Win2D. But my knowledge of WinUI is not that deep.
So, should we take Win2D and solve the problem? Not at all. First, this is a pet project where I am free to experiment as much as I want. Second, the status of the Win2D project is concerning: the last release was almost a year ago, they don’t answer issues, and the code in the repository hasn’t been touched for about a year either. So, there is every reason to look for an alternative solution.
As an alternative, I chose SkiaSharp. Whether that’s good or bad, time will tell. For now, it seems I managed to solve the given task.
How does it work? We add the SkiaSharp.Views.WinUI package. Then, we place an SKXamlCanvas and write a function to draw the image:
<skia:SKXamlCanvas x:Name="Canvas" PaintSurface="Canvas_PaintSurface" SizeChanged="Canvas_SizeChanged"/>
private void Canvas_PaintSurface(object sender, SkiaSharp.Views.Windows.SKPaintSurfaceEventArgs e)
{
// Get the drawing surface and clear it to transparent
var canvas = e.Surface.Canvas;
canvas.Clear(SKColors.Transparent);
// Create OSD (On-Screen Display) object based on current state
var osd = Osd.Create(state);
var info = e.Info;
// If no OSD data, nothing to draw
if (osd is null)
{
return;
}
// Set font size based on position: bigger for center, smaller for sides
var fontSize = osd.Position is OsdPosition.Center ? 80 : 50;
// Create font for icons (using Segoe Fluent Icons font)
using var iconFont = new SKFont
{
Size = fontSize,
Typeface = SKTypeface.FromFamilyName("Segoe Fluent Icons"),
Embolden = true, // Make text bold
};
// Create paint for filling text (white color)
using var textPaint = new SKPaint
{
Color = SKColors.White,
IsAntialias = true, // Smooth edges
Style = SKPaintStyle.Fill, // Fill inside shapes
};
// Create paint for text outline (black stroke)
using var outlinePaint = new SKPaint
{
Color = SKColors.Black,
IsAntialias = true, // Smooth edges
Style = SKPaintStyle.Stroke, // Draw outlines only
StrokeWidth = 2, // Outline thickness
};
// Measure icon width to calculate positioning
iconFont.MeasureText(osd.IconForMeasure, out var iconBounds);
var osdWidth = iconBounds.Width;
// If OSD has text (not just icon)
if (osd.Text is not null)
{
// Create font for regular text
using var textFont = new SKFont
{
Size = fontSize,
Typeface = SKTypeface.FromFamilyName(FontFamily.Source),
Embolden = true, // Make text bold
};
// Measure text and space ("0" character) to calculate total width
textFont.MeasureText(osd.Text, out var textBounds);
textFont.MeasureText("0", out var spaceTextBounds);
// Calculate total OSD width: icon + space + text
// For center position, use half space width
osdWidth += spaceTextBounds.Width / (osd.Position is OsdPosition.Center ? 2 : 1) + textBounds.Width;
// Calculate X position for text based on screen placement
var textX = osd.Position switch
{
OsdPosition.Left => info.Width / 10 + osdWidth, // 10% from left + OSD width
OsdPosition.Right => info.Width * 9 / 10 - osdWidth, // 90% from right - OSD width
_ => (info.Width + osdWidth) / 2 // Center of screen
};
// Calculate Y position to align text vertically with icon
var textY = info.Height / 2 + (iconBounds.MidY - textBounds.MidY);
// Draw text outline first (black border)
canvas.DrawText(
osd.Text,
textX,
textY,
osd.Position is OsdPosition.Right ? SKTextAlign.Left : SKTextAlign.Right,
textFont,
outlinePaint);
// Draw filled text on top (white fill)
canvas.DrawText(
osd.Text,
textX,
textY,
osd.Position is OsdPosition.Right ? SKTextAlign.Left : SKTextAlign.Right,
textFont,
textPaint);
}
// Calculate X position for icon based on screen placement
var iconX = osd.Position switch
{
OsdPosition.Left => info.Width / 10, // 10% from left
OsdPosition.Right => info.Width * 9 / 10, // 90% from right
_ => (info.Width - osdWidth) / 2 // Center of screen
};
// Calculate Y position for icon (center vertically)
var iconY = info.Height / 2;
// Draw icon outline first (black border)
canvas.DrawText(
osd.Icon,
iconX,
iconY,
osd.Position is OsdPosition.Right ? SKTextAlign.Right : SKTextAlign.Left,
iconFont,
outlinePaint);
// Draw filled icon on top (white fill)
canvas.DrawText(
osd.Icon,
iconX,
iconY,
osd.Position is OsdPosition.Right ? SKTextAlign.Right : SKTextAlign.Left,
iconFont,
textPaint);
}
private void Canvas_SizeChanged(object sender, SizeChangedEventArgs e)
{
// Check if the sender is actually an SKXamlCanvas
if (sender is SKXamlCanvas canvas)
{
// Tell the canvas to redraw itself
// This is needed because when the size changes,
// the drawing needs to adjust to the new size
canvas.Invalidate();
}
}
// Helper class to store OSD (On-Screen Display) information
private sealed class Osd
{
// Position on screen: Left, Right, or Center
public OsdPosition Position { get; init; }
// Icon character(s) to display
public string Icon { get; init; }
// Text used for measuring icon width (might differ from display icon)
public string IconForMeasure { get; }
// Optional text to display next to icon
public string? Text { get; init; }
}
I won’t go into the details, this is just a working piece of code from my current project. It all looks familiar, doesn’t it? It brings back memories of System.Drawing. But this one is also cross-platform (which, in this case, of course, doesn’t matter at all).
To be honest, this is my first experience with SkiaSharp. I did everything in the simplest way (from my point of view). There seem to be several types of Canvas there (and they are, of course, different). But as a proof-of-concept, it’s not bad at all. The main thing is that I solved the task:

I like it so far. In the future, I’ll try to rewrite my image viewer code using SkiaSharp and see how it turns out.
Happy coding!