In Windows Forms programming, you can draw a smooth curve by using the `Graphics` object’s `DrawCurve` method. For some reason, Microsoft did not provide the ability to draw a smooth curve in WPF. Fortunatrely, you can use a series of Bézier curves to draw a smooth curve in WPF. Unfortunately, it’s fairly hard. This sort of “feature” is what lead me to my unofficial slogan for WPF, “Twice as flexible and only five times as hard!”

This post explains how you can create the Bézier curves that you need to draw a smooth curve. It’s somewhat involved, so I’ve broken the discussion into sections.

# Bézier Curves

You can use a Bézier curve to draw a smooth curve. The curve is defined by a start point, and end point, and two control points that determine the direction that the curve should be pointing at the start and end points.

The picture on the right shows how the four points determine the shape of the curve. The curve begins at the start point and moves in the direction of the first control point. It ends at the end point, coming from the direction of the second control point. The picture’s dashed lines show the directions between the end points and the control points.

The distance from the start and end points to their control points determine how closely the curve follows the dashed lines. If the control points are farther away, then the curve follows the dashed lines longer.

To make a smooth curve joining a series of points, you can build multiple Bézier curves that start and end at those points. To make the result smooth, you need to align the control points in adjacent curves so their dashed lines point in the same direction. The picture on the right shows two Bézier curves connected smoothly. The first curve’s second control point (labeled “Control 1b”) and the second curve’s first control point (labeled “Control 2a”) are colinear with the point that joins the two Bézier curves.

This is where the “twice as flexible” part of the unofficial slogan comes into play. If you want, you can move the control points Control 1b and Control 2a closer or farther from the point that they control as long as the three points are colinear. For example, you can move Control 2a closer to the point to make the second Bézier curve turn more tightly when it begins. The `DrawCurve` method provides one parameter, `tension`, that lets you adjust how close the control points are to the points on the curve. In contrast, when you build a series of Bézier curves you can place each control point individually. Unfortunately, that’s a lot of work. (Perhaps the slogan for this particular feature should be, “Twice as flexible and only ten times as hard!”)

# PolyBezierSegment

WPF even provides a class that you can use to hold a group of connected Bézier curves: `PolyBezierSegment`. That object contains a starting point and a sequence of points that includes control points and curve points for its Bézier curves. That would be useful enough (with some work), but you can’t simply display a `PolyBezierSegment`. It must be inside a `PathFigure` object contained in a `PathGeometry` object that is within a `Path` object. The following code shows the example on Microsoft’s web page PolyBezierSegment Class.

<Path Stroke="Black" StrokeThickness="1"> <Path.Data> <PathGeometry> <PathGeometry.Figures> <PathFigureCollection> <PathFigure StartPoint="10,100"> <PathFigure.Segments> <PathSegmentCollection> <PolyBezierSegment Points="0,0 200,0 300,100 300,0 400,0 600,100" /> </PathSegmentCollection> </PathFigure.Segments> </PathFigure> </PathFigureCollection> </PathGeometry.Figures> </PathGeometry> </Path.Data> </Path>

(At this point the slogan for smooth WPF curves could be, “Twice as flexible and only twnety times as hard!”)

At this point, you can take a stab at building a smooth curve with WPF. First, use the points that you want to connect to find appropriate control points. Then use them to build a `Path` that contains a `PolyBezierSegment` object and all of the other necessary intermediate objects.

# Finding Control Points

So how do you define the control points? Look at the picture on the right, which shows three Bézier curves connecting the points A, B, C, and D. Now focus on the blue curve. It needs two control points, one following point B and one before point C.

To find the control point near data point B, we look at the line segment defined by point B’s two neighbors A and C. The red dashed segment connects those points. Now we move from point B in the direction of that line segment. The green dashed segment shows the red segment translated so it intersects point B. The distance along this segment that we move to place the control point depends on the curve’s tension. You’ll see how that works when you look at the code.

Notice that you use the same segment to define the control points on both sides of a particular data point. In the picture, you use the same green dashed segment to define the control points that come before and after point B. Because those control points lie on a line that intersects point B, the two Bézier curves on either side of point B will meet smoothly.

To find the blue curve’s control point near point C, you similarly look at the segment between points B and D.

There are two special cases for building the series of curves. The start point and the end point do not have neighbors on both sides, so they are used in place of their missing neighbors. For example, in the preceding picture, the red dashed line would lead from point A to point B. That means the curve starts out pointed toward the second point.

Similarly, point D’s red dashed segment points from point D to point C so the curve ends moving away from its second-to-last point.

# Finding Control Points

The following method takes as input an array of points and returns an array that contains those points plus control points between them. The `tension` parameter determines how far the control points are from the data points.

// Make an array containing Bezier curve points and control points. private Point[] MakeCurvePoints(Point[] points, double tension) { if (points.Length < 2) return null; double control_scale = tension / 0.5 * 0.175; // Make a list containing the points and // appropriate control points. List<Point> result_points = new List<Point>(); result_points.Add(points[0]); for (int i = 0; i < points.Length - 1; i++) { // Get the point and its neighbors. Point pt_before = points[Math.Max(i - 1, 0)]; Point pt = points[i]; Point pt_after = points[i + 1]; Point pt_after2 = points[Math.Min(i + 2, points.Length - 1)]; double dx1 = pt_after.X - pt_before.X; double dy1 = pt_after.Y - pt_before.Y; Point p1 = points[i]; Point p4 = pt_after; double dx = pt_after.X - pt_before.X; double dy = pt_after.Y - pt_before.Y; Point p2 = new Point( pt.X + control_scale * dx, pt.Y + control_scale * dy); dx = pt_after2.X - pt.X; dy = pt_after2.Y - pt.Y; Point p3 = new Point( pt_after.X - control_scale * dx, pt_after.Y - control_scale * dy); // Save points p2, p3, and p4. result_points.Add(p2); result_points.Add(p3); result_points.Add(p4); } // Return the points. return result_points.ToArray(); }

The method first verifies that it has received at least two points and returns if it did not. It then calculates a scale factor that it will use to set the control points’ distances from the data points. I picked the scale factor to provide a result that agrees fairly well with the results of the `DrawCurve` method.

Next, the code creates a `result_points` list to hold the data points and the control points. It adds the curve’s’ first point to the list.

The method then loops through the data points, stopping before it reaches the last one. For each data point, the code must find the control points for the Bézier curve that begins at that data point.

To do that, the program finds the points that lie before this one, after this one, and two positions after this one. The points before this one or two positions after this one will not exist if the data point is the first or last point. In that case, the code uses the data point instead to get the red dashed lines described in the preceding section for those special cases.

The code then calculates the change in X and Y coordinates between the points before and after this one. It multiplies those values by the scale factor `control_scale` and adds the results to the coordinates of the current point to get the location of its control point `p2`.

The method then performs similar calculations to find the curve’s second control point `p3`.

After it has found the control points, the code adds them and the following data point to the `result_points` list.

When it has finished processing all of the data points, the method returns the `result_points` list.

# Making a Path

The following method takes as input an array holding data and control points and builds a `Path` object holding the appropriate `PolyBezierSegment`.

// Make a Path holding a series of Bezier curves. // The points parameter includes the points to visit // and the control points. private Path MakeBezierPath(Point[] points) { // Create a Path to hold the geometry. Path path = new Path(); // Add a PathGeometry. PathGeometry path_geometry = new PathGeometry(); path.Data = path_geometry; // Create a PathFigure. PathFigure path_figure = new PathFigure(); path_geometry.Figures.Add(path_figure); // Start at the first point. path_figure.StartPoint = points[0]; // Create a PathSegmentCollection. PathSegmentCollection path_segment_collection = new PathSegmentCollection(); path_figure.Segments = path_segment_collection; // Add the rest of the points to a PointCollection. PointCollection point_collection = new PointCollection(points.Length - 1); for (int i = 1; i < points.Length; i++) point_collection.Add(points[i]); // Make a PolyBezierSegment from the points. PolyBezierSegment bezier_segment = new PolyBezierSegment(); bezier_segment.Points = point_collection; // Add the PolyBezierSegment to othe segment collection. path_segment_collection.Add(bezier_segment); return path; }

