Draw an outlined path in WPF and C#

[outlined path]

Most of the techniques needed to draw an outlined path are included in those you need to draw compound lines. For the basic idea behind compound lines and a technique that lets you draw symmetric compound lines relatively easily, see the post Draw symmetric compound lines in WPF and C#.

Drawing more general compound lines and drawing an outlined path are much harder tasks. The code used by this example is pretty long so I’m going to omit most of the code from this post. Instead I’ll explain the technique and the methods that implement it. You can look at the code to see the details.

First, what is a path? A Path object contains a collection of lines and curves. Each non-connected piece of the Path is stored in a figure.

The path mini-language lets you “easily” specify shapes in a Path in XAML code. The following code shows how the example program defines its Path.

<Path Stroke="Black" StrokeThickness="20" Name="scribblePath">
        <PathGeometry x:Name="scribbleGeometry"
            Figures="M 20,20 C 200,0 0,200 40,60
                L 150,30 C 60,200 200,150 200,60
                M 70,140 L 150,240 180,170 30,200
                M 270,70 L 270,230 200,200 Z"/>

The Path object contains a Data property that contains a PathGeometry object. That object’s Figures property uses the path mini-language to specify the shapes. The commands it uses are:

  • M 20,20 – Moves the position to (20, 20).
  • C 200,0 0,200 40,60 – Draws a curve starting at the current point, using control points (200, 0) and (0, 200), and finishing at (40, 60).
  • L 150,30 – Draws a line from the current point to the point (150, 30).
  • C 60,200 200,150 200,60 – Draws a curve starting at the current point, using control points (60, 200) and (200, 150), and finishing at (200, 60).
  • M 70,140 – Moves the position to (70, 140).
  • L 150,240 180,170 30,200 – Starts at the current point draws lines to the points (250, 240), (180, 170), and (30, 200).
  • M 270,70 – Moves the position to (270, 70).
  • L 270,230 200,200 – Starts at the current point draws lines to the points (270, 230) and (200, 200).
  • Z – Closes the current figure. In this case drawing a line to the start of the current figure, which is (270, 70).

Making an outlined path directly from a Path object would be extremely hard because of all of the shapes it might contain. You can simplify the problem if you first flatten the path so it is made up of lines and polylines. The following GetFlattenedFigurePoints does just that.

The program uses the following statement to convert the initial PathGeometry object into a flattened version.

PathGeometry flat = scribbleGeometry.GetFlattenedPathGeometry();

The result is a new PathGeometry that contains only LineSegment and PolylineSegment objects.

So the first step is flattening the path. Next we need to find the points that make up the left and right edges of the flattened path. I'll explain why we need those points in my next post. For now, we'll use them to outline the path.

[outlined path]
To understand the general idea, take a look at Figure 1, which shows two segments along the flattened path. The path moves from point p1 to point p2 and then to point p3.

The red point marks the left edge of the path at point p2. Similarly the green point marks the right edge at point p2.

To find the red point, we need to find the intersection of the left edge of segment p1→p2 and the left edge of segment p2→p3. (Similarly the green point is at the intersection of the segments' right edges.)

[outlined path]
To see how you find the left edge of a segment, look at Figure 2. The values dx and dy are the changes in the X and Y coordinates moving from a segment's starting point to its ending point. In that case, the directions <-dy, dx> and <dy, -dx> are both perpendicular to the line segment.

After calculating dx and dy, the program divides them by the length of the segment and then multiplies the result by half of the segment's thickness to get the values px and py. In other words:

px = dx / length * half_thickness
py = dy / length * half_thickness

If p1 and p2 are the segment's starting and ending points, then p1 + <-py, px> and p2 + <-py, px> are points on the left edge of the segment. (Similarly p1 + <py, -px> and p2 + <py, -px> are points on the right edge of the segment.)

At this point, you know everything you need to know to find a path's left and right edge points.

  1. Loop through the path's figures. For each figure:
    1. Flatten the figure.
    2. Loop through the flattened figure's LineSegment and PolylineSegment objects and make a list of their points.
    3. Loop through the segments. For each pair of adjacent segments:
      1. Find points on the left edges of the segments.
      2. Find the intersection of the lines defined by the left edge points. Add that point of intersection to the left edge point list.
      3. Repeat for the right edges.

After you've found the left and right edge points, outlining a figure is relatively simple. Start by following the left edge points. When you get to the end of the figure, follow the right edge points backwards to the beginning. The result is a loop that outlines the figure.

This high-level description skims over a lot of details. Look at the code to see all of the minutiae.

While the code that performs all of those calculations is pretty involved, the final result is fairly easy to use. The following code shows how the example program outlines its path.

private void btnGo_Click(object sender, RoutedEventArgs e)
    // Get the outline path.
    PathGeometry flat =
    double thickness = scribblePath.StrokeThickness;

    // Get polygons representing the path's outlines.
    List polygons = GetPathOutlinePolygons(flat, thickness);

    // Add the polygons to the main Grid.
    foreach (Polygon polygon in polygons)
        polygon.Stroke = Brushes.Red;
        polygon.StrokeThickness = 1;

    // Disable the Go button.
    btnGo.IsEnabled = false;
    btnGo.Opacity = 0.5;

Recall that the scribbleGeometry object is the PathGeometry object that contains the shapes. The code starts by calling its GetFlattenedPathGeometry method (provided by the .NET Framework PathGeometry class) to get the flattened path. It also copies the Path object's thickness into a variable.

Next the code calls the GetPathOutlinePolygons method to do all of the example's heavy lifting. The result is a list of Polygon objects, each of which represents the outline of a figure in the path. The program sets each polygon object's color and thickness, and adds it to the program's main Grid control.

The code finishes by disabling the Go button.

Download the example and experiment with it. In my next post I'll explain how you can use the left and right edge points to create general compound lines.

Download Example   Follow me on Twitter   RSS feed   Donate

About RodStephens

Rod Stephens is a software consultant and author who has written more than 30 books and 250 magazine articles covering C#, Visual Basic, Visual Basic for Applications, Delphi, and Java.
This entry was posted in algorithms, drawing, graphics, mathematics and tagged , , , , , , , , , , , , , , , , , . Bookmark the permalink.

3 Responses to Draw an outlined path in WPF and C#

  1. Pingback: Make compound lines in WPF and C# - C# HelperC# Helper

  2. songhwa says:

    I have a question about this.
    I want to make ouline about 3d model.
    How can i make outline about 3d model in viewport3d?

    • RodStephens says:

      I’m not sure I understand. Do you mean you want to draw the outline around an object as seen from a particular point of view?

      I don’t think that’s easy, at least not analytically. You could draw the model onto a bitmap and then process that image to generate the outline.

      Or you could transform the model’s vertices so you’re looking at the model from the top. Then you could ignore their Y coordinates and generate polygons in the X-Z plane.

      If I misunderstand the question and you want a wireframe, search this site for “wireframe” and you’ll see some examples.

Leave a Reply

Your email address will not be published. Required fields are marked *