C# - Creating Rounded Rectangles Using A Graphics Path

Skill

C# - Creating Rounded Rectangles Using A Graphics Path

Posted in:

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, and of course, if you have any questions, please leave them in the comments.

Karl
03/16/2008 - 01:34

Great tool,
very nicely explained, thanks

reply

Paul
03/23/2008 - 07:45

Excellent, shows what you can do if you look deeper.

Never used clip regions before either...

Thanks!

reply

Youtube
04/13/2008 - 13:20

that is useful
thak you very much

reply

Mintautas
04/15/2008 - 07:48

I'd like to suggest an alternative way to do the math part. I think such code would be more readable.

public static GraphicsPath Create(Rectangle r, int radius,
                                  RectangleCorners corners)
{
    Rectangle tlc = new Rectangle(r.Left, r.Top, 2 * radius,
                                  2 * radius);
    Rectangle trc = tlc;
    trc.X = r.Right - 2 * radius;
    Rectangle blc = tlc;
    blc.Y = r.Bottom - 2 * radius;
    Rectangle brc = blc;
    brc.X = r.Right - 2 * radius;

    Point[] n = new Point[] {
        new Point(tlc.Left, tlc.Bottom), tlc.Location,
        new Point(tlc.Right, tlc.Top), trc.Location,
        new Point(trc.Right, trc.Top),
        new Point(trc.Right, trc.Bottom),
        new Point(brc.Right, brc.Top),
        new Point(brc.Right, brc.Bottom),
        new Point(brc.Left, brc.Bottom),
        new Point(blc.Right, blc.Bottom),
        new Point(blc.Left, blc.Bottom), blc.Location
    };

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


    //Top Left Corner
    if ((RectangleCorners.TopLeft && corners)
        == RectangleCorners.TopLeft)
    {
        p.AddArc(tlc, 180, 90);
    }
    else
    {
        p.AddLines(new Point[] { n[0], n[1], n[2] });
    }

    //Top Edge
    p.AddLine(n[2], n[3]);

    //Top Right Corner
    if ((RectangleCorners.TopRight && corners)
        == RectangleCorners.TopRight)
    {
        p.AddArc(trc, 270, 90);
    }
    else
    {
        p.AddLines(new Point[] { n[3], n[4], n[5] });
    }

    //Right Edge
    p.AddLine(n[5], n[6]);

    //Bottom Right Corner
    if ((RectangleCorners.BottomRight && corners)
        == RectangleCorners.BottomRight)
    {
        p.AddArc(brc, 0, 90);
    }
    else
    {
        p.AddLines(new Point[] { n[6], n[7], n[8] });
    }

    //Bottom Edge
    p.AddLine(n[8], n[9]);

    //Bottom Left Corner
    if ((RectangleCorners.BottomLeft && corners)
        == RectangleCorners.BottomLeft)
    {
        p.AddArc(blc, 90, 90);
    }
    else
    {
        p.AddLines(new Point[] { n[9], n[10], n[11] });
    }

    //Left Edge
    p.AddLine(n[11], n[0]);

    p.CloseFigure();
    return p;
}

reply

Mintautas
04/16/2008 - 07:56

Oh, by the way it would be wise to make sure that radius is not insane. So, the first rectangle we could construct in such way

Rectangle tlc = new Rectangle(r.Left, r.Top,
    Math.Min(2 * radius, r.Width),
    Math.Min(2 * radius, r.Height));

reply

Sotn0r
04/17/2008 - 08:03

great code, thanks a lot

reply

Amit
04/30/2008 - 03:34

this is very nice code which i am looking at for long time.

i also want to make rounded group with upper box name.

thanks dear

reply

Youtube
05/25/2008 - 09:48

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);

reply

Sean
11/20/2008 - 16:07

Exactly what I needed. Cheers.

reply

Luke Franklin
01/05/2009 - 18:22

I wrote a similar code myself before viewing this article, but I have a problem with filling the rounded rectangle path, or any other graphicspath for that matter, including square ones.
When I use DrawPath the entire shape’s outline is drawn exactly the way it should be, but if I use FillPath one pixel from the bottom and right sides are not drawn? This leads to a deformed shape when accuracy is very important.
It’s the same story for SetClip, or New Region(GraphicsPath).
I zoomed in on the sample image above and noticed that the blue rectangle drawn using FillPath is deformed as well on the right and bottom edges, most noticeably on the corners.
I’m writing an advanced toolstrip renderer and accuracy is very very important, I have been searching frantically for a solution. Any ideas?

