Skip to content

Commit 17c060b

Browse files
DarkMode (b): Fix rendering of disabled TextBox types (#13721)
Fixes the `ComboBox` and `RichTextBox` BackColor issue in dark mode, where the background color was the same as in Classic mode (LightMode). Note, that ComboBox internally uses depending on the ComboBox DropDownStyle different nested controls for different modes. So, practically there are inner ListBox and TextBox controls which had to be addressed: <img width="553" height="456" alt="image" src="https://github.com/user-attachments/assets/61821717-b509-4ecf-b703-89f7a0970c60" /> Again: Tested by CTI and made sure, that the Classic mode CodePaths remain as they were. ###### Microsoft Reviewers: [Open in CodeFlow](https://microsoft.github.io/open-pr/?codeflow=https://github.com/dotnet/winforms/pull/13721)
2 parents 61c8bce + 13733fc commit 17c060b

File tree

6 files changed

+181
-74
lines changed

6 files changed

+181
-74
lines changed

src/System.Windows.Forms/System/Windows/Forms/Control.cs

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -298,11 +298,6 @@ public unsafe partial class Control :
298298

299299
internal byte LayoutSuspendCount { get; private set; }
300300

301-
// Flag for the youngest control in the descendant-inheritance hierarchy as the
302-
// ultimate truth if the control - by having set the ControlStyles. -
303-
// indicates to participate in automatic dark mode theming or not.
304-
internal bool? _darkModeRequestState;
305-
306301
/// <summary>
307302
/// Initializes a new instance of the <see cref="Control"/> class.
308303
/// </summary>
@@ -491,6 +486,32 @@ internal int OriginalDeviceDpiInternal
491486
}
492487
}
493488

489+
/// <summary>
490+
/// Caches whether the youngest control in the inheritance hierarchy has requested
491+
/// participation in automatic dark mode theming based on its <see cref="ControlStyles"/> settings.
492+
/// </summary>
493+
/// <remarks>
494+
/// <para>
495+
/// This property acts as a cache for the dark mode theming request state due to the architectural pattern
496+
/// of Control inheritance in WinForms. The constructor of <see cref="Control"/> calls <see cref="CreateParams"/>,
497+
/// which means that <see cref="CreateParams"/> in derived controls is invoked before their respective constructors run.
498+
/// </para>
499+
/// <para>
500+
/// As a result, if a control indicates its intent to participate in dark mode theming by setting
501+
/// <see cref="ControlStyles.ApplyThemingImplicitly"/>, we need to cache this state in this internal property
502+
/// rather than relying on the setting it does in its constructor. This is necessary because theming decisions
503+
/// and related logic may be triggered during the base <see cref="Control"/> construction process, before the
504+
/// derived control's constructor has a chance to execute.
505+
/// </para>
506+
/// <para>
507+
/// Furthermore, since base classes (such as <c>TextBoxBase</c> or <c>ButtonBase</c>) may request dark mode
508+
/// theming, but a more derived control might opt out, this <see cref="DarkModeRequestState"/> property serves
509+
/// as the definitive source of truth for a particular control's dark mode participation. It ensures that the
510+
/// most derived control's preference is respected, regardless of what its ancestors may have set.
511+
/// </para>
512+
/// </remarks>
513+
internal bool? DarkModeRequestState { get; set; }
514+
494515
/// <summary>
495516
/// The Accessibility Object for this Control
496517
/// </summary>
@@ -7384,7 +7405,7 @@ protected virtual void OnHandleCreated(EventArgs e)
73847405

