C# - Creating Rounded Rectangles Using A Graphics Path [Beginner]


Do you ever do UI development in C#? Do you know what a GraphicsPath is, or how to use it? No? Well, then this tutorial is for you. We will be covering how to create a graphics path and use it, by coding a way to easily create and draw rounded rectangles. When we are done, you will have a set of functions for creating rounded rectangles, and hopefully a new way to think about certain types of custom painting in C#.

First off, what is a graphics path? The short tooltip in Visual Studio is that a graphics path "represents a series of lines and curves." All well and good, but what that really means is that you can easily create and work with custom shapes. What we are going to cover today only scratches the surface of what can be done with a graphics path, but for more information you can always read the MSDN documentation.

So lets get down to it - lets create a rounded rectangle. Now, like any rectangle, we are going to need an (x,y) coordinate for the upper left corner of the rectangle, and a width and a height. But we are also going to need a value for "how rounded" the corners will be - we will call that value radius. Finally, our rounded rectangle creation function will take an argument saying which corners of the rectangle should be rounded. So the function declaration looks like this:
public static GraphicsPath Create(int x, int y, int width, int height, 
                                  int radius, RectangleCorners corners)
RectangleCorners here is a simple enum, defined like the following:
public enum RectangleCorners
{
  None = 0, TopLeft = 1, TopRight = 2, BottomLeft = 4, BottomRight = 8,
  All = TopLeft | TopRight | BottomLeft | BottomRight
}
 
This enum takes advantage of the fact that an enum can contain values representing the logical combination of other values in that enum. So, say we had an instance of this enum, and wanted to know if we should round the top left corner. We can do an easy check like the following:
RectangeCorners corners = RectangleCorners.All;

if ((RectangleCorners.TopLeft & corners) == RectangleCorners.TopLeft)
{
  //Do stuff
}
 
Since RectangleCorners.All is actually the value 15 (or in binary 1111), by and-ing it with the value for RectangeCorners.TopLeft (1, or in binary 0001), we get the value 1 out - which is equal to RectangleCorners.Left. If the variable corners held the value RectangleCorners.TopLeft, this if statement would also be true - but for any of the other enum values, the if statement would end up being false.

But you don't want to hear about enums and logic - you want to hear about rounded rectangles! So, moving on.

Lets take a look at some of the meat of this Create function:
GraphicsPath p = new GraphicsPath();
p.StartFigure();

//Top Left Corner
if ((RectangleCorners.TopLeft & corners) == RectangleCorners.TopLeft)
{
  p.AddArc(x, y, 2*radius, 2*radius, 180, 90);
}
else
{
  p.AddLine(x, y+radius, x, y);
  p.AddLine(x, y, x+radius;, y);
}

//Top Edge
p.AddLine(x+radius, y, x+width-radius, y);
 
So first, we create a GraphicsPath, and then we call StartFigure so that we can start adding edges to the path. The rest of this code is for the top left corner and the top line of the rounded rectangle. If we are supposed to make this corner rounded, we add an arc - otherwise we add two short edges (one on the left edge at the top, and one on the top edge at the left) to make up for where the arc was going to be. Then we add the top edge, which goes from the right side of the top left arc (or the right side of the little short edge we put there), to the left side of what we will do on the top right corner.

Now that we have the basic structure for a corner and an edge down, we just repeat it for the other 3 corners with slightly different numbers:
//Top Right Corner
if ((RectangleCorners.TopRight & corners) == RectangleCorners.TopRight)
{
  p.AddArc(x+w-2*radius, y, 2*radius, 2*radius, 270, 90);
}
else
{
  p.AddLine(x+width-radius, y, x+width, y);
  p.AddLine(x+width, y, x+width, y+radius);
}

//Right Edge
p.AddLine(x+width, y+radius, x+width, y+height-radius);

//Bottom Right Corner
if ((RectangleCorners.BottomRight & corners) == RectangleCorners.BottomRight)
{
  p.AddArc(x+width-2*radius, y+height-2*radius, 2*radius, 2*radius, 0, 90);
}
else
{
  p.AddLine(x+width, y+height-radius, x+width, y+height);
  p.AddLine(x+width, y+height, x+width-radius, y+height);
}

//Bottom Edge
p.AddLine(x+width-radius, y+height, x+radius, y+height);

//Bottom Left Corner
if ((RectangleCorners.BottomLeft & corners) == RectangleCorners.BottomLeft)
{
  p.AddArc(x, y+height-2*radius, 2*radius, 2*radius, 90, 90);
}
else
{
  p.AddLine(x+radius, y+height, x, y+height);
  p.AddLine(x, y+height, x, y+height-radius);
}

//Left Edge
p.AddLine(x, y+height-radius, x, y+radius);
 
And there we go, we have gone around the rectangle. Its actually not too bad, as you see. If the math for all the individual function calls doesn't make much sense to you, I would suggest you drop in some value, and actually go through it on paper. Its not too bad once you see the pattern.

Finally, we close the graphics path and return it:
p.CloseFigure();
return p;
 
