From f00f61c79c3e81df66aad3ee80caf1e6599723c3 Mon Sep 17 00:00:00 2001 From: Klaus Loeffelmann Date: Wed, 22 Jul 2026 15:58:54 -0700 Subject: [PATCH] Fix rounded-rectangle rendering artifacts with high-quality inset stroke Graphics.DrawRoundedRectangle and Graphics.FillRoundedRectangle now render under SmoothingMode.AntiAlias + PixelOffsetMode.HighQuality (saved and restored around the call), and DrawRoundedRectangle insets the stroke by half the pen width so the whole border sits inside the requested bounds on a consistent sub-pixel grid. This realigns the corner arcs with the straight edges and removes the reported artifacts. Public API signatures are unchanged. Fixes #14804 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 19883d86-6d5f-4d51-94c2-81d0bf3fc3ff --- .../src/System/Drawing/Graphics.cs | 57 ++++++++++++++++++- .../tests/System/Drawing/GraphicsTests.cs | 39 +++++++++++++ 2 files changed, 93 insertions(+), 3 deletions(-) diff --git a/src/System.Drawing.Common/src/System/Drawing/Graphics.cs b/src/System.Drawing.Common/src/System/Drawing/Graphics.cs index f58c6bdfc32..76a26022c6c 100644 --- a/src/System.Drawing.Common/src/System/Drawing/Graphics.cs +++ b/src/System.Drawing.Common/src/System/Drawing/Graphics.cs @@ -730,9 +730,41 @@ public void DrawRoundedRectangle(Pen pen, Rectangle rect, Size radius) => /// The radius width and height used to round the corners of the rectangle. public void DrawRoundedRectangle(Pen pen, RectangleF rect, SizeF radius) { + ArgumentNullException.ThrowIfNull(pen); + + // Inset the stroke by half the pen width so the entire border sits inside the requested + // bounds on a consistent sub-pixel grid. Combined with high-quality pixel offset and + // anti-aliasing this realigns the corner arcs with the straight edges and removes the + // artifacts a centered, default-offset stroke would otherwise produce. + RectangleF strokeRect = rect; + SizeF strokeRadius = radius; + + if (rect.Width > pen.Width && rect.Height > pen.Width) + { + float inset = pen.Width / 2f; + strokeRect = RectangleF.Inflate(rect, -inset, -inset); + strokeRadius = new( + Math.Max(0f, radius.Width - inset), + Math.Max(0f, radius.Height - inset)); + } + using GraphicsPath path = new(); - path.AddRoundedRectangle(rect, radius); - DrawPath(pen, path); + path.AddRoundedRectangle(strokeRect, strokeRadius); + + Drawing2D.SmoothingMode previousSmoothingMode = this.SmoothingMode; + PixelOffsetMode previousPixelOffsetMode = this.PixelOffsetMode; + + try + { + this.SmoothingMode = Drawing2D.SmoothingMode.AntiAlias; + this.PixelOffsetMode = PixelOffsetMode.HighQuality; + DrawPath(pen, path); + } + finally + { + this.SmoothingMode = previousSmoothingMode; + this.PixelOffsetMode = previousPixelOffsetMode; + } } #endif @@ -1204,9 +1236,28 @@ public void FillRoundedRectangle(Brush brush, Rectangle rect, Size radius) => /// The radius width and height used to round the corners of the rectangle. public void FillRoundedRectangle(Brush brush, RectangleF rect, SizeF radius) { + ArgumentNullException.ThrowIfNull(brush); + using GraphicsPath path = new(); path.AddRoundedRectangle(rect, radius); - FillPath(brush, path); + + // Anti-aliasing plus high-quality pixel offset gives the filled corners the same crisp, + // uniform edge as the straight sides instead of the softer, offset arcs the default + // rendering mode produces. + Drawing2D.SmoothingMode previousSmoothingMode = this.SmoothingMode; + PixelOffsetMode previousPixelOffsetMode = this.PixelOffsetMode; + + try + { + this.SmoothingMode = Drawing2D.SmoothingMode.AntiAlias; + this.PixelOffsetMode = PixelOffsetMode.HighQuality; + FillPath(brush, path); + } + finally + { + this.SmoothingMode = previousSmoothingMode; + this.PixelOffsetMode = previousPixelOffsetMode; + } } #endif diff --git a/src/System.Drawing.Common/tests/System/Drawing/GraphicsTests.cs b/src/System.Drawing.Common/tests/System/Drawing/GraphicsTests.cs index e921aedb521..ed39da837fc 100644 --- a/src/System.Drawing.Common/tests/System/Drawing/GraphicsTests.cs +++ b/src/System.Drawing.Common/tests/System/Drawing/GraphicsTests.cs @@ -3002,5 +3002,44 @@ public void Graphics_FillRoundedRectangle_Float() graphics.FillRoundedRectangle(Brushes.Green, new RectangleF(0, 0, 10, 10), new(2, 2)); VerifyBitmapNotEmpty(bitmap); } + + [Fact] + public void Graphics_DrawRoundedRectangle_ThickPen() + { + using Bitmap bitmap = new(20, 20); + using Graphics graphics = Graphics.FromImage(bitmap); + using Pen pen = new(Color.Blue, 3); + graphics.DrawRoundedRectangle(pen, new RectangleF(0, 0, 20, 20), new(6, 6)); + VerifyBitmapNotEmpty(bitmap); + } + + [Fact] + public void Graphics_DrawRoundedRectangle_RestoresRenderingModes() + { + using Bitmap bitmap = new(20, 20); + using Graphics graphics = Graphics.FromImage(bitmap); + graphics.SmoothingMode = SmoothingMode.None; + graphics.PixelOffsetMode = PixelOffsetMode.Half; + + using Pen pen = new(Color.Blue, 3); + graphics.DrawRoundedRectangle(pen, new RectangleF(0, 0, 20, 20), new(6, 6)); + + graphics.SmoothingMode.Should().Be(SmoothingMode.None); + graphics.PixelOffsetMode.Should().Be(PixelOffsetMode.Half); + } + + [Fact] + public void Graphics_FillRoundedRectangle_RestoresRenderingModes() + { + using Bitmap bitmap = new(20, 20); + using Graphics graphics = Graphics.FromImage(bitmap); + graphics.SmoothingMode = SmoothingMode.None; + graphics.PixelOffsetMode = PixelOffsetMode.Half; + + graphics.FillRoundedRectangle(Brushes.Green, new RectangleF(0, 0, 20, 20), new(6, 6)); + + graphics.SmoothingMode.Should().Be(SmoothingMode.None); + graphics.PixelOffsetMode.Should().Be(PixelOffsetMode.Half); + } #endif }