Title: Make an intuitive extension method to draw an elliptical arc in WPF and C#
In my previous post Draw an elliptical arc in WPF and XAML, I explained how WPF makes you define an elliptical arc. Their method almost makes sense if you want draw an arc as part of a path. however, if you just want to draw a simple elliptical arc, for example to make a pie chart, that method is pretty much useless.
In this post, I'll explain how you can convert a more natural representation of an elliptical arc into the style that WPF requires.
The Windows Forms Graphics.DrawArc method takes as parameters a rectangle or a size and position that defines a rectangle. It uses those parameters to define the ellipse. The method also takes parameters that give the arc's start and end angles. The picture at the right shows the geometry used by that method. Here θ_{1} and θ_{2} are the start and end angles.
This method of defining an elliptical arc isn't perfect. For example, it would be nice if the method told you where the arc's end points are in case you need to do something with them. But it's more intuitive than WPF's approach.
Finding Start and End Points
If you want to use this specification in WPF, you need to know the defining rectangle's dimensions and where the start and end points are. We already know the rectangle's dimensions, so we just need to find the end points. To do that, we can use the equations described in my post Calculate where a line segment and an ellipse intersect in C#.
The following method calculates the points where a line segment intersects an ellipse.
// Find the points of intersection between
// an ellipse and a line segment.
private static Point[] FindEllipseSegmentIntersections(
Rect rect, Point pt1, Point pt2, bool segment_only)
{
// If the ellipse or line segment are empty, return no intersections.
if ((rect.Width == 0)  (rect.Height == 0) 
((pt1.X == pt2.X) && (pt1.Y == pt2.Y)))
return new Point[] { };
// Make sure the rectangle has nonnegative width and height.
if (rect.Width < 0)
{
rect.X = rect.Right;
rect.Width = rect.Width;
}
if (rect.Height < 0)
{
rect.Y = rect.Bottom;
rect.Height = rect.Height;
}
// Translate so the ellipse is centered at the origin.
double cx = rect.Left + rect.Width / 2f;
double cy = rect.Top + rect.Height / 2f;
rect.X = cx;
rect.Y = cy;
pt1.X = cx;
pt1.Y = cy;
pt2.X = cx;
pt2.Y = cy;
// Get the semimajor and semiminor axes.
double a = rect.Width / 2;
double b = rect.Height / 2;
// Calculate the quadratic parameters.
double A = (pt2.X  pt1.X) * (pt2.X  pt1.X) / a / a +
(pt2.Y  pt1.Y) * (pt2.Y  pt1.Y) / b / b;
double B = 2 * pt1.X * (pt2.X  pt1.X) / a / a +
2 * pt1.Y * (pt2.Y  pt1.Y) / b / b;
double C = pt1.X * pt1.X / a / a + pt1.Y * pt1.Y / b / b  1;
// Make a list of t values.
List<double> t_values = new List<double>();
// Calculate the discriminant.
double discriminant = B * B  4 * A * C;
if (discriminant == 0)
{
// One real solution.
t_values.Add(B / 2 / A);
}
else if (discriminant > 0)
{
// Two real solutions.
t_values.Add((double)((B + Math.Sqrt(discriminant)) / 2 / A));
t_values.Add((double)((B  Math.Sqrt(discriminant)) / 2 / A));
}
// Convert the t values into points.
List<Point> points = new List<Point>();
foreach (double t in t_values)
{
// If the points are on the segment (or we
// don't care if they are), add them to the list.
if (!segment_only  ((t >= 0f) && (t <= 1f)))
{
double x = pt1.X + (pt2.X  pt1.X) * t + cx;
double y = pt1.Y + (pt2.Y  pt1.Y) * t + cy;
points.Add(new Point(x, y));
}
}
// Return the points.
return points.ToArray();
}
See the earlier post for an explanation of how this method works. For this post, we really just need to know how to use it.
The rect parameter gives the rectangle that defines the ellipse. The pt1 and pt2 parameters are the line segment's end points.
The segment_only parameter indicates whether the method should only return points of intersection that lie on the line segment, or whether it should also return points that lie in the segment's extension. This example will use a segment that starts at the ellipse's center and extends beyond the ellipse's edge. We'll set the segment_only parameter to true so the method only returns the point of intersection and not the intersection that you get if you extend the segment in the opposite direction.
The following method uses the FindEllipseSegmentIntersections method to find the elliptical arc's start and end points.
// Find the points of on an ellipse
// at the indicated angles from is center.
private static Point[] FindEllipsePoints(
Rect rect, double angle1, double angle2)
{
// Find the ellipse's center.
Point center = new Point(
rect.X + rect.Width / 2.0,
rect.Y + rect.Height / 2.0);
// Find segments from the center in the
// desired directions and long enough to
// cut the ellipse.
double dist = rect.Width + rect.Height;
Point pt1 = new Point(
center.X + dist * Math.Cos(angle1),
center.Y + dist * Math.Sin(angle1));
Point pt2 = new Point(
center.X + dist * Math.Cos(angle2),
center.Y + dist * Math.Sin(angle2));
// Find the points of intersection.
Point[] intersections1 =
FindEllipseSegmentIntersections(
rect, center, pt1, true);
Point[] intersections2 =
FindEllipseSegmentIntersections(
rect, center, pt2, true);
return new Point[]
{
intersections1[0],
intersections2[0]
};
}
This method finds the ellipse's center. It then uses the angles' sines and cosines to find a point along the lines starting at the center and pointing in the directions of the start and end angles. The segment's length is the ellipse's width plus its height, so the segment is long enough to intersect with the ellipse.
The method then calls FindEllipseSegmentIntersections to see where the segments intersect the ellipse. The method returns the two points of intersection in an array.
Drawing the Elliptical Arc
The following extension method uses the WPF style specification to draw an elliptical arc.
// Add an Arc to a Canvas.
public static Path DrawArc(this Canvas canvas,
Brush fill, Brush stroke, double stroke_thickness,
Point start_point, Point end_point, Size size,
double rotation_angle, bool is_large_arc,
SweepDirection sweep_direction, bool is_stroked)
{
// Create a Path to hold the geometry.
Path path = new Path();
canvas.Children.Add(path);
path.Fill = fill;
path.Stroke = stroke;
path.StrokeThickness = stroke_thickness;
// 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 = start_point;
// Create a PathSegmentCollection.
PathSegmentCollection path_segment_collection =
new PathSegmentCollection();
path_figure.Segments = path_segment_collection;
// Create the ArcSegment.
ArcSegment arc_segment = new ArcSegment(
end_point, size, rotation_angle,
is_large_arc, sweep_direction, is_stroked);
path_segment_collection.Add(arc_segment);
return path;
}
This method creates a Path and sets its drawing properties. It then makes a PathGeometry object and adds it to the Path object's Figures collection. It sets the PathFigure object's StartPoint property to the arc's starting point.
Next, the method sets the PathGeometry object's Segments property equal to a new PathSegmentCollection. Finally, the code creates a new ArcSegment and adds it to the PathSegmentCollection.
The following code shows an extension method that uses the new Graphics.DrawArc style for specifying the elliptical arc.
// Draw an elliptical arc. Return the end points.
public static Path DrawArc(this Canvas canvas,
Brush fill, Brush stroke, double stroke_thickness,
Rect rect, double angle1, double angle2,
bool is_large_arc, SweepDirection sweep_direction,
out Point point1, out Point point2)
{
Point[] points = FindEllipsePoints(
rect, angle1, angle2);
point1 = points[0];
point2 = points[1];
Size size = new Size(rect.Width / 2, rect.Height / 2);
return canvas.DrawArc(
fill, stroke, stroke_thickness,
points[0], points[1], size, 0, is_large_arc,
sweep_direction, true);
}
This version takes as parameters a rectangle that defines the ellipse's size and position, and the arc's start and end angles. It passes those values to the FindEllipsePoints method. It then passes the returned start and end points into the previous DrawArc extension method to draw the arc.
You still need to figure out whether the arc should be large and whether it should run clockwise or counterclockwise, but that's a lot easier than calculating where the arc's endpoints should be.
Summary
There are some special cases where WPF's method for specifying an elliptical arc is almost manageable. For example, if you want to connect a vertical line segment with a horizontal segment as shown in the picture on the right, then you can probably figure out how to tell WPF what arc to draw. If you just want to draw a simple arc, however, the second DrawArc extension described here will probably be a lot easier.
In my next post, I'll show how you can modify the DrawArc extension method to draw pie slices.
Download the example to experiment with it and to see additional details.