Now that we have the basics for a rounded rectangle, let's optimize it a bit and actually put it in a container:
using System;
using System.Drawing;
using System.Drawing.Drawing2D;

namespace RoundedRectangles
{
  public abstract class RoundedRectangle
  {
    public enum RectangleCorners
    {
      None = 0, TopLeft = 1, TopRight = 2, BottomLeft = 4, BottomRight = 8,
      All = TopLeft | TopRight | BottomLeft | BottomRight
    }

    public static GraphicsPath Create(int x, int y, int width, int height, 
                                      int radius, RectangleCorners corners)
    {
      int xw = x + width;
      int yh = y + height;
      int xwr = xw - radius;
      int yhr = yh - radius;
      int xr = x + radius;
      int yr = y + radius;
      int r2 = radius * 2;
      int xwr2 = xw - r2;
      int yhr2 = yh - r2;

      GraphicsPath p = new GraphicsPath();
      p.StartFigure();

      //Top Left Corner
      if ((RectangleCorners.TopLeft & corners) == RectangleCorners.TopLeft)
      {
        p.AddArc(x, y, r2, r2, 180, 90);
      }
      else
      {
        p.AddLine(x, yr, x, y);
        p.AddLine(x, y, xr, y);
      }

      //Top Edge
      p.AddLine(xr, y, xwr, y);

      //Top Right Corner
      if ((RectangleCorners.TopRight & corners) == RectangleCorners.TopRight)
      {
        p.AddArc(xwr2, y, r2, r2, 270, 90);
      }
      else
      {
        p.AddLine(xwr, y, xw, y);
        p.AddLine(xw, y, xw, yr);
      }

      //Right Edge
      p.AddLine(xw, yr, xw, yhr);

      //Bottom Right Corner
      if ((RectangleCorners.BottomRight & corners) == RectangleCorners.BottomRight)
      {
        p.AddArc(xwr2, yhr2, r2, r2, 0, 90);
      }
      else
      {
        p.AddLine(xw, yhr, xw, yh);
        p.AddLine(xw, yh, xwr, yh);
      }

      //Bottom Edge
      p.AddLine(xwr, yh, xr, yh);

      //Bottom Left Corner
      if ((RectangleCorners.BottomLeft & corners) == RectangleCorners.BottomLeft)
      {
        p.AddArc(x, yhr2, r2, r2, 90, 90);
      }
      else
      {
        p.AddLine(xr, yh, x, yh);
        p.AddLine(x, yh, x, yhr);
      }

      //Left Edge
      p.AddLine(x, yhr, x, yr);

      p.CloseFigure();
      return p;
    }

    public static GraphicsPath Create(Rectangle rect, int radius, RectangleCorners c)
    { return Create(rect.X, rect.Y, rect.Width, rect.Height, radius, c); }

    public static GraphicsPath Create(int x, int y, int width, int height, int radius)
    { return Create(x, y, width, height, radius, RectangleCorners.All); }

    public static GraphicsPath Create(Rectangle rect, int radius)
    { return Create(rect.X, rect.Y, rect.Width, rect.Height, radius); }

    public static GraphicsPath Create(int x, int y, int width, int height)
    { return Create(x, y, width, height, 5); }

    public static GraphicsPath Create(Rectangle rect)
    { return Create(rect.X, rect.Y, rect.Width, rect.Height); }
  }
}
 
Instead of doing all the math over and over in each function call, now we do all the math up front, which saves us a couple repeated calculations. We also now have a couple wrapper functions so that we don't have to pass in all the arguments - in fact, there is a very useful wrapper that just takes a rectangle and returns a graphics path for that rectangle with all the corners rounded with a radius of 5 (generally a good radius value).

But OK, now that we have all of this rounded rectangle code, what can we do with it? Well, there are a number of things that you can do with a graphics path. You can draw it, fill it, or even use it as a clipping region (my personal favorite). The code below shows all of these possible uses:
protected override void OnPaint(PaintEventArgs e)
{
  base.OnPaint(e);

  GraphicsPath path = RoundedRectangle.Create(5, 5, 20, 20);
  e.Graphics.DrawPath(Pens.Black, path);

  path = RoundedRectangle.Create(30, 5, 40, 40, 5);
  e.Graphics.FillPath(Brushes.Blue, path);

  path = RoundedRectangle.Create(8, 50, 50, 50, 5);
  e.Graphics.DrawPath(Pens.Black, path);

  e.Graphics.SetClip(path);
  using (Font f = new Font("Tahoma", 12, FontStyle.Bold))
    e.Graphics.DrawString("Draw Me!!", f, Brushes.Red, 0, 70);
  e.Graphics.ResetClip();

}
 
In the first call, we just draw the outline of a rounded rectangle in black, and in the second we fill a rounded rectangle with blue. In the third example, we draw the rounded rectangle in black and then set it as the clipping region on the graphics object. What this means is that only stuff drawn inside of the clipping region will get drawn to the screen. We draw some text, and then reset the clipping region. This code creates a form that looks like the following:

Rounded Rectangle
Example

And that is it for this introduction to graphics paths and how to create rounded rectangles. You are welcome to take and use the rounded rectangle class shown here.

Comments