Skip to content

Commit 4d55eb1

Browse files
[Preview7/Release] Backport 13747 DarkMode: SystemControlButton rendering (#13748)
Fixes System Control Button rendering issues in dark mode, where a control button is either rendering not in dark mode, or has rendering artefacts when rendering in the context of the property grid. These issues have been pointed out by customers, and are, among others, one of the blockers for GitHub Extensions, which are emotionally discussed on their GitHub issues gitextensions/gitextensions#9191 (comment) (and also might even save lives gitextensions/gitextensions#11904) Note while the latter is certainly an ironic-funny remark, those discussions around dark mode for WinForms app shows the high participation of the community and the desire to make this work, and note also that other issues which have been pointed out in other projects show us clearly, why we should accomodate rendering issues as early as possible to unblock them: GitExtension in particular but also other projects can provide their aspired .NET 10 based DarkMode-enabled versions as soon as .NET 10 becomes available. Fixes rendering issues in dark mode rendering, where a button is used in the context of another control: * DropDownArrow * UpDown based controls * Ellipse-Control in Property browsers These buttons have not been rendered correctly in dark mode, and a new system control button renderer (tested by CTI) fixes these issues. Note that this PR does not change the Classic (Light Mode) code paths in any way. Examples: <img width="230" height="60" alt="image" src="https://github.com/user-attachments/assets/9bd4750b-076d-45cd-9160-997b50ff98ad" /> --- <img width="565" height="72" alt="image" src="https://github.com/user-attachments/assets/26c4eba5-11cd-44ad-8964-bb3d586fee41" /> --- <img width="763" height="151" alt="image" src="https://github.com/user-attachments/assets/ce4f409f-051b-422a-aa1f-26c5a8470e0f" /> --- Current versions have been tested by CTI and they also reassured that the Classic mode CodePaths (LightMode rendering) remain unchanged to minimize any risks. ###### Microsoft Reviewers: [Open in CodeFlow](https://microsoft.github.io/open-pr/?codeflow=https://github.com/dotnet/winforms/pull/13748)
2 parents fbd28a7 + c35b79a commit 4d55eb1

File tree

8 files changed

+763
-138
lines changed

8 files changed

+763
-138
lines changed

src/System.Windows.Forms/System/Windows/Forms/Controls/PropertyGrid/PropertyGridInternal/DropDownButton.cs

Lines changed: 81 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System.Drawing;
55
using System.Windows.Forms.ButtonInternal;
66
using System.Windows.Forms.VisualStyles;
7+
using static System.Windows.Forms.ControlPaint;
78

89
namespace System.Windows.Forms.PropertyGridInternal;
910

@@ -17,7 +18,20 @@ public DropDownButton()
1718
SetAccessibleName();
1819
}
1920

20-
// When the holder is open, we don't fire clicks.
21+
/// <summary>
22+
/// Indicates whether the control should be rendered in dark mode.
23+
/// Set this property if you use this class for a control in dark mode.
24+
/// </summary>
25+
public bool RequestDarkModeRendering { get; set; }
26+
27+
/// <summary>
28+
/// Gets or sets the style used for rendering the control button.
29+
/// </summary>
30+
public ModernControlButtonStyle ControlButtonStyle { get; set; }
31+
32+
/// <summary>
33+
/// Gets or sets a value indicating whether mouse events should be ignored when the holder is open.
34+
/// </summary>
2135
public bool IgnoreMouse { get; set; }
2236

2337
/// <summary>
@@ -64,52 +78,82 @@ protected override void OnMouseDown(MouseEventArgs e)
6478
}
6579
}
6680

