Title: Render polygons and polylines in a WPF program using C#
WPF lets you do all sorts of interesting things that are much harder in Windows Forms applications. However sometimes, as in this example, it makes simple things much harder. (WPF's slogan should be, "Twice as flexible and only five times as hard!")
Perhaps Microsoft is trying to "gently" encourage us to use drawing objects (such as the Polygon object) to draw shapes instead of trying to render them in code. It is definitely easier to use that approach. If that was Microsoft's idea, it might have been better if they just didn't allow this kind of rendering instead of giving us a confusing and poorly-documented method for doing it.
Anyway, with the rant out of the way, let me explain how the program works. The DrawingContext class provides a few drawing methods such as DrawEllipse, DrawLine, and DrawRectangle. Logically that class should also provide DrawPolygon, DrawPolyline, and other drawing methods but it doesn't. So for this example I decided to add them as extension methods. The following code shows the private DrawPolygonOrPolyline method that draws either polygons or polylines.
// Draw a polygon or polyline.
private static void DrawPolygonOrPolyline(
this DrawingContext drawingContext,
Brush brush, Pen pen, Point[] points, FillRule fill_rule,
bool draw_polygon)
{
// Make a StreamGeometry to hold the drawing objects.
StreamGeometry geo = new StreamGeometry();
geo.FillRule = fill_rule;
// Open the context to use for drawing.
using (StreamGeometryContext context = geo.Open())
{
// Start at the first point.
context.BeginFigure(points[0], true, draw_polygon);
// Add the points after the first one.
context.PolyLineTo(points.Skip(1).ToArray(), true, false);
}
// Draw.
drawingContext.DrawGeometry(brush, pen, geo);
}
This method creates a StreamGeometry object to represent the shape. The StreamGeometry class is a geometry class (there are others such as LineGeometry and RectangleGeometry) that represents a sequence of drawing commands that can include shapes such as lines, arcs, ellipses, and rectangles. (The PathGeometry class is similar but heavier because it supports data binding, animation, and modification. Because this example doesn't need those, it uses the lighter-weight StreamGeometry class.)
After it creates the StreamGeometry object, the code sets its FillRule property. This can have the values EvenOdd or Nonzero. This example uses the EvenOdd setting so there is an unfilled hole in the middle of the green outer star. (See the picture.) If this property were set to Nonzero, then the interior of the star would be filled completely.
Next the program "opens" the StreamGeometry to get a context that it can use to draw. It calls the context's BeginFigure method to start a drawing sequence. You need to call this method before you start drawing. Its first parameter indicates where drawing should start, in this case at the points array's first point.
The second parameter to BeginFigure indicates whether the shape should be filled. This example sets this value to true. If you don't want to fill the shape, you can simply pass this method a null brush to make the method "fill" the shape with nothing.
The final parameter to BeginFigure indicates whether the shape should be closed. The method uses the value of its draw_polygon parameter so this code closes the shape only if it is drawing a polygon.
After starting a new figure, the code calls the context's PolyLineTo method. Its first parameter is the array of points that should be connected. Unfortunately if the first point in the array duplicates the point used in the call to BeginFigure, then the polyline includes that point twice and that messes up the connection between the last point and the first point. For example, if the green star used mitered instead of rounded corners, then the final corner between the first and last point would not be mitered. (To see the effect, pass the entire points array in here and change the main program to not use rounded corners.)
To work around this problem, the code uses the LINQ Skip extension method to skip the first point in the points array and only pass the rest of the points into the call to PolyLineTo.
The second parameter to PolyLineTo determines whether the line segments between the points in the polyline should be "stroked" (drawn). This example sets this to true so the points are always drawn. If you don't want to draw the line segments, simply pass the method a null pen to make method "draw" them with nothing.
The final parameter to PolyLineTo indicates whether the lines should be joined smoothly. The example sets this value to false. If you want the lines joined smoothly, you can specify the pen's LineJoin property to Rounded. (Described shortly.)
Finally, after those short but hard-to-explain steps, the program calls the DrawingContext object's DrawGeometry method to draw the StreamGeometry containing the polygon or polyline.
The DrawPolygonOrPolyline method is declared private so it is only visible inside the static DrawingContextExtensions class that defines it. You could make it public, but then the main program would need to use the same method to draw both polygons and polylines. While that wouldn't be the end of the world, it's usually better to make a method perform a single well-defined task instead of making one super-method that does a lot.
Instead of making this method public, I created the following two public methods that call the private one.
// Draw a polygon.
public static void DrawPolygon(this DrawingContext drawingContext,
Brush brush, Pen pen, Point[] points, FillRule fill_rule)
{
drawingContext.DrawPolygonOrPolyline(
brush, pen, points, fill_rule, true);
}
// Draw a polyline.
public static void DrawPolyline(this DrawingContext drawingContext,
Brush brush, Pen pen, Point[] points, FillRule fill_rule)
{
drawingContext.DrawPolygonOrPolyline(
brush, pen, points, fill_rule, false);
}
Now using these methods from the main program is just easy as using the other methods provided by the DrawingContext class. The following code show how the program draws the green star.
// Draw the polygon.
Pen pen = new Pen(Brushes.Green, line_thickness);
pen.LineJoin = PenLineJoin.Round;
drawingContext.DrawPolygon(Brushes.LightGreen,
pen, points, FillRule.EvenOdd);
This code creates a pen and sets its LineJoin property to Rounded. It then calls the DrawPolygon extension method, passing it a light green brush, the pen, the points (defined earlier in code that isn't interesting enough to show), and the desired fill rule.
The following code shows how the program draws the smaller blue polyline.
// Draw the polyline.
pen = new Pen(Brushes.Blue, line_thickness / 2);
drawingContext.DrawPolyline(null, pen,
points, FillRule.EvenOdd);
This code creates a new pen. It calls the DrawPolyline extension method, passing it a null brush (so the shape isn't filled), the pen, the points (re-defined earlier in code that isn't interesting enough to show), and the desired fill rule.
If you look back at the DrawPolygonOrPolyline method, you'll see that the code isn't really all that hard. It was just hard to find out how to do this. And I do wonder why Microsoft didn't include simple methods such as this one in WPF to begin with. At least making this an extension method makes it as easy to use as the other DrawingContext methods.
Download the example to experiment with it and to see additional details.
|