使用 SkiaSharp 重新创建 Gdiplus 填充





5.00/5 (5投票s)
一种将填充效果从 Gdiplus 快速移植到 SkiaSharp 的简单技术。
此示例项目使用 Visual Studio 2022 编写。该项目是基于 .NET Core 6 框架的 WindowsForm 应用程序,使用 C# 10 编写。兼容 Windows 7 及以上版本。
引言
我正在重写一个最初基于 Gdiplus 生成图形输出的 Windows 应用程序。目标是迁移到 SkiaSharp,同时保持相同的功能,以实现向后兼容。本文详细介绍了为重现 Gdiplus 的图案填充效果所采取的方法。
什么是 HatchBrush(图案填充画笔)
如果您不熟悉这个概念,在 Gdiplus 中,HatchBrush 是一种特殊的画笔类型,可以让你用一种特定的图案填充一个区域,这种图案称为 HatchStyle(图案样式)。Gdiplus 提供了 52 种内置图案,可用于创建画笔。
以下是一些示例
Percent20 填充样式 | DiagonalBrick 填充样式 | DiagonalCross 填充样式 |
![]() | ![]() | ![]() |
什么是 HatchStyle(图案样式)
52 种内置 Gdiplus HatchStyle 中的每一种都是基于一个 8x8 像素的交替开关(on/off)矩阵组成的图案(在某些情况下,会有抗锯齿像素,后面会详细说明)。如果你仔细观察每种样式,你会看到构成图案的基本网格。
以下是一些放大示例
Percent20 图案 | DiagonalBrick 图案 | DiagonalCross 图案 |
![]() | ![]() | ![]() |
为了能够使用 SkiaSharp 重现它们,第一步是能够收集每种图案所需的信息。
破解 HatchStyles(图案样式)
与其手动逐一分析每种 HatchStyle(这会耗费大量精力),然后创建我的 8x8 点数组(或列表),我决定让机器来完成这项工作,使用以下方法:
- 创建一个 8x8 的位图并用图案填充它。
- 读取像素值并构建“开启”点的数组。
- 利用这些信息在 SkiaSharp 中重现图案。
读取图案像素的方法代码如下:
// 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;
}
GetHatchPattern
方法接受一个 `HatchStyle` 类型的参数,并生成一个 `SKPoint` 列表,我们稍后可以在 SkiaSharp 中使用它。
该方法还接受 `inverted` 参数,以便列表中的点可以根据背景/前景颜色进行反转。
请注意,对于 52 种 `HatchStyle` 中的 3 种,Gdiplus 会创建带有抗锯齿像素的图案,而不管目标 `Graphic` 对象中的 `SmoothingMode` 设置如何。我们将忽略这些信息,只将所有内容转换为简单的开/关像素,因为最终效果似乎不需要 SkiaSharp 中的任何抗锯齿。
收集完信息后,我们就可以进入 SkiaSharp 并构建相同的效果。
使用 SkiaSharp 绘制图案填充效果
SkiaSharp 基于 Skia,这是一个由 Google 当前拥有和维护的开源、跨平台图形库。有关如何使用 SkiaSharp 使用效果填充路径的更多信息,请参阅 这个很棒的教程。
SkiaSharp 使用 `SKPaint` 对象来定义如何在表面上绘制路径和线条。为了能够创建图案填充效果,我们需要将 `SKPaint` 对象的 `PathEffect` 成员设置为通过 `SKPathEffect` 类的 `Create2DPath` 静态方法创建的效果。
这里最重要的数据位是,我们需要将一个 `SKPath` 对象传递给此方法,该对象包含我们想要绘制的图案。因此,我们将创建一个路径,并为从 Gdiplus 创建的源图案位图中读取的每个像素添加一个矩形(或者更准确地说,一个正方形)。
这是代码
// 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 GetHatchPattern
SKPath 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();
在此代码片段中,`canvas` 变量是从目标控件(通常来自 `PaintSurface` 事件)获得的绘图表面。
要使效果在特定原点绘制并一直延伸到区域的末端,需要几个步骤。首先,需要进行平移,以确保图案的原点与我们正在填充的区域的左上角匹配。其次,目标路径包含一个比目标区域大的矩形,但被裁剪到该区域,以确保图案绘制到区域的右下角。这是必需的,因为 SkiaSharp 会截断任何不完全适合该区域的行/列。
以下是本文附加的示例程序生成的并排比较结果:

请注意,SkiaSharp 版本中的图案图像使用了径向渐变颜色,从黑色到蓝色,这是 Gdiplus 无法实现的,Gdiplus 只接受纯色作为前景色和背景色。
优化数组,或者不优化
在许多情况下,可以将“开启”点的数组简化,以包含可以一次构成多个“开启”点的较大矩形。此外,对于仅包含直线的图案,您还可以尝试使用 `Create2DLine` `PathEffect` 来简化图案路径。
嗯,在我的实验中,这些方法都没有取得好的结果。出于我知识范围之外的原因,一旦使用这两种技术中的任何一种应用了“优化”的图案,就会由于渲染结果中的瑕疵而产生不好的结果。
尽管从资源和性能的角度来看,用 8 个连续的正方形创建一条 8 像素的线可能没有意义,但事实证明这是最可靠的方法。
实际应用
在实际场景中,您不会希望在运行时即时重现 `HatchStyles`(正如附加示例所示),而是希望在项目中某处有一个预编译的点列表,因为 `HatchStyles` 是不变的图案。
在随附的项目源代码中,您将在 `Form1.cs` 的底部找到一个 `Helper` 部分,其中包含两个方法,它们将为 52 种 `HatchStyle` 中的每一种返回正确的 `SKPoint` 数组:`PathPointsNormal` 和 `PathPointsReverse`。
使用 SkiaSharp 的优缺点
以下几点总结了我使用 SkiaSharp 的经验:
优点
- 它是跨平台的,这是一个很大的优点。
- 可能比 Gdiplus 快,尤其是在与 OpenGL 控件一起使用时。
缺点
- 文档很薄弱。Xamarin 的开发人员做得很棒,写了一些教程可以帮助您入门,之后就只能靠自己了。
- 路径经常会产生不需要的瑕疵,尤其是 `SKPaint` 类返回的那些。
- 缺少 复合线条。
历史
- 2022 年 12 月 28 日:首次发布日期