Fix issue 14807: CheckBox/RadioButton: ToggleSwitch renders black background when BackColor is Transparent - #14813
Fix issue 14807: CheckBox/RadioButton: ToggleSwitch renders black background when BackColor is Transparent#14813SimonZhao888 wants to merge 6 commits into
Conversation
…kground when BackColor is Transparent
There was a problem hiding this comment.
Pull request overview
This PR fixes ToggleSwitch-style CheckBox/RadioButton rendering when BackColor is Transparent by avoiding Graphics.Clear(BackColor) (which bypasses WinForms background painting semantics) and instead routing background painting through the standard Control.PaintBackground(...) pipeline when transparency/background images are involved.
Changes:
- Replaced unconditional
graphics.Clear(Control.BackColor)with a newPaintControlBackground(Graphics)helper. - Added a fast path for opaque solid backgrounds (no
BackgroundImage) and a correctness path for transparent/background-image scenarios usingControl.PaintBackground(...)with a clipped rectangle.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| @@ -1,4 +1,4 @@ | |||
| // Licensed to the .NET Foundation under one or more agreements. | |||
| // Licensed to the .NET Foundation under one or more agreements. | |||
|
Not sure if intentional, but the existing rendering-matrix test always uses the default
[WinFormsTheory]
[InlineData(true)]
[InlineData(false)]
public void CheckBox_ToggleSwitch_TransparentBackColor_DoesNotThrow(bool useBackgroundImage)
{
using SystemVisualSettingsTestScope settingsScope = new(
clientAreaAnimationEnabled: false,
highContrastEnabled: false);
using Bitmap backgroundImage = new(10, 10);
using Panel parent = new() { BackColor = Color.LightBlue, Size = new Size(200, 100) };
using CheckBox checkBox = new()
{
Parent = parent,
Appearance = Appearance.ToggleSwitch,
VisualStylesMode = VisualStylesMode.Net11,
BackColor = Color.Transparent,
BackgroundImage = useBackgroundImage ? backgroundImage : null,
Size = new Size(140, 36),
Text = "Matrix"
};
parent.CreateControl();
using Bitmap bitmap = new(checkBox.Width, checkBox.Height);
checkBox.DrawToBitmap(bitmap, new Rectangle(Point.Empty, checkBox.Size));
} |
LeafShi1
left a comment
There was a problem hiding this comment.
There are no automated tests, but the fix looks fine.
9cc3191 to
03d8a9e
Compare
|
@SimonZhao888, could you do a few more visual tests with different background colors and also in darkmode? Thanks! |
Got it! |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 7 out of 7 changed files in this pull request and generated no new comments.
Comments suppressed due to low confidence (4)
src/test/unit/System.Windows.Forms/System/Windows/Forms/RadioButtonTests.cs:287
- PaintEventArgs implements IDisposable; this instance should be disposed to avoid leaking GDI resources in longer-running test runs.
PaintEventArgs e = new(graphics, control.ClientRectangle);
src/test/unit/System.Windows.Forms/System/Windows/Forms/RadioButtonTests.cs:388
- CountPixels ignores the alpha channel, which can produce false positives (e.g., a newly created Bitmap is often initialized to transparent black, so searching for black may succeed even if nothing was rendered). Consider excluding fully transparent pixels so the color assertions actually validate rendering output.
Color pixel = bitmap.GetPixel(x, y);
if (Math.Abs(pixel.R - color.R) <= channelTolerance
&& Math.Abs(pixel.G - color.G) <= channelTolerance
&& Math.Abs(pixel.B - color.B) <= channelTolerance)
{
src/test/unit/System.Windows.Forms/System/Windows/Forms/CheckBoxTests.cs:647
- PaintEventArgs implements IDisposable; wrap this in a using declaration to ensure any underlying GDI state is released promptly.
PaintEventArgs e = new(graphics, box.ClientRectangle);
src/System.Windows.Forms/System/Windows/Forms/Rendering/RadioButton/AnimatedRadioGlyphRenderer.cs:131
- checkedProgress is computed using Math.Max(_dotScaleCurrent, _dotScaleTarget), which will immediately clamp to 1 when transitioning from unchecked to checked (because the target is 1). That prevents the outer circle/border from actually blending "as checked progress advances". Use the current animated value only so the blend follows the animation.
float checkedProgress = Math.Clamp(
Math.Max(_dotScaleCurrent, _dotScaleTarget),
0f,
1f);
Fixes #14807
Root Cause
AnimatedToggleSwitchRenderer.RenderControl currently clears with graphics.Clear(Control.BackColor). This bypasses the normal WinForms background pipeline (parent background, transparent background, background image/custom-painted parent), which can cause visual regressions on complex backgrounds.
Using the standard WinForms background painting path (e.g., Control.PaintBackground(...) with PaintEventArgs and ClientRectangle) instead of direct Clear. That preserves both:
parent/transparent background semantics, and
proper BackColor blending behavior (including semi-transparent colors).
Proposed changes
Implemented a background-painting optimization in AnimatedToggleSwitchRenderer to balance rendering correctness and animation performance. Replaced the unconditional Control.PaintBackground(...) call with a new helper: PaintControlBackground(Graphics graphics).
Customer Impact
Improves rendering behavior and animation efficiency for ToggleSwitch-style CheckBox/RadioButton controls.
Regression?
Risk
Screenshots
Before
After
Light:
14807.mp4
2026-07-30.145810.mp4
DarkMode:
2026-07-30.142639.mp4
2026-07-30.145436.mp4
Test methodology
Test environment(s)
Microsoft Reviewers: Open in CodeFlow