This article was written by Alan Mendelevich

The Problem

Creating a shape with bullets on the joints of it’s segments sounds like a really trivial task at a first glance. Just plaster some bullets on top of the shape. And it is really something like this until you decide you want to have transparent outlined bullets or opaque bullets with transparent outline around them. Like the ones in this picture:

bulleted_path_samples

 

In raster scenarios this could be solved by simply deleting the pixels around the joint points but since we are talking about WPF or Silverlight (vector graphics) this is not an option.

If you have a simplistic figure like the first rectangle in the above picture you may think about just creating shorter lines for it’s sides. This is not a very complicated task. However in the second (triangle) example calculating the end points for the lines gets a little more complicated. And try doing this without a masters degree in mathematics for the third and fourth (bottom) examples utilizing arcs and Bezier curves.

The second option is to cheat a little. You can use background color for bullet’s fill or outline. This would work well on a solid color background. But what if you have a gradient background (like in our sample picture), image background or if your bullet falls on the edge of some object or the shape itself?

I have faced this issue when working on a line chart portion of amCharts for WPF and here’s how I solved it.

 

The Solution

The magic word for my solution is OpacityMask. To quote MSDN documentation what it does is:

Gets or sets an opacity mask, as a Brush implementation that is applied to any alpha-channel masking for the rendered content of this element.

In other words it lets you apply different levels of opacity to different portions of your element.

So, the general idea is to create an opacity mask which has zero opacity spots in place where we want to put our bullets and 100% opacity everywhere else. Below are the main parts of the implementation along with some small but very important details.

  • We derive our BulletedPath class from the abstract Shape class. Path could’ve been a better base class candidate, but unfortunately it’s sealed.
  • We add several bullet related dependency properties for controlling bullet shape, fill and outline.
  • We add a Data property which will store PathGeometry defining our shape.

With all this done (you can see the implementation by downloading the source code for the control linked at the end of the article) we override OnRender method of our shape.

Here’s the method in it’s entirety and we’ll examine what it does below:

   1: protected override void OnRender(DrawingContext drawingContext)

   2: {

   3:     Pen shapePen = new Pen(this.Stroke, this.StrokeThickness);

   4:     shapePen.StartLineCap = this.StrokeStartLineCap;

   5:     shapePen.EndLineCap = this.StrokeEndLineCap;

   6:     shapePen.DashCap = this.StrokeDashCap;

   7:     shapePen.LineJoin = this.StrokeLineJoin;

   8:     shapePen.MiterLimit = this.StrokeMiterLimit;

   9:     shapePen.DashStyle = 

  10:         new DashStyle(this.StrokeDashArray, this.StrokeDashOffset);

  11:     // a create temporary drawing to find out 

  12:     // real bounds of our shape using specified pen

  13:     GeometryDrawing maskBoundsDrawing = 

  14:         new GeometryDrawing(Brushes.Black, shapePen, this.Data);

  15:     Rect bounds = maskBoundsDrawing.Bounds;

  16:     // create opacity mask for bullet holes

  17:     GeometryGroup holes = this.GetHoles();

  18:     RectangleGeometry maskRect = new RectangleGeometry(bounds);

  19:     Geometry maskGeometry = 

  20:         Geometry.Combine(maskRect, holes, GeometryCombineMode.Exclude, null);

  21:     GeometryDrawing maskDrawing = 

  22:         new GeometryDrawing(Brushes.Black, null, maskGeometry);

  23:     DrawingBrush maskBrush = new DrawingBrush(maskDrawing);

  24:     maskBrush.Stretch = Stretch.None;

  25:     // apply opacity mask

  26:     drawingContext.PushOpacityMask(maskBrush);

  27:     // draw main shape

  28:     drawingContext.DrawGeometry(this.Fill, shapePen, this.Data);

  29:     drawingContext.Pop();

  30:     // draw bullets

  31:     RenderBullets(drawingContext);

  32: }

  33:  