81+
#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.
6782
protected override void OnPaint(PaintEventArgs pevent)
6883
{
69-
base.OnPaint(pevent);
84+
ComboBoxState state = ComboBoxState.Normal;
7085

71-
#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.
72-
if (!Application.IsDarkModeEnabled
73-
&& (Application.RenderWithVisualStyles & _useComboBoxTheme))
86+
if (MouseIsDown)
7487
{
75-
ComboBoxState state = ComboBoxState.Normal;
76-
77-
if (MouseIsDown)
78-
{
79-
state = ComboBoxState.Pressed;
80-
}
81-
else if (MouseIsOver)
82-
{
83-
state = ComboBoxState.Hot;
84-
}
88+
state = ComboBoxState.Pressed;
89+
}
90+
else if (MouseIsOver)
91+
{
92+
state = ComboBoxState.Hot;
93+
}
8594

86-
Rectangle dropDownButtonRect = new(0, 0, Width, Height);
87-
if (state == ComboBoxState.Normal)
88-
{
89-
pevent.Graphics.FillRectangle(SystemBrushes.Window, dropDownButtonRect);
90-
}
95+
base.OnPaint(pevent);
9196

92-
using (DeviceContextHdcScope hdc = new(pevent))
97+
if (Application.IsDarkModeEnabled && RequestDarkModeRendering)
98+
{
99+
ModernControlButtonState buttonState = state switch
93100
{
94-
ComboBoxRenderer.DrawDropDownButtonForHandle(
95-
hdc,
96-
dropDownButtonRect,
97-
state,
98-
ScaleHelper.IsScalingRequirementMet ? HWNDInternal : HWND.Null);
99-
}
101+
ComboBoxState.Disabled => ModernControlButtonState.Disabled,
102+
ComboBoxState.Hot => ModernControlButtonState.Hover,
103+
ComboBoxState.Pressed => ModernControlButtonState.Pressed,
104+
_ => ModernControlButtonState.Normal
105+
};
106+
107+
DrawModernControlButton(
108+
pevent.Graphics,
109+
new Rectangle(0, 0, Width, Height),
110+
ControlButtonStyle,
111+
buttonState,
112+
isDarkMode: true);
113+
114+
return;
115+
}
100116

101-
// Redraw focus cues.
102-
//
103-
// For consistency with other PropertyGrid buttons, i.e. those opening system dialogs ("..."), that
104-
// always show visual cues when focused, we need to do the same for this custom button, painted as
105-
// a ComboBox control part (drop-down).
106-
if (Focused)
107-
{
108-
dropDownButtonRect.Inflate(-1, -1);
109-
ControlPaint.DrawFocusRectangle(pevent.Graphics, dropDownButtonRect, ForeColor, BackColor);
110-
}
117+
if (Application.RenderWithVisualStyles & _useComboBoxTheme)
118+
{
119+
RenderComboBoxButtonWithVisualStyles(pevent, state);
111120
}
121+
}
112122
#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.
123+
private void RenderComboBoxButtonWithVisualStyles(PaintEventArgs pevent, ComboBoxState state)
124+
{
125+
Rectangle dropDownButtonRect = new(0, 0, Width, Height);
126+
127+
if (state == ComboBoxState.Normal)
128+
{
129+
pevent.Graphics.FillRectangle(
130+
SystemBrushes.Window,
131+
dropDownButtonRect);
132+
}
133+
134+
using (DeviceContextHdcScope hdc = new(pevent))
135+
{
136+
ComboBoxRenderer.DrawDropDownButtonForHandle(
137+
hdc,
138+
dropDownButtonRect,
139+
state,
140+
ScaleHelper.IsScalingRequirementMet ? HWNDInternal : HWND.Null);
141+
}
142+
143+
// Redraw focus cues.
144+
//
145+
// For consistency with other PropertyGrid buttons, i.e. those opening system dialogs ("..."), that
146+
// always show visual cues when focused, we need to do the same for this custom button, painted as
147+
// a ComboBox control part (drop-down).
148+
if (Focused)
149+
{
150+
dropDownButtonRect.Inflate(-1, -1);
151+
DrawFocusRectangle(
152+
pevent.Graphics,
153+
dropDownButtonRect,
154+
ForeColor,
155+
BackColor);
156+
}
113157
}
114158

115159
internal void PerformButtonClick()

src/System.Windows.Forms/System/Windows/Forms/Controls/PropertyGrid/PropertyGridInternal/PropertyGridView.cs

Lines changed: 82 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
using Microsoft.Win32;
1212
using Windows.Win32.System.Variant;
1313
using Windows.Win32.UI.Accessibility;
14+
using static System.Windows.Forms.ControlPaint;
1415

1516
namespace System.Windows.Forms.PropertyGridInternal;
1617

