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:
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
Post a Comment