Writing a custom shape for WPF

The problem

The project I’m currently working on involves the development of a prototype user interface to an existing application using WPF. One of the visual effects I wanted was to have two elements joined together by a "tail", the purpose of which was to help visually tie the input from the controls in one box to the result displayed in the other. Something like this:

WPF custom shape wireframe

The result box was going to change size depending on the input so the tail had to change too. Due to its shape, it couldn’t be achieved using a Rectangle and a simple skew transform, so I had to choose another approach. My first effort involved using a Polygon and binding its Points property to properties of the two elements and creating a custom Converter to generate the appropriate points collection. This approach worked well, until it came to tracking the position of the elements. Due to the way that WPF data binding works, the values of the properties I was binding to, to determine the element positions, weren’t being updated until *after* the converter was called. This meant that when one of the elements moved, the tail was one step behind. This lead me to investigate developing a custom shape.

So what’s involved in writing a custom shape?

Surprisingly not much! You simply need to inherit from the abstract Shape class and provide an override for the getter of the DefiningGeometry property. This returns the Geometry object that defines the shape of your shape. Easy!

 
using System;
using System.Collections.Generic;
using System.Text;
using System.Windows.Shapes;
using System.Windows.Media;

namespace CustomWpfShape {
    class ElementJoiner : Shape {
        protected override Geometry DefiningGeometry {
            get {
             
            }
        }
    }
}

In order to draw the shape we’re going to need to have references to the two elements we want to "join" with the tail, so we’ll add two dependency properties so that we can bind to those elements:

public FrameworkElement SourceElement {
    get { return (FrameworkElement)GetValue(SourceElementProperty); }
    set { SetValue(SourceElementProperty, value); }
}

public FrameworkElement TargetElement {
    get { return (FrameworkElement)GetValue(TargetElementProperty); }
    set { SetValue(TargetElementProperty, value); }
}

public static readonly DependencyProperty SourceElementProperty =
    DependencyProperty.Register("SourceElement", typeof(FrameworkElement),
        typeof(ElementJoiner), new UIPropertyMetadata(new PropertyChangedCallback(WireUpSource)));

public static readonly DependencyProperty TargetElementProperty =
    DependencyProperty.Register("TargetElement", typeof(FrameworkElement),
        typeof(ElementJoiner), new UIPropertyMetadata(new PropertyChangedCallback(WireUpTarget)));

You’ll need to add a using statement for the System.Windows namespace too:

using System.Windows;

You might have noticed the reference to two handler methods passed into the DependencyProperty.Register method: WireUpSource and WireUpTarget. These methods will get called whenever the dependency properties are set, and we’re going to use them to add event handlers to some key events of the elements we’re joining, namely those events that indicate changes to the elements’ layout:

private static void WireUpSource(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
    Debug.WriteLine("ElementJoiner dependency property changed");

    ElementJoiner ej = d as ElementJoiner;
    FrameworkElement el = e.NewValue as FrameworkElement;

    if (ej != null && el != null) {
        el.SizeChanged += new SizeChangedEventHandler(ej.FrameworkElement_SizeChanged);
        el.LayoutUpdated += new EventHandler(ej.SourceFrameworkElement_LayoutUpdated);
    }
}

private static void WireUpTarget(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
    Debug.WriteLine("ElementJoiner dependency property changed");

    ElementJoiner ej = d as ElementJoiner;
    FrameworkElement el = e.NewValue as FrameworkElement;

    if (ej != null && el != null) {
        el.SizeChanged += new SizeChangedEventHandler(ej.FrameworkElement_SizeChanged);
        el.LayoutUpdated += new EventHandler(ej.TargetFrameworkElement_LayoutUpdated);
    }
}

As you can see, we’ve added handlers to the SizeChanged and LayoutUpdated events of the source and target elements. Note that the handlers are instance methods on our custom shape and that we have to cast the passed in DependencyObject parameter to the type of our custom shape before we wire them up.

The handlers simply need to inform WPF that our custom shape needs to be redrawn. Doing this will cause the overridden DefiningGeometry property to be evaluated again and our shape will be updated. This is easily done by calling the InvalidateVisual method:

private Point _lastSourceOffset;
private Point _lastTargetOffset;

private void SourceFrameworkElement_LayoutUpdated(object sender, EventArgs e)
{
    Debug.WriteLine("ElementJoiner source element layout updated - " + DateTime.Now.Ticks);

    Visual anc = SourceElement.FindCommonVisualAncestor(TargetElement) as Visual;
    GeneralTransform sourceOffsetTransform = SourceElement.TransformToAncestor(anc);

    Point sourceOffset = sourceOffsetTransform.Transform(new Point(0, 0));
    
    if (!sourceOffset.Equals(_lastSourceOffset))
    {
        // Only redraw if position has changed
        _lastSourceOffset = sourceOffset;
        this.InvalidateVisual();
    }
}

