Shapes

Use the Shape class to paint a geometrical figure. You can control what kind of shape is drawn and how it is stroked and filled.

Shapes, like TextBlocks and Pictures, are "atomic" objects -- they cannot contain any other objects. So a Shape will never draw some text or an image.

In these simplistic demonstrations, the code programmatically creates a Part and adds it to the Diagram. Once you learn about models and data binding you will generally not create parts (nodes or links) programmatically.

Figures

You can set the Shape.Figure property to commonly named kinds of shapes. You may also need to set the GraphObject.DesiredSize or GraphObject.Width and GraphObject.Height properties, although it is also common to have the size determined by the Panel that the shape is in.

Here are several of the most often used Shape figures:


  diagram.Add(
    new Part("Horizontal")
      .Add(
        new Shape("Rectangle") { Width = 40, Height = 60, Margin = 4, Fill = null },
        new Shape("RoundedRectangle") { Width = 40, Height = 60, Margin = 4, Fill = null },
        new Shape("Ellipse") { Width = 40, Height = 40, Margin = 4, Fill = null },
        new Shape("Diamond") { Width = 40, Height = 40, Margin = 4, Fill = null },
        new Shape("TriangleRight") { Width = 40, Height = 40, Margin = 4, Fill = null },
        new Shape("TriangleDown") { Width = 40, Height = 40, Margin = 4, Fill = null },
        new Shape("TriangleLeft") { Width = 40, Height = 40, Margin = 4, Fill = null },
        new Shape("TriangleUp") { Width = 40, Height = 40, Margin = 4, Fill = null },
        new Shape("MinusLine") { Width = 40, Height = 40, Margin = 4, Fill = null },
        new Shape("PlusLine") { Width = 40, Height = 40, Margin = 4, Fill = null },
        new Shape("XLine") { Width = 40, Height = 40, Margin = 4, Fill = null }
      )
  );

You can see all of the named geometrical figures in the shapes sample. Some of the most commonly used figures are predefined in the GoDiagram library. But most figures are defined in the Figures.cs file in the extensions directory.

Fill and Strokes

The Shape.Stroke property specifies the brush used to draw the shape's outline. The Shape.Fill property specifies the brush used to fill the shape's outline. Additional "Stroke..." properties also control how the shape's outline is drawn. The most common such property is Shape.StrokeWidth.


  diagram.Add(
    new Part("Horizontal")
      .Add(
        new Shape("Diamond") { Width = 40, Height = 40, Margin = 4 },  // default fill and stroke are "black"
        new Shape("Diamond") {
          Width = 40, Height = 40, Margin = 4,
          Fill = "green"
        },
        new Shape("Diamond") {
          Width = 40, Height = 40, Margin = 4,
          Fill = "green", Stroke = null
        },
        new Shape("Diamond") {
          Width = 40, Height = 40, Margin = 4,
          Fill = null, Stroke = "green"
        },
        new Shape("Diamond") {
          Width = 40, Height = 40, Margin = 4,
          Fill = null, Stroke = "green", StrokeWidth = 3
        },
        new Shape("Diamond") {
          Width = 40, Height = 40, Margin = 4,
          Fill = null, Stroke = "green", StrokeWidth = 6
        },
        new Shape("Diamond") {
          Width = 40, Height = 40, Margin = 4,
          Fill = "green", Background = "orange"
        }
      )
  );

