Recreating Gdiplus hatches with SkiaSharp (2023)

This article is aimed at developers who are porting projects based on the System.Drawing.Drawing2D Namespace to the newer SkiaSharp graphic library. If your project makes use of Gidplus HatchBrushes and you need to re-create them with SkiaSharp, read on.

  • Download
  • Download

This sample project was written using Visual Studio 2022. The project is a WindowsForm app based on the .NET Core 6 framework and written with C# version 10. It's compatible with Windows 7 and above.


I'm in the process of re-writing a Windowsapplication that originally generatedgraphic output based on Gdiplus. The goal is to move to SkiaSharpbutmaintain the same features, for backward compatibility. This article details the approach I've taken to be able to recreate the same hatch effects that were based on Gdiplus.

What is a HatchBrush

If you are not familiar with the concept, in Gdiplus a HatchBrush is a special type of brush that will enable you to fill an area with a particular pattern, called HatchStyle. Gdiplus has 52 built-in patterns that can be used to create a brush.

Here's a few examples:

Percent20 HatchStyleDiagonalBrickHatchStyleDiagonalCrossHatchStyle
Recreating Gdiplus hatches with SkiaSharp (1)Recreating Gdiplus hatches with SkiaSharp (2)Recreating Gdiplus hatches with SkiaSharp (3)

What is a HatchStyle

Each one of the 52 built-in Gdiplus HatchStyle is a pattern based on an 8-by-8 matrix of alternating on/off pixels (in some cases there are antialias pixels, more later). If you look closely at each style,you'll see the basic grid that makes up the pattern.

Here's a few enlarged examples:

Percent20 patternDiagonalBrickpatternDiagonalCrosspattern
Recreating Gdiplus hatches with SkiaSharp (4)Recreating Gdiplus hatches with SkiaSharp (5)Recreating Gdiplus hatches with SkiaSharp (6)

In order to be able to recreate them using SkiaSharp, the first step is to be able to gather the information needed for each one of the pattern.

Hacking HatchStyles

Rather than manually analyzing each HatchStyle one by one (read: lose eye-sight) and create my arrays (or Lists) of 8-by-8 points, I decided to let the machine do the work using the following approach:

  • Create an 8-by-8 bitmap and fill it with the pattern.
  • Read pixel values and build arrays of "on" points.
  • Use that information to recreate the pattern in SkiaSharp.

Here's the code for the methodthat reads pattern pixels:


// Size of the bitmap that will hold the pattern.// It's 8x8 pixels, since all patterns are based on an 8x8 matrix.readonly static int _matrixSize = 8;static List<SKPoint> GetHatchPattern(HatchStyle hatch, bool inverted){ // Build a list of filled points that define the pattern. List<SKPoint> ret = new(); // Build an 8x8 bitmap. var bitmap = new Bitmap(_matrixSize, _matrixSize); using Graphics graphics = Graphics.FromImage(bitmap); graphics.SmoothingMode = SmoothingMode.Default; // No antialias // Define colors. Color color1 = inverted ? Color.White : Color.Black; Color color2 = inverted ? Color.Black : Color.White; // Draw the hatch pattern. using HatchBrush brush = new(hatch, color1, color2); Rectangle rect = new(0, 0, _matrixSize, _matrixSize); graphics.FillRectangle(brush, rect); // Define black threshold. int blackValue = hatch switch { HatchStyle.ForwardDiagonal or HatchStyle.BackwardDiagonal or HatchStyle.DiagonalCross => -15395563,// Special cases due to antialias. _ => Color.Black.ToArgb(), }; // Read the pixels. for (int row = 0; row < _matrixSize; row++) { for (int col = 0; col < _matrixSize; col++) { Color pixel = bitmap.GetPixel(col, row); if (pixel.ToArgb() == blackValue) { ret.Add(new(col, row)); } } } return ret;}

TheGetHatchPattern method takes a parameter of typeHatchStyle and generates a list of SKPoint(s)that we can later use in SkiaSharp.

Themethod also takes the inverted parameter so that the list of points can be inverted in terms of background/foreground color.

Note that for 3 of the 52 HatchStyles Gdiplus creates a patternwith antialias pixels, regardless of the SmoothingMode settingin the destination Graphic object. We will disregard such information and just convert everything tosimple on/offpixels, as the resulting effectdoesn't seem to need any antialiasing in SkiaSharp.

Once the information is gathered, we can move to SkiaSharp and build the same effect.

Draw a hatch effect with SkiaSharp

SkiaSharp is based on Skia, an open source, cross-platform graphic library currently owned and maintained by Google. Have a look at this great tutorial for more information on how to fill paths with effects using SkiaSharp.

SkiaSharp uses an SKPaint object to define the way paths and lines are drawn onto a surface. To be able to create a hatch effect we'll need set the PathEffect member of the SKPaint object to an effect created via the Create2DPath static method of the SKPathEffect class.

