4

I have written some code which creates a rounded rectangle GraphicsPath, based on a custom structure, BorderRadius (which allows me to define the top left, top right, bottom left and bottom right radius of the rectangle), and the initial Rectangle itself:

public static GraphicsPath CreateRoundRectanglePath(BorderRadius radius, Rectangle rectangle)
{
    GraphicsPath result = new GraphicsPath();

    if (radius.TopLeft > 0)
    {
        result.AddArc(rectangle.X, rectangle.Y, radius.TopLeft, radius.TopLeft, 180, 90);
    }
    else
    {
        result.AddLine(new System.Drawing.Point(rectangle.X, rectangle.Y), new System.Drawing.Point(rectangle.X, rectangle.Y));
    }
    if (radius.TopRight > 0)
    {
        result.AddArc(rectangle.X + rectangle.Width - radius.TopRight, rectangle.Y, radius.TopRight, radius.TopRight, 270, 90);
    }
    else
    {
        result.AddLine(new System.Drawing.Point(rectangle.X + rectangle.Width, rectangle.Y), new System.Drawing.Point(rectangle.X + rectangle.Width, rectangle.Y));
    }
    if (radius.BottomRight > 0)
    {
        result.AddArc(rectangle.X + rectangle.Width - radius.BottomRight, rectangle.Y + rectangle.Height - radius.BottomRight, radius.BottomRight, radius.BottomRight, 0, 90);
    }
    else
    {
        result.AddLine(new System.Drawing.Point(rectangle.X + rectangle.Width, rectangle.Y + rectangle.Height), new System.Drawing.Point(rectangle.X + rectangle.Width, rectangle.Y + rectangle.Height));
    }
    if (radius.BottomLeft > 0)
    {
        result.AddArc(rectangle.X, rectangle.Y + rectangle.Height - radius.BottomLeft, radius.BottomLeft, radius.BottomLeft, 90, 90);
    }
    else
    {
        result.AddLine(new System.Drawing.Point(rectangle.X, rectangle.Y + rectangle.Height), new System.Drawing.Point(rectangle.X, rectangle.Y + rectangle.Height));
    }

    return result;
}

Now if I use this along with FillPath and DrawPath, I notice some odd results:

GraphicsPath path = CreateRoundRectanglePath(new BorderRadius(8), new Rectangle(10, 10, 100, 100));
e.Graphics.DrawPath(new Pen(Color.Black, 1), path);
e.Graphics.FillPath(new SolidBrush(Color.Black), path);

I've zoomed into each resulting Rectangle (right hand side) so you can see clearly, the problem:

rectangles

What I would like to know is: Why are all of the arcs on the drawn rectangle equal, and all of the arcs on the filled rectangle, odd?

Better still, can it be fixed, so that the filled rectangle draws correctly?

EDIT: Is it possible to fill the inside of a GraphicsPath without using FillPath?

EDIT: As per comments....here is an example of the BorderRadius struct

public struct BorderRadius
{
    public Int32 TopLeft { get; set; }
    public Int32 TopRight { get; set; }
    public Int32 BottomLeft { get; set; }
    public Int32 BottomRight { get; set; }

    public BorderRadius(int all) : this()
    {
        this.TopLeft = this.TopRight = this.BottomLeft = this.BottomRight = all;
    }
}
Matthew Layton
  • 39,871
  • 52
  • 185
  • 313

4 Answers4

4

I experienced the same problem and found a solution. Might be too late for you @seriesOne but it can be useful to other people if they have this problem. Basically when using the fill methods (and also when setting the rounded rectangle as the clipping path with Graphics.SetClip) we have to move by one pixel the right and bottom lines. So I came up with a method that accepts a parameter to fix the rectangle is using the fill or not. Here it is:

private static GraphicsPath CreateRoundedRectangle(Rectangle b, int r, bool fill = false)
{
    var path = new GraphicsPath();
    var r2 = (int)r / 2;
    var fix = fill ? 1 : 0;

    b.Location = new Point(b.X - 1, b.Y - 1);
    if (!fill)
        b.Size = new Size(b.Width - 1, b.Height - 1);

    path.AddArc(b.Left, b.Top, r, r, 180, 90);
    path.AddLine(b.Left + r2, b.Top, b.Right - r2 - fix, b.Top);

    path.AddArc(b.Right - r - fix, b.Top, r, r, 270, 90);
    path.AddLine(b.Right, b.Top + r2, b.Right, b.Bottom - r2);

    path.AddArc(b.Right - r - fix, b.Bottom - r - fix, r, r, 0, 90);
    path.AddLine(b.Right - r2, b.Bottom, b.Left + r2, b.Bottom);

    path.AddArc(b.Left, b.Bottom - r - fix, r, r, 90, 90);
    path.AddLine(b.Left, b.Bottom - r2, b.Left, b.Top + r2);

    return path;
}