The Shape.Stroke and Shape.Fill properties take Brushes but most often are given a color string to denote solid color brushes. These two properties default to a solid black brush. However it is common to assign one of them to be either null or "transparent". A null brush means that nothing is drawn for that stroke or fill. A transparent brush produces the same appearance but different hit-testing behavior. A shape with a null Shape.Fill produces a "hollow" shape -- clicking inside the shape will not "hit" that shape and thus not select the Node that that shape is in. But a shape with a transparent fill produces a "filled" shape -- a mouse event inside the shape will "hit" that shape.


  diagram.Add(
    new Part("Table")
      .Add(
        new Shape {
          Row = 0, Column = 0, Figure = "Diamond", Width = 60, Height = 60, Margin = 4,
          Fill = "green"
        },
        new TextBlock("green") { Row = 1, Column = 0 },
        new Shape {
          Row = 0, Column = 1, Figure = "Diamond", Width = 60, Height = 60, Margin = 4,
          Fill = "white"
        },
        new TextBlock("white") { Row = 1, Column = 1 },
        new Shape {
          Row = 0, Column = 2, Figure = "Diamond", Width = 60, Height = 60, Margin = 4,
          Fill = "transparent"
        },
        new TextBlock("transparent") { Row = 1, Column = 2 },
        new Shape {
          Row = 0, Column = 3, Figure = "Diamond", Width = 60, Height = 60, Margin = 4,
          Fill = null
        },
        new TextBlock("null") { Row = 1, Column = 3 }
      )
  );

Note that with the "transparent" fill you can see the diagram background, yet when you click in it you "hit" the Shape. Only the last one, with a null fill, is truly "hollow". Clicking in the last shape will only result in a click on the diagram background, unless you click on the stroke outline.

Geometry

Every Shape gets its "shape" from the Geometry that it uses. A Geometry is just a saved description of how to draw some lines given a set of points. Setting Shape.Figure uses a named predefined geometry that can be parameterized. In general it is most efficient to give a Shape a Geometry rather than giving it a figure.

If you want something different from all of the predefined figures in GoDiagram, you can construct your own Geometry and set Shape.Geometry. One way of building your own Geometry is by building PathFigures consisting of PathSegments. This is often necessary when building a geometry whose points are computed based on some data.

But an easier way to create constant geometries is by calling Geometry.Parse to read a string that has a geometry-defining path expression, or to set Shape.GeometryString to such a string. These expressions have commands for moving an imaginary "pen". The syntax for geometry paths is documented in the Geometry Path Strings page.

This example creates a Geometry that looks like the letter "W" and uses it in several Shape objects with different stroke characteristics. Geometry objects may be shared by multiple Shapes. Note that there may be no need to specify the GraphObject.DesiredSize or GraphObject.Width and GraphObject.Height, because the Geometry defines its own size. If the size is set or if it is imposed by the containing Panel, the effective geometry is determined by the Shape.GeometryStretch property. Depending on the value of the GeometryStretch property, this may result in extra empty space or the clipping of the shape.


  var wGeometry = Geometry.Parse("M 0, 0 L 10, 50 20, 10 30, 50 40, 0");
  diagram.Add(
    new Part("Horizontal")
      .Add(
        new Shape { Geometry = wGeometry, StrokeWidth = 2 },
        new Shape {
          Geometry = wGeometry, Stroke = "blue", StrokeWidth = 10,
          StrokeJoin = LineJoin.Miter, StrokeCap = LineCap.Butt
        },
        new Shape {
          Geometry = wGeometry, Stroke = "blue", StrokeWidth = 10,
          StrokeJoin = LineJoin.Miter, StrokeCap = LineCap.Round
        },
        new Shape {
          Geometry = wGeometry, Stroke = "blue", StrokeWidth = 10,
          StrokeJoin = LineJoin.Miter, StrokeCap = LineCap.Square
        },
        new Shape {
          Geometry = wGeometry, Stroke = "green", StrokeWidth = 10,
          StrokeJoin = LineJoin.Bevel, StrokeCap = LineCap.Butt
        },
        new Shape {
          Geometry = wGeometry, Stroke = "green", StrokeWidth = 10,
          StrokeJoin = LineJoin.Bevel, StrokeCap = LineCap.Round
        },
        new Shape {
          Geometry = wGeometry, Stroke = "green", StrokeWidth = 10,
          StrokeJoin = LineJoin.Bevel, StrokeCap = LineCap.Square
        },
        new Shape {
          Geometry = wGeometry, Stroke = "red", StrokeWidth = 10,
          StrokeJoin = LineJoin.Round, StrokeCap = LineCap.Butt
        },
        new Shape {
          Geometry = wGeometry, Stroke = "red", StrokeWidth = 10,
          StrokeJoin = LineJoin.Round, StrokeCap = LineCap.Round
        },
        new Shape {
          Geometry = wGeometry, Stroke = "red", StrokeWidth = 10,
          StrokeJoin = LineJoin.Round, StrokeCap = LineCap.Square
        },
        new Shape {
          Geometry = wGeometry, Stroke = "purple", StrokeWidth = 2,
          StrokeDashArray = new[] { 4f, 2f }
        },
        new Shape {
          Geometry = wGeometry, Stroke = "purple", StrokeWidth = 2,
          StrokeDashArray = new[] { 6f, 6f, 2f, 2f }
        }
      )
  );