reply

Luis Dragotto
04/03/2010 - 15:23

Luke, I had the same problem as your - the borders on the sample are incorrect, it's so evident even without magnifing - I can't understand why we were the only ones to see this imperfection. I was wondering why the line-only border was perfect and the filled one was not... and I found the problem and the solution: when you call the FillPath method, as others said, both width and height are incremented exactly by one pixel.

So if you put a label, for istance, of 100x30 it becomes 101x31 after the FillPath. The fault is not on the graphicpath construction as I was thinking, since until you don't fill it, it's ok. The point it's that, since there's a change in size and you are drawing it INSIDE the control itself, the effect is a drastic "CUT" of the right and bottom margin, because your button is 100x30 but the rounded image drawn is 101x31.

Togheter with this, there's also another aspect related to the default Smoothing mode.

So the final solution is:
1) Oh the Label_Paint add as the first line:
e.Graphics.SmoothingMode = SmoothingMode.HighQuality; this will produce a really nice antialias and correct calculation of the curve.

2) On the GetRoundRectPath function add the following to the first two lines:
width = width - 1;
height = height - 1;
this will fix the BUG of the FillPath adding 1 pixel.

Funny how .net can make your life easier with harder things and a total pain for simpliest ones... I really hope this will help someone else too ;-).

Cheers.

Luis Dragotto.

reply

DiamondDrake
09/15/2009 - 16:21

Luke
the problem you are having is that when a rectangle is drawn, the first pixel is always on the left, and the last pixel is always cut off, so when drawing something like this, you need to create 2 rounded rectangles, one to be drawn slightly smaller, this gives you the look you want, and one to be just slightly bigger, this one is for the region, so not to cut off part of your drawn rectangle, I know its a pain, but that's the work around.

reply

Anonymous
12/20/2009 - 06:01

Or use DLLImports to Import GDI32.DLL and use the built in funtion CreateRoundRectRgn which works BEAUTIFULLY.. Dont for get to also import the DeleteObject function as well to "dispose" objects.

        [System.Runtime.InteropServices.DllImport("Gdi32.dll", EntryPoint = "CreateRoundRectRgn")]
        private static extern System.IntPtr CreateRoundRectRgn
        (
            int nLeftRect, // x-coordinate of upper-left corner
            int nTopRect, // y-coordinate of upper-left corner
            int nRightRect, // x-coordinate of lower-right corner
            int nBottomRect, // y-coordinate of lower-right corner
            int nWidthEllipse, // height of ellipse
            int nHeightEllipse // width of ellipse
        );
        [System.Runtime.InteropServices.DllImport("gdi32.dll", EntryPoint = "DeleteObject")]
        private static extern bool DeleteObject(System.IntPtr hObject);

reply

Anonymous
12/20/2009 - 06:08

Here is an easy example.

            System.IntPtr ptrBorder = CreateRoundRectRgn(0, 0, this.Width, this.Height, _BorderRadius, _BorderRadius);

            try
            {
                this.Region = System.Drawing.Region.FromHrgn(ptrBorder);
            }
            finally
            {
                DeleteObject(ptrBorder);
            }

[csharp]

Make sure you use DeleteObject or you WILL have memory leakage.

reply

Anonymous
02/01/2010 - 16:22

Well, it does not work very well.
With more than 161 of height, it doesnt draw one line.
With more than 208 of width, it also doesnt draw one line.

reply

Jedai
03/01/2010 - 13:05

Thanks!

reply

Dheeraj
05/14/2010 - 09:07

Very Nice code, I took above code and put in custom panel to draw round corner, It is getting fine, But what problem is when i resize panel will flicker. Can i get the code...?

reply

Zorik
07/04/2010 - 00:16

use a double buffer in constructor of your panel

this.DoubleBuffer = true;

Cheers

reply

Hooman
08/29/2010 - 11:33

Great Article
Thanks

*****************************************************
Hooman Mohajeri Moghaddam
Department of Mathematical Sciences, Computer Science Division
Sharif University of Techonlogy
http://cs.sharif.edu/~mohajeri
*****************************************************

reply

Add Comment

Put code snippets inside language tags:
[language] [/language]

Examples:
[javascript] [/javascript]
[actionscript] [/actionscript]
[csharp] [/csharp]

See here for supported languages.

Javascript must be enabled to submit anonymous comments - or you can login.

Sponsors