Title: Draw a smooth curve in WPF and C#
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 secondtolast 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 to experiment with it and to see additional details.