Angle and Scale

Besides setting the GraphObject.DesiredSize or GraphObject.Width and GraphObject.Height to declare the size of a Shape, you can also set other properties to affect the appearance. For example, you can set the GraphObject.Angle and GraphObject.Scale properties.


  diagram.Add(
    new Part("Table")
      .Add(
        new Shape {
          Row = 0, Column = 0, Figure = "Diamond",
          Width = 40, Height = 40, Fill = "green"
        },  // default angle is zero; default scale is one
        new Shape {
          Row = 0, Column = 1, Figure = "Diamond",
          Width = 40, Height = 40, Fill = "green",
          Angle = 30
        },
        new Shape {
          Row = 0, Column = 2, Figure = "Diamond",
          Width = 40, Height = 40, Fill = "green",
          Scale = 1.5
        },
        new Shape {
          Row = 0, Column = 3, Figure = "Diamond",
          Width = 40, Height = 40, Fill = "green",
          Angle = 30, Scale = 1.5
        }
      )
  );

The Shape.Fill and GraphObject.Background brushes scale and rotate along with the shape.

Custom Figures

As shown above, one can easily create custom shapes just by setting Shape.Geometry or Shape.GeometryString. However it is also possible to define additional named figures, which is convenient when you want to be able to easily specify or change the geometry of an existing Shape by setting or data binding the Shape.Figure property.

