From 3688fd64b892ceed07c8ab3f412667bce4832710 Mon Sep 17 00:00:00 2001 From: "Leaf Shi (BEYONDSOFT CONSULTING INC)" Date: Wed, 5 Aug 2026 02:01:01 -0700 Subject: [PATCH] Fix disabled ComboBox rendering to use system disabled palette in modern visual style --- .../DarkMode/ModernButtonDarkModeRenderer.cs | 23 +-- .../DarkMode/ModernFlatButtonRenderer.cs | 19 +-- .../ComboBox/ComboBox.ModernComboAdapter.cs | 51 ++---- .../Forms/Controls/ComboBox/ComboBox.cs | 21 +++ .../Forms/Rendering/ModernControlColorMath.cs | 46 +++++- .../System/Windows/Forms/ComboBoxTests.cs | 150 +++++++++++++++++- 6 files changed, 236 insertions(+), 74 deletions(-) diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/Buttons/ButtonInternal/DarkMode/ModernButtonDarkModeRenderer.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/Buttons/ButtonInternal/DarkMode/ModernButtonDarkModeRenderer.cs index 81ecfd0f5cc..1b2e10de4e6 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Controls/Buttons/ButtonInternal/DarkMode/ModernButtonDarkModeRenderer.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/Buttons/ButtonInternal/DarkMode/ModernButtonDarkModeRenderer.cs @@ -32,9 +32,6 @@ internal sealed class ModernButtonDarkModeRenderer : ButtonDarkModeRendererBase private static readonly Color s_darkNormal = Color.FromArgb(0x2D, 0x2D, 0x2D); private static readonly Color s_darkNormalHover = Color.FromArgb(0x32, 0x32, 0x32); private static readonly Color s_darkNormalPressed = Color.FromArgb(0x2A, 0x2A, 0x2A); - private static readonly Color s_darkDisabled = Color.FromArgb(0x25, 0x25, 0x25); - - private static readonly Color s_darkDisabledText = Color.FromArgb(0x88, 0x88, 0x88); private static readonly Color s_darkGap = Color.FromArgb(0x0A, 0x0A, 0x0A); private static readonly Color s_darkFocusRing = Color.White; @@ -43,11 +40,8 @@ internal sealed class ModernButtonDarkModeRenderer : ButtonDarkModeRendererBase private static readonly Color s_lightNormal = Color.FromArgb(0xFB, 0xFB, 0xFB); private static readonly Color s_lightNormalHover = Color.FromArgb(0xF9, 0xF9, 0xF9); private static readonly Color s_lightNormalPressed = Color.FromArgb(0xF5, 0xF5, 0xF5); - private static readonly Color s_lightDisabled = Color.FromArgb(0xFA, 0xFA, 0xFA); private static readonly Color s_lightBorder = Color.FromArgb(0xD0, 0xD0, 0xD0); - private static readonly Color s_lightDisabledText = Color.FromArgb(0xA0, 0xA0, 0xA0); - private static bool IsDark => Application.IsDarkModeEnabled; private int FocusRingThickness @@ -182,25 +176,16 @@ public override void DrawFocusIndicator(Graphics graphics, Rectangle bounds, boo public override Color GetTextColor(PushButtonState state, bool isDefault, Color backColor) { - if (state == PushButtonState.Disabled) - { - Color preferredForeColor = IsDark - ? s_darkDisabledText - : s_lightDisabledText; - - return ModernControlColorMath.GetDisabledTextColor( - preferredForeColor, - backColor); - } - - return ModernButtonColorMath.GetReadableForeColor(backColor); + return state == PushButtonState.Disabled + ? ModernControlColorMath.GetDisabledForeColor(backColor) + : ModernButtonColorMath.GetReadableForeColor(backColor); } public override Color GetBackgroundColor(PushButtonState state, bool isDefault) { if (state == PushButtonState.Disabled) { - return IsDark ? s_darkDisabled : s_lightDisabled; + return ModernControlColorMath.GetDisabledSurfaceColor(); } if (isDefault) diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/Buttons/ButtonInternal/DarkMode/ModernFlatButtonRenderer.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/Buttons/ButtonInternal/DarkMode/ModernFlatButtonRenderer.cs index e73b92961af..3dd49e7f2ee 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Controls/Buttons/ButtonInternal/DarkMode/ModernFlatButtonRenderer.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/Buttons/ButtonInternal/DarkMode/ModernFlatButtonRenderer.cs @@ -19,9 +19,7 @@ internal sealed class ModernFlatButtonRenderer : ButtonDarkModeRendererBase private static readonly Color s_lightBorder = Color.FromArgb(0xD0, 0xD0, 0xD0); private static readonly Color s_darkBorder = Color.FromArgb(0x55, 0x55, 0x55); private static readonly Color s_lightNormal = Color.FromArgb(0xFB, 0xFB, 0xFB); - private static readonly Color s_lightDisabled = Color.FromArgb(0xFA, 0xFA, 0xFA); private static readonly Color s_darkNormal = Color.FromArgb(0x2D, 0x2D, 0x2D); - private static readonly Color s_darkDisabled = Color.FromArgb(0x25, 0x25, 0x25); private static bool IsDark => Application.IsDarkModeEnabled; @@ -96,25 +94,16 @@ public override void DrawFocusIndicator(Graphics graphics, Rectangle contentBoun public override Color GetTextColor(PushButtonState state, bool isDefault, Color backColor) { - if (state == PushButtonState.Disabled) - { - Color preferredForeColor = IsDark - ? Color.FromArgb(0x88, 0x88, 0x88) - : Color.FromArgb(0xA0, 0xA0, 0xA0); - - return ModernControlColorMath.GetDisabledTextColor( - preferredForeColor, - backColor); - } - - return ModernButtonColorMath.GetReadableForeColor(backColor); + return state == PushButtonState.Disabled + ? ModernControlColorMath.GetDisabledForeColor(backColor) + : ModernButtonColorMath.GetReadableForeColor(backColor); } public override Color GetBackgroundColor(PushButtonState state, bool isDefault) { if (state == PushButtonState.Disabled) { - return IsDark ? s_darkDisabled : s_lightDisabled; + return ModernControlColorMath.GetDisabledSurfaceColor(); } if (isDefault) diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/ComboBox/ComboBox.ModernComboAdapter.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/ComboBox/ComboBox.ModernComboAdapter.cs index 1c0c272383a..2623d41f726 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Controls/ComboBox/ComboBox.ModernComboAdapter.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/ComboBox/ComboBox.ModernComboAdapter.cs @@ -139,10 +139,8 @@ private static void DrawStandardFrame( Rectangle clientBounds, Rectangle borderBounds) { - Color background = GetEffectiveBackColor(comboBox); Color borderColor = GetBorderColor( comboBox, - background, useAccent: false); CutOutRoundedCorners( @@ -166,10 +164,8 @@ private static void DrawFlatFrame( return; } - Color background = GetEffectiveBackColor(comboBox); Color borderColor = GetBorderColor( comboBox, - background, useAccent: false); using var pen = borderColor.GetCachedPenScope( GetBorderThickness(comboBox)); @@ -182,10 +178,8 @@ private static void DrawPopupFrame( Rectangle clientBounds, Rectangle borderBounds) { - Color background = GetEffectiveBackColor(comboBox); Color borderColor = GetBorderColor( comboBox, - background, useAccent: true); CutOutRoundedCorners( comboBox, @@ -223,9 +217,7 @@ private void DrawDropDownButton( 0.035f); if (!comboBox.Enabled) { - buttonColor = PopupButtonColorMath.Mute( - buttonColor, - 0.55f); + buttonColor = ModernControlColorMath.GetDisabledSurfaceColor(); } using (var brush = buttonColor.GetCachedSolidBrushScope()) @@ -235,9 +227,7 @@ private void DrawDropDownButton( Color chevronColor = comboBox.Enabled ? PopupButtonColorMath.GetReadableForeColor(buttonColor) - : ModernControlColorMath.GetDisabledTextColor( - comboBox.ForeColor, - buttonColor); + : ModernControlColorMath.GetDisabledForeColor(buttonColor); int halfWidth = Math.Max( 2, ScaleHelper.ScaleToDpi(3, _deviceDpi)); @@ -316,9 +306,7 @@ private void DrawDropDownListText( ? comboBox.ForeColor : PopupButtonColorMath.GetReadableForeColor( background) - : ModernControlColorMath.GetDisabledTextColor( - comboBox.ForeColor, - background); + : ModernControlColorMath.GetDisabledForeColor(background); TextFormatFlags flags = TextFormatFlags.SingleLine | TextFormatFlags.VerticalCenter | TextFormatFlags.EndEllipsis @@ -421,20 +409,16 @@ private static void DrawRoundedBorder( parentColor); } - private static Color GetBorderColor( - ComboBox comboBox, - Color background, - bool useAccent) + private static Color GetBorderColor(ComboBox comboBox, bool useAccent) { - Color borderColor = useAccent + if (!comboBox.Enabled) + { + return ModernControlColorMath.GetDisabledBorderColor(); + } + + return useAccent ? Application.SystemVisualSettings.AccentColor : comboBox.ForeColor; - - return comboBox.Enabled - ? borderColor - : ModernControlColorMath.GetDisabledTextColor( - borderColor, - background); } private static int GetBorderThickness(ComboBox comboBox) @@ -480,16 +464,13 @@ private static Color GetEffectiveBackColor(ComboBox comboBox) ?? SystemColors.Window; /// - /// Returns the field surface color, muted when the ComboBox is disabled so a disabled - /// control no longer shows its full custom (issue #14797). + /// Returns the field surface color. A disabled ComboBox does not honor user-set + /// values and uses the shared modern disabled surface + /// instead, which adapts to the current color mode (issue #14797). /// private static Color GetEffectiveFieldColor(ComboBox comboBox) - { - Color background = GetEffectiveBackColor(comboBox); - - return comboBox.Enabled - ? background - : PopupButtonColorMath.Mute(background, 0.55f); - } + => comboBox.Enabled + ? GetEffectiveBackColor(comboBox) + : ModernControlColorMath.GetDisabledSurfaceColor(); } } diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/ComboBox/ComboBox.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/ComboBox/ComboBox.cs index 0e2e5c6e0a7..3fe91f3fd09 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Controls/ComboBox/ComboBox.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/ComboBox/ComboBox.cs @@ -3826,6 +3826,27 @@ protected override unsafe void WndProc(ref Message m) return; } + // Modern NET11 mode, disabled DropDown: the native edit child sends + // WM_CTLCOLORSTATIC and the OS default would produce a white background, + // inconsistent with the disabled surface ModernComboAdapter paints for the rest + // of the field. In light mode, explicitly set disabled colors via system values. + // In dark mode, ModernComboAdapter already renders the disabled surface correctly, + // and the edit child inherits appropriate rendering from the parent control. + if (hwndChild == _childEdit?.HWND + && UsesModernComboAdapter + && !Enabled + && !Application.IsDarkModeEnabled) + { + PInvokeCore.SetBkColor( + (HDC)m.WParamInternal, + ColorTranslator.ToWin32(SystemColors.ButtonFace)); + PInvokeCore.SetTextColor( + (HDC)m.WParamInternal, + ColorTranslator.ToWin32(SystemColors.GrayText)); + m.ResultInternal = (LRESULT)(nint)PInvokeCore.GetSysColorBrush(SystemColors.ButtonFace); + return; + } + // Additional handling for Simple style listbox when disabled if (DropDownStyle == ComboBoxStyle.Simple && Application.IsDarkModeEnabled diff --git a/src/System.Windows.Forms/System/Windows/Forms/Rendering/ModernControlColorMath.cs b/src/System.Windows.Forms/System/Windows/Forms/Rendering/ModernControlColorMath.cs index 89b95acaf14..78832f78731 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Rendering/ModernControlColorMath.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Rendering/ModernControlColorMath.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; @@ -16,6 +16,50 @@ internal static class ModernControlColorMath private const float DisabledMuteAmount = 0.45f; private const int ContrastSearchIterations = 10; + // Shared disabled-state palette for modern renderers. Modern controls do not honor user-set + // BackColor/ForeColor while disabled, so these fixed surfaces replace them. This is the single + // source of truth: the modern Button renderers and the modern ComboBox adapter all read from + // here, so a disabled Button and a disabled ComboBox cannot drift apart. + private static readonly Color s_darkModeDisabledSurface = Color.FromArgb(0x25, 0x25, 0x25); + private static readonly Color s_lightModeDisabledSurface = Color.FromArgb(0xFA, 0xFA, 0xFA); + private static readonly Color s_darkModeDisabledBorder = Color.FromArgb(0x55, 0x55, 0x55); + private static readonly Color s_lightModeDisabledBorder = Color.FromArgb(0xD0, 0xD0, 0xD0); + private static readonly Color s_darkModeDisabledForeground = Color.FromArgb(0x88, 0x88, 0x88); + private static readonly Color s_lightModeDisabledForeground = Color.FromArgb(0xA0, 0xA0, 0xA0); + + /// + /// Gets the surface color for a disabled modern control, honoring the current color mode + /// and high contrast settings. + /// + internal static Color GetDisabledSurfaceColor() + => SystemInformation.HighContrast + ? SystemColors.Control + : Application.IsDarkModeEnabled + ? s_darkModeDisabledSurface + : s_lightModeDisabledSurface; + + /// + /// Gets the border color for a disabled modern control, honoring the current color mode + /// and high contrast settings. + /// + internal static Color GetDisabledBorderColor() + => SystemInformation.HighContrast + ? SystemColors.GrayText + : Application.IsDarkModeEnabled + ? s_darkModeDisabledBorder + : s_lightModeDisabledBorder; + + /// + /// Gets the contrast-adjusted foreground color for content drawn on + /// . + /// + internal static Color GetDisabledForeColor(Color backColor) + => GetDisabledTextColor( + Application.IsDarkModeEnabled + ? s_darkModeDisabledForeground + : s_lightModeDisabledForeground, + backColor); + internal static Color GetDisabledTextColor( Color preferredForeColor, Color backColor) diff --git a/src/test/unit/System.Windows.Forms/System/Windows/Forms/ComboBoxTests.cs b/src/test/unit/System.Windows.Forms/System/Windows/Forms/ComboBoxTests.cs index 8ba593350fd..2af7ac2f04d 100644 --- a/src/test/unit/System.Windows.Forms/System/Windows/Forms/ComboBoxTests.cs +++ b/src/test/unit/System.Windows.Forms/System/Windows/Forms/ComboBoxTests.cs @@ -497,7 +497,7 @@ public void ComboBox_ModernVisualStyles_FramesUseExpectedGeometryAndColor( control.ClientSize.Height); using Graphics graphics = Graphics.FromImage(actual); var adapter = - (ComboBox.ModernComboAdapter)control.CreateAdapter(); + (ModernComboAdapter)control.CreateAdapter(); adapter.DrawFlatCombo(control, graphics); @@ -516,6 +516,148 @@ public void ComboBox_ModernVisualStyles_FramesUseExpectedGeometryAndColor( actual.GetPixel(0, 0).ToArgb()); } + /// + /// When Enabled is set to in NET11 VisualStylesMode, the modern + /// adapter must render the field with the shared disabled surface rather than the + /// user-supplied . + /// + [WinFormsTheory] + [InlineData(FlatStyle.Standard, ComboBoxStyle.DropDown)] + [InlineData(FlatStyle.Flat, ComboBoxStyle.DropDown)] + [InlineData(FlatStyle.Popup, ComboBoxStyle.DropDown)] + [InlineData(FlatStyle.Standard, ComboBoxStyle.DropDownList)] + [InlineData(FlatStyle.Flat, ComboBoxStyle.DropDownList)] + [InlineData(FlatStyle.Popup, ComboBoxStyle.DropDownList)] + public void ComboBox_ModernVisualStyles_Disabled_UsesDisabledSurfaceInsteadOfBackColor( + FlatStyle flatStyle, + ComboBoxStyle dropDownStyle) + { + using SystemVisualSettingsTestScope settingsScope = new( + clientAreaAnimationEnabled: false, + highContrastEnabled: false); + using Panel parent = new() + { + BackColor = Color.White, + Size = new Size(120, 60) + }; + + // Use a highly saturated color so it is clearly distinguishable from the disabled surface. + Color customBackColor = Color.Yellow; + using VisualStylesComboBox control = new() + { + BackColor = customBackColor, + DropDownStyle = dropDownStyle, + FlatStyle = flatStyle, + Size = new Size(100, 36), + VisualStylesMode = VisualStylesMode.Net11 + }; + parent.Controls.Add(control); + parent.CreateControl(); + control.CreateControl(); + + // Verify the enabled render shows the custom BackColor in the field area. + var adapterEnabled = (ModernComboAdapter)control.CreateAdapter(); + using Bitmap enabledBitmap = new(control.ClientSize.Width, control.ClientSize.Height); + using (Graphics g = Graphics.FromImage(enabledBitmap)) + { + adapterEnabled.DrawFlatCombo(control, g); + } + + // Verify the disabled render does NOT show the full custom BackColor. + control.Enabled = false; + var adapterDisabled = (ModernComboAdapter)control.CreateAdapter(); + using Bitmap disabledBitmap = new(control.ClientSize.Width, control.ClientSize.Height); + using (Graphics g = Graphics.FromImage(disabledBitmap)) + { + adapterDisabled.DrawFlatCombo(control, g); + } + + // The enabled bitmap should contain pixels that are close to the original BackColor. + Assert.True( + CountPixels(enabledBitmap, customBackColor, channelTolerance: 20) > 0, + "Enabled ComboBox should render field with the BackColor."); + + // The disabled bitmap must NOT contain the user BackColor, and must instead use the + // shared modern disabled surface (issue #14797). + Assert.True( + CountPixels(disabledBitmap, customBackColor, channelTolerance: 20) == 0, + "Disabled ComboBox must not render field with the BackColor."); + Assert.True( + CountPixels( + disabledBitmap, + ModernControlColorMath.GetDisabledSurfaceColor(), + channelTolerance: 4) > 0, + "Disabled ComboBox should render field with the disabled surface color."); + } + + /// + /// The drop-down button chevron and border must also use the disabled color palette + /// when the ComboBox is disabled in NET11 VisualStylesMode. + /// + [WinFormsTheory] + [InlineData(FlatStyle.Standard)] + [InlineData(FlatStyle.Flat)] + [InlineData(FlatStyle.Popup)] + public void ComboBox_ModernVisualStyles_Disabled_UsesDisabledBorderAndButtonColors( + FlatStyle flatStyle) + { + using SystemVisualSettingsTestScope settingsScope = new( + clientAreaAnimationEnabled: false, + highContrastEnabled: false, + focusBorderMetrics: new Size(2, 2)); + using Panel parent = new() + { + BackColor = Color.White, + Size = new Size(120, 60) + }; + Color customForeColor = Color.Blue; + using VisualStylesComboBox control = new() + { + BackColor = Color.White, + FlatStyle = flatStyle, + ForeColor = customForeColor, + Size = new Size(100, 36), + VisualStylesMode = VisualStylesMode.Net11 + }; + parent.Controls.Add(control); + parent.CreateControl(); + control.CreateControl(); + + // Enabled border should contain the custom ForeColor (for Standard/Flat). + var adapterEnabled = (ModernComboAdapter)control.CreateAdapter(); + using Bitmap enabledBitmap = new(control.ClientSize.Width, control.ClientSize.Height); + using (Graphics g = Graphics.FromImage(enabledBitmap)) + { + adapterEnabled.DrawFlatCombo(control, g); + } + + // Disabled border must NOT use the raw ForeColor. + control.Enabled = false; + var adapterDisabled = (ModernComboAdapter)control.CreateAdapter(); + using Bitmap disabledBitmap = new(control.ClientSize.Width, control.ClientSize.Height); + using (Graphics g = Graphics.FromImage(disabledBitmap)) + { + adapterDisabled.DrawFlatCombo(control, g); + } + + if (flatStyle != FlatStyle.Popup) + { + // Standard and Flat use the ForeColor for the border; it must be absent when disabled. + Assert.True( + CountPixels(enabledBitmap, customForeColor, channelTolerance: 16) > 0, + "Enabled ComboBox should render border with ForeColor."); + Assert.True( + CountPixels(disabledBitmap, customForeColor, channelTolerance: 16) == 0, + "Disabled ComboBox must not render border with the ForeColor."); + Assert.True( + CountPixels( + disabledBitmap, + ModernControlColorMath.GetDisabledBorderColor(), + channelTolerance: 8) > 0, + "Disabled ComboBox should render border with the disabled border color."); + } + } + [WinFormsFact] public void ComboBox_Padding_HasDesignerSerializationConfiguration() { @@ -1278,7 +1420,7 @@ public void ComboBox_SupportsModernDropDownCorners_ReturnsExpected( { Assert.Equal( expected, - ComboBox.SupportsModernDropDownCorners( + SupportsModernDropDownCorners( new Version(major, minor, build))); } @@ -3978,7 +4120,7 @@ internal override bool RaiseAutomationEvent(UIA_EVENT_ID eventId) private sealed class VisualStylesComboBox : SubComboBox { public FlatComboAdapter CreateAdapter() - => base.CreateFlatComboAdapterInstance(); + => CreateFlatComboAdapterInstance(); public (int left, int right) GetEditMargins() { @@ -4090,7 +4232,7 @@ public void SetTestDeviceDpi(int deviceDpi) public void RaiseSystemVisualSettingsChanged( SystemVisualSettingsChangedEventArgs e) - => base.OnSystemVisualSettingsChanged(e); + => OnSystemVisualSettingsChanged(e); internal override bool IsHighContrast => false; }