Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

A class for creating round rectangles in GDI+ with pixel accurate symmetry

0.00/5 (No votes)
24 Jun 2008 1  
This class overcomes the asymmetry issue associated with round rectangles created in GDI+.

RoundRect.png

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)
{
    // diameter can't exceed width or height
    if(dia > r.Width)    dia = r.Width;
    if(dia > r.Height)    dia = r.Height;

    // define a corner 
    Rect Corner(r.X, r.Y, dia, dia);

    // begin path
    pPath->Reset();

    // top left
    pPath->AddArc(Corner, 180, 90);    

    // tweak needed for radius of 10 (dia of 20)
    if(dia == 20)
    {
        Corner.Width += 1; 
        Corner.Height += 1; 
        r.Width -=1; r.Height -= 1;
    }

    // top right
    Corner.X += (r.Width - dia - 1);
    pPath->AddArc(Corner, 270, 90);    
    
    // bottom right
    Corner.Y += (r.Height - dia - 1);
    pPath->AddArc(Corner,   0, 90);    
    
    // bottom left
    Corner.X -= (r.Width - dia - 1);
    pPath->AddArc(Corner,  90, 90);

    // end path
    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;

    // set to pixel mode
    int oldPageUnit = pGraphics->SetPageUnit(UnitPixel);

    // define the pen
    Pen pen(color, 1);    
    pen.SetAlignment(PenAlignmentCenter);

    // get the corner path
    GraphicsPath path;

    // get path
    GetRoundRectPath(&path, r, dia);

    // draw the round rect
    pGraphics->DrawPath(&pen, &path);

    // if width > 1
    for(int i=1; i<width; i++)
    {
        // left stroke
        r.Inflate(-1, 0);
        // get the path
        GetRoundRectPath(&path, r, dia);
            
        // draw the round rect
        pGraphics->DrawPath(&pen, &path);

        // up stroke
        r.Inflate(0, -1);

        // get the path
        GetRoundRectPath(&path, r, dia);
            
        // draw the round rect
        pGraphics->DrawPath(&pen, &path);
    }

    // restore page unit
    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;

    // set to pixel mode
    int oldPageUnit = pGraphics->SetPageUnit(UnitPixel);

    // define the pen
    Pen pen(border, 1);    
    pen.SetAlignment(PenAlignmentCenter);

    // get the corner path
    GraphicsPath path;

    // get path
    GetRoundRectPath(&path, r, dia);

    // fill
    pGraphics->FillPath(pBrush, &path);

    // draw the border last so it will be on top
    pGraphics->DrawPath(&pen, &path);

    // restore page unit
    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

Demo.PNG

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 …

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here