Introduction
If you have ever created a round rectangle in GDI+, you may have noticed that it is often not completely symmetric at the pixel level. The asymmetry occurs when one of the following is true: (radius = 10) or (pen width > 1) or (FillPath
is used). This can be seen in the above image, which has been magnified with a 4x zoom. The asymmetry doesn’t seem to occur for large radii with values greater than about 15. It may be because there are more pixels to work with, or it is just not noticeable.
Background
Since rounded rectangles are not a native shape to GDI+, they are typically created using a GraphicsPath
or some other mechanism. It doesn’t matter if you use AddArc
, AddBezeir
, Polygon points, Transformations etc., the asymmetry is there if any of the previously stated conditions are true. The reason none of these methods produce accurate results is because the problem is not in the definition of the points. All the methods above will accurately define the points. The problem occurs when the shape is rendered (drawn or filled). I won’t speculate on what the underlying cause of this asymmetry is.
CRoundRect
The CRoundRect
class is implemented entirely in the header file, and provides these basic functions: GetRoundRectPath()
, DrawRoundRect()
, and FillRoundRect()
. Three workarounds were needed to get the symmetric results.
GetRoundRectPath
This function uses the AddArc
method for defining the rounded rectangle path. The first workaround handles the special case where the radius is 10. It offsets the arc's rectangle and increases its size at a strategic point. I don’t have a good theory for why this works or why it is only needed for a radius of 10.
void GetRoundRectPath(GraphicsPath *pPath, Rect r, int dia)
{
if(dia > r.Width) dia = r.Width;
if(dia > r.Height) dia = r.Height;
Rect Corner(r.X, r.Y, dia, dia);
pPath->Reset();
pPath->AddArc(Corner, 180, 90);
if(dia == 20)
{
Corner.Width += 1;
Corner.Height += 1;
r.Width -=1; r.Height -= 1;
}
Corner.X += (r.Width - dia - 1);
pPath->AddArc(Corner, 270, 90);
Corner.Y += (r.Height - dia - 1);
pPath->AddArc(Corner, 0, 90);
Corner.X -= (r.Width - dia - 1);
pPath->AddArc(Corner, 90, 90);
pPath->CloseFigure();
}
DrawRoundRect
This function draws a rounded rectangle using the passed rectangle, radius, pen color, and pen width. The second workaround involves using a pen width of 1 and drawing “width” number of rectangles, decrementing the size of the rect each time. That alone is insufficient, because it will leave holes at the corners. Instead, this deflates only the x, draws the rect, then deflates the y, and draws again.
void DrawRoundRect(Graphics* pGraphics, Rect r, Color color, int radius, int width)
{
int dia = 2*radius;
int oldPageUnit = pGraphics->SetPageUnit(UnitPixel);
Pen pen(color, 1);
pen.SetAlignment(PenAlignmentCenter);
GraphicsPath path;
GetRoundRectPath(&path, r, dia);
pGraphics->DrawPath(&pen, &path);
for(int i=1; i<width; i++)
{
r.Inflate(-1, 0);
GetRoundRectPath(&path, r, dia);
pGraphics->DrawPath(&pen, &path);
r.Inflate(0, -1);
GetRoundRectPath(&path, r, dia);
pGraphics->DrawPath(&pen, &path);
}
pGraphics->SetPageUnit((Unit)oldPageUnit);
}
FillRoundRect
This function fills a rounded rectangle using the passed rectangle, radius, and brush color. The third workaround involves filling the rect, then drawing the border to fix the edges.
void FillRoundRect(Graphics* pGraphics, Brush* pBrush, Rect r, Color border, int radius)
{
int dia = 2*radius;
int oldPageUnit = pGraphics->SetPageUnit(UnitPixel);
Pen pen(border, 1);
pen.SetAlignment(PenAlignmentCenter);
GraphicsPath path;
GetRoundRectPath(&path, r, dia);
pGraphics->FillPath(pBrush, &path);
pGraphics->DrawPath(&pen, &path);
pGraphics->SetPageUnit((Unit)oldPageUnit);
}
FillRoundRect – Alternate
There is an alternate version of this function that takes a Brush
as one of its arguments. This is necessary if you want to fill with something other than a SolidBrush
. The color argument is needed so the function knows what color to make the border. This function can also be used to do a border and fill in a single call, assuming you wanted a border width of one.
void FillRoundRect(Graphics* pGraphics, Rect r, Color color, int radius)
{
SolidBrush sbr(color);
FillRoundRect(pGraphics, &sbr, r, color, radius);
}
The Demo Program
The demo program is compiled with VC7, but this code should work with any compiler and OS that supports GDI+. The demo program also demonstrates a technique for creating concentric borders that don’t have holes in the corners.
Additional Comments
This class does not set any of the modes like SmoothingModeAntiAlias
. It will use the currently defined state. It will set the PageUnit
to UnitPixel
, but it will restore it on completion. It took a lot of work to get to this solution, because I view this as a bit of a hack, and I tried everything else first. Unfortunately, I had to make this work for a very cool class that I’m working on now. Stay tuned …