73857406
#pragma warning disable WFO5001 // Type is for evaluation purposes only and is subject to change or removal in future updates.
73867407
if (Application.IsDarkModeEnabled
7387-
&& _darkModeRequestState is true
7408+
&& DarkModeRequestState is true
73887409
&& !RecreatingHandle)
73897410
{
73907411
_ = PInvoke.SetWindowTheme(
@@ -9349,7 +9370,7 @@ internal virtual void RecreateHandleCore()
93499370
// ensure that the theming is applied to all child controls as well.
93509371
#pragma warning disable WFO5001
93519372
if (Application.IsDarkModeEnabled
9352-
&& _darkModeRequestState is true)
9373+
&& DarkModeRequestState is true)
93539374
{
93549375
_ = PInvoke.SetWindowTheme(
93559376
hwnd: HWND,
@@ -10345,9 +10366,9 @@ protected void SetStyle(ControlStyles flag, bool value)
1034510366
// reset those settings - the source of truth ultimately is the value which gets set
1034610367
// for the first time a control sets or clears this style explicitly in CreateParams.
1034710368
if ((flag & ControlStyles.ApplyThemingImplicitly) == ControlStyles.ApplyThemingImplicitly
10348-
&& !_darkModeRequestState.HasValue)
10369+
&& !DarkModeRequestState.HasValue)
1034910370
{
10350-
_darkModeRequestState = value;
10371+
DarkModeRequestState = value;
1035110372
}
1035210373
}
1035310374
#pragma warning restore WFO5001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
@@ -10412,7 +10433,7 @@ protected virtual void SetVisibleCore(bool value)
1041210433
// We shouldn't mess with the color mode if users haven't specifically set it.
1041310434
// https://github.com/dotnet/winforms/issues/12014
1041410435
// And we shouldn't prepare dark mode, if the form opted out.
10415-
if (value && Application.ColorModeSet && _darkModeRequestState is true)
10436+
if (value && Application.ColorModeSet && DarkModeRequestState is true)
1041610437
{
1041710438
PrepareDarkMode(HWND, Application.IsDarkModeEnabled);
1041810439
}

src/System.Windows.Forms/System/Windows/Forms/Controls/Buttons/Button.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,7 +168,7 @@ private protected override bool OwnerDraw
168168
&& Image is null
169169

170170
// ...the user wants to opt out of implicit DarkMode rendering.
171-
&& _darkModeRequestState is true
171+
&& DarkModeRequestState is true
172172

173173
// And all of this only counts for FlatStyle.Standard. For the
174174
// rest, we're using specific renderers anyway, which check

src/System.Windows.Forms/System/Windows/Forms/Controls/ComboBox/ComboBox.cs

Lines changed: 94 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -249,15 +249,31 @@ public AutoCompleteStringCollection AutoCompleteCustomSource
249249
}
250250
}
251251

252+
#pragma warning disable WFO5001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
252253
/// <summary>
253254
/// The background color of this control. This is an ambient property and
254255
/// will always return a non-null value.
255256
/// </summary>
256257
public override Color BackColor
257258
{
258-
get => ShouldSerializeBackColor() ? base.BackColor : SystemColors.Window;
259+
get
260+
{
261+
if (ShouldSerializeBackColor())
262+
{
263+
return base.BackColor;
264+
}
265+
else
266+
{
267+
return Application.IsDarkModeEnabled
268+
&& DarkModeRequestState is true
269+
? SystemColors.ControlDarkDark
270+
: SystemColors.Window;
271+
}
272+
}
273+
259274
set => base.BackColor = value;
260275
}
276+
#pragma warning restore WFO5001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
261277

262278
[Browsable(false)]
263279
[EditorBrowsable(EditorBrowsableState.Never)]
@@ -1841,11 +1857,8 @@ protected override void Dispose(bool disposing)
18411857
_autoCompleteCustomSource.CollectionChanged -= OnAutoCompleteCustomSourceChanged;
18421858
}
18431859

1844-
if (_stringSource is not null)
1845-
{
1846-
_stringSource.ReleaseAutoComplete();
1847-
_stringSource = null;
1848-
}
1860+
_stringSource?.ReleaseAutoComplete();
1861+
_stringSource = null;
18491862
}
18501863

18511864
base.Dispose(disposing);
@@ -2384,11 +2397,8 @@ protected override void OnHandleDestroyed(EventArgs e)
23842397
_selectedIndex = SelectedIndex;
23852398
}
23862399

2387-
if (_stringSource is not null)
2388-
{
2389-
_stringSource.ReleaseAutoComplete();
2390-
_stringSource = null;
2391-
}
2400+
_stringSource?.ReleaseAutoComplete();
2401+
_stringSource = null;
23922402

23932403
base.OnHandleDestroyed(e);
23942404
}
@@ -3042,23 +3052,14 @@ protected override void RefreshItem(int index)
30423052
/// </summary>
30433053
private void ReleaseChildWindow()
30443054
{
3045-
if (_childEdit is not null)
3046-
{
3047-
_childEdit.ReleaseHandle();
3048-
_childEdit = null;
3049-
}
3055+
_childEdit?.ReleaseHandle();
3056+
_childEdit = null;
30503057

3051-
if (_childListBox is not null)
3052-
{
3053-
_childListBox.ReleaseHandle();
3054-
_childListBox = null;
3055-
}
3058+
_childListBox?.ReleaseHandle();
3059+
_childListBox = null;
30563060

3057-
if (_childDropDown is not null)
3058-
{
3059-
_childDropDown.ReleaseHandle();
3060-
_childDropDown = null;
3061-
}
3061+
_childDropDown?.ReleaseHandle();
3062+
_childDropDown = null;
30623063
}
30633064

30643065
internal override void ReleaseUiaProvider(HWND handle)
@@ -3620,6 +3621,9 @@ private unsafe void WmReflectMeasureItem(ref Message m)
36203621
m.ResultInternal = (LRESULT)1;
36213622
}
36223623

