Custom Rendering Ink

The DrawingAttributes property of a stroke allows you to specify the appearance of a stroke, such as its size, color, and shape, but there may be times that you want to customize the appearance beyond what DrawingAttributes allow. You may want to customize the appearance of ink by rendering in the appearance of an air brush, oil paint, and many other effects. The Windows Presentation Foundation (WPF) allows you to custom render ink by implementing a custom DynamicRenderer and Stroke object.

This topic contains the following subsections:

  • Architecture

  • Implementing a Dynamic Renderer

  • Implementing a Custom Stroke

  • Implementing a Custom InkCanvas

  • Conclusion

Architecture

Ink rendering occurs two times; when a user writes ink to an inking surface, and again after the stroke is added to the ink-enabled surface. The DynamicRenderer renders the ink when the user moves the tablet pen on the digitizer, and the Stroke renders itself once it is added to an element.

There are three classes to implement when dynamically rendering ink.

  1. DynamicRenderer: Implement a class that derives from DynamicRenderer. This class is a specialized StylusPlugIn that renders the stroke as it is drawn. The DynamicRenderer does the rendering on a separate thread, so the inking surface appears to collect ink even when the application user interface (UI) thread is blocked. For more information about the threading model, see The Ink Threading Model. To customize dynamically rendering a stroke, override the OnDraw method.

  2. Stroke: Implement a class that derives from Stroke. This class is responsible for static rendering of the StylusPoint data after it has been converted into a Stroke object. Override the DrawCore method to ensure that static rendering of the stroke is consistent with dynamic rendering.

  3. InkCanvas: Implement a class that derives from InkCanvas. Assign the customized DynamicRenderer to the DynamicRenderer property. Override the OnStrokeCollected method and add a custom stroke to the Strokes property. This ensures that the appearance of the ink is consistent.

Implementing a Dynamic Renderer

Although the DynamicRenderer class is a standard part of WPF, to perform more specialized rendering, you must create a customized dynamic renderer that derives from the DynamicRenderer and override the OnDraw method.

The following example demonstrates a customized DynamicRenderer that draws ink with a linear gradient brush effect.

Imports System
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
    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 'OnStylusDown


    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 'OnDraw
End Class 'CustomDynamicRenderer
using System;
using System.Windows.Media;
using System.Windows;
using System.Windows.Input.StylusPlugIns;
using System.Windows.Input;
using 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.
        if (brush == null)
        {
            brush = new LinearGradientBrush(Colors.Red, Colors.Blue, 20d);
        }

        // Create a new Pen, if necessary.
        if (pen == null)
        {
            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;
            }
        }
    }
}

Implementing Custom Strokes

Implement a class that derives from Stroke. This class is responsible for rendering StylusPoint data after it has been converted into a Stroke object. Override the DrawCore class to do the actual drawing.

Your Stroke class can also store custom data by using the AddPropertyData method. This data is stored with the stroke data when persisted.

The Stroke class can also perform hit testing. You can also implement your own hit testing algorithm by overriding the HitTest method in the current class.

The following C# code demonstrates a custom Stroke class that renders StylusPoint data as a 3-D stroke.

Imports System
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
    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 'New


    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 'DrawCore
End Class 'CustomStroke
using System;
using System.Windows.Media;
using System.Windows;
using System.Windows.Input.StylusPlugIns;
using System.Windows.Input;
using 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;
            }
        }
    }
}

Implementing a Custom InkCanvas

The easiest way to use your customized DynamicRenderer and stroke is to implement a class that derives from InkCanvas and uses these classes. The InkCanvas has a DynamicRenderer property that specifies how the stroke is rendered when the user is drawing it.

To custom render strokes on an InkCanvas do the following:

The following C# code demonstrates a custom InkCanvas class that uses a customized DynamicRenderer and collects custom strokes.

Public Class CustomRenderingInkCanvas
    Inherits InkCanvas

    Private customRenderer As New CustomDynamicRenderer()

    Public Sub New()
        ' Use the custom dynamic renderer on the 
        ' custom InkCanvas. 
        Me.DynamicRenderer = customRenderer

    End Sub 'New 

    Protected Overrides Sub OnStrokeCollected(ByVal e As InkCanvasStrokeCollectedEventArgs)

        ' Remove the original stroke and add a custom stroke. 
        Me.Strokes.Remove(e.Stroke)
        Dim customStroke As New CustomStroke(e.Stroke.StylusPoints)
        Me.Strokes.Add(customStroke)

        ' Pass the custom stroke to base class' OnStrokeCollected method. 
        Dim args As New InkCanvasStrokeCollectedEventArgs(customStroke)
        MyBase.OnStrokeCollected(args)

    End Sub 'OnStrokeCollected 
End Class 'CustomRenderingInkCanvas
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);

    }

}

An InkCanvas can have more than one DynamicRenderer. You can add multiple DynamicRenderer objects to the InkCanvas by adding them to the StylusPlugIns property.

Conclusion

You can customize the appearance of ink by deriving your own DynamicRenderer, Stroke, and InkCanvas classes. Together, these classes ensure that the appearance of the stroke is consistent when the user draws the stroke and after it is collected.

See Also

Other Resources

Advanced Ink Handling