Skip to main content

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

Popular posts from this blog

C# Snippet - Shuffling a Dictionary [Beginner]

Randomizing something can be a daunting task, especially with all the algorithms out there. However, sometimes you just need to shuffle things up, in a simple, yet effective manner. Today we are going to take a quick look at an easy and simple way to randomize a dictionary, which is most likely something that you may be using in a complex application. The tricky thing about ordering dictionaries is that...well they are not ordered to begin with. Typically they are a chaotic collection of key/value pairs. There is no first element or last element, just elements. This is why it is a little tricky to randomize them. Before we get started, we need to build a quick dictionary. For this tutorial, we will be doing an extremely simple string/int dictionary, but rest assured the steps we take can be used for any kind of dictionary you can come up with, no matter what object types you use. Dictionary < String , int > origin = new Dictionary < string , int >(); ...

C# WPF Printing Part 2 - Pagination [Intermediate]

About two weeks ago, we had a tutorial here at SOTC on the basics of printing in WPF . It covered the standard stuff, like popping the print dialog, and what you needed to do to print visuals (both created in XAML and on the fly). But really, that's barely scratching the surface - any decent printing system in pretty much any application needs to be able to do a lot more than that. So today, we are going to take one more baby step forward into the world of printing - we are going to take a look at pagination. The main class that we will need to do pagination is the DocumentPaginator . I mentioned this class very briefly in the previous tutorial, but only in the context of the printing methods on PrintDialog , PrintVisual (which we focused on last time) and PrintDocument (which we will be focusing on today). This PrintDocument function takes a DocumentPaginator to print - and this is why we need to create one. Unfortunately, making a DocumentPaginator is not as easy as...

C# WPF Tutorial - Implementing IScrollInfo [Advanced]

The ScrollViewer in WPF is pretty handy (and quite flexible) - especially when compared to what you had to work with in WinForms ( ScrollableControl ). 98% of the time, I can make the ScrollViewer do what I need it to for the given situation. Those other 2 percent, though, can get kind of hairy. Fortunately, WPF provides the IScrollInfo interface - which is what we will be talking about today. So what is IScrollInfo ? Well, it is a way to take over the logic behind scrolling, while still maintaining the look and feel of the standard ScrollViewer . Now, first off, why in the world would we want to do that? To answer that question, I'm going to take a an example from a tutorial that is over a year old now - Creating a Custom Panel Control . In that tutorial, we created our own custom WPF panel (that animated!). One of the issues with that panel though (and the WPF WrapPanel in general) is that you have to disable the horizontal scrollbar if you put the panel in a ScrollV...