private void TargetFrameworkElement_LayoutUpdated(object sender, EventArgs e)
{
    Debug.WriteLine("ElementJoiner target layout layout updated - " + DateTime.Now.Ticks);

    Visual anc = SourceElement.FindCommonVisualAncestor(TargetElement) as Visual;
    GeneralTransform targetOffsetTransform = TargetElement.TransformToAncestor(anc);

    Point targetOffset = targetOffsetTransform.Transform(new Point(0, 0));
    if (!targetOffset.Equals(_lastTargetOffset))
    {
        // Only redraw if position has changed
        _lastTargetOffset = targetOffset;
        this.InvalidateVisual();
    }
}

private void FrameworkElement_SizeChanged(object sender, SizeChangedEventArgs e)
{
    Debug.WriteLine("ElementJoiner source or target size changed - instance");
    this.InvalidateVisual();
}

Notice that the handlers for the LayoutUpdated event are only calling InvalidateVisual if the calculated offset from the common ancestor of the source and target elements has changed. The LayoutUpdated event gets raised very frequently and not doing this results in an immediately obvious performance issue. The FrameworkElement_SizeChanged handler could be modified to do the same in order to save a few cycles too.

Now that our elements are all wired up the only thing left to do is implement the overridden property DefiningGeometry:

protected override Geometry DefiningGeometry {
    get {
        if (SourceElement == null || TargetElement == null) {
            return new PathGeometry();
        }

        Visual anc = SourceElement.FindCommonVisualAncestor(TargetElement) as Visual;

        if (anc == null)
            throw new InvalidOperationException();

        GeneralTransform sourceOffsetTransform = SourceElement.TransformToAncestor(anc);
        GeneralTransform targetOffsetTransform = TargetElement.TransformToAncestor(anc);

        Point sourceOffset = sourceOffsetTransform.Transform(new Point(0, 0));
        Point targetOffset = targetOffsetTransform.Transform(new Point(0, 0));

        // Point order is: SourceElement top right corner, TargetElement top left corner,
        //                 TargetElement bottom left corner, SourceElement bottom right corner
        Point point1 = new Point(SourceElement.ActualWidth + sourceOffset.X, sourceOffset.Y);
        Point point2 = new Point(targetOffset.X, targetOffset.Y);
        Point point3 = new Point(targetOffset.X, TargetElement.ActualHeight + targetOffset.Y);
        Point point4 = new Point(SourceElement.ActualWidth + sourceOffset.X, SourceElement.ActualHeight + sourceOffset.Y);

        List<PathSegment> segments = new List<PathSegment>(4);
        segments.Add(new LineSegment(point2, false));
        segments.Add(new LineSegment(point3, false));
        segments.Add(new LineSegment(point4, false));
        segments.Add(new LineSegment(point1, false));

        List<PathFigure> figures = new List<PathFigure>(1);
        PathFigure pf = new PathFigure(point1, segments, true);
        figures.Add(pf);

        Geometry g = new PathGeometry(figures, FillRule.EvenOdd, null);
        return g;
    }
}

Basically what’s happening here is the offset position of the source and target elements to their common ancestor is calculated and then used to create four points that will define the tail, with the first point being the top right hand corner of the source element. We then work our way around the shape until we have all four points.

WPF custom shape point order

These points are used to build a list of PathSegments which in turn define a PathFigure (in our case only one PathFigure is required but your shape could contain as many PathFigures as you want). The PathFigure is then used to create a PathGeometry, which in turn defines our custom shape’s shape! And because we’re notified when the source or target elements’ layout or size is changed via the event handlers, our tail stays in sync between the two elements when they change.

Note that if you apply a render transform to the source or target elements, the shape will *not* change to suit, as render transforms do not affect the actual layout of the element, only their appearance.

Also note that due to the way that this custom shape’s geometry is determined, via the common ancestor of the source and target elements, any instance of the shape must be a direct child of that common ancestor in your element hierarchy. So if the common ancestor of the source and target elements is a grid, then the custom shape must be placed directly in that grid, in the first row and column and be spanned across all the grid’s rows and columns.

As a bonus you don’t have to do anything special to support the normal formatting and layout properties for your custom shape, the base class does that all for you. You can paint your shape with any brush you want, stroke the border, apply a transform, animate its properties and it displays in Expression Blend properly too.

That’s all it takes to create a custom shape for WPF. Let your imagine run wild and and see what wild and wacky shapes you can come up with.

You can download a sample solution of the ElementJoiner shape described in this article, including the failed custom converter approach, from here. You can run an XBAP version of the sample from here.

Next article I’ll show how I extended the shape with some more properties to aid in creating a particular animation effect.


2 Comments on “Writing a custom shape for WPF”

  1. Damian says:

    Thanks George, I’ve updated the links in this article so you can now download the project and view a live example.


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: