Introduction
Recently I found myself in need of a rounded corner polygon, which I needed to build from the user interface, at runtime. WPF has a polygon shape, but it does not provide round corners. I started searching the internet for examples, but didn't find many examples about this, so I started implementing my own custom rounded corners polygon. This article describes the implementation of the polygon and the way you can use it in your own applications. You can find the full implementation in the RoundedCornersPolygon
class in the attached demo application source code.
The attached demo application has two main sections of examples:
- Rounded corners polygons built in XAML.
- Rounded corners polygon built at runtime.
Background
Polygons can be thought of as an equiangular set of points along a circle specified by a radius and a number of points/sides.
In WPF, you can build a polygon by adding a list of points to the Points
property of the Polygon
object.
XAML example:
<Canvas>
<Polygon Points="10,50 180,50 180,150 10,150"
StrokeThickness="1" Stroke="Black" />
</Canvas>
C# example:
var cnv = new Canvas();
var polygon = new Polygon {StrokeThickness = 1, Fill = Brushes.Black};
polygon.Points.Add(new Point(10, 50));
polygon.Points.Add(new Point(180, 50));
polygon.Points.Add(new Point(180, 150));
polygon.Points.Add(new Point(10, 150));
cnv.Children.Add(polygon);
this.Content = cnv;
And the output will be the following rectangle:
Using the Code
RoundedCornersPolygon
is very similar to a normal polygon, but we have a few more properties used for rounding the corners. First, let's look at an example of how we build the same rectangle, but with rounded corners.
XAML example:
<Canvas>
<CustomRoundedCornersPolygon:RoundedCornersPolygon Points="10,50 180,50 180,150 10,150"
StrokeThickness="1" Stroke="Black" ArcRoundness="25"
UseAnglePercentage="False" IsClosed="True"/>
<Canvas>
C# example:
var cnv = new Canvas();
var roundedPolygon = new RoundedCornersPolygon
{
Stroke = Brushes.Black, StrokeThickness = 1,
ArcRoundness = 25, UseAnglePercentage = false, IsClosed = true
};
roundedPolygon.Points.Add(new Point(10, 50));
roundedPolygon.Points.Add(new Point(180, 50));
roundedPolygon.Points.Add(new Point(180, 150));
roundedPolygon.Points.Add(new Point(10, 150));
cnv.Children.Add(roundedPolygon);
this.Content = cnv;
And the output will be the following rectangle with rounded corners:
The polygon has four main properties:
The ArcRoundness
property specifies at what distance, from the LineSegment
end, starts the curve. This property is used together with the UseRoundnessPercentage
property. The UseRoundnessPercentage
property specifies if the ArcRoundness
value is a percentage or a value from the connecting segments.
If, for example, ArcRoundness
is set to 10 and UseRoundnessPercentage
is set to false
, then the curve will start at a distance of 10 before the end of the first segment, and it will end at a distance of 10 after the second segment starts. If UseRoundnessPercentage
is set to true
, then the distance will be 10% from those segments.
public double ArcRoundness { get; set; }
public bool UseRoundnessPercentage { get; set; }
The IsClosed
property specifies if the polygon connects the last point with the first point; to finish the polygon, this property should be set to true
.
public bool IsClosed { get; set; }
The Points
property represents the collection of points of the polygon.
public PointCollection Points{ get; set; }
Implementation
The control implements the Shape
class. The shape used to draw the polygon is a Path
object where we add segments of type LineSegment
and QuadraticBezierSegment
. The second segment, QuadraticBezierSegment
, represents a Quadric Bézier curve, which is defined by three points. More information about Bézier curves can be found at http://en.wikipedia.org/wiki/B%C3%A9zier_curve.
For a normal polygon, only LineSegment
would be necessary, but to make the rounded corner the Bézier curve is needed. Every time a Poin
t is added or a property is modified, the shape is redrawn. The main method that does the rounding of the corners is the ConnectLinePoints
method:
private static void ConnectLinePoints(PathFigure pathFigure, Point p1,
Point p2, Point p3,
double roundness, bool usePercentage)
{
Point backPoint;
Point nextPoint;
if (usePercentage)
{
backPoint = GetPointAtDistancePercent(p1, p2, roundness, false);
nextPoint = GetPointAtDistancePercent(p2, p3, roundness, true);
}
else
{
backPoint = GetPointAtDistance(p1, p2, roundness, false);
nextPoint = GetPointAtDistance(p2, p3, roundness, true);
}
int lastSegmentIndex = pathFigure.Segments.Count - 1;
((LineSegment)(pathFigure.Segments[lastSegmentIndex])).Point = backPoint;
var curve = new QuadraticBezierSegment { Point1 = p2, Point2 = nextPoint };
pathFigure.Segments.Add(curve);
var line = new LineSegment { Point = p3 };
pathFigure.Segments.Add(line);
}
There are two methods that calculate the point where the curve should start: GetPointAtDistance
and GetPointAtDistancePercent
, one by value and the other by percent:
private static Point GetPointAtDistance(Point p1, Point p2,
double distance, bool firstPoint)
{
double totalDistance = Math.Sqrt(Math.Pow((p2.X - p1.X), 2) +
Math.Pow((p2.Y - p1.Y), 2));
double rap = firstPoint ? distance / totalDistance :
(totalDistance - distance) / totalDistance;
return new Point(p1.X + (rap * (p2.X - p1.X)), p1.Y + (rap * (p2.Y - p1.Y)));
}
private static Point GetPointAtDistancePercent(Point p1, Point p2,
double distancePercent, bool firstPoint)
{
double rap = firstPoint ? distancePercent / 100 : (100 - distancePercent) / 100;
return new Point(p1.X + (rap * (p2.X - p1.X)), p1.Y + (rap * (p2.Y - p1.Y)));
}
Conclusion
There are many other details that could be implemented. This is just a first attempt for a rounded corners polygon. For example, in other cases, a random rounding would be needed for each corner; WPF offers tools to make almost anything possible when it comes to graphical interfaces. My purpose for this article was to create a rounded corners polygon that people could use and, if needed, improve further for their use.
History
- 18th November, 2010: Initial post.