diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/Buttons/ButtonInternal/CheckBoxStandardAdapter.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/Buttons/ButtonInternal/CheckBoxStandardAdapter.cs index 3a7f7d3b0c8..d846da9c067 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Controls/Buttons/ButtonInternal/CheckBoxStandardAdapter.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/Buttons/ButtonInternal/CheckBoxStandardAdapter.cs @@ -89,7 +89,7 @@ internal override Size GetPreferredSizeCore(Size proposedSize) { if (Control.Appearance == Appearance.Button) { - ButtonStandardAdapter adapter = new(Control); + ButtonBaseAdapter adapter = DarkModeAdapterFactory.CreateStandardAdapter(Control); return adapter.GetPreferredSizeCore(proposedSize); } else @@ -105,9 +105,9 @@ internal override Size GetPreferredSizeCore(Size proposedSize) } } - private new ButtonStandardAdapter ButtonAdapter => (ButtonStandardAdapter)base.ButtonAdapter; + private new ButtonBaseAdapter ButtonAdapter => base.ButtonAdapter; - protected override ButtonBaseAdapter CreateButtonAdapter() => new ButtonStandardAdapter(Control); + protected override ButtonBaseAdapter CreateButtonAdapter() => DarkModeAdapterFactory.CreateStandardAdapter(Control); protected override LayoutOptions Layout(PaintEventArgs e) { diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/Buttons/ButtonInternal/DarkMode/ButtonDarkModeAdapter.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/Buttons/ButtonInternal/DarkMode/ButtonDarkModeAdapter.cs index a85dd3ed4cc..47775d46960 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Controls/Buttons/ButtonInternal/DarkMode/ButtonDarkModeAdapter.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/Buttons/ButtonInternal/DarkMode/ButtonDarkModeAdapter.cs @@ -158,22 +158,28 @@ internal override void PaintOver(PaintEventArgs e, CheckState state) g.SmoothingMode = Drawing.Drawing2D.SmoothingMode.AntiAlias; LayoutData layout = CommonLayout().Layout(); + + // A checked toggle-button must remain visually pressed even on hover. + PushButtonState pushButtonState = state == CheckState.Checked + ? PushButtonState.Pressed + : PushButtonState.Hot; + ButtonDarkModeRenderer.RenderButton( g, Control.ClientRectangle, Control.FlatStyle, - PushButtonState.Hot, + pushButtonState, Control.IsDefault, Control.Focused, Control.ShowFocusCues, Control.Parent?.BackColor ?? Control.BackColor, - GetButtonBackColor(PushButtonState.Hot), + GetButtonBackColor(pushButtonState), _ => PaintImage(e, layout), () => PaintField( e, layout, PaintDarkModeRender(e).Calculate(), - GetButtonTextColor(e, PushButtonState.Hot), + GetButtonTextColor(e, pushButtonState), drawFocus: false) ); diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/Buttons/ButtonInternal/RadioButtonStandardAdapter.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/Buttons/ButtonInternal/RadioButtonStandardAdapter.cs index 0181286c263..4a75be1ebf2 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Controls/Buttons/ButtonInternal/RadioButtonStandardAdapter.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/Buttons/ButtonInternal/RadioButtonStandardAdapter.cs @@ -50,9 +50,9 @@ internal override void PaintOver(PaintEventArgs e, CheckState state) } } - private new ButtonStandardAdapter ButtonAdapter => (ButtonStandardAdapter)base.ButtonAdapter; + private new ButtonBaseAdapter ButtonAdapter => base.ButtonAdapter; - protected override ButtonBaseAdapter CreateButtonAdapter() => new ButtonStandardAdapter(Control); + protected override ButtonBaseAdapter CreateButtonAdapter() => DarkModeAdapterFactory.CreateStandardAdapter(Control); protected override LayoutOptions Layout(PaintEventArgs e) { diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/Buttons/CheckBox.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/Buttons/CheckBox.cs index 57526b2644b..e3139311bca 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Controls/Buttons/CheckBox.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/Buttons/CheckBox.cs @@ -96,17 +96,7 @@ public Appearance Appearance } } - private protected override bool OwnerDraw => - // We want NO owner draw ONLY when we're - // * In Dark Mode - // * When _then_ the Appearance is Button - // * But then ONLY when we're rendering with FlatStyle.Standard - // (because that would let us usually let us draw with the VisualStyleRenderers, - // which cause HighDPI issues in Dark Mode). - (!Application.IsDarkModeEnabled - || Appearance != Appearance.Button - || FlatStyle != FlatStyle.Standard) - && base.OwnerDraw; + private protected override bool OwnerDraw => base.OwnerDraw; [SRCategory(nameof(SR.CatPropertyChanged))] [SRDescription(nameof(SR.CheckBoxOnAppearanceChangedDescr))] diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/Buttons/RadioButton.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/Buttons/RadioButton.cs index a6674f1ea69..f5c190832bc 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Controls/Buttons/RadioButton.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/Buttons/RadioButton.cs @@ -173,18 +173,7 @@ public bool Checked } } - private protected override bool OwnerDraw => - // Order is key here - do NOT change! - // We want NO owner draw ONLY when we're - // * in Dark Mode - // * when _then_ the Appearance is Button - // * but then ONLY when we're rendering with FlatStyle.Standard - // (because that would let us usually let us draw with the VisualStyleRenderers, - // which cause HighDPI issues in Dark Mode). - (!Application.IsDarkModeEnabled - || Appearance != Appearance.Button - || FlatStyle != FlatStyle.Standard) - && base.OwnerDraw; + private protected override bool OwnerDraw => base.OwnerDraw; /// [Browsable(false)] 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 506c893a1d7..12891c5e22f 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 @@ -631,6 +631,31 @@ public void CheckBox_ToStringTest() Assert.Equal(expected, actual); } +#pragma warning disable SYSLIB5002 // Type is for evaluation purposes only and is subject to change or removal in future updates. + [WinFormsFact] + public void CheckBox_OwnerDraw_AppearanceButton_FlatStyleStandard_InDarkMode_IsTrue() + { + // Regression test for https://github.com/dotnet/winforms/issues/14347: + // CheckBox with Appearance.Button + FlatStyle.Standard must always use owner-draw, + // including in dark mode. If OwnerDraw returns false the control falls back to + // native ComCtl32 painting which renders an invisible button on dark backgrounds. + Application.SetColorMode(SystemColorMode.Dark); + try + { + using SubCheckBox control = new() + { + Appearance = Appearance.Button, + FlatStyle = FlatStyle.Standard + }; + Assert.True(control.GetStyle(ControlStyles.UserPaint)); + } + finally + { + Application.SetColorMode(SystemColorMode.Classic); + } + } +#pragma warning restore SYSLIB5002 + public class SubCheckBox : CheckBox { public new bool CanEnableIme => base.CanEnableIme; diff --git a/src/test/unit/System.Windows.Forms/System/Windows/Forms/RadioButtonTests.cs b/src/test/unit/System.Windows.Forms/System/Windows/Forms/RadioButtonTests.cs index 08142ccb0cf..ac6f72e0d33 100644 --- a/src/test/unit/System.Windows.Forms/System/Windows/Forms/RadioButtonTests.cs +++ b/src/test/unit/System.Windows.Forms/System/Windows/Forms/RadioButtonTests.cs @@ -1665,5 +1665,30 @@ internal override bool RaiseAutomationPropertyChangedEvent(UIA_PROPERTY_ID prope [InlineData(Appearance.Normal, FlatStyle.System)] public void RadioButton_OverChangeRectangle_Get(Appearance appearance, FlatStyle flatStyle) => base.ButtonBase_OverChangeRectangle_Get(appearance, flatStyle); +#pragma warning disable SYSLIB5002 // Type is for evaluation purposes only and is subject to change or removal in future updates. + [WinFormsFact] + public void RadioButton_OwnerDraw_AppearanceButton_FlatStyleStandard_InDarkMode_IsTrue() + { + // Regression test for https://github.com/dotnet/winforms/issues/14347: + // RadioButton with Appearance.Button + FlatStyle.Standard must always use owner-draw, + // including in dark mode. If OwnerDraw returns false the control falls back to + // native ComCtl32 painting which renders an invisible button on dark backgrounds. + Application.SetColorMode(SystemColorMode.Dark); + try + { + using SubRadioButton control = new() + { + Appearance = Appearance.Button, + FlatStyle = FlatStyle.Standard + }; + Assert.True(control.GetStyle(ControlStyles.UserPaint)); + } + finally + { + Application.SetColorMode(SystemColorMode.Classic); + } + } +#pragma warning restore SYSLIB5002 + protected override ButtonBase CreateButton() => new SubRadioButton(); }