Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

WPF rounded corners polygon

0.00/5 (No votes)
18 Nov 2010 1  
A custom WPF polygon that supports round corners.

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:

  1. Rounded corners polygons built in XAML.
  2. Examples_polygon1.JPG

  3. Rounded corners polygon built at runtime.
  4. Runtime_polygon1.JPG

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.

PolygonStar.gif

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:

Rectangle_normal.JPG

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:

Rectangle_rounded.JPG

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.

/// <summary> 
/// Gets or sets a value that specifies the arc roundness.
/// </summary>
public double ArcRoundness { get; set; }

/// <summary>  
/// Gets or sets a value that specifies if the ArcRoundness property
/// value will be used as a percentage of the connecting segment or not.
/// </summary>
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.

/// <summary> 
/// Gets or sets a value that specifies if the polygon will be closed or not.
/// </summary> 
public bool IsClosed { get; set; }

The Points property represents the collection of points of the polygon.

/// <summary> 
/// Gets or sets a collection that contains the points of the polygon.
/// </summary>
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 Point 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:

/// <summary>
/// Method used to connect 2 segments with a common point,
/// defined by 3 points and aplying a rounding between them
/// </summary>
/// <param name="pathFigure"></param>
/// <param name="p1">First point, of the first segment</param>
/// <param name="p2">Second point, the common point</param>
/// <param name="p3">Third point, the second point of the second segment</param>
/// <param name="roundness">The roundness of the arc</param>
/// <param name="usePercentage">A value that indicates
/// if the roundness of the arc will be used as a percentage or not</param>
private static void ConnectLinePoints(PathFigure pathFigure, Point p1, 
                                      Point p2, Point p3,
                                      double roundness, bool usePercentage)
{
      //The point on the first segment where the curve will start.
      Point backPoint;
      //The point on the second segment where the curve will end.
      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;
      //Set the ending point of the first segment.
      ((LineSegment)(pathFigure.Segments[lastSegmentIndex])).Point = backPoint;

      //Create and add the curve.
      var curve = new QuadraticBezierSegment { Point1 = p2, Point2 = nextPoint };
      pathFigure.Segments.Add(curve);

      //Create and add the new segment.
      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:

/// <summary>
/// Gets a point on a segment, defined by two points, at a given distance.
/// </summary>
/// <param name="p1">First point of the segment</param>
/// <param name="p2">Second point of the segment</param>
/// <param name="distance">Distance  to the point</param>
/// <param name="firstPoint">A value that indicates
///    if the distance is calculated by the first or the second point</param>
/// <returns>The point calculated.</returns>
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.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here