So this is how you use it:

g.DrawPath(new Pen(Color.Red), CreateRoundedRectangle(rect, 24, false));
g.FillPath(new SolidBrush(Color.Red), CreateRoundedRectangle(rect, 24, true));
gigi
  • 796
  • 9
  • 21
  • It's never too late, even though I have turned my hand to WPF, however still interested to see this working in Windows Forms :-) – Matthew Layton Feb 17 '14 at 14:03
  • This almost worked correctly, except there was still a slight artifact on the bottom right. After reading [link](https://stackoverflow.com/questions/3147569/pixel-behaviour-of-fillrectangle-and-drawrectangle), there are 3 things I did to rectify this; 1) Change the `var fix = fill ? 1 : 0;` to `var fix = fill ? 0.5 : 0;` 2) Add flaot to all reference where the fix is in place `(float)(... - fix)` and 3) when using the `g.FillPath(...)` first define the path, then use `graphics.SmoothingMode = SmoothingMode.AntiAlias;` before filling the path `graphics.FillPath(brush, path);` – Dev Ops Sep 10 '18 at 07:25
1

I'd suggest explicitly adding a line from the end of each arc to the beginning of the next one.

You could also try using the Flatten method to approximate all curves in your path with lines. That should remove any ambiguity.

The result you're getting from FillPath looks similar to an issue I had where the points on a Path were interpreted incorrectly, essentially leading to a quadratic instead of a cubic bezier spline.

You can examine the points on your path using the GetPathData function: http://msdn.microsoft.com/en-us/library/ms535534%28v=vs.85%29.aspx

Bezier curves (which GDI+ uses to approximate arcs) are represented by 4 points. The first is an end point and can be any type. The second and third are control points and have type PathPointBezier. The last is the other end point and has type PathPointBezier. In other words, when GDI+ sees PathPointBezier, it uses the path's current position and the 3 Bezier points to draw the curve. Bezier curves can be strung together but the number of bezier points should always be divisible by 3.

What you're doing is a bit strange, in that you are drawing curves in different places without explicit lines to join them. I'd guess it creates a pattern like this:

PathPointStart - end point of first arc
PathPointBezier - control point of first arc
PathPointBezier - control point of first arc
PathPointBezier - end point of first arc
PathPointLine - end point of second arc
PathPointBezier - control point of second arc
PathPointBezier - control point of second arc
PathPointBezier - end point of second arc
PathPointLine - end point of third arc
PathPointBezier - control point of third arc
PathPointBezier - control point of third arc
PathPointBezier - end point of third arc

That looks reasonable. GDI+ should be drawing a line from the last endpoint of each curve to the first endpoint of the next one. DrawPath clearly does this, but I think FillPath is interpreting the points differently. Some end points are being treated as control points and vice versa.

Esme Povirk
  • 3,004
  • 16
  • 24
  • Thanks for this, this is a very thorough explanation! Do you think I need to rewrite my entire CreateRoundRectanglePath method, or just add the lines to it? – Matthew Layton Oct 04 '13 at 08:35
  • I think it has a reasonable chance of working if you add lines, but it's only an educated guess that the point types have anything to do with your issue. I don't know what's going to happen if you add the lines, what exactly FillPath is doing with the points, or what the path really looks like. – Esme Povirk Oct 07 '13 at 23:36
  • I tried adding lines, and using bezier curves instead of arcs, and also tried Idle_Mind's answer...nothing seems to work! :-( – Matthew Layton Oct 09 '13 at 09:48
1

The real reason for the behavior is explained at Pixel behaviour of FillRectangle and DrawRectangle.

It has to do with the default pixel rounding and the fact that FillRectangle/FillPath with integer coordinates end up drawing in the middle of a pixel and get rounded (according to Graphics.PixelOffsetMode).

On the other hand, DrawRectangle/DrawPath draw with a 1px pen that gets perfectly rounded on the pixel boundaries.

Depending on your usage the solution could be to inflate/deflate the rectangle for FillRectangle/FillPath by .5px.

Filip Navara
  • 4,818
  • 1
  • 26
  • 37
0

"Is it possible to fill the inside of a GraphicsPath without using FillPath?"

Yes...but I think this is more of a parlor trick (and it might not work as you expect for more complex shapes). You can clip the graphics to the path, and then just fill the entire encompassing rectangle:

        Rectangle rc = new Rectangle(10, 10, 100, 100);
        GraphicsPath path = CreateRoundRectanglePath(new BorderRadius(8), rc);
        e.Graphics.SetClip(path);
        e.Graphics.FillRectangle(Brushes.Black, rc);
        e.Graphics.ResetClip();
Idle_Mind
  • 38,363
  • 3
  • 29
  • 40
  • Unfortunately this has the same effect as the FillPath: the rectangle is odd like in the figure – gigi Feb 17 '14 at 11:58