WPF Diagrams: Custom Diagram Shapes

When creating a diagramming application, you may find that lots of different nodes in your diagram are only distinguished by their shape and number of connection points, but they all have the same connection validation and functionality. WPF Diagrams 2.0 provides a type of node called “ShapeNode” which is perfect for these scenarios. Different instances of ShapeNode can have completely different shapes and connection point setups. WPF Diagrams 2.0 includes more than 70 predefined shapes to get you started. You can of course extend the ShapeNode class if you need to provide additional functionality and properties specific to the needs of your applications — but if all you want is a new shape, then the built-in ShapeNode class will do fine.

In this blog post I will show you how to define your own shapes that can be used by a ShapeNode. Defining a custom shape takes very little code, and the diagramming foundation will then automatically create the toolbox icons, cursor visuals and node templates all for you. Here is a preview of what we will be creating:

Custom diagram shapes

Getting started

If you don’t already have a diagramming application that you can start adding custom shapes to, then start by reading this ‘getting started’ blog post.

Defining a custom shape is made up of 2 parts: a DiagramShape, and a ShapeLayout. The DiagramShape contains the logical information such as the number of connection points on each side and its display name. This is written in a C# file. The ShapeLayout provides the graphical information including the path geometries, connection point positions and path styles. This is written in a XAML resource dictionary. For all the custom shapes described in this blog post, I have put the DiagramShape definitions in a static class called CustomShapes, and the ShapeLayout definitions are in a resource dictionary in MainWindow.xaml.

Defining a simple shape

To start off, we will define a very simple shape made up of a single path. The DiagramShape code for this looks like the following:

public static class CustomShapes {
  public static readonly DiagramShape Link = new DiagramShape("Link");

As you can see it is very simple. The string passed into the DiagramShape constructor is the name of the shape and is used for serialization. By default this is also used as the display name, but you can also specify the display name by setting the DisplayName property. Display names are displayed in the tooltip of the node tool in the tool box. This custom shape also doesn’t specify the number of connection points. By default, there will be one connection point on the left, top, right and bottom sides of the shape.

Next we need to write the ShapeLayout to define the graphical parts of the shape. Here is an example:

<ms:ShapeLayout x:Key="{x:Static local:CustomShapes.Link}"
                Geometry="M 0 0 L 0.8 0 L 0.8 0.3 L 1 0.3 L 1 0.7 L 0.8 0.7 L 0.8 1 L 0 1 L 0 0.7 L 0.2 0.7 L 0.2 0.3 L 0 0.3 Z"
                ConnectionPointPositions="L 0.2,0.5"

The first thing to notice is that the DiagramShape object we created is being used as the resource key for the ShapeLayout. This is how WPF Diagrams matches up shape layouts to shape declarations.

The geometry for the shape itself is defined by the Geometry property, in this case using the PathGeometry mini-language. Note that all the values used in the path geometry are between 0 and 1. This makes it easier to scale the shape to any size. I have also customized the connection point positions and content margin which are topics outside the scope of this blog post (see the docs for more info).

The only thing left to do now is include this custom shape in the tool box. The diagramming foundation then automatically creates the tool box icon, cursor visual, diagram node template and applies the default styles to match all the built in shapes. Adding the custom shape to the tool box is done by adding the following line of code to a DiagramToolBoxGroup:

<ms:DiagramNodeTool ms:ShapeTool.Shape="{x:Static local:CustomShapes.Link}" />

The value of the ShapeTool.Shape attached property again just needs to be the DiagramShape object itself — the diagramming foundation will take it from there.

Defining a shape with multiple paths

Sometimes a single path geometry just isn’t going to do it. You may want to define shapes that are made up of multiple paths, each which may have different colors or styles. Fortunately the ShapeLayout class can support this too. Rather than setting the Geometry property, you can populate the Paths collection. The nuclear symbol seen in the image above has been achieved with this approach. The DiagramShape definition for this is the same as the previous shape but has a different name. The ShapeLayout is displayed below:

<ms:ShapeLayout x:Key="{x:Static local:CustomShapes.Nuke}">
    <Path Fill="Black" Data="M 0.5 0 A 0.5 0.5 180 0 1 0.5 1 A 0.5 0.5 180 0 1 0.5 0 Z"
          ms:DiagramNodeElement.IsGeometryProvider="True" />
    <Path Fill="#FFD60F" Data="M 0 0 L 0.5 0.071 A 0.429 0.429 180 0 1 0.5 0.929 A 0.429 0.429 180 0 1 0.5 0.071 L 1 1 L 0.5 0.071 Z"
          Stretch="Fill" />
    <Path Fill="Black" Data="M 0 0 L 0.129 0.507 A 0.327 0.327 60 0 1 0.311 0.191 L 0.436 0.409 A 0.118 0.118 60 0 0 0.382 0.507 L 0.129 0.507 L 1 1 L 0.129 0.507 Z"
          Stretch="Fill" />
    <Path Fill="Black" Data="M 0 0 L 0.684 0.191 A 0.327 0.327 60 0 1 0.867 0.507 L 0.617 0.507 A 0.118 0.118 60 0 0 0.56 0.409 L 0.684 0.191 L 1 1 L 0.684 0.191 Z"
          Stretch="Fill" />
    <Path Fill="Black" Data="M 0 0 L 0.68 0.822 A 0.327 0.327 60 0 1 0.32 0.822 L 0.444 0.609 A 0.118 0.118 60 0 0 0.555 0.609 L 0.68 0.822 L 1 1 L 0.68 0.822 Z"
          Stretch="Fill" />
    <Path Fill="Black" Data="M 0 0 L 0.5 0.444 A 0.056 0.056 180 0 1 0.5 0.556 A 0.056 0.056 180 0 1 0.5 0.444 L 1 1 L 0.5 0.444 Z"
          Stretch="Fill" />

One thing to note here is the attached property being set on the first path. This attached property defines which path should be used as the hit testing area for the shape. The first path in the nuclear symbol is simply the outer circle. If you do not set this attached property, then the bounding box of the shape will be used as the hit testing area by default.

Flexible styling

In a few scenarios, you may find you want to have different path styles applied to the tool box icon, cursor visual and node template for the same shape. The lightning bolt shape seen in the image above has a blue tool box icon, a semi-transparent cursor visual, and a yellow node template. This can be achieved by using attached properties on a Path object as seen here:

<ms:ShapeLayout x:Key="{x:Static local:CustomShapes.LightningBolt}"
                ConnectionPointPositions="L 0.255,0.453; 0.481,0.689 T 0,0.198; 0.396,0 R 0.604,0.292; 0.764,0.557 B 1,1">
    <Path Data="M 0 0.198 L 0.396 0 L 0.604 0.292 L 0.519 0.321 L 0.764 0.557 L 0.689 0.594 L 1 1 L 0.481 0.689 L 0.557 0.642 L 0.255 0.453 L 0.358 0.396 Z"
          Stretch="Fill" StrokeLineJoin="Round"
          ms:ShapeLayout.ToolboxIconStyle="{StaticResource LightningToolBoxStyle}"
          ms:ShapeLayout.CursorVisualStyle="{StaticResource LightningCursorVisualStyle}"
          ms:ShapeLayout.NodeStyle="{StaticResource LightningNodeStyle}"
          ms:DiagramNodeElement.IsGeometryProvider="True" />

Each attached property is setting a path style to be used in either the tool box, cursor visual or node template.

A full sample demonstrating all of these concepts can be downloaded here. To run the sample, make sure to include a reference to your copy of the WPF Diagrams Foundation DLL.

The WPF Diagrams documentation includes more info about creating custom shapes, including how to serialise and deserialise diagrams that use custom shapes. See Help Topics > Creating Your Own Shapes in the online docs or the help file.

If you have any further questions about implementing custom diagram shapes then we would love to hear from you. Either leave a comment on this blog post, or stop by our forum.

Tagged as WPF Diagrams

Leave a Reply


Join our mailer

You should join our newsletter! Sent monthly:

Back to Top