The static function Shape.DefineFigureGenerator can be used to define new figure names. The second argument is a function that is called with the Shape and the expected width and height in order to generate and return a Geometry. This permits parameterization of the geometry based on properties of the Shape and the expected size. In particular, the Shape.Parameter1 and Shape.Parameter2 properties can be considered, in addition to the width and height, while producing the Geometry. To be valid, the generated Geometry bounds must be equal to or less than the supplied width and height.


  Shape.DefineFigureGenerator("RoundedTopRectangle", (shape, w, h) => {
    // this figure takes one parameter, the size of the corner
    var p1 = 5d;  // default corner size
    if (shape != null) {
      var param1 = shape.Parameter1;
      if (!double.IsNaN(param1) && param1 >= 0) p1 = param1;  // can't be negative or NaN
    }
    p1 = Math.Min(p1, w / 2);
    p1 = Math.Min(p1, h / 2);  // limit by whole height or by half height?
    var geo = new Geometry();
    // a single figure consisting of straight lines and quarter-circle arcs
    geo.Add(new PathFigure(0, p1)
         .Add(new PathSegment(SegmentType.Arc, 180, 90, p1, p1, p1, p1))
         .Add(new PathSegment(SegmentType.Line, w - p1, 0))
         .Add(new PathSegment(SegmentType.Arc, 270, 90, w - p1, p1, p1, p1))
         .Add(new PathSegment(SegmentType.Line, w, h))
         .Add(new PathSegment(SegmentType.Line, 0, h).Close()));
    // don't intersect with two top corners when used in an "Auto" Panel
    geo.Spot1 = new Spot(0, 0, 0.3 * p1, 0.3 * p1);
    geo.Spot2 = new Spot(1, 1, -0.3 * p1, 0);
    return geo;
  });

  Shape.DefineFigureGenerator("RoundedBottomRectangle", (shape, w, h) => {
    // this figure takes one parameter, the size of the corner
    var p1 = 5d;  // default corner size
    if (shape != null) {
      var param1 = shape.Parameter1;
      if (!double.IsNaN(param1) && param1 >= 0) p1 = param1;  // can't be negative or NaN
    }
    p1 = Math.Min(p1, w / 2);
    p1 = Math.Min(p1, h / 2);  // limit by whole height or by half height?
    var geo = new Geometry();
    // a single figure consisting of straight lines and quarter-circle arcs
    geo.Add(new PathFigure(0, 0)
         .Add(new PathSegment(SegmentType.Line, w, 0))
         .Add(new PathSegment(SegmentType.Line, w, h - p1))
         .Add(new PathSegment(SegmentType.Arc, 0, 90, w - p1, h - p1, p1, p1))
         .Add(new PathSegment(SegmentType.Line, p1, h))
         .Add(new PathSegment(SegmentType.Arc, 90, 90, p1, h - p1, p1, p1).Close()));
    // don't intersect with two bottom corners when used in an "Auto" Panel
    geo.Spot1 = new Spot(0, 0, 0.3 * p1, 0);
    geo.Spot2 = new Spot(1, 1, -0.3 * p1, -0.3 * p1);
    return geo;
  });

  diagram.NodeTemplate =
    new Part("Spot") {
        SelectionAdorned = false,  // don't show the standard selection handle
        Resizable = true, ResizeElementName = "SHAPE",  // user can resize the Shape
        Rotatable = true, RotateElementName = "SHAPE"  // user can rotate the Shape
                                                       // without rotating the label
      }
      .Add(
        new Shape {
            Name = "SHAPE",
            Fill = new Brush(new LinearGradientPaint(new[] { (0f, "white"), (1f, "gray") })),
            DesiredSize = new Size(100, 50)
          }
          .Bind("Figure", "Fig")
          .Bind("Parameter1", "P1"),
        new Panel("Vertical")
          .Add(
            new TextBlock().Bind("Text", "Fig"),
            new TextBlock { Stroke = "blue" }
              .Bind(new Binding("Text", "Parameter1", p => p.ToString()).OfElement("SHAPE"))
          )
      );

  diagram.Model = new MyModel {
    NodeDataSource = new List<NodeData> {
      new NodeData { Fig = "RoundedTopRectangle" },
      new NodeData { Fig = "RoundedTopRectangle", P1 = 0 },
      new NodeData { Fig = "RoundedTopRectangle", P1 = 3 },
      new NodeData { Fig = "RoundedTopRectangle", P1 = 10 },
      new NodeData { Fig = "RoundedTopRectangle", P1 = 50 },
      new NodeData { Fig = "RoundedTopRectangle", P1 = 250 },
      new NodeData { Fig = "RoundedBottomRectangle" },
      new NodeData { Fig = "RoundedBottomRectangle", P1 = 0 },
      new NodeData { Fig = "RoundedBottomRectangle", P1 = 3 },
      new NodeData { Fig = "RoundedBottomRectangle", P1 = 10 },
      new NodeData { Fig = "RoundedBottomRectangle", P1 = 50 },
      new NodeData { Fig = "RoundedBottomRectangle", P1 = 250 }
    }
  };

Note how the Shape.Parameter1 property, data bound to the "P1" property, controls how rounded the corners are. The definition of each figure limits the roundedness based on the actual size of the geometry. You can see the effects by resizing the last shape -- the curve on the shape with P1==250 can be huge if the shape becomes huge.

You can find the definitions for many figures at: Figures.cs.