65.9K
CodeProject 正在变化。 阅读更多。
Home

使用 SkiaSharp 重新创建 Gdiplus 填充

starIconstarIconstarIconstarIconstarIcon

5.00/5 (5投票s)

2022 年 12 月 28 日

CPOL

5分钟阅读

viewsIcon

14385

downloadIcon

368

一种将填充效果从 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 填充样式
Percent20 example DiagonalBrick example DiagonalCross example

什么是 HatchStyle(图案样式)

52 种内置 Gdiplus HatchStyle 中的每一种都是基于一个 8x8 像素的交替开关(on/off)矩阵组成的图案(在某些情况下,会有抗锯齿像素,后面会详细说明)。如果你仔细观察每种样式,你会看到构成图案的基本网格。

以下是一些放大示例

Percent20 图案 DiagonalBrick 图案 DiagonalCross 图案
Percent20 example DiagonalBrick example DiagonalCross example

为了能够使用 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 会截断任何不完全适合该区域的行/列。

以下是本文附加的示例程序生成的并排比较结果:

Screenshot

请注意,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 日:首次发布日期
© . All rights reserved.