Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Rounded Rectangle Not Accurate

Every sample code I've ever found for drawing rounded rectangles using GDI+ goes something like this (lifted and slightly modified from BobPowell.net):

  Private Sub Panel1_Paint(ByVal sender As Object, ByVal e As PaintEventArgs) Handles Panel1.Paint
    e.Graphics.Clear(SystemColors.Window)
    e.Graphics.SmoothingMode = SmoothingMode.None

    Call DrawRoundRect(e.Graphics, Pens.Red, 10, 10, 48, 24, 6)
  End Sub

  Public Sub DrawRoundRect(ByVal g As Graphics, ByVal p As Pen, ByVal x As Single, ByVal y As Single, ByVal width As Single, ByVal height As Single, ByVal radius As Single)
    Using gp As New GraphicsPath()
      gp.StartFigure()
      gp.AddArc(x + width - radius, y, radius * 2, radius * 2, 270, 90)
      gp.AddArc(x + width - radius, y + height - radius, radius * 2, radius * 2, 0, 90)
      gp.AddArc(x, y + height - radius, radius * 2, radius * 2, 90, 90)
      gp.AddArc(x, y, radius * 2, radius * 2, 180, 90)
      gp.CloseFigure()
      g.DrawPath(p, gp)
    End Using
  End Sub

This produces a rounded rectangle where only the top left corner is accurate.

AntiAliasing has to be turned off because it is going through a remote desktop connection, and I can't depend on it being available. Besides, I am looking for a crisp rounded rectangle.

enter image description here

I've tried resizing the other corners and changing the pen alignments, but nothing seems to produce a simple, accurate rounded rectangle.

Is there a way to draw a better rounded rectangle than this in good old winforms?

like image 939
LarsTech Avatar asked May 19 '11 14:05

LarsTech


3 Answers

I have found the best solution to be just old-school Windows API:

Private Sub DrawRoundRect(ByVal g As Graphics, ByVal r As Rectangle)
  Dim hDC As IntPtr = g.GetHdc
  Dim hPen As IntPtr = CreatePen(PS_SOLID, 0, ColorTranslator.ToWin32(Color.Red))
  Dim hOldPen As IntPtr = SelectObject(hDC, hPen)
  SelectObject(hDC, GetStockObject(NULL_BRUSH))
  RoundRect(hDC, r.Left, r.Top, r.Right - 1, r.Bottom - 1, 12, 12)
  SelectObject(hDC, hOldPen)
  DeleteObject(hPen)
  g.ReleaseHdc(hDC)
End Sub

This produces the symmetrical rounded rectangle I've been looking for:

enter image description here

like image 182
LarsTech Avatar answered Nov 20 '22 13:11

LarsTech


On occasion, I've used a "low tech" approach to deal with the rounding errors in GDI+

1) Resize your source image to a binary multiple of its original size. Typically, I'll resample to a width and height 4 times greater (or 8, or 16) than the original.

2) Perform all my GDI+ drawing operations (taking into account, of course, that my co-ordinates will need to be multiplied by a factor of 4). There is no need to use any fancy anti-aliasing.

3) Re-sample the image back down to the original dimensions. Shrinking the image results in a nice smoothing effect, and minimizes any rounding errors in lines, curves, etc.

like image 30
Brad Karjama Avatar answered Nov 20 '22 14:11

Brad Karjama


Because no-one's answered you yet here is a trick I have used in the past. It works reasonably well, and definitely looks better than the classic implementation with AddArc().

It uses circles and clipping to achieve the result you want. It may show slight artefacts when using pens with a width greater than 1px, but other than that it works well.