3624+
private static readonly IntPtr s_darkEditBrush
3625+
= PInvokeCore.CreateSolidBrush(ColorTranslator.ToWin32(Color.FromArgb(64, 64, 64)));
3626+
36233627
/// <summary>
36243628
/// The ComboBox's window procedure. Inheriting classes can override this
36253629
/// to add extra functionality, but should not forget to call
@@ -3672,6 +3676,47 @@ protected override unsafe void WndProc(ref Message m)
36723676
}
36733677

36743678
break;
3679+
3680+
#pragma warning disable WFO5001
3681+
case PInvokeCore.WM_CTLCOLORSTATIC:
3682+
3683+
HWND hwndChild = (HWND)m.LParamInternal;
3684+
if (hwndChild == _childEdit?.HWND && Application.IsDarkModeEnabled)
3685+
{
3686+
PInvokeCore.SetBkColor(
3687+
(HDC)m.WParamInternal,
3688+
ColorTranslator.ToWin32(Color.FromArgb(64, 64, 64)));
3689+
3690+
PInvokeCore.SetTextColor(
3691+
(HDC)m.WParamInternal,
3692+
ColorTranslator.ToWin32(Color.FromArgb(180, 180, 180)));
3693+
3694+
m.ResultInternal = (LRESULT)s_darkEditBrush;
3695+
return;
3696+
}
3697+
3698+
// Additional handling for Simple style listbox when disabled
3699+
if (DropDownStyle == ComboBoxStyle.Simple
3700+
&& Application.IsDarkModeEnabled
3701+
&& !Enabled
3702+
&& hwndChild == _childListBox?.HWND)
3703+
{
3704+
PInvokeCore.SetBkColor(
3705+
(HDC)m.WParamInternal,
3706+
ColorTranslator.ToWin32(Color.FromArgb(64, 64, 64)));
3707+
3708+
PInvokeCore.SetTextColor(
3709+
(HDC)m.WParamInternal,
3710+
ColorTranslator.ToWin32(Color.FromArgb(180, 180, 180)));
3711+
3712+
m.ResultInternal = (LRESULT)s_darkEditBrush;
3713+
3714+
return;
3715+
}
3716+
#pragma warning restore WFO5001
3717+
3718+
break;
3719+
36753720
case PInvokeCore.WM_CTLCOLOREDIT:
36763721
case PInvokeCore.WM_CTLCOLORLISTBOX:
36773722
m.ResultInternal = (LRESULT)(nint)InitializeDCForWmCtlColor((HDC)(nint)m.WParamInternal, m.MsgInternal);
@@ -3767,6 +3812,29 @@ protected override unsafe void WndProc(ref Message m)
37673812
using Graphics g = Graphics.FromHdcInternal((IntPtr)dc);
37683813
FlatComboBoxAdapter.DrawFlatCombo(this, g);
37693814

3815+
#pragma warning disable WFO5001
3816+
// Special handling for disabled DropDownList in dark mode
3817+
if (Application.IsDarkModeEnabled && !Enabled && DropDownStyle == ComboBoxStyle.DropDownList)
3818+
{
3819+
// The text area for DropDownList (excluding the dropdown button)
3820+
Rectangle textBounds = ClientRectangle;
3821+
textBounds.Width -= SystemInformation.VerticalScrollBarWidth;
3822+
3823+
// Fill the background
3824+
using var bgBrush = new SolidBrush(Color.FromArgb(64, 64, 64));
3825+
g.FillRectangle(bgBrush, textBounds);
3826+
3827+
// Draw the text
3828+
TextRenderer.DrawText(
3829+
g,
3830+
Text,
3831+
Font,
3832+
textBounds,
3833+
Color.FromArgb(180, 180, 180),
3834+
TextFormatFlags.Left | TextFormatFlags.VerticalCenter | TextFormatFlags.EndEllipsis);
3835+
}
3836+
#pragma warning restore WFO5001
3837+
37703838
return;
37713839
}
37723840

src/System.Windows.Forms/System/Windows/Forms/Controls/ListView/ListView.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4673,7 +4673,7 @@ protected override void OnHandleCreated(EventArgs e)
46734673
private void ApplyDarkModeOnDemand()
46744674
{
46754675
if (Application.IsDarkModeEnabled
4676-
&& _darkModeRequestState is true)
4676+
&& DarkModeRequestState is true)
46774677
{
46784678
// Enable double buffering when in dark mode to reduce flicker.
46794679
uint exMask = PInvoke.LVS_EX_ONECLICKACTIVATE | PInvoke.LVS_EX_TWOCLICKACTIVATE |

0 commit comments

Comments
 (0)