First we construct a pen to use for drawing the outline of our shape. Oddly enough Shape class doesn’t provide one property for setting the Pen. This is justified by the fact that it’s easier to set (and animate) individual properties of the pen on the whole shape object in XAML rather than creating a dedicated object for the outline pen.

This is understandable, however it would be really nice of Microsoft to provide a method to get a real Pen out of all these individual properties. This is a good candidate for an extension method, but that’s another story and for now we just construct the pen manually.

   1: Pen shapePen = new Pen(this.Stroke, this.StrokeThickness);

   2: shapePen.StartLineCap = this.StrokeStartLineCap;

   3: shapePen.EndLineCap = this.StrokeEndLineCap;

   4: shapePen.DashCap = this.StrokeDashCap;

   5: shapePen.LineJoin = this.StrokeLineJoin;

   6: shapePen.MiterLimit = this.StrokeMiterLimit;

   7: shapePen.DashStyle = 

   8:         new DashStyle(this.StrokeDashArray, this.StrokeDashOffset);

Now comes a tricky part.

 

We need to construct our opacity mask. Opacity mask is a Brush and it gets applied to the whole drawing area of the element. The size of that area depends not only on the geometry of our shape but on the size of it’s stroke, it’s miter, etc. For example if you have a line from point (0, 0) to (0, 100) and the StrokeThickness is set to 100 you’d get a line that (at the very least) occupies space from -50 to 50 on the Y-axis and it could be more dependent on the caps of that line and other settings. So, in order to get the final bounds of our shape before we actually draw it we create a temporary drawing using our actual pen settings and use it’s bounds as a rectangle area for our mask:

   1: GeometryDrawing maskBoundsDrawing = 

   2:     new GeometryDrawing(Brushes.Black, shapePen, this.Data);

   3: Rect bounds = maskBoundsDrawing.Bounds;

Then we get the geometries for our bullet holes (see the source code for details) and exclude them from our rectangular area. This way we get a geometry which is a rectangle with a holes in it.

   1: RectangleGeometry maskRect = new RectangleGeometry(bounds);

   2: Geometry maskGeometry = 

   3:     Geometry.Combine(maskRect, holes, GeometryCombineMode.Exclude, null);

Now we need to create a brush for our opacity mask from this geometry.

We create a GeometryDrawing using any opaque brush for it’s fill (Brushes.Black for example) and create a DrawingBrush from this drawing. It’s important to set brush’s Stretch property to None so it’s not distorted in any way.

   1: GeometryDrawing maskDrawing = 

   2:     new GeometryDrawing(Brushes.Black, null, maskGeometry);

   3: DrawingBrush maskBrush = new DrawingBrush(maskDrawing);

   4: maskBrush.Stretch = Stretch.None;

And now all that’s left is to push our opacity mask on a DrawingContext, draw our shape (opacity mask will make sure that there are holes on the junction points), pop the mask and draw the bullets:

   1: // apply opacity mask

   2: drawingContext.PushOpacityMask(maskBrush);

   3: // draw main shape

   4: drawingContext.DrawGeometry(this.Fill, shapePen, this.Data);

   5: drawingContext.Pop();

   6: // draw bullets

   7: RenderBullets(drawingContext);

And that’s it.

 

I’ve omitted implementations of the other reasonably trivial tasks like drawing the bullets from the article but you can always take a look at the implementation by downloading the source code of BulletedPath control.

 

You can download the binary control, C# source code and class reference from download section of my blog. It’s released under the BSD license so feel free to use and improve it.

 

About the author: Alan Mendelevich is a software developer from Vilnius, Lithuania. He has more than 10 years of professional software development experience for both Web and Windows. Currently he works as a lead developer on amCharts for WPF project – a charting control suite for WPF platform and SPAW Editor – open source web based WYSIWYG HTML editor control.

Tags :

1 Trackback(s)

  1. Sep 6, 2009: WPF: 90+ Miejsc które warto zna? « Dawid Po?li?ski

Post a Comment