@@ -190,33 +191,44 @@ public bool CanUndo
190191
/// the selected row's <see cref="GridEntry"/>.
191192
/// </para>
192193
/// </remarks>
194+
#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.
193195
internal DropDownButton DropDownButton
194196
{
195197
get
196198
{
197-
if (_dropDownButton is null)
199+
if (_dropDownButton is not null)
198200
{
199-
OwnerGrid.CheckInCreate();
201+
return _dropDownButton;
202+
}
200203

201-
_dropDownButton = new()
202-
{
203-
UseComboBoxTheme = true
204-
};
204+
OwnerGrid.CheckInCreate();
205205

206-
Bitmap bitmap = CreateResizedBitmap("Arrow", DownArrowIconWidth, DownArrowIconHeight);
207-
_dropDownButton.Image = bitmap;
208-
_dropDownButton.BackColor = SystemColors.Control;
209-
_dropDownButton.ForeColor = SystemColors.ControlText;
210-
_dropDownButton.Click += OnButtonClick;
211-
_dropDownButton.GotFocus += OnDropDownButtonGotFocus;
212-
_dropDownButton.LostFocus += OnChildLostFocus;
213-
_dropDownButton.TabIndex = 2;
206+
_dropDownButton = new()
207+
{
208+
UseComboBoxTheme = true,
209+
RequestDarkModeRendering = Application.IsDarkModeEnabled,
210+
ControlButtonStyle = ModernControlButtonStyle.OpenDropDown | ModernControlButtonStyle.RoundedBorder
211+
};
214212

215-
CommonEditorSetup(_dropDownButton);
216-
_dropDownButton.Size = ScaleHelper.IsScalingRequirementMet
217-
? new(SystemInformation.VerticalScrollBarArrowHeightForDpi(DeviceDpiInternal), RowHeight)
218-
: new(SystemInformation.VerticalScrollBarArrowHeight, RowHeight);
219-
}
213+
Bitmap bitmap = CreateResizedBitmap(
214+
"Arrow",
215+
DownArrowIconWidth,
216+
DownArrowIconHeight);
217+
218+
// For classic mode/backwards compatibility.
219+
_dropDownButton.Image = bitmap;
220+
_dropDownButton.BackColor = SystemColors.Control;
221+
_dropDownButton.ForeColor = SystemColors.ControlText;
222+
_dropDownButton.Click += OnButtonClick;
223+
_dropDownButton.GotFocus += OnDropDownButtonGotFocus;
224+
_dropDownButton.LostFocus += OnChildLostFocus;
225+
_dropDownButton.TabIndex = 2;
226+
227+
CommonEditorSetup(_dropDownButton);
228+
229+
_dropDownButton.Size = ScaleHelper.IsScalingRequirementMet
230+
? new(SystemInformation.VerticalScrollBarArrowHeightForDpi(DeviceDpiInternal), RowHeight)
231+
: new(SystemInformation.VerticalScrollBarArrowHeight, RowHeight);
220232

221233
return _dropDownButton;
222234
}
@@ -235,32 +247,42 @@ internal Button DialogButton
235247
{
236248
get
237249
{
238-
if (_dialogButton is null)
250+
if (_dialogButton is not null)
239251
{
240-
OwnerGrid.CheckInCreate();
252+
return _dialogButton;
253+
}
241254

242-
_dialogButton = new DropDownButton
243-
{
244-
BackColor = SystemColors.Control,
245-
ForeColor = SystemColors.ControlText,
246-
TabIndex = 3,
247-
Image = CreateResizedBitmap("dotdotdot", DotDotDotIconWidth, DotDotDotIconHeight)
248-
};
255+
OwnerGrid.CheckInCreate();
249256

250-
_dialogButton.Click += OnButtonClick;
251-
_dialogButton.KeyDown += OnButtonKeyDown;
252-
_dialogButton.GotFocus += OnDropDownButtonGotFocus;
253-
_dialogButton.LostFocus += OnChildLostFocus;
254-
_dialogButton.Size = ScaleHelper.IsScalingRequirementMet
255-
? new Size(SystemInformation.VerticalScrollBarArrowHeightForDpi(DeviceDpiInternal), RowHeight)
256-
: new Size(SystemInformation.VerticalScrollBarArrowHeight, RowHeight);
257+
_dialogButton = new DropDownButton
258+
{
259+
RequestDarkModeRendering = Application.IsDarkModeEnabled,
260+
ControlButtonStyle = ModernControlButtonStyle.Ellipse | ModernControlButtonStyle.RoundedBorder,
261+
BackColor = SystemColors.Control,
262+
ForeColor = SystemColors.ControlText,
263+
TabIndex = 3,
257264

258-
CommonEditorSetup(_dialogButton);
259-
}
265+
// For classic mode/backwards compatibility.
266+
Image = CreateResizedBitmap(
267+
"dotdotdot",
268+
DotDotDotIconWidth,
269+
DotDotDotIconHeight)
270+
};
271+
272+
_dialogButton.Click += OnButtonClick;
273+
_dialogButton.KeyDown += OnButtonKeyDown;
274+
_dialogButton.GotFocus += OnDropDownButtonGotFocus;
275+
_dialogButton.LostFocus += OnChildLostFocus;
276+
_dialogButton.Size = ScaleHelper.IsScalingRequirementMet
277+
? new Size(SystemInformation.VerticalScrollBarArrowHeightForDpi(DeviceDpiInternal), RowHeight)
278+
: new Size(SystemInformation.VerticalScrollBarArrowHeight, RowHeight);
279+
280+
CommonEditorSetup(_dialogButton);
260281

261282
return _dialogButton;
262283
}
263284
}
285+
#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.
264286

