自定义呈现墨迹

通过笔划的 DrawingAttributes 属性可指定笔划的外观,例如笔划的大小、颜色和形状,但有时你可能想要自定义 DrawingAttributes 不能实现的外观。 建议通过使用喷笔、油画和多种其他效果呈现外观,从而自定义墨迹的外观。 Windows Presentation Foundation (WPF) 允许通过实现自定义 DynamicRendererStroke 对象来自定义呈现墨迹。

本主题包含以下小节:

体系结构

墨迹呈现会出现两次:用户将墨迹写入墨迹书写表面时,以及将笔划添加到启用了墨迹的表面之后。 用户在数字化器上移动触笔时,DynamicRenderer 会呈现墨迹,并且将 Stroke 添加到元素后它会呈现自身。

动态呈现墨迹时需要实现三个类。

  1. DynamicRenderer:实现派生自 DynamicRenderer 的类。 此类是专用的 StylusPlugIn,可按绘制的原本形式呈现笔划。 DynamicRenderer 在一个单独线程上执行呈现,因此即使在应用程序用户界面 (UI) 线程被阻止时也会出现墨迹书写表面来收集墨迹。 有关线程模型的详细信息,请参阅墨迹线程模型。 若要自定义动态呈现笔划,请重写 OnDraw 方法。

  2. 笔划:实现派生自 Stroke 的类。 此类负责在 StylusPoint 数据被转换为 Stroke 对象后静态呈现该数据。 重写 DrawCore 方法是为了确保笔划的静态呈现与动态呈现一致。

  3. InkCanvas:实现派生自 InkCanvas 的类。 将自定义的 DynamicRenderer 分配给 DynamicRenderer 属性。 重写 OnStrokeCollected 方法并将自定义笔划添加到 Strokes 属性。 这样可以确保墨迹外观一致。

实现动态呈现器

尽管 DynamicRenderer 类是 WPF 的标准组成部分,但是若要执行更专业的呈现,必须创建派生自 DynamicRenderer 的自定义动态呈现器,并重写 OnDraw 方法。

以下示例演示可绘制具有线性渐变画笔效果的自定义的 DynamicRenderer 墨迹。

using System;
using System.Windows.Media;
using System.Windows;
using System.Windows.Input.StylusPlugIns;
using System.Windows.Input;
using System.Windows.Ink;
Imports System.Windows.Media
Imports System.Windows
Imports System.Windows.Input.StylusPlugIns
Imports System.Windows.Input
Imports System.Windows.Ink
// A StylusPlugin that renders ink with a linear gradient brush effect.
class CustomDynamicRenderer : DynamicRenderer
{
    [ThreadStatic]
    static private Brush brush = null;

    [ThreadStatic]
    static private Pen pen = null;

    private Point prevPoint;

    protected override void OnStylusDown(RawStylusInput rawStylusInput)
    {
        // Allocate memory to store the previous point to draw from.
        prevPoint = new Point(double.NegativeInfinity, double.NegativeInfinity);
        base.OnStylusDown(rawStylusInput);
    }

