From 7e1dd572f8a69694ec27757fcbfa31a6fd5445e3 Mon Sep 17 00:00:00 2001 From: "Leaf Shi (BEYONDSOFT CONSULTING INC)" Date: Thu, 27 Aug 2026 02:21:27 -0700 Subject: [PATCH 1/3] Fix ToggleSwitch focus border rendering for CheckBox and RadioButton --- .../CheckBox/AnimatedToggleSwitchRenderer.cs | 82 +++++++++++++++---- .../System/Windows/Forms/CheckBoxTests.cs | 68 ++++++++++++++- 2 files changed, 129 insertions(+), 21 deletions(-) diff --git a/src/System.Windows.Forms/System/Windows/Forms/Rendering/CheckBox/AnimatedToggleSwitchRenderer.cs b/src/System.Windows.Forms/System/Windows/Forms/Rendering/CheckBox/AnimatedToggleSwitchRenderer.cs index e9f3aecae36..4cd4490de17 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Rendering/CheckBox/AnimatedToggleSwitchRenderer.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Rendering/CheckBox/AnimatedToggleSwitchRenderer.cs @@ -1,4 +1,4 @@ -// Licensed to the .NET Foundation under one or more agreements. +// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using System.Drawing; @@ -78,7 +78,7 @@ public override void RenderControl(Graphics graphics) ToggleSwitchMetrics metrics = ToggleSwitchMetrics.Create(Control); Size textSize = TextRenderer.MeasureText(Control.Text, Control.Font); - Rectangle contentBounds = ToggleSwitchMetrics.GetContentBounds(Control); + Rectangle contentBounds = ToggleSwitchMetrics.GetContentBounds(Control, metrics); int totalHeight = Math.Max(textSize.Height, metrics.SwitchHeight); int contentTop = contentBounds.Top + Math.Max(0, (contentBounds.Height - totalHeight) / 2); int textY = contentTop + ((totalHeight - textSize.Height) / 2); @@ -109,12 +109,36 @@ public override void RenderControl(Graphics graphics) if (Control.Focused && ShowFocusCues) { - Rectangle focusBounds = Rectangle.Inflate(Control.ClientRectangle, -1, -1); - ControlPaint.DrawFocusRectangle( - graphics, - focusBounds, - Control.ForeColor, - Control.BackColor); + RenderFocusBorder(graphics, metrics); + } + } + + private void RenderFocusBorder(Graphics graphics, ToggleSwitchMetrics metrics) + { + Rectangle focusBounds = ToggleSwitchMetrics.GetFocusBounds(Control.ClientRectangle, metrics); + if (focusBounds.Width <= 0 || focusBounds.Height <= 0) + { + return; + } + + Color focusColor = TextBoxBase.GetVisualStylesFocusColor( + Application.SystemVisualSettings.HighContrastEnabled); + using var focusPen = focusColor.GetCachedPenScope(metrics.FocusBorderThickness); + using GraphicsPath focusPath = new(); + + focusPath.AddRoundedRectangle( + focusBounds, + new Size(focusBounds.Height, focusBounds.Height)); + + SmoothingMode previousSmoothingMode = graphics.SmoothingMode; + try + { + graphics.SmoothingMode = SmoothingMode.AntiAlias; + graphics.DrawPath(focusPen, focusPath); + } + finally + { + graphics.SmoothingMode = previousSmoothingMode; } } @@ -161,7 +185,7 @@ private static Rectangle GetSwitchBounds( ToggleSwitchMetrics metrics, Size textSize) { - Rectangle contentBounds = ToggleSwitchMetrics.GetContentBounds(control); + Rectangle contentBounds = ToggleSwitchMetrics.GetContentBounds(control, metrics); int totalHeight = Math.Max(textSize.Height, metrics.SwitchHeight); int contentTop = contentBounds.Top + Math.Max(0, (contentBounds.Height - totalHeight) / 2); int switchY = contentTop + ((totalHeight - metrics.SwitchHeight) / 2); @@ -394,7 +418,9 @@ private ToggleSwitchMetrics( int thumbDiameter, int hoverThumbDiameter, int borderThickness, - int textGap) + int textGap, + int focusBorderThickness, + int focusMargin) { SwitchWidth = switchWidth; SwitchHeight = switchHeight; @@ -402,6 +428,8 @@ private ToggleSwitchMetrics( HoverThumbDiameter = hoverThumbDiameter; BorderThickness = borderThickness; TextGap = textGap; + FocusBorderThickness = focusBorderThickness; + FocusMargin = focusMargin; } internal int SwitchWidth { get; } @@ -416,6 +444,10 @@ private ToggleSwitchMetrics( internal int TextGap { get; } + internal int FocusBorderThickness { get; } + + internal int FocusMargin { get; } + internal static ToggleSwitchMetrics Create(Control control) { int switchHeight = Math.Max( @@ -423,6 +455,8 @@ internal static ToggleSwitchMetrics Create(Control control) (int)(control.Font.Height * 0.9f)); int minimumMetric = Math.Max(1, control.LogicalToDeviceUnits(1)); int borderThickness = Math.Max(minimumMetric, switchHeight / 12); + int focusBorderThickness = Math.Max(minimumMetric, control.LogicalToDeviceUnits(2)); + int focusMargin = focusBorderThickness + Math.Max(minimumMetric, control.LogicalToDeviceUnits(2)); int maximumThumbDiameter = Math.Max(1, switchHeight - (2 * borderThickness)); int thumbDiameter = Math.Max( 1, @@ -440,24 +474,38 @@ internal static ToggleSwitchMetrics Create(Control control) thumbDiameter: thumbDiameter, hoverThumbDiameter: hoverThumbDiameter, borderThickness: borderThickness, - textGap: textGap); + textGap: textGap, + focusBorderThickness: focusBorderThickness, + focusMargin: focusMargin); } internal static Rectangle GetContentBounds(Control control) + => GetContentBounds(control, Create(control)); + + internal static Rectangle GetContentBounds(Control control, ToggleSwitchMetrics metrics) { Padding padding = control.Padding; + int horizontalInset = padding.Horizontal + (2 * metrics.FocusMargin); + int verticalInset = padding.Vertical + (2 * metrics.FocusMargin); + return new Rectangle( - padding.Left, - padding.Top, - Math.Max(0, control.ClientSize.Width - padding.Horizontal), - Math.Max(0, control.ClientSize.Height - padding.Vertical)); + padding.Left + metrics.FocusMargin, + padding.Top + metrics.FocusMargin, + Math.Max(0, control.ClientSize.Width - horizontalInset), + Math.Max(0, control.ClientSize.Height - verticalInset)); + } + + internal static Rectangle GetFocusBounds(Rectangle clientRectangle, ToggleSwitchMetrics metrics) + { + int inset = metrics.FocusBorderThickness; + return Rectangle.Inflate(clientRectangle, -inset, -inset); } internal Size GetPreferredSize(Control control) { Size textSize = TextRenderer.MeasureText(control.Text, control.Font); return new Size( - SwitchWidth + TextGap + textSize.Width + control.Padding.Horizontal, - Math.Max(SwitchHeight, textSize.Height) + control.Padding.Vertical); + SwitchWidth + TextGap + textSize.Width + control.Padding.Horizontal + (2 * FocusMargin), + Math.Max(SwitchHeight, textSize.Height) + control.Padding.Vertical + (2 * FocusMargin)); } } diff --git a/src/test/unit/System.Windows.Forms/System/Windows/Forms/CheckBoxTests.cs b/src/test/unit/System.Windows.Forms/System/Windows/Forms/CheckBoxTests.cs index 3680393822d..adf333eb6a5 100644 --- a/src/test/unit/System.Windows.Forms/System/Windows/Forms/CheckBoxTests.cs +++ b/src/test/unit/System.Windows.Forms/System/Windows/Forms/CheckBoxTests.cs @@ -926,6 +926,50 @@ public void CheckBox_ToggleSwitch_HoverAndFocusAnimateWithoutChangingPreferredSi Assert.Equal(preferredSize, box.GetPreferredSize(Size.Empty)); } + [WinFormsFact] + public void CheckBox_ToggleSwitch_FocusedPaintsRoundedSolidFocusBorderAndReservesMargin() + { + using SystemVisualSettingsTestScope settingsScope = new( + clientAreaAnimationEnabled: false, + highContrastEnabled: false, + accentColor: Color.Red); + using CheckBox box = new() + { + Appearance = Appearance.ToggleSwitch, + BackColor = Color.White, + ForeColor = Color.Black, + Size = new Size(140, 36), + Text = "Toggle", + VisualStylesMode = VisualStylesMode.Net11 + }; + + Rendering.CheckBox.ToggleSwitchMetrics metrics = Rendering.CheckBox.ToggleSwitchMetrics.Create(box); + Rectangle contentBounds = Rendering.CheckBox.ToggleSwitchMetrics.GetContentBounds(box, metrics); + Rectangle focusBounds = Rendering.CheckBox.ToggleSwitchMetrics.GetFocusBounds(box.ClientRectangle, metrics); + Size textSize = TextRenderer.MeasureText(box.Text, box.Font); + Size preferredSizeWithoutFocusMargin = new( + metrics.SwitchWidth + metrics.TextGap + textSize.Width + box.Padding.Horizontal, + Math.Max(metrics.SwitchHeight, textSize.Height) + box.Padding.Vertical); + + Assert.Equal(box.Padding.Left + metrics.FocusMargin, contentBounds.Left); + Assert.Equal(box.Padding.Top + metrics.FocusMargin, contentBounds.Top); + Assert.Equal( + preferredSizeWithoutFocusMargin + new Size(2 * metrics.FocusMargin, 2 * metrics.FocusMargin), + box.GetPreferredSize(Size.Empty)); + + Rendering.CheckBox.AnimatedToggleSwitchRenderer renderer = + box.TestAccessor.Dynamic.ToggleSwitchRenderer; + renderer.SynchronizeState(); + using Bitmap bitmap = new(box.Width, box.Height); + using Graphics graphics = Graphics.FromImage(bitmap); + + renderer.TestAccessor.Dynamic.RenderFocusBorder(graphics, metrics); + + Assert.True(CountPixels(bitmap, Color.Red) > 0); + Assert.NotEqual(Color.Red.ToArgb(), bitmap.GetPixel(focusBounds.Left, focusBounds.Top).ToArgb()); + Assert.Equal(0, CountPixelsInColumn(bitmap, box.ClientRectangle.Right - 1, Color.Red)); + } + [WinFormsFact] public void CheckBox_ToggleSwitch_SynchronizeState_SettlesInteractionChannels() { @@ -1034,6 +1078,22 @@ private static int CountPixels(Bitmap bitmap, Color color) return count; } + private static int CountPixelsInColumn(Bitmap bitmap, int column, Color color) + { + int argb = color.ToArgb(); + int count = 0; + + for (int y = 0; y < bitmap.Height; y++) + { + if (bitmap.GetPixel(column, y).ToArgb() == argb) + { + count++; + } + } + + return count; + } + [WinFormsFact] public void CheckBox_GetAutoSizeMode_Invoke_ReturnsExpected() { @@ -1442,10 +1502,10 @@ public void CheckBox_ProcessMnemonic_ValidCases(bool useMnemonic, char charCode, } [WinFormsTheory] - [InlineData(Appearance.Button, FlatStyle.Standard, "Test", 12, 8, 100, 20)] - [InlineData(Appearance.Normal, FlatStyle.System, "Test", 12, 8, 100, 20)] - [InlineData(Appearance.Normal, FlatStyle.Flat, "Test", 12, 8, 100, 20)] - [InlineData(Appearance.Normal, FlatStyle.Standard, "Test", 12, 8, 100, 20)] + [InlineData(Appearance.Button, FlatStyle.Standard, "Test", 12, 8, 100, 20)] + [InlineData(Appearance.Normal, FlatStyle.System, "Test", 12, 8, 100, 20)] + [InlineData(Appearance.Normal, FlatStyle.Flat, "Test", 12, 8, 100, 20)] + [InlineData(Appearance.Normal, FlatStyle.Standard, "Test", 12, 8, 100, 20)] public void CheckBox_GetPreferredSizeCore_VariousStyles_ReturnsExpected( Appearance appearance, FlatStyle flatStyle, string text, int fontSize, int padding, int width, int height) { From 4d41ae2e8368cd4a53cd4df2c8e69155d4d64a57 Mon Sep 17 00:00:00 2001 From: "Leaf Shi (BEYONDSOFT CONSULTING INC)" Date: Thu, 27 Aug 2026 02:58:55 -0700 Subject: [PATCH 2/3] Draw the focus border before returning for empty content bounds, so small CheckBox and RadioButton ToggleSwitch controls can still show keyboard focus even when there is not enough room to paint the switch or text. --- .../Rendering/CheckBox/AnimatedToggleSwitchRenderer.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/System.Windows.Forms/System/Windows/Forms/Rendering/CheckBox/AnimatedToggleSwitchRenderer.cs b/src/System.Windows.Forms/System/Windows/Forms/Rendering/CheckBox/AnimatedToggleSwitchRenderer.cs index 4cd4490de17..f4e1e0abc70 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Rendering/CheckBox/AnimatedToggleSwitchRenderer.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Rendering/CheckBox/AnimatedToggleSwitchRenderer.cs @@ -90,6 +90,11 @@ public override void RenderControl(Graphics graphics) PaintControlBackground(graphics); + if (Control.Focused && ShowFocusCues) + { + RenderFocusBorder(graphics, metrics); + } + if (contentBounds.Width <= 0 || contentBounds.Height <= 0) { return; @@ -106,11 +111,6 @@ public override void RenderControl(Graphics graphics) RenderSwitch(graphics, switchBounds, metrics); RenderText(graphics, new Point(contentBounds.Left + metrics.SwitchWidth + metrics.TextGap, textY)); } - - if (Control.Focused && ShowFocusCues) - { - RenderFocusBorder(graphics, metrics); - } } private void RenderFocusBorder(Graphics graphics, ToggleSwitchMetrics metrics) From b86034894baa4231c0a4f4a10a15ae58fa2ef639 Mon Sep 17 00:00:00 2001 From: "Leaf Shi (BEYONDSOFT CONSULTING INC)" Date: Thu, 27 Aug 2026 03:10:49 -0700 Subject: [PATCH 3/3] Clamp the corner size to the smaller dimension before calling AddRoundedRectangle. --- .../Forms/Rendering/CheckBox/AnimatedToggleSwitchRenderer.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/System.Windows.Forms/System/Windows/Forms/Rendering/CheckBox/AnimatedToggleSwitchRenderer.cs b/src/System.Windows.Forms/System/Windows/Forms/Rendering/CheckBox/AnimatedToggleSwitchRenderer.cs index f4e1e0abc70..c6c7026a68e 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Rendering/CheckBox/AnimatedToggleSwitchRenderer.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Rendering/CheckBox/AnimatedToggleSwitchRenderer.cs @@ -125,10 +125,11 @@ private void RenderFocusBorder(Graphics graphics, ToggleSwitchMetrics metrics) Application.SystemVisualSettings.HighContrastEnabled); using var focusPen = focusColor.GetCachedPenScope(metrics.FocusBorderThickness); using GraphicsPath focusPath = new(); + int cornerSize = Math.Max(1, Math.Min(focusBounds.Width, focusBounds.Height)); focusPath.AddRoundedRectangle( focusBounds, - new Size(focusBounds.Height, focusBounds.Height)); + new Size(cornerSize, cornerSize)); SmoothingMode previousSmoothingMode = graphics.SmoothingMode; try