This method is messy but straightforward. It simply creates the WPF objects that are needed to hold the `PolyBezierSegment`.

# MakeCurve

The following `MakeCurve` method builds a series of Bézier curves from a set of points to connect.

// Make a Bezier curve connecting these points. private Path MakeCurve(Point[] points, double tension) { if (points.Length < 2) return null; Point[] result_points = MakeCurvePoints(points, tension); // Use the points to create the path. return MakeBezierPath(result_points.ToArray()); }

The method takes as parameters an array of points to connect and a tension value. It calls `MakeCurvePoints` to make control points and then calls `MakeBezierPath` to build the Bézier curves.

# The Example

When you adjust the program’s scroll bar, the following event handler executes.

// Update the tension and rebuild the curve. private void scrTension_Scroll(object sender, ScrollEventArgs e) { // Get the tension. double tension = scrTension.Value / 10; lblTension.Content = tension.ToString("0.0"); // Rebuild the curve. DrawCurve(tension); }

This code gets the scroll bar’s value and divides it by 10 so you can select values in increments of 1/10. The scroll bar’s `Minimum` property is 0 and its `Maximum` property is 50, so after you divide by 10, you can select values between 0.0 and 5.0.

After calculating the tension value, the code calls the following method to draw the curve.

// Make the curve. private void DrawCurve(double tension) { // Remove any previous curves. canDrawing.Children.Clear(); // Make a path. Point[] points1 = { new Point(60, 30), new Point(200, 130), new Point(100, 150), new Point(200, 50), }; Path path1 = MakeCurve(points1, tension); path1.Stroke = Brushes.LightGreen; path1.StrokeThickness = 5; canDrawing.Children.Add(path1); foreach (Point point in points1) { Rectangle rect = new Rectangle(); rect.Width = 6; rect.Height = 6; Canvas.SetLeft(rect, point.X - 3); Canvas.SetTop(rect, point.Y - 3); rect.Fill = Brushes.White; rect.Stroke = Brushes.Black; rect.StrokeThickness = 1; canDrawing.Children.Add(rect); } }

This method removes any previous curves from the `canDrawing Canvas` control. It then defines some points to connect and calls `MakeCurve` to build an appropriate `Path` object holding the smooth curve. It makes the object light green and adds it to the `Canvas`.

The method then loops through the data points and creates rectangles for them so you can see them.

# Summary

By not providing an equivalent to the `DrawCurve` method, WPF makes drawing smooth curves much harder. Fortunately, the `MakeCurve`, `MakeBezierPath`, and `MakeCurvePoints` methods make building smooth curves simple again.

Download the example program to experiment with it.

Pingback: Draw a smooth closed curve in WPF and C# - C# HelperC# Helper

Pingback: Let the user draw a smooth curve with WPF and C# - C# HelperC# Helper

Thank you so much for this! I am trying to write a graphics program and this is exactly what I needed. I haven’t tried it yet, but it looks very promising. I really appreciate it!

Hopefully you are still reading this blog in case I have a follow up question or two 🙂

Cheers,

Bill Rappaport

Hello,

I am just going over this code and have a question:

In the for loop:

for (int i = 0; i lt; points.Length – 1; i++)

I am not sure what the “lt” means and can’t really find information on it.

Can you let me know what “lt” means?

Thank you!

Bill

Sorry. That’s a typo. The HTML code < means “use the less than symbol” but it looks like I missed the & when writing this.

The code should be:

I’ll fix it. Thanks for pointing it out!

Thank you!

Still studying this code so I really understand how it works. I plan to move on to the next session after I wrap my mind around this one which lets the user draw the path…

In the section “Finding Control Points” in the second paragraph after the code example, where it says: “Next, the code creates a result_list”, should it say “Next, the code creates a result_points list”? Just to clarify.

Yes. I’ll fix it. Thanks for pointing this out.

Hi Rod,

Back to studying your code after being away for a day or two.

In the fourth paragraph after the code example in the section “Finding Control Points”

it says “In that case, the code uses the data point instead to get the read dashed lines described in the preceding section for those special cases.”

I’m having a bit of a hard time understanding that language…

Could you elucidate?

That should be “red dashed lines.”