The most important bit of data here is that we need to pass an SKPath object to this method that contains the pattern that we want to draw, so we'll create a path and add a rectangle (or rather a square) for eachpixel we read from the source hatch bitmap created with Gdiplus.

Here's the code:


// Get fill rectangle.SKRect rect = canvas.DeviceClipBounds;// Create fill path.using SKPath fillPath = new();fillPath.AddRect(rect);// Build an SKPath containing the pattern.// _currentPattern contains the results of a call to GetHatchPatternSKPath patternPath = new();foreach (var point in _currentPattern) patternPath.AddRect(new(point.X, point.Y, point.X + 1, point.Y + 1));// Give it padding.rect.Inflate(-_padding, -_padding);// Translate the pattern to coordinate 0, 0 of the fill rectangle.// This is the equivalent of Gdiplus Graphics.RenderingOrigin.float halfMtx = _matrixSize / 2;float modX = rect.Left % _matrixSize;float modY = rect.Top % _matrixSize;if (modX >= halfMtx) modX -= _matrixSize;if (modY >= halfMtx) modY -= _matrixSize;modX -= halfMtx;modY -= halfMtx;patternPath.Transform(SKMatrix.CreateTranslation(modX, modY));// Build paint with a 2-color gradient.var colors = new SKColor[] { SKColors.Black, SKColors.Blue };using SKPaint paint = new(){ Shader = SKShader.CreateRadialGradient(new SKPoint(rect.MidX, rect.MidY), rect.Width / 2, colors, SKShaderTileMode.Clamp)};// Build path effect.SKMatrix matrix = SKMatrix.CreateScale(_matrixSize, _matrixSize);using SKPathEffect effect = SKPathEffect.Create2DPath(matrix, patternPath);if (effect == null) return;paint.PathEffect = effect;// Cleanup previous run.canvas.Clear(SKColors.White);// Add clipping.canvas.Save();canvas.ClipRect(rect);// Fill.canvas.DrawPath(fillPath, paint);// Remove clipping.canvas.Restore();

In this snippet, the variable canvas is the drawing surface obtained from the destination control (normally from the PaintSurface event).

A few steps are required to have the effect drawn at a specific origin point and all the way to the end of the area. First, a translation is in order to ensure that the point of origin of the pattern matches that of the top/left corner of the area we are filling. Second, the target path contains a rectangle larger then the destination area, but clipped to it, to ensure that the pattern is drawn up to the lower/right corner of the area. This is needed since SkiaSharp will truncate any rows/columns that don't entirely fit the area.

Here's the side-by-side resulting comparison, generated by the sample program attached to this article:

Recreating Gdiplus hatches with SkiaSharp (7)

Note that the SkiaSharp version of the hatch image was drawn using a radialgradient color, going from black to blue, something not achievable with Gdiplus, which only takes solid colors for foreground and background.

Optimizing the arrays, or maybe not

In many cases the array of"on" points might be simplified to include larger rectangles that will make up forseveral"on" points at once. Also, for patterns comprising of straight lines only, you could also try theCreate2DLinePathEffect, to simplify the pattern path.

Well, in my experimenting none of these approaches gave good results. For reasons beyond my knowledge, once an "optimized" pattern is applied using either of these 2 techniques, bad results were produced, due to artifacts in the resulting rendering.

Even though in terms of resources and performance it might not make sense to create an 8 pixel line with 8 squares one after the other, it turned out to be the most reliable approach.

Real world usage

In a real world scenario you will not wantto recreate HatchStyles on the fly as the attachedexample shows, but rather have a pre-compiled list of points somewhere in your project, since HatchStyles are invariable patterns.

In the attachedsource code for the project you will find a Helper region at the bottom ofForm1.cswith 2 methods that will return the proper array of SKPoint(s) for each one of the 52 HatckStyle,PathPointsNormal andPathPointsReverse.

Pros and cons of using SkiaSharp

Here's a few bullet points to sum up my experience with SkiaSharp.


  • It's cross-platform, a big plus.
  • Can be faster than Gdiplus, especially if used with OpenGL controls.


  • Documentation is weak. The good people at Xamarin dida great job and wrote a few tutorials that will get you started, after that you're on your own.
  • Paths can often be generated with unwanted artifacts, especially the ones returned by the SKPaint class.
  • Lack of compound lines.


Date of first publication: December 28th, 2022.

Top Articles
Latest Posts
Article information

Author: Eusebia Nader

Last Updated: 09/09/2023

Views: 5725

Rating: 5 / 5 (60 voted)

Reviews: 83% of readers found this page helpful

Author information

Name: Eusebia Nader

Birthday: 1994-11-11

Address: Apt. 721 977 Ebert Meadows, Jereville, GA 73618-6603

Phone: +2316203969400

Job: International Farming Consultant

Hobby: Reading, Photography, Shooting, Singing, Magic, Kayaking, Mushroom hunting

Introduction: My name is Eusebia Nader, I am a encouraging, brainy, lively, nice, famous, healthy, clever person who loves writing and wants to share my knowledge and understanding with you.