    protected override void OnDraw(DrawingContext drawingContext,
                                   StylusPointCollection stylusPoints,
                                   Geometry geometry, Brush fillBrush)
    {
        // Create a new Brush, if necessary.
        brush ??= new LinearGradientBrush(Colors.Red, Colors.Blue, 20d);

        // Create a new Pen, if necessary.
        pen ??= new Pen(brush, 2d);

        // Draw linear gradient ellipses between
        // all the StylusPoints that have come in.
        for (int i = 0; i < stylusPoints.Count; i++)
        {
            Point pt = (Point)stylusPoints[i];
            Vector v = Point.Subtract(prevPoint, pt);

            // Only draw if we are at least 4 units away
            // from the end of the last ellipse. Otherwise,
            // we're just redrawing and wasting cycles.
            if (v.Length > 4)
            {
                // Set the thickness of the stroke based
                // on how hard the user pressed.
                double radius = stylusPoints[i].PressureFactor * 10d;
                drawingContext.DrawEllipse(brush, pen, pt, radius, radius);
                prevPoint = pt;
            }
        }
    }
}
' A StylusPlugin that renders ink with a linear gradient brush effect.
Class CustomDynamicRenderer
    Inherits DynamicRenderer
    <ThreadStatic()> _
    Private Shared brush As Brush = Nothing

    <ThreadStatic()> _
    Private Shared pen As Pen = Nothing

    Private prevPoint As Point


    Protected Overrides Sub OnStylusDown(ByVal rawStylusInput As RawStylusInput)
        ' Allocate memory to store the previous point to draw from.
        prevPoint = New Point(Double.NegativeInfinity, Double.NegativeInfinity)
        MyBase.OnStylusDown(rawStylusInput)

    End Sub


    Protected Overrides Sub OnDraw(ByVal drawingContext As DrawingContext, _
                                   ByVal stylusPoints As StylusPointCollection, _
                                   ByVal geometry As Geometry, _
                                   ByVal fillBrush As Brush)

        ' Create a new Brush, if necessary.
        If brush Is Nothing Then
            brush = New LinearGradientBrush(Colors.Red, Colors.Blue, 20.0)
        End If

        ' Create a new Pen, if necessary.
        If pen Is Nothing Then
            pen = New Pen(brush, 2.0)
        End If

        ' Draw linear gradient ellipses between 
        ' all the StylusPoints that have come in.
        Dim i As Integer
        For i = 0 To stylusPoints.Count - 1

            Dim pt As Point = CType(stylusPoints(i), Point)
            Dim v As Vector = Point.Subtract(prevPoint, pt)

            ' Only draw if we are at least 4 units away 
            ' from the end of the last ellipse. Otherwise, 
            ' we're just redrawing and wasting cycles.
            If v.Length > 4 Then
                ' Set the thickness of the stroke based 
                ' on how hard the user pressed.
                Dim radius As Double = stylusPoints(i).PressureFactor * 10.0
                drawingContext.DrawEllipse(brush, pen, pt, radius, radius)
                prevPoint = pt
            End If
        Next i

    End Sub
End Class

实现自定义笔划

实现从 Stroke 派生的类。 此类负责在 StylusPoint 数据被转换为 Stroke 对象后呈现该数据。 重写 DrawCore 类进行实际绘制。

通过使用 AddPropertyData 方法,笔划类还可存储自定义数据。 此数据持续存在时会与笔划数据一起存储。

Stroke 类还可执行命中测试。 也可通过重写当前类中的 HitTest 方法实现自己的命中测试算法。

以下 C# 代码演示了将 StylusPoint 数据呈现为三维笔划的自定义 Stroke 类。

using System;
using System.Windows.Media;
using System.Windows;
using System.Windows.Input.StylusPlugIns;
using System.Windows.Input;
using System.Windows.Ink;
Imports System.Windows.Media
Imports System.Windows
Imports System.Windows.Input.StylusPlugIns
Imports System.Windows.Input
Imports System.Windows.Ink
// A class for rendering custom strokes
class CustomStroke : Stroke
{
    Brush brush;
    Pen pen;

    public CustomStroke(StylusPointCollection stylusPoints)
        : base(stylusPoints)
    {
        // Create the Brush and Pen used for drawing.
        brush = new LinearGradientBrush(Colors.Red, Colors.Blue, 20d);
        pen = new Pen(brush, 2d);
    }

