WPF Diagrams defines a number of standard shapes that cover many common diagramming needs. You can also create your own shapes which users can manipulate in the same way as the standard shapes.
Declaring a Custom Shape
The first step is to create an identifier for the custom shape. This is an object of type DiagramShape. The identifier just specifies some basic information about the shape, such as its name.public static class MyShapes { public static readonly DiagramShape WShape = new DiagramShape("WShape").Freeze(); }
By default, the shape’s display name is the same as its name, and it has one connection point on each side. You can override these defaults using properties of DiagramShape:
public static readonly DiagramShape WShape = new DiagramShape("WShape") { DisplayName = "W Shape", TopConnectionPointCount = 0, BottomConnectionPointCount = 3 }.Freeze();
Laying Out the Custom Shape
The identifier doesn’t specify the geometry of the custom shape, because it’s usually more convenient to describe geometry and layout in XAML rather than in code. To specify the layout of the shape, create a ShapeLayout resource in XAML, whose key is the shape identifier:
<ms:ShapeLayout x:Key="{x:Static local:MyShapes.WShape}" Geometry="M 0 0 L 0 1 L 0.5 0.5 L 1 1 L 1 0 Z" />
The geometry is relative to the bounding box, so the top left is 0 0 and the bottom right is 1 1. WPF Diagrams will scale the geometry to the actual size of the node as required.
By default, the connection point positions are halfway along each side of the shape’s bounding box. You can override this using ShapeLayout.ConnectionPointPositions. You can populate this with ShapeConnectionPointPosition objects using XAML collection syntax, or use a shorthand notation similar to the geometry notation:
<!-- Use L, T, R, B to indicate which side the positions are for Use semicolons to separate positions Specify each position as x,y where 0 is top/left and 1 is bottom/right of the bounding box Optionally apply absolute position modifiers using + and - e.g. 0 + 20 is 20px from top/left, 1 - 30 is 30px from bottom/right NOTE: Unlike geometry notation, commas in positions are NOT optional --> <ms:ShapeLayout ConnectionPointPositions="L 0,0.25 R 1,0.25 B 0,1; 0.5,0.5; 1,1" />
By default, the content of the node is displayed at the centre of the bounding box. If the shape layout is imbalanced, this may not be central within the shape. To control the position of the content, set the ShapeLayout.ContentMargins property. You can use star sizing to specify that the margin should be a proportion of the shape size. For example, in the W shape the content might be placed towards the top of the shape, so you would specify a large bottom margin compared to the top.
<ms:ShapeLayout ContentMargin="*,*,*,4*" /> <!-- LTRB order -->
Laying Out Compound Shapes
Some shapes cannot be described as a single geometry because they consist of multiple sections which may need different shading or styling. An example of this in the built-in shapes is the curved arrow, where the ‘back’ of the arrow appears shaded. To do this in your own shapes, specify the ShapeLayout.Paths collection instead of ShapeLayout.Geometry. You can then apply styles directly to the individual paths.
Adding the Custom Shape to the Toolbox
You can add the custom shape to the toolbox in the same way as built-in shapes, using the DiagramNodeTool control and the ShapeTool.Shape attached property. To identify the shape in ShapeTool.Shape, specify the shape identifier object:
<ms:DiagramNodeTool ms:ShapeTool.Shape="{x:Static local:MyShapes.WShape}" />
If your shape is a compound shape, defined using ShapeLayout.Paths instead of ShapeLayout.Geometry, you may find that the styles you have defined for paths do not work well in the reduced-size environment of the toolbox. For each path, you can specify a style to be used in the toolbox by setting the ShapeLayout.ToolboxIconStyle attached property on that path.
Similarly, you can control how paths are rendered when dragging the tool over the diagram surface using the ShapeLayout.CursorVisualStyle attached property.
Serializing and Deserializing Custom Shapes
The DiagramXmlSerializer serializes custom shape nodes using the name you specified in the shape identifier. You will therefore want to make sure that all the shapes you allow have unique names.
However, when deserializing, the DiagramXmlSerializer does not know how to resolve user-defined shape names. You must therefore set DiagramXmlSerializer.ShapeNameResolver to a function which maps custom shape names to custom shape identifier objects:
serializer.ShapeNameResolver = name => name == "WShape" ? MyShapes.WShape : null;