I hope it will be good enough for your project.

    private void DrawRoundedRectangle(Graphics g, Pen pen, Rectangle rect, int radius)
    {
        g.DrawLine(pen, rect.Left + radius, rect.Top, rect.Right - radius, rect.Top);
        g.DrawLine(pen, rect.Right, rect.Top+radius, rect.Right, rect.Bottom - radius);
        g.DrawLine(pen, rect.Left + radius, rect.Bottom, rect.Right - radius, rect.Bottom);
        g.DrawLine(pen, rect.Left, rect.Top + radius, rect.Left, rect.Bottom - radius);

        g.SetClip(new Rectangle(rect.Left, rect.Top, radius, radius));
        g.DrawEllipse(pen, rect.Left, rect.Top, radius * 2, radius * 2);
        g.ResetClip();

        g.SetClip(new Rectangle(rect.Right-radius, rect.Top, radius+1, radius+1));
        g.DrawEllipse(pen, rect.Right - radius * 2, rect.Top, radius * 2, radius * 2);
        g.ResetClip();

        g.SetClip(new Rectangle(rect.Right - radius, rect.Bottom-radius, radius+1, radius+1));
        g.DrawEllipse(pen, rect.Right - radius * 2, rect.Bottom - (radius * 2), radius * 2, radius * 2);
        g.ResetClip();

        g.SetClip(new Rectangle(rect.Left, rect.Bottom - radius, radius+1, radius+1));
        g.DrawEllipse(pen, rect.Left, rect.Bottom - (radius * 2), radius * 2, radius * 2);
        g.ResetClip();
    }

The method's interface is straightforward, but post a comment if you need assistance.

Edit: Something else that should work is to draw the same arc four times, but flipped using TranslateTransform and TranslateScale. That should mean the arc appears identical in each corner.

    private void DrawRoundedRectangle(Graphics g, Pen pen, Rectangle rect, int radius)
    {
        g.DrawLine(pen, rect.Left + radius, rect.Top, rect.Right - radius, rect.Top);
        g.DrawLine(pen, rect.Right-1, rect.Top+radius, rect.Right-1, rect.Bottom - radius);
        g.DrawLine(pen, rect.Left + radius, rect.Bottom-1, rect.Right - radius, rect.Bottom-1);
        g.DrawLine(pen, rect.Left, rect.Top + radius, rect.Left, rect.Bottom - radius);

        g.TranslateTransform(rect.Left, rect.Top);
        g.DrawArc(pen, 0, 0, radius * 2, radius * 2, 180, 90);
        g.ResetTransform();

        g.TranslateTransform(rect.Right, rect.Top);
        g.ScaleTransform(-1, 1);
        g.DrawArc(pen, 1, 0, radius * 2, radius * 2, 180, 90);
        g.ResetTransform();

        g.TranslateTransform(rect.Right, rect.Bottom);
        g.ScaleTransform(-1, -1);
        g.DrawArc(pen, 1, 1, radius * 2, radius * 2, 180, 90);
        g.ResetTransform();

        g.TranslateTransform(rect.Left, rect.Bottom);
        g.ScaleTransform(1, -1);
        g.DrawArc(pen, 0, 1, radius * 2, radius * 2, 180, 90);
        g.ResetTransform();
    }

This is similar to the old Computer Graphics method of drawing a circle, where you'd draw a quarter circle four times to avoid rounding errors such as the one in GDI.

Another alternative is to draw the first arc onto an image, and then draw the image four times, flipping as required. Below is a variation on the second method, using an image to draw the arcs.

    private void DrawRoundedRectangle(Graphics g, Pen pen, Rectangle rect, int radius)
    {
        g.DrawLine(pen, rect.Left + radius, rect.Top, rect.Right - radius, rect.Top);
        g.DrawLine(pen, rect.Right - 1, rect.Top + radius, rect.Right - 1, rect.Bottom - radius);
        g.DrawLine(pen, rect.Left + radius, rect.Bottom - 1, rect.Right - radius, rect.Bottom - 1);
        g.DrawLine(pen, rect.Left, rect.Top + radius, rect.Left, rect.Bottom - radius);

        Bitmap arc = new Bitmap(radius, radius, g);
        Graphics.FromImage(arc).DrawArc(pen, 0, 0, radius * 2, radius * 2, 180, 90);

        g.TranslateTransform(rect.Left, rect.Top);
        g.DrawImage(arc, 0, 0);
        g.ResetTransform();

        g.TranslateTransform(rect.Right, rect.Top);
        g.ScaleTransform(-1, 1);
        g.DrawImage(arc, 0, 0);
        g.ResetTransform();

        g.TranslateTransform(rect.Right, rect.Bottom);
        g.ScaleTransform(-1, -1);
        g.DrawImage(arc, 0, 0);
        g.ResetTransform();

        g.TranslateTransform(rect.Left, rect.Bottom);
        g.ScaleTransform(1, -1);
        g.DrawImage(arc, 0, 0);
        g.ResetTransform();

        arc.Dispose();
    }
like image 1
Edwin Groenendaal Avatar answered Nov 20 '22 15:11

Edwin Groenendaal