265287
/// <summary>
266288
/// The common text box for editing values.
@@ -269,30 +291,32 @@ private GridViewTextBox EditTextBox
269291
{
270292
get
271293
{
272-
if (_editTextBox is null)
294+
if (_editTextBox is not null)
273295
{
274-
OwnerGrid.CheckInCreate();
296+
return _editTextBox;
297+
}
275298

276-
_editTextBox = new(this)
277-
{
278-
BorderStyle = BorderStyle.None,
279-
AutoSize = false,
280-
TabStop = false,
281-
AcceptsReturn = true,
282-
BackColor = BackColor,
283-
ForeColor = ForeColor
284-
};
299+
OwnerGrid.CheckInCreate();
285300

286-
_editTextBox.KeyDown += OnEditKeyDown;
287-
_editTextBox.KeyPress += OnEditKeyPress;
288-
_editTextBox.GotFocus += OnEditGotFocus;
289-
_editTextBox.LostFocus += OnEditLostFocus;
290-
_editTextBox.MouseDown += OnEditMouseDown;
291-
_editTextBox.TextChanged += OnEditChange;
301+
_editTextBox = new(this)
302+
{
303+
BorderStyle = BorderStyle.None,
304+
AutoSize = false,
305+
TabStop = false,
306+
AcceptsReturn = true,
307+
BackColor = BackColor,
308+
ForeColor = ForeColor
309+
};
292310

293-
_editTextBox.TabIndex = 1;
294-
CommonEditorSetup(_editTextBox);
295-
}
311+
_editTextBox.KeyDown += OnEditKeyDown;
312+
_editTextBox.KeyPress += OnEditKeyPress;
313+
_editTextBox.GotFocus += OnEditGotFocus;
314+
_editTextBox.LostFocus += OnEditLostFocus;
315+
_editTextBox.MouseDown += OnEditMouseDown;
316+
_editTextBox.TextChanged += OnEditChange;
317+
318+
_editTextBox.TabIndex = 1;
319+
CommonEditorSetup(_editTextBox);
296320

297321
return _editTextBox;
298322
}
@@ -2210,7 +2234,7 @@ protected override void OnGotFocus(EventArgs e)
22102234
if ((Size.Width > doubleOffset) && (Size.Height > doubleOffset))
22112235
{
22122236
using Graphics g = CreateGraphicsInternal();
2213-
ControlPaint.DrawFocusRectangle(g, new Rectangle(_offset2Units, _offset2Units, Size.Width - doubleOffset, Size.Height - doubleOffset));
2237+
DrawFocusRectangle(g, new Rectangle(_offset2Units, _offset2Units, Size.Width - doubleOffset, Size.Height - doubleOffset));
22142238
}
22152239
}
22162240
}

0 commit comments

Comments
 (0)