    protected override void DrawCore(DrawingContext drawingContext,
                                     DrawingAttributes drawingAttributes)
    {
        // Allocate memory to store the previous point to draw from.
        Point prevPoint = new Point(double.NegativeInfinity,
                                    double.NegativeInfinity);

        // Draw linear gradient ellipses between
        // all the StylusPoints in the Stroke.
        for (int i = 0; i < this.StylusPoints.Count; i++)
        {
            Point pt = (Point)this.StylusPoints[i];
            Vector v = Point.Subtract(prevPoint, pt);

            // Only draw if we are at least 4 units away
            // from the end of the last ellipse. Otherwise,
            // we're just redrawing and wasting cycles.
            if (v.Length > 4)
            {
                // Set the thickness of the stroke
                // based on how hard the user pressed.
                double radius = this.StylusPoints[i].PressureFactor * 10d;
                drawingContext.DrawEllipse(brush, pen, pt, radius, radius);
                prevPoint = pt;
            }
        }
    }
}
' A class for rendering custom strokes
Class CustomStroke
    Inherits Stroke
    Private brush As Brush
    Private pen As Pen


    Public Sub New(ByVal stylusPoints As StylusPointCollection)
        MyBase.New(stylusPoints)
        ' Create the Brush and Pen used for drawing.
        brush = New LinearGradientBrush(Colors.Red, Colors.Blue, 20.0)
        pen = New Pen(brush, 2.0)

    End Sub


    Protected Overrides Sub DrawCore(ByVal drawingContext As DrawingContext, _
                                     ByVal drawingAttributes As DrawingAttributes)

        ' Allocate memory to store the previous point to draw from.
        Dim prevPoint As New Point(Double.NegativeInfinity, Double.NegativeInfinity)

        ' Draw linear gradient ellipses between 
        ' all the StylusPoints in the Stroke.
        Dim i As Integer
        For i = 0 To Me.StylusPoints.Count - 1
            Dim pt As Point = CType(Me.StylusPoints(i), Point)
            Dim v As Vector = Point.Subtract(prevPoint, pt)

            ' Only draw if we are at least 4 units away 
            ' from the end of the last ellipse. Otherwise, 
            ' we're just redrawing and wasting cycles.
            If v.Length > 4 Then
                ' Set the thickness of the stroke 
                ' based on how hard the user pressed.
                Dim radius As Double = Me.StylusPoints(i).PressureFactor * 10.0
                drawingContext.DrawEllipse(brush, pen, pt, radius, radius)
                prevPoint = pt
            End If
        Next i

    End Sub
End Class

实现自定义 InkCanvas

使用自定义 DynamicRenderer 和笔划最简单的方式是实现派生自 InkCanvas 的类并使用这些类。 InkCanvas 具有 DynamicRenderer 属性,该属性可以指定用户绘制笔划时笔划的呈现方式。

若要在 InkCanvas 上呈现笔划,请执行以下操作:

以下 C# 代码演示了一个自定义 InkCanvas 类,该类使用了自定义的 DynamicRenderer 并收集自定义笔划。

public class CustomRenderingInkCanvas : InkCanvas
{
    CustomDynamicRenderer customRenderer = new CustomDynamicRenderer();

    public CustomRenderingInkCanvas() : base()
    {
        // Use the custom dynamic renderer on the
        // custom InkCanvas.
        this.DynamicRenderer = customRenderer;
    }

    protected override void OnStrokeCollected(InkCanvasStrokeCollectedEventArgs e)
    {
        // Remove the original stroke and add a custom stroke.
        this.Strokes.Remove(e.Stroke);
        CustomStroke customStroke = new CustomStroke(e.Stroke.StylusPoints);
        this.Strokes.Add(customStroke);

        // Pass the custom stroke to base class' OnStrokeCollected method.
        InkCanvasStrokeCollectedEventArgs args =
            new InkCanvasStrokeCollectedEventArgs(customStroke);
        base.OnStrokeCollected(args);
    }
}

一个 InkCanvas 可以有多个 DynamicRenderer。 可以通过将多个 DynamicRenderer 对象添加到 StylusPlugIns 属性来将其添加到 InkCanvas

结论

可以通过派生自己的 DynamicRendererStrokeInkCanvas 类来自定义墨迹的外观。 将这些类结合使用可以确保用户绘制笔划时和笔划被收集后的笔划外观保持一致。

另请参阅