From 4a29a8f0fdf138154d515efc2d123f9ff23c1779 Mon Sep 17 00:00:00 2001 From: Klaus Loeffelmann Date: Wed, 21 May 2025 00:19:21 -0700 Subject: [PATCH 01/10] Fix DarkMode StatusStrip background renderer. --- .../System/Windows/Forms/Control.cs | 62 +++++++++++++------ .../Forms/Controls/ToolStrips/StatusStrip.cs | 2 +- .../ToolStrips/ToolStripSystemRenderer.cs | 4 +- 3 files changed, 47 insertions(+), 21 deletions(-) diff --git a/src/System.Windows.Forms/System/Windows/Forms/Control.cs b/src/System.Windows.Forms/System/Windows/Forms/Control.cs index 9300f40dcc6..27adec63d11 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Control.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Control.cs @@ -814,6 +814,7 @@ public virtual object? DataContext { Properties.RemoveValue(s_dataContextProperty); OnDataContextChanged(EventArgs.Empty); + return; } @@ -7332,6 +7333,18 @@ protected virtual void OnHandleCreated(EventArgs e) SetWindowFont(); } +#pragma warning disable WFO5001 // Type is for evaluation purposes only and is subject to change or removal in future updates. + if (Application.IsDarkModeEnabled && GetStyle(ControlStyles.ApplyThemingImplicitly)) + { + HRESULT result = PInvoke.SetWindowTheme( + hwnd: HWND, + pszSubAppName: $"{DarkModeIdentifier}_{ExplorerThemeIdentifier}", + pszSubIdList: null); + + Debug.Assert(result.Succeeded, "SetWindowTheme failed with HRESULT: " + result); + } +#pragma warning restore WFO5001 + HandleHighDpi(); // Restore drag drop status. Ole Initialize happens when the ThreadContext in Application is created. @@ -10388,7 +10401,9 @@ protected virtual void SetVisibleCore(bool value) PrepareDarkMode(HWND, Application.IsDarkModeEnabled); } - PInvoke.ShowWindow(HWND, value ? ShowParams : SHOW_WINDOW_CMD.SW_HIDE); + PInvoke.ShowWindow(HWND, value + ? ShowParams + : SHOW_WINDOW_CMD.SW_HIDE); } } #pragma warning restore WFO5001 @@ -10400,6 +10415,7 @@ protected virtual void SetVisibleCore(bool value) SetState(States.Visible, value); fireChange = true; + try { if (value) @@ -10408,14 +10424,19 @@ protected virtual void SetVisibleCore(bool value) } PInvoke.SetWindowPos( - this, - HWND.Null, - 0, 0, 0, 0, - SET_WINDOW_POS_FLAGS.SWP_NOSIZE + hWnd: this, + hWndInsertAfter: HWND.Null, + X: 0, + Y: 0, + cx: 0, + cy: 0, + uFlags: SET_WINDOW_POS_FLAGS.SWP_NOSIZE | SET_WINDOW_POS_FLAGS.SWP_NOMOVE | SET_WINDOW_POS_FLAGS.SWP_NOZORDER | SET_WINDOW_POS_FLAGS.SWP_NOACTIVATE - | (value ? SET_WINDOW_POS_FLAGS.SWP_SHOWWINDOW : SET_WINDOW_POS_FLAGS.SWP_HIDEWINDOW)); + | (value + ? SET_WINDOW_POS_FLAGS.SWP_SHOWWINDOW + : SET_WINDOW_POS_FLAGS.SWP_HIDEWINDOW)); } catch { @@ -10469,27 +10490,30 @@ protected virtual void SetVisibleCore(bool value) if (IsHandleCreated) { PInvoke.SetWindowPos( - this, - HWND.HWND_TOP, - 0, 0, 0, 0, - SET_WINDOW_POS_FLAGS.SWP_NOSIZE + hWnd: this, + hWndInsertAfter: HWND.HWND_TOP, + X: 0, + Y: 0, + cx: 0, + cy: 0, + uFlags: SET_WINDOW_POS_FLAGS.SWP_NOSIZE | SET_WINDOW_POS_FLAGS.SWP_NOMOVE | SET_WINDOW_POS_FLAGS.SWP_NOZORDER | SET_WINDOW_POS_FLAGS.SWP_NOACTIVATE | (value ? SET_WINDOW_POS_FLAGS.SWP_SHOWWINDOW : SET_WINDOW_POS_FLAGS.SWP_HIDEWINDOW)); } } + } - static unsafe void PrepareDarkMode(HWND hwnd, bool darkModeEnabled) - { - BOOL value = darkModeEnabled; + private static unsafe void PrepareDarkMode(HWND hwnd, bool darkModeEnabled) + { + BOOL value = darkModeEnabled; - PInvoke.DwmSetWindowAttribute( - hwnd, - DWMWINDOWATTRIBUTE.DWMWA_USE_IMMERSIVE_DARK_MODE, - &value, - (uint)sizeof(BOOL)).AssertSuccess(); - } + PInvoke.DwmSetWindowAttribute( + hwnd, + DWMWINDOWATTRIBUTE.DWMWA_USE_IMMERSIVE_DARK_MODE, + &value, + (uint)sizeof(BOOL)).AssertSuccess(); } /// diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/StatusStrip.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/StatusStrip.cs index db4ae69246c..e9fd8229e03 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/StatusStrip.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/StatusStrip.cs @@ -118,7 +118,7 @@ public override DockStyle Dock set => base.LayoutStyle = value; } - // we do some custom stuff with padding to accomodate size grip. + // we do some custom stuff with padding to accommodate size grip. // changing this is not supported at DT [Browsable(false)] public new Padding Padding diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStripSystemRenderer.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStripSystemRenderer.cs index a8539cc6c51..ee0b15b750f 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStripSystemRenderer.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStripSystemRenderer.cs @@ -766,9 +766,10 @@ private static void RenderStatusStripBorder(ToolStripRenderEventArgs e) } } +#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. private static void RenderStatusStripBackground(ToolStripRenderEventArgs e) { - if (Application.RenderWithVisualStyles) + if (!Application.IsDarkModeEnabled && Application.RenderWithVisualStyles) { VisualStyleRenderer vsRenderer = VisualStyleRenderer!; vsRenderer.SetParameters(VisualStyleElement.Status.Bar.Normal); @@ -782,6 +783,7 @@ private static void RenderStatusStripBackground(ToolStripRenderEventArgs e) } } } +#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. private static void RenderLabelInternal(ToolStripItemRenderEventArgs e) { From 97a5e9fde63552dfef7a0a2a7faa7ef3e65a86ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Klaus=20L=C3=B6ffelmann?= Date: Wed, 23 Apr 2025 22:40:08 -0700 Subject: [PATCH 02/10] Implement ToolStripSystemDarkModeRenderer. --- .../Controls/ToolStrips/ToolStripManager.cs | 22 +- .../ToolStripSystemDarkModeRenderer.cs | 752 ++++++++++++++++++ .../ToolStrips/ToolStripSystemRenderer.cs | 108 ++- 3 files changed, 835 insertions(+), 47 deletions(-) create mode 100644 src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStripSystemDarkModeRenderer.cs diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStripManager.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStripManager.cs index 1ebaa91aefb..7d3673213cb 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStripManager.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStripManager.cs @@ -546,24 +546,12 @@ public static bool VisualStylesEnabled internal static ToolStripRenderer CreateRenderer(ToolStripManagerRenderMode renderMode) { - switch (renderMode) + return renderMode switch { - case ToolStripManagerRenderMode.System: - return new ToolStripSystemRenderer(isDefault: true); - case ToolStripManagerRenderMode.Professional: -#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. - if (Application.IsDarkModeEnabled) - { - return new ToolStripProfessionalRenderer(new DarkProfessionalColors()); - } -#pragma warning restore WFO5001 - - return new ToolStripProfessionalRenderer(isDefault: true); - - case ToolStripManagerRenderMode.Custom: - default: - return new ToolStripSystemRenderer(isDefault: true); - } + ToolStripManagerRenderMode.System => new ToolStripSystemRenderer(isDefault: true), + ToolStripManagerRenderMode.Professional => new ToolStripProfessionalRenderer(isDefault: true), + _ => new ToolStripSystemRenderer(isDefault: true), + }; } internal static ToolStripRenderer CreateRenderer(ToolStripRenderMode renderMode) diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStripSystemDarkModeRenderer.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStripSystemDarkModeRenderer.cs new file mode 100644 index 00000000000..59ad60ac31f --- /dev/null +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStripSystemDarkModeRenderer.cs @@ -0,0 +1,752 @@ +// 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; + +namespace System.Windows.Forms; + +/// +/// Provides dark mode rendering capabilities for ToolStrip controls in Windows Forms. +/// +/// +/// +/// This renderer is designed to be used with the ToolStripSystemRenderer to provide dark mode +/// styling while maintaining accessibility features. It inherits from ToolStripRenderer +/// and overrides necessary methods to provide dark-themed rendering. +/// +/// +internal class ToolStripSystemDarkModeRenderer : ToolStripRenderer +{ + /// + /// Initializes a new instance of the ToolStripSystemDarkModeRenderer class. + /// + public ToolStripSystemDarkModeRenderer() + { + } + + /// + /// Initializes a new instance of the ToolStripSystemDarkModeRenderer class with the specified default state. + /// + /// true if this is the default renderer; otherwise, false. + internal ToolStripSystemDarkModeRenderer(bool isDefault) : base(isDefault) + { + } + + /// + /// Gets dark-appropriate system colors based on the control type. + /// + /// The color to convert to a dark mode equivalent. + /// A color suitable for dark mode. + private static Color GetDarkModeColor(Color color) + { + // Map common system colors to their dark mode equivalents + if (color == SystemColors.Control) + return Color.FromArgb(45, 45, 45); + if (color == SystemColors.ControlLight) + return Color.FromArgb(60, 60, 60); + if (color == SystemColors.ControlDark) + return Color.FromArgb(30, 30, 30); + if (color == SystemColors.ControlText) + return Color.FromArgb(240, 240, 240); + if (color == SystemColors.ButtonFace) + return Color.FromArgb(45, 45, 45); + if (color == SystemColors.Highlight) + return Color.FromArgb(0, 120, 215); + if (color == SystemColors.HighlightText) + return Color.White; + if (color == SystemColors.Window) + return Color.FromArgb(32, 32, 32); + if (color == SystemColors.WindowText) + return Color.FromArgb(240, 240, 240); + if (color == SystemColors.GrayText) + return Color.FromArgb(153, 153, 153); + if (color == SystemColors.InactiveBorder) + return Color.FromArgb(70, 70, 70); + if (color == SystemColors.ButtonHighlight) + return Color.FromArgb(80, 80, 80); + if (color == SystemColors.ButtonShadow) + return Color.FromArgb(20, 20, 20); + if (color == SystemColors.Menu) + return Color.FromArgb(45, 45, 45); + if (color == SystemColors.MenuText) + return Color.FromArgb(240, 240, 240); + + // For any other colors, darken them if they're bright + if (color.GetBrightness() > 0.5) + { + // Create a darker version for light colors + return ControlPaint.Dark(color, 0.2f); + } + + return color; + } + + /// + /// Creates a dark mode compatible brush. + /// + /// The system color to convert. + /// A brush with the dark mode color. + private static SolidBrush GetDarkModeBrush(Color color) + { + return new SolidBrush(GetDarkModeColor(color)); + } + + /// + /// Creates a dark mode compatible pen. + /// + /// The system color to convert. + /// A pen with the dark mode color. + private static Pen GetDarkModePen(Color color) + { + return new Pen(GetDarkModeColor(color)); + } + + /// + /// Returns whether the background should be painted. + /// + /// The ToolStrip to check. + /// true if the background should be painted; otherwise, false. + private static bool ShouldPaintBackground(ToolStrip toolStrip) + { + if (toolStrip is null) + return true; + + if (toolStrip.BackgroundImage is not null) + return false; + + return true; + } + + /// + /// Fills the background with the specified color. + /// + /// The Graphics to draw on. + /// The bounds to fill. + /// The background color. + private static void FillBackground(Graphics g, Rectangle bounds, Color backColor) + { + if (bounds.Width <= 0 || bounds.Height <= 0) + return; + + // Use a dark mode color + using Brush brush = GetDarkModeBrush(backColor); + g.FillRectangle(brush, bounds); + } + + /// + /// Raises the RenderToolStripBackground event. + /// + /// A ToolStripRenderEventArgs that contains the event data. + protected override void OnRenderToolStripBackground(ToolStripRenderEventArgs e) + { + ArgumentNullException.ThrowIfNull(e); + + ToolStrip toolStrip = e.ToolStrip; + Graphics g = e.Graphics; + Rectangle bounds = e.AffectedBounds; + + if (!ShouldPaintBackground(toolStrip)) + return; + + if (toolStrip is StatusStrip) + { + RenderStatusStripBackground(e); + } + else + { + if (toolStrip.IsDropDown) + { + // Dark mode dropdown background + FillBackground(g, bounds, GetDarkModeColor(SystemColors.Menu)); + } + else if (toolStrip is MenuStrip) + { + // Dark mode menu background + FillBackground(g, bounds, GetDarkModeColor(SystemColors.Menu)); + } + else + { + // Standard ToolStrip background + FillBackground(g, bounds, GetDarkModeColor(e.BackColor)); + } + } + } + + /// + /// Renders the StatusStrip background in dark mode. + /// + /// A ToolStripRenderEventArgs that contains the event data. + private static void RenderStatusStripBackground(ToolStripRenderEventArgs e) + { + Graphics g = e.Graphics; + Rectangle bounds = e.AffectedBounds; + + // Dark mode StatusStrip background + FillBackground(g, bounds, GetDarkModeColor(SystemColors.Control)); + } + + /// + /// Raises the RenderToolStripBorder event. + /// + /// A ToolStripRenderEventArgs that contains the event data. + protected override void OnRenderToolStripBorder(ToolStripRenderEventArgs e) + { + ArgumentNullException.ThrowIfNull(e); + + ToolStrip toolStrip = e.ToolStrip; + Graphics g = e.Graphics; + Rectangle bounds = e.ToolStrip.ClientRectangle; + + if (toolStrip is StatusStrip) + { + RenderStatusStripBorder(e); + } + else if (toolStrip is ToolStripDropDown) + { + ToolStripDropDown? toolStripDropDown = toolStrip as ToolStripDropDown; + + Debug.Assert(toolStripDropDown is not null, $"ToolStripDropDown cannot be null in {nameof(OnRenderToolStripBorder)}."); + + if (toolStripDropDown.DropShadowEnabled) + { + bounds.Width -= 1; + bounds.Height -= 1; + + using Pen borderPen = GetDarkModePen(SystemColors.ControlDark); + g.DrawRectangle(borderPen, bounds); + } + else + { + using Pen borderPen = GetDarkModePen(SystemColors.ControlDark); + g.DrawRectangle(borderPen, bounds); + } + } + else + { + // Draw a subtle bottom border for toolstrips + using Pen borderPen = GetDarkModePen(SystemColors.ControlDark); + g.DrawLine(borderPen, 0, bounds.Bottom - 1, bounds.Width, bounds.Bottom - 1); + } + } + + /// + /// Renders the StatusStrip border in dark mode. + /// + /// A ToolStripRenderEventArgs that contains the event data. + private static void RenderStatusStripBorder(ToolStripRenderEventArgs e) + { + Graphics g = e.Graphics; + Rectangle bounds = e.ToolStrip.ClientRectangle; + + // Dark mode StatusStrip border (usually top border only) + using Pen borderPen = GetDarkModePen(SystemColors.ControlDark); + g.DrawLine(borderPen, 0, 0, bounds.Width, 0); + } + + /// + /// Raises the RenderItemBackground event. + /// + /// A ToolStripItemRenderEventArgs that contains the event data. + protected override void OnRenderItemBackground(ToolStripItemRenderEventArgs e) + { + ArgumentNullException.ThrowIfNull(e); + + Rectangle bounds = new Rectangle(Point.Empty, e.Item.Size); + + // For items on dropdowns, adjust the bounds + if (e.Item.IsOnDropDown) + { + bounds.X += 2; + bounds.Width -= 3; + } + + if (e.Item.Selected || e.Item.Pressed) + { + // Dark mode selection highlight + using Brush highlightBrush = GetDarkModeBrush(SystemColors.Highlight); + e.Graphics.FillRectangle(highlightBrush, bounds); + } + else + { + // Render background image if available + if (e.Item.BackgroundImage is not null) + { + ControlPaint.DrawBackgroundImage( + e.Graphics, + e.Item.BackgroundImage, + GetDarkModeColor(e.Item.BackColor), + e.Item.BackgroundImageLayout, + e.Item.ContentRectangle, + bounds); + } + else if (e.Item.BackColor != Color.Transparent && e.Item.BackColor != Color.Empty) + { + // Custom background color (apply dark mode transformation) + FillBackground(e.Graphics, bounds, e.Item.BackColor); + } + } + } + + /// + /// Raises the RenderButtonBackground event. + /// + /// A ToolStripItemRenderEventArgs that contains the event data. + protected override void OnRenderButtonBackground(ToolStripItemRenderEventArgs e) + { + ArgumentNullException.ThrowIfNull(e); + + Rectangle bounds = new Rectangle(Point.Empty, e.Item.Size); + bool isPressed; + bool isSelected; + + if (e.Item is ToolStripButton button) + { + isPressed = button.Pressed; + isSelected = button.Selected || button.Checked; + } + else + { + isPressed = e.Item.Pressed; + isSelected = e.Item.Selected; + } + + if (isPressed || isSelected) + { + Color fillColor = isPressed + ? GetDarkModeColor(SystemColors.ControlDark) + : GetDarkModeColor(SystemColors.Highlight); + + using Brush fillBrush = new SolidBrush(fillColor); + + e.Graphics.FillRectangle(fillBrush, bounds); + } + } + + /// + /// Raises the RenderDropDownButtonBackground event. + /// + /// A ToolStripItemRenderEventArgs that contains the event data. + protected override void OnRenderDropDownButtonBackground(ToolStripItemRenderEventArgs e) + { + // Reuse button background drawing for dropdown buttons + OnRenderButtonBackground(e); + } + + /// + /// Raises the RenderSplitButtonBackground event. + /// + /// A ToolStripItemRenderEventArgs that contains the event data. + protected override void OnRenderSplitButtonBackground(ToolStripItemRenderEventArgs e) + { + ArgumentNullException.ThrowIfNull(e); + + if (e.Item is not ToolStripSplitButton splitButton) + { + return; + } + + Rectangle bounds = new Rectangle(Point.Empty, e.Item.Size); + + // Render the background based on state + if (splitButton.Selected || splitButton.Pressed) + { + Color fillColor = splitButton.Pressed + ? GetDarkModeColor(SystemColors.ControlDark) + : GetDarkModeColor(SystemColors.Highlight); + + using Brush fillBrush = new SolidBrush(fillColor); + e.Graphics.FillRectangle(fillBrush, bounds); + } + + // Draw the split line + Rectangle dropDownRect = splitButton.DropDownButtonBounds; + using Pen linePen = GetDarkModePen(SystemColors.ControlDark); + + e.Graphics.DrawLine( + linePen, + dropDownRect.Left - 1, + dropDownRect.Top + 2, + dropDownRect.Left - 1, + dropDownRect.Bottom - 2); + } + + /// + /// Raises the RenderSeparator event. + /// + /// A ToolStripSeparatorRenderEventArgs that contains the event data. + protected override void OnRenderSeparator(ToolStripSeparatorRenderEventArgs e) + { + ArgumentNullException.ThrowIfNull(e); + + Rectangle bounds = e.Item.ContentRectangle; + bool isVertical = e.Vertical; + Graphics g = e.Graphics; + + if (bounds.Width <= 0 || bounds.Height <= 0) + { + return; + } + + Color foreColor = GetDarkModeColor(SystemColors.ControlDark); + + bool rightToLeft = e.Item.RightToLeft == RightToLeft.Yes; + + using Pen foreColorPen = new Pen(foreColor); + + if (isVertical) + { + // Vertical separator + using Pen leftPen = rightToLeft + ? foreColorPen + : GetDarkModePen(SystemColors.ButtonHighlight); + + using Pen rightPen = rightToLeft + ? GetDarkModePen(SystemColors.ButtonHighlight) + : foreColorPen; + + int startX = bounds.Width / 2; + g.DrawLine(leftPen, startX, bounds.Top, startX, bounds.Bottom); + startX++; + g.DrawLine(rightPen, startX, bounds.Top, startX, bounds.Bottom); + } + else + { + // Horizontal separator + if (bounds.Width >= 4) + { + bounds.Inflate(-2, 0); // Scoot over 2px and start drawing + } + + int startY = bounds.Height / 2; + g.DrawLine(foreColorPen, bounds.Left, startY, bounds.Right, startY); + startY++; + g.DrawLine(GetDarkModePen(SystemColors.ButtonHighlight), bounds.Left, startY, bounds.Right, startY); + } + } + + /// + /// Raises the RenderOverflowButtonBackground event. + /// + /// A ToolStripItemRenderEventArgs that contains the event data. + protected override void OnRenderOverflowButtonBackground(ToolStripItemRenderEventArgs e) + { + ArgumentNullException.ThrowIfNull(e); + + if (e.Item is not ToolStripOverflowButton item) + return; + + Rectangle bounds = new Rectangle(Point.Empty, e.Item.Size); + + // Render the background based on state + if (item.Selected || item.Pressed) + { + Color fillColor = item.Pressed ? + GetDarkModeColor(SystemColors.ControlDark) : + GetDarkModeColor(SystemColors.Highlight); + + using Brush fillBrush = new SolidBrush(fillColor); + e.Graphics.FillRectangle(fillBrush, bounds); + } + + // Draw the overflow arrow + Rectangle arrowRect = item.ContentRectangle; + + Point middle = new Point( + arrowRect.Left + arrowRect.Width / 2, + arrowRect.Top + arrowRect.Height / 2); + + // Default to down arrow for overflow buttons + ArrowDirection arrowDirection = ArrowDirection.Down; + + // Determine actual direction based on dropdown direction + ToolStripDropDownDirection direction = item.DropDownDirection; + + if (direction is ToolStripDropDownDirection.AboveLeft or ToolStripDropDownDirection.AboveRight) + { + arrowDirection = ArrowDirection.Up; + } + else if (direction == ToolStripDropDownDirection.Left) + { + arrowDirection = ArrowDirection.Left; + } + else if (direction == ToolStripDropDownDirection.Right) + { + arrowDirection = ArrowDirection.Right; + } + + // else default to ArrowDirection.Down + + // Set arrow color based on state + Color arrowColor = GetDarkModeColor(SystemColors.ControlText); + + if (item.Pressed || item.Selected) + { + arrowColor = GetDarkModeColor(SystemColors.HighlightText); + } + + // Define arrow polygon based on direction + Point[] arrow = arrowDirection switch + { + ArrowDirection.Up => + [ + new Point(middle.X - 2, middle.Y + 1), + new Point(middle.X + 3, middle.Y + 1), + new Point(middle.X, middle.Y - 2) + ], + ArrowDirection.Left => + [ + new Point(middle.X + 2, middle.Y - 4), + new Point(middle.X + 2, middle.Y + 4), + new Point(middle.X - 2, middle.Y) + ], + ArrowDirection.Right => + [ + new Point(middle.X - 2, middle.Y - 4), + new Point(middle.X - 2, middle.Y + 4), + new Point(middle.X + 2, middle.Y) + ], + _ => + [ + new Point(middle.X - 2, middle.Y - 1), + new Point(middle.X + 3, middle.Y - 1), + new Point(middle.X, middle.Y + 2) + ], + }; + + // Draw the arrow + using Brush arrowBrush = new SolidBrush(arrowColor); + e.Graphics.FillPolygon(arrowBrush, arrow); + } + + /// + /// Raises the RenderGrip event. + /// + /// A ToolStripGripRenderEventArgs that contains the event data. + protected override void OnRenderGrip(ToolStripGripRenderEventArgs e) + { + ArgumentNullException.ThrowIfNull(e); + + if (e.GripBounds.Width <= 0 || e.GripBounds.Height <= 0) + return; + + // Use dark mode colors for the grip dots + Color darkColor = GetDarkModeColor(SystemColors.ControlDark); + Color lightColor = GetDarkModeColor(SystemColors.ControlLight); + + ToolStrip toolStrip = e.ToolStrip; + Graphics g = e.Graphics; + Rectangle bounds = e.GripBounds; + + // Draw grip dots + if (toolStrip.Orientation == Orientation.Horizontal) + { + // Draw vertical grip + int y = bounds.Top + 2; + + while (y < bounds.Bottom - 3) + { + g.FillRectangle(new SolidBrush(darkColor), bounds.Left + 2, y, 1, 1); + g.FillRectangle(new SolidBrush(lightColor), bounds.Left + 3, y + 1, 1, 1); + y += 3; + } + } + else + { + // Draw horizontal grip + int x = bounds.Left + 2; + + while (x < bounds.Right - 3) + { + g.FillRectangle(new SolidBrush(darkColor), x, bounds.Top + 2, 1, 1); + g.FillRectangle(new SolidBrush(lightColor), x + 1, bounds.Top + 3, 1, 1); + x += 3; + } + } + } + + /// + /// Raises the RenderArrow event. + /// + /// A ToolStripArrowRenderEventArgs that contains the event data. + protected override void OnRenderArrow(ToolStripArrowRenderEventArgs e) + { + ArgumentNullException.ThrowIfNull(e); + Debug.Assert(e.Item is not null, "The ToolStripItem should not be null on rendering the Arrow."); + + Color arrowColor = GetDarkModeColor(e.ArrowColor); + + // Use white arrow for selected/highlighted items + if (e.Item.Selected || e.Item.Pressed) + { + arrowColor = GetDarkModeColor(SystemColors.HighlightText); + } + + // Draw the arrow with the appropriate color + Rectangle rect = e.ArrowRectangle; + Graphics g = e.Graphics; + + Point middle = new Point( + rect.Left + rect.Width / 2, + rect.Top + rect.Height / 2); + + // Define the arrow polygon based on direction + Point[] arrow; + + arrow = e.Direction switch + { + ArrowDirection.Up => + [ + new Point(middle.X - 2, middle.Y + 1), + new Point(middle.X + 3, middle.Y + 1), + new Point(middle.X, middle.Y - 2) + ], + ArrowDirection.Left => + [ + new Point(middle.X + 2, middle.Y - 4), + new Point(middle.X + 2, middle.Y + 4), + new Point(middle.X - 2, middle.Y) + ], + ArrowDirection.Right => + [ + new Point(middle.X - 2, middle.Y - 4), + new Point(middle.X - 2, middle.Y + 4), + new Point(middle.X + 2, middle.Y) + ], + _ => + [ + new Point(middle.X - 2, middle.Y - 1), + new Point(middle.X + 3, middle.Y - 1), + new Point(middle.X, middle.Y + 2) + ] + }; + + using Brush brush = new SolidBrush(arrowColor); + g.FillPolygon(brush, arrow); + } + + /// + /// Raises the RenderImageMargin event. + /// + /// A ToolStripRenderEventArgs that contains the event data. + protected override void OnRenderImageMargin(ToolStripRenderEventArgs e) + { + ArgumentNullException.ThrowIfNull(e); + + // Fill the image margin with a slightly different color than the background + Color marginColor = GetDarkModeColor(SystemColors.ControlLight); + using Brush brush = new SolidBrush(marginColor); + e.Graphics.FillRectangle(brush, e.AffectedBounds); + } + + /// + /// Raises the RenderItemText event. + /// + /// A ToolStripItemTextRenderEventArgs that contains the event data. + protected override void OnRenderItemText(ToolStripItemTextRenderEventArgs e) + { + ArgumentNullException.ThrowIfNull(e); + + // Set text color based on selection state + Color textColor; + + if (e.Item.Selected || e.Item.Pressed) + { + textColor = GetDarkModeColor(SystemColors.HighlightText); + } + else if (!e.Item.Enabled) + { + textColor = GetDarkModeColor(SystemColors.GrayText); + } + else + { + // Use the original text color but make sure it's dark mode compatible + textColor = GetDarkModeColor(e.TextColor); + } + + // Draw the text + TextRenderer.DrawText( + e.Graphics, + e.Text, + e.TextFont, + e.TextRectangle, + textColor, + e.TextFormat); + } + + /// + /// Raises the RenderItemImage event. + /// + /// A ToolStripItemImageRenderEventArgs that contains the event data. + protected override void OnRenderItemImage(ToolStripItemImageRenderEventArgs e) + { + ArgumentNullException.ThrowIfNull(e); + + if (e.Image is null) + return; + + // Draw the image normally, no need to adjust for dark mode as images + // should already be designed for the appropriate context + if (!e.Item.Enabled) + { + // For disabled items, draw a grayscale version + ControlPaint.DrawImageDisabled( + e.Graphics, + e.Image, + e.ImageRectangle.X, + e.ImageRectangle.Y, + GetDarkModeColor(SystemColors.Control)); + } + else + { + e.Graphics.DrawImage(e.Image, e.ImageRectangle); + } + } + + /// + /// Raises the RenderLabelBackground event. + /// + /// A ToolStripItemRenderEventArgs that contains the event data. + protected override void OnRenderLabelBackground(ToolStripItemRenderEventArgs e) + { + // Use default item background rendering + OnRenderItemBackground(e); + } + + /// + /// Raises the RenderToolStripStatusLabelBackground event. + /// + /// A ToolStripItemRenderEventArgs that contains the event data. + protected override void OnRenderStatusStripSizingGrip(ToolStripRenderEventArgs e) + { + ArgumentNullException.ThrowIfNull(e); + + Rectangle bounds = e.AffectedBounds; + Graphics g = e.Graphics; + + // Draw a dark mode sizing grip + Color darkColor = GetDarkModeColor(SystemColors.ControlDark); + Color lightColor = GetDarkModeColor(SystemColors.ControlLight); + + // Start at the bottom-right corner and move up and left + int x = bounds.Right - 3; + int y = bounds.Bottom - 3; + + for (int i = 0; i < 3; i++) + { + int tempX = x; + int tempY = y; + + for (int j = 0; j <= i; j++) + { + g.FillRectangle(new SolidBrush(darkColor), tempX, tempY, 2, 2); + g.FillRectangle(new SolidBrush(lightColor), tempX - 1, tempY - 1, 1, 1); + + tempX -= 4; + tempY += 4; + } + + x -= 4; + y -= 4; + } + } +} diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStripSystemRenderer.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStripSystemRenderer.cs index ee0b15b750f..f84d83e0f74 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStripSystemRenderer.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStripSystemRenderer.cs @@ -6,47 +6,83 @@ namespace System.Windows.Forms; +/// +/// Provides system rendering for ToolStrip controls with support for dark mode. +/// public class ToolStripSystemRenderer : ToolStripRenderer { [ThreadStatic] private static VisualStyleRenderer? t_renderer; + private ToolStripRenderer? _toolStripHighContrastRenderer; + private ToolStripRenderer? _toolStripDarkModeRenderer; + /// + /// Initializes a new instance of the ToolStripSystemRenderer class. + /// public ToolStripSystemRenderer() { } + /// + /// Initializes a new instance of the ToolStripSystemRenderer class with the specified default state. + /// + /// true if this is the default renderer; otherwise, false. internal ToolStripSystemRenderer(bool isDefault) : base(isDefault) { } - internal override ToolStripRenderer? RendererOverride + /// + /// Gets the HighContrastRenderer for accessibility support. + /// + internal ToolStripRenderer HighContrastRenderer { get { - if (DisplayInformation.HighContrast) - { - return HighContrastRenderer; - } + _toolStripHighContrastRenderer ??= new ToolStripHighContrastRenderer(/*renderLikeSystem*/true); + return _toolStripHighContrastRenderer; + } + } - return null; + /// + /// Gets the DarkModeRenderer for dark mode support. + /// + internal ToolStripRenderer DarkModeRenderer + { + get + { + _toolStripDarkModeRenderer ??= new ToolStripSystemDarkModeRenderer(); + return _toolStripDarkModeRenderer; } } - internal ToolStripRenderer HighContrastRenderer + /// + /// Gets the renderer that should be used based on current display settings. + /// + internal override ToolStripRenderer? RendererOverride { get { - // If system in high contrast mode 'false' flag should be passed to render filled selected button background. - // This is in consistence with ToolStripProfessionalRenderer. - _toolStripHighContrastRenderer ??= new ToolStripHighContrastRenderer(systemRenderMode: false); + // First, check for high contrast mode (accessibility takes precedence) + if (DisplayInformation.HighContrast) + { + return HighContrastRenderer; + } - return _toolStripHighContrastRenderer; + // Then check for dark mode +#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. + if (Application.IsDarkModeEnabled) + { + return DarkModeRenderer; + } +#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. + + return null; } } /// - /// Draw the background color + /// Get the Visual Style Renderer. This is used to draw the background of the ToolStrip. /// private static VisualStyleRenderer? VisualStyleRenderer { @@ -203,19 +239,24 @@ protected override void OnRenderToolStripBackground(ToolStripRenderEventArgs e) } else if (DisplayInformation.LowResolution) { - FillBackground(g, bounds, (toolStrip is ToolStripDropDown) ? SystemColors.ControlLight : e.BackColor); + FillBackground(g, bounds, (toolStrip is ToolStripDropDown) + ? SystemColors.ControlLight + : e.BackColor); } else if (toolStrip.IsDropDown) { - FillBackground(g, bounds, (!ToolStripManager.VisualStylesEnabled) ? - e.BackColor : SystemColors.Menu); + FillBackground(g, bounds, ToolStripManager.VisualStylesEnabled + ? SystemColors.Menu + : e.BackColor); } else if (toolStrip is MenuStrip) { - FillBackground(g, bounds, (!ToolStripManager.VisualStylesEnabled) ? - e.BackColor : SystemColors.MenuBar); + FillBackground(g, bounds, ToolStripManager.VisualStylesEnabled + ? SystemColors.MenuBar + : e.BackColor); } - else if (ToolStripManager.VisualStylesEnabled && VisualStyleRenderer.IsElementDefined(VisualStyleElement.Rebar.Band.Normal)) + else if (ToolStripManager.VisualStylesEnabled + && VisualStyleRenderer.IsElementDefined(VisualStyleElement.Rebar.Band.Normal)) { VisualStyleRenderer vsRenderer = VisualStyleRenderer!; vsRenderer.SetParameters(VisualStyleElement.ToolBar.Bar.Normal); @@ -223,8 +264,9 @@ protected override void OnRenderToolStripBackground(ToolStripRenderEventArgs e) } else { - FillBackground(g, bounds, (!ToolStripManager.VisualStylesEnabled) ? - e.BackColor : SystemColors.MenuBar); + FillBackground(g, bounds, ToolStripManager.VisualStylesEnabled + ? SystemColors.MenuBar + : e.BackColor); } } } @@ -279,7 +321,8 @@ protected override void OnRenderGrip(ToolStripGripRenderEventArgs e) Rectangle bounds = new(Point.Empty, e.GripBounds.Size); bool verticalGrip = e.GripDisplayStyle == ToolStripGripDisplayStyle.Vertical; - if (ToolStripManager.VisualStylesEnabled && VisualStyleRenderer.IsElementDefined(VisualStyleElement.Rebar.Gripper.Normal)) + if (ToolStripManager.VisualStylesEnabled + && VisualStyleRenderer.IsElementDefined(VisualStyleElement.Rebar.Gripper.Normal)) { VisualStyleRenderer vsRenderer = VisualStyleRenderer!; @@ -521,11 +564,18 @@ protected override void OnRenderSplitButtonBackground(ToolStripItemRenderEventAr Graphics g = e.Graphics; bool rightToLeft = splitButton.RightToLeft == RightToLeft.Yes; - Color arrowColor = splitButton.Enabled ? SystemColors.ControlText : SystemColors.ControlDark; + Color arrowColor = splitButton.Enabled + ? SystemColors.ControlText + : SystemColors.ControlDark; // in right to left - we need to swap the parts so we don't draw v][ toolStripSplitButton - VisualStyleElement splitButtonDropDownPart = rightToLeft ? VisualStyleElement.ToolBar.SplitButton.Normal : VisualStyleElement.ToolBar.SplitButtonDropDown.Normal; - VisualStyleElement splitButtonPart = rightToLeft ? VisualStyleElement.ToolBar.DropDownButton.Normal : VisualStyleElement.ToolBar.SplitButton.Normal; + VisualStyleElement splitButtonDropDownPart = rightToLeft + ? VisualStyleElement.ToolBar.SplitButton.Normal + : VisualStyleElement.ToolBar.SplitButtonDropDown.Normal; + + VisualStyleElement splitButtonPart = rightToLeft + ? VisualStyleElement.ToolBar.DropDownButton.Normal + : VisualStyleElement.ToolBar.SplitButton.Normal; if (ToolStripManager.VisualStylesEnabled && VisualStyleRenderer.IsElementDefined(splitButtonDropDownPart) @@ -536,10 +586,10 @@ protected override void OnRenderSplitButtonBackground(ToolStripItemRenderEventAr // Draw the SplitButton Button portion of it. vsRenderer.SetParameters(splitButtonPart.ClassName, splitButtonPart.Part, GetSplitButtonItemState(splitButton)); - // the lovely Windows theming for split button comes in three pieces: + // the Windows theming for split button comes in three pieces: // SplitButtonDropDown: [ v | - // Separator: | - // SplitButton: | ] + // Separator: | + // SplitButton: | ] // this is great except if you want to swap the button in RTL. In this case we need // to use the DropDownButton instead of the SplitButtonDropDown and paint the arrow ourselves. Rectangle splitButtonBounds = splitButton.ButtonBounds; @@ -766,10 +816,9 @@ private static void RenderStatusStripBorder(ToolStripRenderEventArgs e) } } -#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. private static void RenderStatusStripBackground(ToolStripRenderEventArgs e) { - if (!Application.IsDarkModeEnabled && Application.RenderWithVisualStyles) + if (Application.RenderWithVisualStyles) { VisualStyleRenderer vsRenderer = VisualStyleRenderer!; vsRenderer.SetParameters(VisualStyleElement.Status.Bar.Normal); @@ -783,7 +832,6 @@ private static void RenderStatusStripBackground(ToolStripRenderEventArgs e) } } } -#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. private static void RenderLabelInternal(ToolStripItemRenderEventArgs e) { From 8cf4568fc47206a9e38256d42170bff3dbb50400 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Klaus=20L=C3=B6ffelmann?= Date: Tue, 29 Apr 2025 18:50:53 -0700 Subject: [PATCH 03/10] Fix the StatusStripSizingGrip. --- .../Forms/Controls/ToolStrips/ToolStrip.cs | 7 + .../Controls/ToolStrips/ToolStripRenderer.cs | 337 ++++++++++++------ .../ToolStripSystemDarkModeRenderer.cs | 298 ++++++---------- .../System/Windows/Forms/Form.cs | 10 - 4 files changed, 344 insertions(+), 308 deletions(-) diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStrip.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStrip.cs index b2c1df93b04..20e983b1371 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStrip.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStrip.cs @@ -3636,6 +3636,7 @@ protected override void OnPaintBackground(PaintEventArgs e) Graphics g = e.GraphicsInternal; GraphicsState graphicsState = g.Save(); + try { using (Region? transparentRegion = Renderer.GetTransparentRegion(this)) @@ -3647,6 +3648,12 @@ protected override void OnPaintBackground(PaintEventArgs e) } } + if (Renderer.RendererOverride is ToolStripRenderer renderer) + { + renderer.DrawToolStripBackground(new ToolStripRenderEventArgs(g, this)); + return; + } + Renderer.DrawToolStripBackground(new ToolStripRenderEventArgs(g, this)); } finally diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStripRenderer.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStripRenderer.cs index bf91c4d9d16..eaa33326e6b 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStripRenderer.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStripRenderer.cs @@ -53,7 +53,8 @@ public abstract class ToolStripRenderer // Used in building up the half pyramid of rectangles that are drawn in a // status strip sizing grip. - private static readonly Rectangle[] s_baseSizeGripRectangles = [ + private static readonly Rectangle[] s_baseSizeGripRectangles = + [ new(8, 0, 2, 2), new(8, 4, 2, 2), new(8, 8, 2, 2), @@ -66,20 +67,49 @@ protected ToolStripRenderer() { } - internal ToolStripRenderer(bool isAutoGenerated) - { + internal ToolStripRenderer(bool isAutoGenerated) => _isAutoGenerated = isAutoGenerated; - } // Used in building disabled images. private static ColorMatrix DisabledImageColorMatrix { get { - if (s_disabledImageColorMatrix is null) + if (s_disabledImageColorMatrix is not null) { - // This is the result of a GreyScale matrix multiplied by a transparency matrix of .5 + return s_disabledImageColorMatrix; + } + +#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. + bool isDarkMode = Application.IsDarkModeEnabled; +#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. + + if (isDarkMode) + { + // Dark mode color matrix + float[][] greyscale = + [ + [0.2125f, 0.2125f, 0.2125f, 0, 0], + [0.2577f, 0.2577f, 0.2577f, 0, 0], + [0.0361f, 0.0361f, 0.0361f, 0, 0], + [0, 0, 0, 1, 0], + [-0.1f, -0.1f, -0.1f, 0, 1], + ]; + + float[][] transparency = + [ + [1, 0, 0, 0, 0], + [0, 1, 0, 0, 0], + [0, 0, 1, 0, 0], + [0, 0, 0, 0.8f, 0], + [0, 0, 0, 0, 0], + ]; + s_disabledImageColorMatrix = ControlPaint.MultiplyColorMatrix(transparency, greyscale); + } + else + { + // Light mode color matrix float[][] greyscale = [ [0.2125f, 0.2125f, 0.2125f, 0, 0], @@ -94,7 +124,7 @@ private static ColorMatrix DisabledImageColorMatrix [1, 0, 0, 0, 0], [0, 1, 0, 0, 0], [0, 0, 1, 0, 0], - [0, 0, 0, .7F, 0], + [0, 0, 0, 0.7f, 0], [0, 0, 0, 0, 0], ]; @@ -118,19 +148,10 @@ private EventHandlerList Events } } - internal bool IsAutoGenerated - { - get { return _isAutoGenerated; } - } + internal bool IsAutoGenerated => _isAutoGenerated; // if we're in a low contrast, high resolution situation, use this renderer under the covers instead. - internal virtual ToolStripRenderer? RendererOverride - { - get - { - return null; - } - } + internal virtual ToolStripRenderer? RendererOverride => null; public event ToolStripArrowRenderEventHandler RenderArrow { @@ -299,21 +320,13 @@ public event ToolStripSeparatorRenderEventHandler RenderSeparator #region EventHandlerSecurity - private void AddHandler(object key, Delegate value) - { - Events.AddHandler(key, value); - } + private void AddHandler(object key, Delegate value) => Events.AddHandler(key, value); - private void RemoveHandler(object key, Delegate value) - { - Events.RemoveHandler(key, value); - } + private void RemoveHandler(object key, Delegate value) => Events.RemoveHandler(key, value); #endregion - public static Image CreateDisabledImage(Image normalImage) - { - return CreateDisabledImage(normalImage, null); - } + public static Image CreateDisabledImage(Image normalImage) => + CreateDisabledImage(normalImage, null); public void DrawArrow(ToolStripArrowRenderEventArgs e) { @@ -515,7 +528,6 @@ public void DrawToolStripStatusLabelBackground(ToolStripItemRenderEventArgs e) } } - // public void DrawStatusStripSizingGrip(ToolStripRenderEventArgs e) { OnRenderStatusStripSizingGrip(e); @@ -557,10 +569,7 @@ public void DrawToolStripContentPanelBackground(ToolStripContentPanelRenderEvent } // consider make public - internal virtual Region? GetTransparentRegion(ToolStrip toolStrip) - { - return null; - } + internal virtual Region? GetTransparentRegion(ToolStrip toolStrip) => null; protected internal virtual void Initialize(ToolStrip toolStrip) { @@ -601,6 +610,10 @@ protected static void ScaleArrowOffsetsIfNeeded(int dpi) s_offset4Y = ScaleHelper.ScaleToDpi(OFFSET_4PIXELS, dpi); } + /// + /// Renders an arrow on the ToolStrip control. + /// + /// A ToolStripArrowRenderEventArgs that contains the event data. protected virtual void OnRenderArrow(ToolStripArrowRenderEventArgs e) { ArgumentNullException.ThrowIfNull(e); @@ -611,17 +624,33 @@ protected virtual void OnRenderArrow(ToolStripArrowRenderEventArgs e) return; } + RenderArrowCore(e, e.ArrowColor); + } + + /// + /// Base class method that handles shared arrow rendering functionality. + /// + /// The event arguments containing rendering information. + /// The color to use for the arrow. + /// The rendered arrow points. + private protected Point[] RenderArrowCore( + ToolStripArrowRenderEventArgs e, + Color arrowColor) + { + ArgumentNullException.ThrowIfNull(e); + Graphics g = e.Graphics; + Rectangle dropDownRect = e.ArrowRectangle; - using var brush = e.ArrowColor.GetCachedSolidBrushScope(); - Point middle = new(dropDownRect.Left + dropDownRect.Width / 2, dropDownRect.Top + dropDownRect.Height / 2); - // if the width is odd - favor pushing it over one pixel right. - // middle.X += (dropDownRect.Width % 2); - Point[]? arrow = null; + Point middle = new( + dropDownRect.Left + dropDownRect.Width / 2, + dropDownRect.Top + dropDownRect.Height / 2); - // We need to check for null here, since at design time at this point Item can be null. - if (e.Item is not null && e.Item.DeviceDpi != _previousDeviceDpi && ScaleHelper.IsThreadPerMonitorV2Aware) + // Scale arrow offsets if needed + if (e.Item is not null + && e.Item.DeviceDpi != _previousDeviceDpi + && ScaleHelper.IsThreadPerMonitorV2Aware) { _previousDeviceDpi = e.Item.DeviceDpi; ScaleArrowOffsetsIfNeeded(e.Item.DeviceDpi); @@ -631,10 +660,13 @@ protected virtual void OnRenderArrow(ToolStripArrowRenderEventArgs e) ScaleArrowOffsetsIfNeeded(); } - // using (offset4X - Offset2X) instead of (Offset2X) to compensate for rounding error in scaling - int horizontalOffset = ScaleHelper.IsScalingRequirementMet ? s_offset4X - Offset2X : Offset2X; + // Using (offset4X - Offset2X) instead of (Offset2X) to compensate + // for rounding error in scaling + int horizontalOffset = ScaleHelper.IsScalingRequirementMet + ? s_offset4X - Offset2X + : Offset2X; - arrow = e.Direction switch + Point[] arrow = e.Direction switch { ArrowDirection.Up => [ @@ -661,7 +693,11 @@ protected virtual void OnRenderArrow(ToolStripArrowRenderEventArgs e) new(middle.X, middle.Y + Offset2Y) ], }; + + using var brush = arrowColor.GetCachedSolidBrushScope(); g.FillPolygon(brush, arrow); + + return arrow; } /// @@ -780,6 +816,7 @@ protected virtual void OnRenderItemImage(ToolStripItemImageRenderEventArgs e) if (imageRect != Rectangle.Empty && image is not null) { bool disposeImage = false; + if (e.ShiftOnPress && e.Item is not null && e.Item.Pressed) { imageRect.X++; @@ -820,30 +857,32 @@ protected virtual void OnRenderItemCheck(ToolStripItemImageRenderEventArgs e) Rectangle imageRect = e.ImageRectangle; Image? image = e.Image; - if (imageRect != Rectangle.Empty && image is not null) + if (imageRect == Rectangle.Empty || image is null) + { + return; + } + + if (e.Item is not null) { - if (e.Item is not null) + if (!e.Item.Enabled) { - if (!e.Item.Enabled) - { - image = CreateDisabledImage(image, e.ImageAttributes); - } + image = CreateDisabledImage(image, e.ImageAttributes); + } - if (SystemInformation.HighContrast && image is Bitmap bitmap) - { - Color backgroundColor = e.Item.Selected ? SystemColors.Highlight : e.Item.BackColor; + if (SystemInformation.HighContrast && image is Bitmap bitmap) + { + Color backgroundColor = e.Item.Selected ? SystemColors.Highlight : e.Item.BackColor; - if (ControlPaint.IsDark(backgroundColor)) - { - Image invertedImage = ControlPaint.CreateBitmapWithInvertedForeColor(bitmap, e.Item.BackColor); - image = invertedImage; - } + if (ControlPaint.IsDark(backgroundColor)) + { + Image invertedImage = ControlPaint.CreateBitmapWithInvertedForeColor(bitmap, e.Item.BackColor); + image = invertedImage; } } - - e.Graphics.DrawImage(image, imageRect, 0, 0, imageRect.Width, - imageRect.Height, GraphicsUnit.Pixel, e.ImageAttributes); } + + e.Graphics.DrawImage(image, imageRect, 0, 0, imageRect.Width, + imageRect.Height, GraphicsUnit.Pixel, e.ImageAttributes); } /// @@ -867,25 +906,41 @@ protected virtual void OnRenderItemText(ToolStripItemTextRenderEventArgs e) string? text = e.Text; Rectangle textRect = e.TextRectangle; TextFormatFlags textFormat = e.TextFormat; - // if we're disabled draw in a different color. - textColor = (item is not null && item.Enabled) ? textColor : SystemColors.GrayText; - if (e.TextDirection != ToolStripTextDirection.Horizontal && textRect.Width > 0 && textRect.Height > 0) - { - // Perf: this is a bit heavy handed.. perhaps we can share the bitmap. - Size textSize = LayoutUtils.FlipSize(textRect.Size); - using Bitmap textBmp = new(textSize.Width, textSize.Height, PixelFormat.Format32bppPArgb); - using Graphics textGraphics = Graphics.FromImage(textBmp); - // now draw the text.. - textGraphics.TextRenderingHint = TextRenderingHint.AntiAlias; - TextRenderer.DrawText(textGraphics, text, textFont, new Rectangle(Point.Empty, textSize), textColor, textFormat); - textBmp.RotateFlip((e.TextDirection == ToolStripTextDirection.Vertical90) ? RotateFlipType.Rotate90FlipNone : RotateFlipType.Rotate270FlipNone); - g.DrawImage(textBmp, textRect); - } - else + // If we're disabled draw in a different color. + textColor = (item is not null && item.Enabled) + ? textColor + : SystemColors.GrayText; + + if (e.TextDirection == ToolStripTextDirection.Horizontal + || textRect.Width <= 0 + || textRect.Height <= 0) { TextRenderer.DrawText(g, text, textFont, textRect, textColor, textFormat); + return; } + + // Perf: this is a bit heavy handed.. perhaps we can share the bitmap. + Size textSize = LayoutUtils.FlipSize(textRect.Size); + using Bitmap textBmp = new(textSize.Width, textSize.Height, PixelFormat.Format32bppPArgb); + using Graphics textGraphics = Graphics.FromImage(textBmp); + + // Now draw the text. + textGraphics.TextRenderingHint = TextRenderingHint.AntiAlias; + + TextRenderer.DrawText( + dc: textGraphics, + text: text, + font: textFont, + bounds: new Rectangle(Point.Empty, textSize), + foreColor: textColor, + flags: textFormat); + + textBmp.RotateFlip((e.TextDirection == ToolStripTextDirection.Vertical90) + ? RotateFlipType.Rotate90FlipNone + : RotateFlipType.Rotate270FlipNone); + + g.DrawImage(textBmp, textRect); } /// @@ -956,51 +1011,95 @@ protected virtual void OnRenderStatusStripSizingGrip(ToolStripRenderEventArgs e) { ArgumentNullException.ThrowIfNull(e); + OnRenderStatusStripSizingGrip( + eArgs: e, + highLightBrush: SystemBrushes.ButtonHighlight, + shadowBrush: SystemBrushes.ButtonShadow); + } + + private protected void OnRenderStatusStripSizingGrip( + ToolStripRenderEventArgs eArgs, + Brush highLightBrush, + Brush shadowBrush) + { if (RendererOverride is not null) { - RendererOverride.OnRenderStatusStripSizingGrip(e); + RendererOverride.OnRenderStatusStripSizingGrip(eArgs); return; } - Graphics g = e.Graphics; + Graphics g = eArgs.Graphics; // we have a set of stock rectangles. Translate them over to where the grip is to be drawn // for the white set, then translate them up and right one pixel for the grey. - if (e.ToolStrip is StatusStrip statusStrip) + if (eArgs.ToolStrip is not StatusStrip statusStrip) + { + return; + } + + Rectangle sizeGripBounds = statusStrip.SizeGripBounds; + + if (LayoutUtils.IsZeroWidthOrHeight(sizeGripBounds)) { - Rectangle sizeGripBounds = statusStrip.SizeGripBounds; + return; + } + + Rectangle[] whiteRectangles = new Rectangle[s_baseSizeGripRectangles.Length]; + Rectangle[] greyRectangles = new Rectangle[s_baseSizeGripRectangles.Length]; - if (!LayoutUtils.IsZeroWidthOrHeight(sizeGripBounds)) + for (int i = 0; i < s_baseSizeGripRectangles.Length; i++) + { + Rectangle baseRect = s_baseSizeGripRectangles[i]; + + if (statusStrip.RightToLeft == RightToLeft.Yes) { - Rectangle[] whiteRectangles = new Rectangle[s_baseSizeGripRectangles.Length]; - Rectangle[] greyRectangles = new Rectangle[s_baseSizeGripRectangles.Length]; + baseRect.X = sizeGripBounds.Width - baseRect.X - baseRect.Width; + } - for (int i = 0; i < s_baseSizeGripRectangles.Length; i++) - { - Rectangle baseRect = s_baseSizeGripRectangles[i]; - if (statusStrip.RightToLeft == RightToLeft.Yes) - { - baseRect.X = sizeGripBounds.Width - baseRect.X - baseRect.Width; - } - - baseRect.Offset(sizeGripBounds.X, sizeGripBounds.Bottom - 12 /*height of pyramid (10px) + 2px padding from bottom*/); - whiteRectangles[i] = baseRect; - if (statusStrip.RightToLeft == RightToLeft.Yes) - { - baseRect.Offset(1, -1); - } - else - { - baseRect.Offset(-1, -1); - } - - greyRectangles[i] = baseRect; - } + // Height of pyramid (10px) + 2px padding from bottom. + baseRect.Offset( + x: sizeGripBounds.X, + y: sizeGripBounds.Bottom - 12); + + whiteRectangles[i] = baseRect; + + int offset = -1 + GetCornerOffset(statusStrip); + + if (statusStrip.RightToLeft == RightToLeft.Yes) + { + baseRect.Offset(1, -1 - offset); + } + else + { + baseRect.Offset(-1, -1 - offset); + } + + greyRectangles[i] = baseRect; + } + + g.FillRectangles(highLightBrush, whiteRectangles); + g.FillRectangles(shadowBrush, greyRectangles); + + // We need to compensate for the rounded Window corners from Windows 11 on. + static int GetCornerOffset(StatusStrip statusStrip) + { + // If we're on Windows 11, offset slightly to avoid hitting rounded corners, + // _if_ we are at all dealing with rounded corners. + int cornerOffset = 0; - g.FillRectangles(SystemBrushes.ButtonHighlight, whiteRectangles); - g.FillRectangles(SystemBrushes.ButtonShadow, greyRectangles); + if (Environment.OSVersion.Version >= new Version(10, 0, 22000) + && statusStrip.FindForm() is Form f) + { + cornerOffset = f.FormCornerPreference switch + { + FormCornerPreference.Round => 5, + FormCornerPreference.RoundSmall => 3, + _ => 3 + }; } + + return cornerOffset; } } @@ -1016,11 +1115,10 @@ protected virtual void OnRenderSplitButtonBackground(ToolStripItemRenderEventArg } } - // Only paint background effects if no BackColor has been set or no background image has been set. - internal static bool ShouldPaintBackground(Control control) - { - return (control.RawBackColor == Color.Empty && control.BackgroundImage is null); - } + // Only paint background effects if no BackColor has been set + // or no background image has been set. + internal static bool ShouldPaintBackground(Control control) => + control.RawBackColor == Color.Empty && control.BackgroundImage is null; private static Bitmap CreateDisabledImage(Image normalImage, ImageAttributes? imgAttrib) { @@ -1033,13 +1131,18 @@ private static Bitmap CreateDisabledImage(Image normalImage, ImageAttributes? im Size size = normalImage.Size; Bitmap disabledBitmap = new(size.Width, size.Height); + using (Graphics graphics = Graphics.FromImage(disabledBitmap)) { - graphics.DrawImage(normalImage, - new Rectangle(0, 0, size.Width, size.Height), - 0, 0, size.Width, size.Height, - GraphicsUnit.Pixel, - imgAttrib); + graphics.DrawImage( + image: normalImage, + destRect: new Rectangle(0, 0, size.Width, size.Height), + srcX: 0, + srcY: 0, + srcWidth: size.Width, + srcHeight: size.Height, + srcUnit: GraphicsUnit.Pixel, + imageAttr: imgAttrib); } return disabledBitmap; diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStripSystemDarkModeRenderer.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStripSystemDarkModeRenderer.cs index 59ad60ac31f..975a61dbb7e 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStripSystemDarkModeRenderer.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStripSystemDarkModeRenderer.cs @@ -6,7 +6,7 @@ namespace System.Windows.Forms; /// -/// Provides dark mode rendering capabilities for ToolStrip controls in Windows Forms. +/// Provides dark mode rendering capabilities for ToolStrip controls in Windows Forms. /// /// /// @@ -18,14 +18,14 @@ namespace System.Windows.Forms; internal class ToolStripSystemDarkModeRenderer : ToolStripRenderer { /// - /// Initializes a new instance of the ToolStripSystemDarkModeRenderer class. + /// Initializes a new instance of the ToolStripSystemDarkModeRenderer class. /// public ToolStripSystemDarkModeRenderer() { } /// - /// Initializes a new instance of the ToolStripSystemDarkModeRenderer class with the specified default state. + /// Initializes a new instance of the ToolStripSystemDarkModeRenderer class with the specified default state. /// /// true if this is the default renderer; otherwise, false. internal ToolStripSystemDarkModeRenderer(bool isDefault) : base(isDefault) @@ -33,13 +33,16 @@ internal ToolStripSystemDarkModeRenderer(bool isDefault) : base(isDefault) } /// - /// Gets dark-appropriate system colors based on the control type. + /// Gets dark-appropriate system colors based on the control type. /// /// The color to convert to a dark mode equivalent. /// A color suitable for dark mode. private static Color GetDarkModeColor(Color color) { - // Map common system colors to their dark mode equivalents + // Map system colors to some slightly different colors we would get + // form the actual system colors in dark mode, since the visual style + // renderer in light mode would also not "hit" (for contrast and styling + // reasons) the exact same palette settings as the system colors. if (color == SystemColors.Control) return Color.FromArgb(45, 45, 45); if (color == SystemColors.ControlLight) @@ -82,43 +85,35 @@ private static Color GetDarkModeColor(Color color) } /// - /// Creates a dark mode compatible brush. + /// Creates a dark mode compatible brush. Important: + /// Always do: `using var brush = GetDarkModeBrush(color)`, + /// since you're dealing with a cached brush => scope, really! /// /// The system color to convert. /// A brush with the dark mode color. - private static SolidBrush GetDarkModeBrush(Color color) - { - return new SolidBrush(GetDarkModeColor(color)); - } + private static SolidBrushCache.Scope GetDarkModeBrush(Color color) + => GetDarkModeColor(color).GetCachedSolidBrushScope(); /// - /// Creates a dark mode compatible pen. + /// Creates a dark mode compatible pen. Important: + /// Always do: `using var somePen = GetDarkModePen(color)`, + /// since you're dealing with a cached pen => scope, really! /// /// The system color to convert. /// A pen with the dark mode color. - private static Pen GetDarkModePen(Color color) - { - return new Pen(GetDarkModeColor(color)); - } + private static PenCache.Scope GetDarkModePen(Color color) + => GetDarkModeColor(color).GetCachedPenScope(); /// - /// Returns whether the background should be painted. + /// Returns whether the background should be painted. /// /// The ToolStrip to check. /// true if the background should be painted; otherwise, false. private static bool ShouldPaintBackground(ToolStrip toolStrip) - { - if (toolStrip is null) - return true; - - if (toolStrip.BackgroundImage is not null) - return false; - - return true; - } + => toolStrip is null || toolStrip.BackgroundImage is null; /// - /// Fills the background with the specified color. + /// Fills the background with the specified color. /// /// The Graphics to draw on. /// The bounds to fill. @@ -129,12 +124,12 @@ private static void FillBackground(Graphics g, Rectangle bounds, Color backColor return; // Use a dark mode color - using Brush brush = GetDarkModeBrush(backColor); + using var brush = GetDarkModeBrush(backColor); g.FillRectangle(brush, bounds); } /// - /// Raises the RenderToolStripBackground event. + /// Raises the RenderToolStripBackground event. /// /// A ToolStripRenderEventArgs that contains the event data. protected override void OnRenderToolStripBackground(ToolStripRenderEventArgs e) @@ -173,7 +168,7 @@ protected override void OnRenderToolStripBackground(ToolStripRenderEventArgs e) } /// - /// Renders the StatusStrip background in dark mode. + /// Renders the StatusStrip background in dark mode. /// /// A ToolStripRenderEventArgs that contains the event data. private static void RenderStatusStripBackground(ToolStripRenderEventArgs e) @@ -186,7 +181,7 @@ private static void RenderStatusStripBackground(ToolStripRenderEventArgs e) } /// - /// Raises the RenderToolStripBorder event. + /// Raises the RenderToolStripBorder event. /// /// A ToolStripRenderEventArgs that contains the event data. protected override void OnRenderToolStripBorder(ToolStripRenderEventArgs e) @@ -212,25 +207,25 @@ protected override void OnRenderToolStripBorder(ToolStripRenderEventArgs e) bounds.Width -= 1; bounds.Height -= 1; - using Pen borderPen = GetDarkModePen(SystemColors.ControlDark); + using var borderPen = GetDarkModePen(SystemColors.ControlDark); g.DrawRectangle(borderPen, bounds); } else { - using Pen borderPen = GetDarkModePen(SystemColors.ControlDark); + using var borderPen = GetDarkModePen(SystemColors.ControlDark); g.DrawRectangle(borderPen, bounds); } } else { // Draw a subtle bottom border for toolstrips - using Pen borderPen = GetDarkModePen(SystemColors.ControlDark); + using var borderPen = GetDarkModePen(SystemColors.ControlDark); g.DrawLine(borderPen, 0, bounds.Bottom - 1, bounds.Width, bounds.Bottom - 1); } } /// - /// Renders the StatusStrip border in dark mode. + /// Renders the StatusStrip border in dark mode. /// /// A ToolStripRenderEventArgs that contains the event data. private static void RenderStatusStripBorder(ToolStripRenderEventArgs e) @@ -239,12 +234,12 @@ private static void RenderStatusStripBorder(ToolStripRenderEventArgs e) Rectangle bounds = e.ToolStrip.ClientRectangle; // Dark mode StatusStrip border (usually top border only) - using Pen borderPen = GetDarkModePen(SystemColors.ControlDark); + using var borderPen = GetDarkModePen(SystemColors.ControlDark); g.DrawLine(borderPen, 0, 0, bounds.Width, 0); } /// - /// Raises the RenderItemBackground event. + /// Raises the RenderItemBackground event. /// /// A ToolStripItemRenderEventArgs that contains the event data. protected override void OnRenderItemBackground(ToolStripItemRenderEventArgs e) @@ -263,7 +258,7 @@ protected override void OnRenderItemBackground(ToolStripItemRenderEventArgs e) if (e.Item.Selected || e.Item.Pressed) { // Dark mode selection highlight - using Brush highlightBrush = GetDarkModeBrush(SystemColors.Highlight); + using var highlightBrush = GetDarkModeBrush(SystemColors.Highlight); e.Graphics.FillRectangle(highlightBrush, bounds); } else @@ -288,7 +283,7 @@ protected override void OnRenderItemBackground(ToolStripItemRenderEventArgs e) } /// - /// Raises the RenderButtonBackground event. + /// Raises the RenderButtonBackground event. /// /// A ToolStripItemRenderEventArgs that contains the event data. protected override void OnRenderButtonBackground(ToolStripItemRenderEventArgs e) @@ -312,18 +307,16 @@ protected override void OnRenderButtonBackground(ToolStripItemRenderEventArgs e) if (isPressed || isSelected) { - Color fillColor = isPressed - ? GetDarkModeColor(SystemColors.ControlDark) - : GetDarkModeColor(SystemColors.Highlight); + using var fillColor = isPressed + ? GetDarkModeBrush(SystemColors.ControlDark) + : GetDarkModeBrush(SystemColors.Highlight); - using Brush fillBrush = new SolidBrush(fillColor); - - e.Graphics.FillRectangle(fillBrush, bounds); + e.Graphics.FillRectangle(fillColor, bounds); } } /// - /// Raises the RenderDropDownButtonBackground event. + /// Raises the RenderDropDownButtonBackground event. /// /// A ToolStripItemRenderEventArgs that contains the event data. protected override void OnRenderDropDownButtonBackground(ToolStripItemRenderEventArgs e) @@ -333,7 +326,7 @@ protected override void OnRenderDropDownButtonBackground(ToolStripItemRenderEven } /// - /// Raises the RenderSplitButtonBackground event. + /// Raises the RenderSplitButtonBackground event. /// /// A ToolStripItemRenderEventArgs that contains the event data. protected override void OnRenderSplitButtonBackground(ToolStripItemRenderEventArgs e) @@ -350,17 +343,16 @@ protected override void OnRenderSplitButtonBackground(ToolStripItemRenderEventAr // Render the background based on state if (splitButton.Selected || splitButton.Pressed) { - Color fillColor = splitButton.Pressed - ? GetDarkModeColor(SystemColors.ControlDark) - : GetDarkModeColor(SystemColors.Highlight); + using var fillColor = splitButton.Pressed + ? GetDarkModeBrush(SystemColors.ControlDark) + : GetDarkModeBrush(SystemColors.Highlight); - using Brush fillBrush = new SolidBrush(fillColor); - e.Graphics.FillRectangle(fillBrush, bounds); + e.Graphics.FillRectangle(fillColor, bounds); } // Draw the split line Rectangle dropDownRect = splitButton.DropDownButtonBounds; - using Pen linePen = GetDarkModePen(SystemColors.ControlDark); + using var linePen = GetDarkModePen(SystemColors.ControlDark); e.Graphics.DrawLine( linePen, @@ -368,10 +360,17 @@ protected override void OnRenderSplitButtonBackground(ToolStripItemRenderEventAr dropDownRect.Top + 2, dropDownRect.Left - 1, dropDownRect.Bottom - 2); + + DrawArrow(new ToolStripArrowRenderEventArgs( + e.Graphics, + e.Item, + dropDownRect, + SystemColors.ControlText, + ArrowDirection.Down)); } /// - /// Raises the RenderSeparator event. + /// Raises the RenderSeparator event. /// /// A ToolStripSeparatorRenderEventArgs that contains the event data. protected override void OnRenderSeparator(ToolStripSeparatorRenderEventArgs e) @@ -387,27 +386,31 @@ protected override void OnRenderSeparator(ToolStripSeparatorRenderEventArgs e) return; } - Color foreColor = GetDarkModeColor(SystemColors.ControlDark); - bool rightToLeft = e.Item.RightToLeft == RightToLeft.Yes; - using Pen foreColorPen = new Pen(foreColor); - if (isVertical) { - // Vertical separator - using Pen leftPen = rightToLeft - ? foreColorPen - : GetDarkModePen(SystemColors.ButtonHighlight); + int startX = bounds.Width / 2; - using Pen rightPen = rightToLeft - ? GetDarkModePen(SystemColors.ButtonHighlight) - : foreColorPen; + if (rightToLeft) + { + using var leftPen = GetDarkModeColor(SystemColors.ControlDark).GetCachedPenScope(); + g.DrawLine(leftPen, startX, bounds.Top, startX, bounds.Bottom); - int startX = bounds.Width / 2; - g.DrawLine(leftPen, startX, bounds.Top, startX, bounds.Bottom); - startX++; - g.DrawLine(rightPen, startX, bounds.Top, startX, bounds.Bottom); + startX++; + + using var rightPen = GetDarkModeColor(SystemColors.ButtonShadow).GetCachedPenScope(); + g.DrawLine(rightPen, startX, bounds.Top, startX, bounds.Bottom); + } + else + { + using var leftPen = GetDarkModeColor(SystemColors.ButtonShadow).GetCachedPenScope(); + g.DrawLine(leftPen, startX, bounds.Top, startX, bounds.Bottom); + + startX++; + using var rightPen = GetDarkModeColor(SystemColors.ControlDark).GetCachedPenScope(); + g.DrawLine(rightPen, startX, bounds.Top, startX, bounds.Bottom); + } } else { @@ -418,14 +421,17 @@ protected override void OnRenderSeparator(ToolStripSeparatorRenderEventArgs e) } int startY = bounds.Height / 2; + using var foreColorPen = GetDarkModeColor(SystemColors.ControlDark).GetCachedPenScope(); g.DrawLine(foreColorPen, bounds.Left, startY, bounds.Right, startY); + startY++; - g.DrawLine(GetDarkModePen(SystemColors.ButtonHighlight), bounds.Left, startY, bounds.Right, startY); + using var darkModePen = GetDarkModeColor(SystemColors.ButtonShadow).GetCachedPenScope(); + g.DrawLine(darkModePen, bounds.Left, startY, bounds.Right, startY); } } /// - /// Raises the RenderOverflowButtonBackground event. + /// Raises the RenderOverflowButtonBackground event. /// /// A ToolStripItemRenderEventArgs that contains the event data. protected override void OnRenderOverflowButtonBackground(ToolStripItemRenderEventArgs e) @@ -440,11 +446,10 @@ protected override void OnRenderOverflowButtonBackground(ToolStripItemRenderEven // Render the background based on state if (item.Selected || item.Pressed) { - Color fillColor = item.Pressed ? - GetDarkModeColor(SystemColors.ControlDark) : - GetDarkModeColor(SystemColors.Highlight); + using var fillBrush = item.Pressed + ? GetDarkModeColor(SystemColors.ControlDark).GetCachedSolidBrushScope() + : GetDarkModeColor(SystemColors.Highlight).GetCachedSolidBrushScope(); - using Brush fillBrush = new SolidBrush(fillColor); e.Graphics.FillRectangle(fillBrush, bounds); } @@ -477,12 +482,11 @@ protected override void OnRenderOverflowButtonBackground(ToolStripItemRenderEven // else default to ArrowDirection.Down // Set arrow color based on state - Color arrowColor = GetDarkModeColor(SystemColors.ControlText); - - if (item.Pressed || item.Selected) - { - arrowColor = GetDarkModeColor(SystemColors.HighlightText); - } + using var arrowBrush = GetDarkModeColor( + item.Pressed || item.Selected + ? SystemColors.HighlightText + : SystemColors.ControlText) + .GetCachedSolidBrushScope(); // Define arrow polygon based on direction Point[] arrow = arrowDirection switch @@ -514,12 +518,11 @@ protected override void OnRenderOverflowButtonBackground(ToolStripItemRenderEven }; // Draw the arrow - using Brush arrowBrush = new SolidBrush(arrowColor); e.Graphics.FillPolygon(arrowBrush, arrow); } /// - /// Raises the RenderGrip event. + /// Raises the RenderGrip event. /// /// A ToolStripGripRenderEventArgs that contains the event data. protected override void OnRenderGrip(ToolStripGripRenderEventArgs e) @@ -530,8 +533,8 @@ protected override void OnRenderGrip(ToolStripGripRenderEventArgs e) return; // Use dark mode colors for the grip dots - Color darkColor = GetDarkModeColor(SystemColors.ControlDark); - Color lightColor = GetDarkModeColor(SystemColors.ControlLight); + using var darkColorBrush = GetDarkModeColor(SystemColors.ControlDark).GetCachedSolidBrushScope(); + using var lightColorBrush = GetDarkModeColor(SystemColors.ControlLight).GetCachedSolidBrushScope(); ToolStrip toolStrip = e.ToolStrip; Graphics g = e.Graphics; @@ -545,8 +548,8 @@ protected override void OnRenderGrip(ToolStripGripRenderEventArgs e) while (y < bounds.Bottom - 3) { - g.FillRectangle(new SolidBrush(darkColor), bounds.Left + 2, y, 1, 1); - g.FillRectangle(new SolidBrush(lightColor), bounds.Left + 3, y + 1, 1, 1); + g.FillRectangle(darkColorBrush, bounds.Left + 2, y, 1, 1); + g.FillRectangle(lightColorBrush, bounds.Left + 3, y + 1, 1, 1); y += 3; } } @@ -557,17 +560,20 @@ protected override void OnRenderGrip(ToolStripGripRenderEventArgs e) while (x < bounds.Right - 3) { - g.FillRectangle(new SolidBrush(darkColor), x, bounds.Top + 2, 1, 1); - g.FillRectangle(new SolidBrush(lightColor), x + 1, bounds.Top + 3, 1, 1); + g.FillRectangle(darkColorBrush, x, bounds.Top + 2, 1, 1); + g.FillRectangle(lightColorBrush, x + 1, bounds.Top + 3, 1, 1); x += 3; } } } /// - /// Raises the RenderArrow event. + /// Raises the RenderArrow event. /// /// A ToolStripArrowRenderEventArgs that contains the event data. + /// + /// Raises the RenderArrow event in the derived class with dark mode support. + /// protected override void OnRenderArrow(ToolStripArrowRenderEventArgs e) { ArgumentNullException.ThrowIfNull(e); @@ -581,51 +587,11 @@ protected override void OnRenderArrow(ToolStripArrowRenderEventArgs e) arrowColor = GetDarkModeColor(SystemColors.HighlightText); } - // Draw the arrow with the appropriate color - Rectangle rect = e.ArrowRectangle; - Graphics g = e.Graphics; - - Point middle = new Point( - rect.Left + rect.Width / 2, - rect.Top + rect.Height / 2); - - // Define the arrow polygon based on direction - Point[] arrow; - - arrow = e.Direction switch - { - ArrowDirection.Up => - [ - new Point(middle.X - 2, middle.Y + 1), - new Point(middle.X + 3, middle.Y + 1), - new Point(middle.X, middle.Y - 2) - ], - ArrowDirection.Left => - [ - new Point(middle.X + 2, middle.Y - 4), - new Point(middle.X + 2, middle.Y + 4), - new Point(middle.X - 2, middle.Y) - ], - ArrowDirection.Right => - [ - new Point(middle.X - 2, middle.Y - 4), - new Point(middle.X - 2, middle.Y + 4), - new Point(middle.X + 2, middle.Y) - ], - _ => - [ - new Point(middle.X - 2, middle.Y - 1), - new Point(middle.X + 3, middle.Y - 1), - new Point(middle.X, middle.Y + 2) - ] - }; - - using Brush brush = new SolidBrush(arrowColor); - g.FillPolygon(brush, arrow); + RenderArrowCore(e, arrowColor); } /// - /// Raises the RenderImageMargin event. + /// Raises the RenderImageMargin event. /// /// A ToolStripRenderEventArgs that contains the event data. protected override void OnRenderImageMargin(ToolStripRenderEventArgs e) @@ -633,13 +599,13 @@ protected override void OnRenderImageMargin(ToolStripRenderEventArgs e) ArgumentNullException.ThrowIfNull(e); // Fill the image margin with a slightly different color than the background - Color marginColor = GetDarkModeColor(SystemColors.ControlLight); - using Brush brush = new SolidBrush(marginColor); - e.Graphics.FillRectangle(brush, e.AffectedBounds); + using var marginColorBrush = GetDarkModeColor(SystemColors.ControlLight).GetCachedSolidBrushScope(); + + e.Graphics.FillRectangle(marginColorBrush, e.AffectedBounds); } /// - /// Raises the RenderItemText event. + /// Raises the RenderItemText event. /// /// A ToolStripItemTextRenderEventArgs that contains the event data. protected override void OnRenderItemText(ToolStripItemTextRenderEventArgs e) @@ -674,7 +640,7 @@ protected override void OnRenderItemText(ToolStripItemTextRenderEventArgs e) } /// - /// Raises the RenderItemImage event. + /// Raises the RenderItemImage event. /// /// A ToolStripItemImageRenderEventArgs that contains the event data. protected override void OnRenderItemImage(ToolStripItemImageRenderEventArgs e) @@ -684,26 +650,17 @@ protected override void OnRenderItemImage(ToolStripItemImageRenderEventArgs e) if (e.Image is null) return; - // Draw the image normally, no need to adjust for dark mode as images - // should already be designed for the appropriate context - if (!e.Item.Enabled) - { - // For disabled items, draw a grayscale version - ControlPaint.DrawImageDisabled( - e.Graphics, - e.Image, - e.ImageRectangle.X, - e.ImageRectangle.Y, - GetDarkModeColor(SystemColors.Control)); - } - else - { - e.Graphics.DrawImage(e.Image, e.ImageRectangle); - } + // DarkMode adjustments for the image are done by + // the base class implementation already. + Image image = !e.Item.Enabled + ? CreateDisabledImage(e.Image) + : e.Image; + + e.Graphics.DrawImage(image, e.ImageRectangle); } /// - /// Raises the RenderLabelBackground event. + /// Raises the RenderLabelBackground event. /// /// A ToolStripItemRenderEventArgs that contains the event data. protected override void OnRenderLabelBackground(ToolStripItemRenderEventArgs e) @@ -713,40 +670,19 @@ protected override void OnRenderLabelBackground(ToolStripItemRenderEventArgs e) } /// - /// Raises the RenderToolStripStatusLabelBackground event. + /// Raises the RenderToolStripStatusLabelBackground event. /// /// A ToolStripItemRenderEventArgs that contains the event data. protected override void OnRenderStatusStripSizingGrip(ToolStripRenderEventArgs e) { ArgumentNullException.ThrowIfNull(e); - Rectangle bounds = e.AffectedBounds; - Graphics g = e.Graphics; - - // Draw a dark mode sizing grip - Color darkColor = GetDarkModeColor(SystemColors.ControlDark); - Color lightColor = GetDarkModeColor(SystemColors.ControlLight); - - // Start at the bottom-right corner and move up and left - int x = bounds.Right - 3; - int y = bounds.Bottom - 3; - - for (int i = 0; i < 3; i++) - { - int tempX = x; - int tempY = y; - - for (int j = 0; j <= i; j++) - { - g.FillRectangle(new SolidBrush(darkColor), tempX, tempY, 2, 2); - g.FillRectangle(new SolidBrush(lightColor), tempX - 1, tempY - 1, 1, 1); + using var highLightBrush = GetDarkModeBrush(SystemColors.ButtonHighlight); + using var shadowBrush = GetDarkModeBrush(SystemColors.ButtonShadow); - tempX -= 4; - tempY += 4; - } - - x -= 4; - y -= 4; - } + OnRenderStatusStripSizingGrip( + eArgs: e, + highLightBrush: highLightBrush, + shadowBrush: shadowBrush); } } diff --git a/src/System.Windows.Forms/System/Windows/Forms/Form.cs b/src/System.Windows.Forms/System/Windows/Forms/Form.cs index 5dbf765331b..92822eeb649 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Form.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Form.cs @@ -2261,7 +2261,6 @@ protected override void SetVisibleCore(bool value) [SRDescription(nameof(SR.FormCornerPreferenceDescr))] [Browsable(false)] [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] - [Experimental(DiagnosticIDs.ExperimentalDarkMode, UrlFormat = DiagnosticIDs.UrlFormat)] public FormCornerPreference FormCornerPreference { get => Properties.GetValueOrDefault(s_propFormCornerPreference, FormCornerPreference.Default); @@ -2299,7 +2298,6 @@ public FormCornerPreference FormCornerPreference /// /// An that contains the event data, in this case empty. /// - [Experimental(DiagnosticIDs.ExperimentalDarkMode, UrlFormat = DiagnosticIDs.UrlFormat)] protected virtual void OnFormCornerPreferenceChanged(EventArgs e) { if (Events[s_formCornerPreferenceChanged] is EventHandler eventHandler) @@ -2308,9 +2306,7 @@ protected virtual void OnFormCornerPreferenceChanged(EventArgs e) } } -#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. private unsafe void SetFormCornerPreferenceInternal(FormCornerPreference cornerPreference) -#pragma warning restore WFO5001 { DWM_WINDOW_CORNER_PREFERENCE dwmCornerPreference = cornerPreference switch { @@ -2353,7 +2349,6 @@ private unsafe void SetFormCornerPreferenceInternal(FormCornerPreference cornerP [SRDescription(nameof(SR.FormBorderColorDescr))] [Browsable(false)] [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] - [Experimental(DiagnosticIDs.ExperimentalDarkMode, UrlFormat = DiagnosticIDs.UrlFormat)] public Color FormBorderColor { get => Properties.GetValueOrDefault(s_propFormBorderColor, Color.Empty); @@ -2381,7 +2376,6 @@ public Color FormBorderColor /// /// An that contains the event data, in this case empty. /// - [Experimental(DiagnosticIDs.ExperimentalDarkMode, UrlFormat = DiagnosticIDs.UrlFormat)] protected virtual void OnFormBorderColorChanged(EventArgs e) { if (Events[s_formBorderColorChanged] is EventHandler eventHandler) @@ -2415,7 +2409,6 @@ protected virtual void OnFormBorderColorChanged(EventArgs e) [SRDescription(nameof(SR.FormCaptionBackColorDescr))] [Browsable(false)] [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] - [Experimental(DiagnosticIDs.ExperimentalDarkMode, UrlFormat = DiagnosticIDs.UrlFormat)] public Color FormCaptionBackColor { get => Properties.GetValueOrDefault(s_propFormCaptionBackColor, Color.Empty); @@ -2444,7 +2437,6 @@ public Color FormCaptionBackColor /// /// An that contains the event data, in this case empty. /// - [Experimental(DiagnosticIDs.ExperimentalDarkMode, UrlFormat = DiagnosticIDs.UrlFormat)] protected virtual void OnFormCaptionBackColorChanged(EventArgs e) { if (Events[s_formCaptionBackColorChanged] is EventHandler eventHandler) @@ -2478,7 +2470,6 @@ protected virtual void OnFormCaptionBackColorChanged(EventArgs e) [SRDescription(nameof(SR.FormCaptionTextColorDescr))] [Browsable(false)] [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] - [Experimental(DiagnosticIDs.ExperimentalDarkMode, UrlFormat = DiagnosticIDs.UrlFormat)] public Color FormCaptionTextColor { get => Properties.GetValueOrDefault(s_propFormCaptionTextColor, Color.Empty); @@ -2507,7 +2498,6 @@ public Color FormCaptionTextColor /// /// An that contains the event data, in this case empty. /// - [Experimental(DiagnosticIDs.ExperimentalDarkMode, UrlFormat = DiagnosticIDs.UrlFormat)] protected virtual void OnFormCaptionTextColorChanged(EventArgs e) { if (Events[s_formCaptionTextColorChanged] is EventHandler eventHandler) From a0277bde38b67f23cb68395f9aabeea3e4b0eb76 Mon Sep 17 00:00:00 2001 From: Klaus Loeffelmann Date: Thu, 8 May 2025 14:22:54 -0700 Subject: [PATCH 04/10] Change Grip renderer to use predefined shape. --- .../Forms/Controls/ToolStrips/ToolStrip.cs | 8 +- .../Controls/ToolStrips/ToolStripRenderer.cs | 111 +++++++++++------- .../ToolStripSystemDarkModeRenderer.cs | 2 +- .../ToolStrips/ToolStripSystemRenderer.cs | 18 +-- 4 files changed, 77 insertions(+), 62 deletions(-) diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStrip.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStrip.cs index 20e983b1371..4d9a2ad3186 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStrip.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStrip.cs @@ -3250,7 +3250,7 @@ protected override void OnLayout(LayoutEventArgs e) { LayoutRequired = false; - // we need to do this to prevent autosizing to happen while we're reparenting. + // we need to do this to prevent auto-sizing to happen while we're reparenting. ToolStripOverflow? overflow = GetOverflow(); if (overflow is not null) { @@ -3648,12 +3648,6 @@ protected override void OnPaintBackground(PaintEventArgs e) } } - if (Renderer.RendererOverride is ToolStripRenderer renderer) - { - renderer.DrawToolStripBackground(new ToolStripRenderEventArgs(g, this)); - return; - } - Renderer.DrawToolStripBackground(new ToolStripRenderEventArgs(g, this)); } finally diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStripRenderer.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStripRenderer.cs index eaa33326e6b..98b2bec9249 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStripRenderer.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStripRenderer.cs @@ -3,6 +3,7 @@ using System.ComponentModel; using System.Drawing; +using System.Drawing.Drawing2D; using System.Drawing.Imaging; using System.Drawing.Text; using System.Windows.Forms.Layout; @@ -55,12 +56,16 @@ public abstract class ToolStripRenderer // status strip sizing grip. private static readonly Rectangle[] s_baseSizeGripRectangles = [ - new(8, 0, 2, 2), + new(12, 0, 2, 2), new(8, 4, 2, 2), - new(8, 8, 2, 2), - new(4, 4, 2, 2), new(4, 8, 2, 2), - new(0, 8, 2, 2) + new(0, 12, 2, 2), + new(8, 0, 2, 2), + new(4, 4, 2, 2), + new(0, 8, 2, 2), + new(4, 0, 2, 2), + new(0, 4, 2, 2), + new(1, 1, 2, 2), ]; protected ToolStripRenderer() @@ -1028,78 +1033,94 @@ private protected void OnRenderStatusStripSizingGrip( return; } - Graphics g = eArgs.Graphics; - - // we have a set of stock rectangles. Translate them over to where the grip is to be drawn - // for the white set, then translate them up and right one pixel for the grey. - if (eArgs.ToolStrip is not StatusStrip statusStrip) { return; } Rectangle sizeGripBounds = statusStrip.SizeGripBounds; - if (LayoutUtils.IsZeroWidthOrHeight(sizeGripBounds)) { return; } - Rectangle[] whiteRectangles = new Rectangle[s_baseSizeGripRectangles.Length]; - Rectangle[] greyRectangles = new Rectangle[s_baseSizeGripRectangles.Length]; + Graphics g = eArgs.Graphics; + + // Use device DPI for scaling + float dpiScale = 1.0f; - for (int i = 0; i < s_baseSizeGripRectangles.Length; i++) + if (statusStrip.DeviceDpi > 0 && ScaleHelper.IsThreadPerMonitorV2Aware) { - Rectangle baseRect = s_baseSizeGripRectangles[i]; + dpiScale = statusStrip.DeviceDpi / 96f; + } - if (statusStrip.RightToLeft == RightToLeft.Yes) - { - baseRect.X = sizeGripBounds.Width - baseRect.X - baseRect.Width; - } + // Scale the base rectangles for the grip dots + Rectangle[] scaledRects = new Rectangle[s_baseSizeGripRectangles.Length]; - // Height of pyramid (10px) + 2px padding from bottom. - baseRect.Offset( - x: sizeGripBounds.X, - y: sizeGripBounds.Bottom - 12); + for (int i = 0; i < s_baseSizeGripRectangles.Length; i++) + { + Rectangle r = s_baseSizeGripRectangles[i]; - whiteRectangles[i] = baseRect; + scaledRects[i] = new Rectangle( + (int)(r.X * dpiScale), + (int)(r.Y * dpiScale), + Math.Max((int)(r.Width * dpiScale), 2), + Math.Max((int)(r.Height * dpiScale), 2)); + } - int offset = -1 + GetCornerOffset(statusStrip); + (int cornerOffset, Rectangle lastRect) = GetCornerOffset(statusStrip); + scaledRects[^1] = lastRect; - if (statusStrip.RightToLeft == RightToLeft.Yes) - { - baseRect.Offset(1, -1 - offset); - } - else - { - baseRect.Offset(-1, -1 - offset); - } + SmoothingMode oldSmoothing = g.SmoothingMode; + g.SmoothingMode = SmoothingMode.AntiAlias; - greyRectangles[i] = baseRect; + // Draw the grip dots, bottom-right aligned (mirrored for RTL) + foreach (Rectangle dotRect in scaledRects) + { + Rectangle actualRect = statusStrip.RightToLeft == RightToLeft.Yes + ? new Rectangle( + x: sizeGripBounds.Left + cornerOffset + dotRect.X, + y: sizeGripBounds.Bottom - cornerOffset - dotRect.Y - dotRect.Height, + width: dotRect.Width, + height: dotRect.Height) + + : new Rectangle( + x: sizeGripBounds.Right - cornerOffset - dotRect.X - dotRect.Width, + y: sizeGripBounds.Bottom - cornerOffset - dotRect.Y - dotRect.Height, + width: dotRect.Width, + height: dotRect.Height); + + // Highlight dot (top-left) + Rectangle highlightRect = actualRect; + highlightRect.Offset(-1, -1); + + g.FillEllipse(highLightBrush, highlightRect); + + // Shadow dot (bottom-right) + Rectangle shadowRect = actualRect; + shadowRect.Offset(1, 1); + + g.FillEllipse(shadowBrush, shadowRect); } - g.FillRectangles(highLightBrush, whiteRectangles); - g.FillRectangles(shadowBrush, greyRectangles); + g.SmoothingMode = oldSmoothing; - // We need to compensate for the rounded Window corners from Windows 11 on. - static int GetCornerOffset(StatusStrip statusStrip) + static (int cornerOffset, Rectangle rect) GetCornerOffset(StatusStrip statusStrip) { - // If we're on Windows 11, offset slightly to avoid hitting rounded corners, - // _if_ we are at all dealing with rounded corners. - int cornerOffset = 0; + (int, Rectangle) cornerDef = (2, new(1, 1, 2, 2)); if (Environment.OSVersion.Version >= new Version(10, 0, 22000) && statusStrip.FindForm() is Form f) { - cornerOffset = f.FormCornerPreference switch + cornerDef = f.FormCornerPreference switch { - FormCornerPreference.Round => 5, - FormCornerPreference.RoundSmall => 3, - _ => 3 + FormCornerPreference.Round => (4, new(1, 1, 2, 2)), + FormCornerPreference.RoundSmall => (3, new(1, 1, 2, 2)), + _ => (2, new(0, 0, 2, 2)) }; } - return cornerOffset; + return cornerDef; } } diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStripSystemDarkModeRenderer.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStripSystemDarkModeRenderer.cs index 975a61dbb7e..d436bcd2d2b 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStripSystemDarkModeRenderer.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStripSystemDarkModeRenderer.cs @@ -677,7 +677,7 @@ protected override void OnRenderStatusStripSizingGrip(ToolStripRenderEventArgs e { ArgumentNullException.ThrowIfNull(e); - using var highLightBrush = GetDarkModeBrush(SystemColors.ButtonHighlight); + using var highLightBrush = GetDarkModeBrush(SystemColors.GrayText); using var shadowBrush = GetDarkModeBrush(SystemColors.ButtonShadow); OnRenderStatusStripSizingGrip( diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStripSystemRenderer.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStripSystemRenderer.cs index f84d83e0f74..cd921e2857e 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStripSystemRenderer.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStripSystemRenderer.cs @@ -39,7 +39,7 @@ internal ToolStripRenderer HighContrastRenderer { get { - _toolStripHighContrastRenderer ??= new ToolStripHighContrastRenderer(/*renderLikeSystem*/true); + _toolStripHighContrastRenderer ??= new ToolStripHighContrastRenderer(systemRenderMode: false); return _toolStripHighContrastRenderer; } } @@ -51,7 +51,7 @@ internal ToolStripRenderer DarkModeRenderer { get { - _toolStripDarkModeRenderer ??= new ToolStripSystemDarkModeRenderer(); + _toolStripDarkModeRenderer ??= new ToolStripSystemDarkModeRenderer(isDefault: false); return _toolStripDarkModeRenderer; } } @@ -240,18 +240,18 @@ protected override void OnRenderToolStripBackground(ToolStripRenderEventArgs e) else if (DisplayInformation.LowResolution) { FillBackground(g, bounds, (toolStrip is ToolStripDropDown) - ? SystemColors.ControlLight - : e.BackColor); + ? e.BackColor + : SystemColors.ControlLight); } else if (toolStrip.IsDropDown) { - FillBackground(g, bounds, ToolStripManager.VisualStylesEnabled + FillBackground(g, bounds, (!ToolStripManager.VisualStylesEnabled) ? SystemColors.Menu : e.BackColor); } else if (toolStrip is MenuStrip) { - FillBackground(g, bounds, ToolStripManager.VisualStylesEnabled + FillBackground(g, bounds, (!ToolStripManager.VisualStylesEnabled) ? SystemColors.MenuBar : e.BackColor); } @@ -264,9 +264,9 @@ protected override void OnRenderToolStripBackground(ToolStripRenderEventArgs e) } else { - FillBackground(g, bounds, ToolStripManager.VisualStylesEnabled - ? SystemColors.MenuBar - : e.BackColor); + FillBackground(g, bounds, (!ToolStripManager.VisualStylesEnabled) + ? e.BackColor + : SystemColors.MenuBar); } } } From 377da34508b6e2761a7a5207fb5d375c346da8ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Klaus=20L=C3=B6ffelmann?= Date: Sun, 11 May 2025 13:02:51 -0700 Subject: [PATCH 05/10] First batch of addressed review suggestions. --- .../Forms/Controls/ToolStrips/ToolStrip.cs | 2 +- .../Controls/ToolStrips/ToolStripManager.cs | 2 +- .../Controls/ToolStrips/ToolStripRenderer.cs | 96 +++++++++++-------- .../ToolStrips/ToolStripRendererSwitcher.cs | 2 +- .../ToolStripSystemDarkModeRenderer.cs | 10 +- .../ToolStrips/ToolStripSystemRenderer.cs | 28 ++++-- 6 files changed, 87 insertions(+), 53 deletions(-) diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStrip.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStrip.cs index 4d9a2ad3186..cc5f21256ce 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStrip.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStrip.cs @@ -1549,7 +1549,7 @@ public ToolStripRenderMode RenderMode return ToolStripRenderMode.ManagerRenderMode; } - if (_renderer is not null && !_renderer.IsAutoGenerated) + if (_renderer is not null && !_renderer.IsSystemDefaultAlternative) { return ToolStripRenderMode.Custom; } diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStripManager.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStripManager.cs index 7d3673213cb..b0a4057f5a6 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStripManager.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStripManager.cs @@ -489,7 +489,7 @@ public static ToolStripManagerRenderMode RenderMode { Type currentType = CurrentRendererType; - if (t_defaultRenderer is not null && !t_defaultRenderer.IsAutoGenerated) + if (t_defaultRenderer is not null && !t_defaultRenderer.IsSystemDefaultAlternative) { return ToolStripManagerRenderMode.Custom; } diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStripRenderer.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStripRenderer.cs index 98b2bec9249..9603d2bad1d 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStripRenderer.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStripRenderer.cs @@ -37,7 +37,7 @@ public abstract class ToolStripRenderer private static ColorMatrix? s_disabledImageColorMatrix; private EventHandlerList? _events; - private readonly bool _isAutoGenerated; + private readonly bool _isSystemDefaultAlternative; private static bool s_isScalingInitialized; internal int _previousDeviceDpi = ScaleHelper.InitialSystemDpi; @@ -52,28 +52,12 @@ public abstract class ToolStripRenderer private static int s_offset4X = OFFSET_4PIXELS; private static int s_offset4Y = OFFSET_4PIXELS; - // Used in building up the half pyramid of rectangles that are drawn in a - // status strip sizing grip. - private static readonly Rectangle[] s_baseSizeGripRectangles = - [ - new(12, 0, 2, 2), - new(8, 4, 2, 2), - new(4, 8, 2, 2), - new(0, 12, 2, 2), - new(8, 0, 2, 2), - new(4, 4, 2, 2), - new(0, 8, 2, 2), - new(4, 0, 2, 2), - new(0, 4, 2, 2), - new(1, 1, 2, 2), - ]; - protected ToolStripRenderer() { } internal ToolStripRenderer(bool isAutoGenerated) => - _isAutoGenerated = isAutoGenerated; + _isSystemDefaultAlternative = isAutoGenerated; // Used in building disabled images. private static ColorMatrix DisabledImageColorMatrix @@ -153,7 +137,13 @@ private EventHandlerList Events } } - internal bool IsAutoGenerated => _isAutoGenerated; + /// + /// Defines, if there is a variation of the system default renderer, that is chosen by the system + /// to address the current environment context. Like DarkMode, HighContrast, LowRes etc. + /// (Used to be 'AutoGenerated', presuming to indicate, that the renderer was 'generated' (picked) by the system.) + /// + internal bool IsSystemDefaultAlternative + => _isSystemDefaultAlternative; // if we're in a low contrast, high resolution situation, use this renderer under the covers instead. internal virtual ToolStripRenderer? RendererOverride => null; @@ -1012,39 +1002,57 @@ protected virtual void OnRenderToolStripStatusLabelBackground(ToolStripItemRende } } + // Used in building up the half pyramid of rectangles that are drawn in a + // status strip sizing grip. + private static readonly Rectangle[] s_baseSizeGripRectangles = + [ + new(12, 0, 2, 2), + new(8, 4, 2, 2), + new(4, 8, 2, 2), + new(0, 12, 2, 2), + new(8, 0, 2, 2), + new(4, 4, 2, 2), + new(0, 8, 2, 2), + new(4, 0, 2, 2), + new(0, 4, 2, 2), + new(1, 1, 2, 2), + ]; + protected virtual void OnRenderStatusStripSizingGrip(ToolStripRenderEventArgs e) { ArgumentNullException.ThrowIfNull(e); + if (RendererOverride is not null) + { + RendererOverride.OnRenderStatusStripSizingGrip(e); + return; + } + OnRenderStatusStripSizingGrip( eArgs: e, highLightBrush: SystemBrushes.ButtonHighlight, - shadowBrush: SystemBrushes.ButtonShadow); + shadowBrush: SystemBrushes.GrayText); } - private protected void OnRenderStatusStripSizingGrip( + private protected static void OnRenderStatusStripSizingGrip( ToolStripRenderEventArgs eArgs, Brush highLightBrush, Brush shadowBrush) { - if (RendererOverride is not null) - { - RendererOverride.OnRenderStatusStripSizingGrip(eArgs); - return; - } - if (eArgs.ToolStrip is not StatusStrip statusStrip) { return; } Rectangle sizeGripBounds = statusStrip.SizeGripBounds; + if (LayoutUtils.IsZeroWidthOrHeight(sizeGripBounds)) { return; } Graphics g = eArgs.Graphics; + ReadOnlySpan baseRects = s_baseSizeGripRectangles; // Use device DPI for scaling float dpiScale = 1.0f; @@ -1054,12 +1062,13 @@ private protected void OnRenderStatusStripSizingGrip( dpiScale = statusStrip.DeviceDpi / 96f; } - // Scale the base rectangles for the grip dots - Rectangle[] scaledRects = new Rectangle[s_baseSizeGripRectangles.Length]; + // Create a buffer on the stack for the scaled rectangles + Span scaledRects = stackalloc Rectangle[baseRects.Length]; - for (int i = 0; i < s_baseSizeGripRectangles.Length; i++) + // Scale the base rectangles for the grip dots + for (int i = 0; i < baseRects.Length; i++) { - Rectangle r = s_baseSizeGripRectangles[i]; + Rectangle r = baseRects[i]; scaledRects[i] = new Rectangle( (int)(r.X * dpiScale), @@ -1074,16 +1083,25 @@ private protected void OnRenderStatusStripSizingGrip( SmoothingMode oldSmoothing = g.SmoothingMode; g.SmoothingMode = SmoothingMode.AntiAlias; + // Optimize for RTL check by determining the calculation function once + bool isRtl = statusStrip.RightToLeft == RightToLeft.Yes; + // Draw the grip dots, bottom-right aligned (mirrored for RTL) - foreach (Rectangle dotRect in scaledRects) + Span workingRects = stackalloc Rectangle[3]; // actualRect, highlightRect, shadowRect + + for (int i = 0; i < scaledRects.Length; i++) { - Rectangle actualRect = statusStrip.RightToLeft == RightToLeft.Yes + ref Rectangle dotRect = ref scaledRects[i]; + ref Rectangle actualRect = ref workingRects[0]; + ref Rectangle highlightRect = ref workingRects[1]; + ref Rectangle shadowRect = ref workingRects[2]; + + actualRect = isRtl ? new Rectangle( x: sizeGripBounds.Left + cornerOffset + dotRect.X, y: sizeGripBounds.Bottom - cornerOffset - dotRect.Y - dotRect.Height, width: dotRect.Width, height: dotRect.Height) - : new Rectangle( x: sizeGripBounds.Right - cornerOffset - dotRect.X - dotRect.Width, y: sizeGripBounds.Bottom - cornerOffset - dotRect.Y - dotRect.Height, @@ -1091,23 +1109,23 @@ private protected void OnRenderStatusStripSizingGrip( height: dotRect.Height); // Highlight dot (top-left) - Rectangle highlightRect = actualRect; + highlightRect = actualRect; highlightRect.Offset(-1, -1); - g.FillEllipse(highLightBrush, highlightRect); // Shadow dot (bottom-right) - Rectangle shadowRect = actualRect; + shadowRect = actualRect; shadowRect.Offset(1, 1); - g.FillEllipse(shadowBrush, shadowRect); } g.SmoothingMode = oldSmoothing; + // We need to account for Windows 11+ AND whatever styled corners we have. static (int cornerOffset, Rectangle rect) GetCornerOffset(StatusStrip statusStrip) { - (int, Rectangle) cornerDef = (2, new(1, 1, 2, 2)); + // Default values + (int offset, Rectangle rect) cornerDef = (2, new(1, 1, 2, 2)); if (Environment.OSVersion.Version >= new Version(10, 0, 22000) && statusStrip.FindForm() is Form f) diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStripRendererSwitcher.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStripRendererSwitcher.cs index 1788be8d09b..3b04339744e 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStripRendererSwitcher.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStripRendererSwitcher.cs @@ -80,7 +80,7 @@ public ToolStripRenderMode RenderMode return ToolStripRenderMode.ManagerRenderMode; } - if (_renderer is not null && !_renderer.IsAutoGenerated) + if (_renderer is not null && !_renderer.IsSystemDefaultAlternative) { return ToolStripRenderMode.Custom; } diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStripSystemDarkModeRenderer.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStripSystemDarkModeRenderer.cs index d436bcd2d2b..5df04054885 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStripSystemDarkModeRenderer.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStripSystemDarkModeRenderer.cs @@ -27,8 +27,12 @@ public ToolStripSystemDarkModeRenderer() /// /// Initializes a new instance of the ToolStripSystemDarkModeRenderer class with the specified default state. /// - /// true if this is the default renderer; otherwise, false. - internal ToolStripSystemDarkModeRenderer(bool isDefault) : base(isDefault) + /// + /// True if this should be seen as a variation of the default renderer + /// (so, no _custom_ renderer provided by the user); otherwise, false. + /// + internal ToolStripSystemDarkModeRenderer(bool isSystemDefaultAlternative) + : base(isSystemDefaultAlternative) { } @@ -171,7 +175,7 @@ protected override void OnRenderToolStripBackground(ToolStripRenderEventArgs e) /// Renders the StatusStrip background in dark mode. /// /// A ToolStripRenderEventArgs that contains the event data. - private static void RenderStatusStripBackground(ToolStripRenderEventArgs e) + internal static void RenderStatusStripBackground(ToolStripRenderEventArgs e) { Graphics g = e.Graphics; Rectangle bounds = e.AffectedBounds; diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStripSystemRenderer.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStripSystemRenderer.cs index cd921e2857e..4dfa8b522d3 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStripSystemRenderer.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStripSystemRenderer.cs @@ -51,7 +51,7 @@ internal ToolStripRenderer DarkModeRenderer { get { - _toolStripDarkModeRenderer ??= new ToolStripSystemDarkModeRenderer(isDefault: false); + _toolStripDarkModeRenderer ??= new ToolStripSystemDarkModeRenderer(isSystemDefaultAlternative: false); return _toolStripDarkModeRenderer; } } @@ -218,6 +218,18 @@ private static ToolBarState GetToolBarState(ToolStripItem item) /// protected override void OnRenderToolStripBackground(ToolStripRenderEventArgs e) { + // It is not entirely clear, why we're not leaving it to the respective renderers + // what to do here, but for keeping as much as possible of the original behavior, + // we need to check if the renderer override is set, test for particular renderer types + // and then decide what to do. + if (RendererOverride is ToolStripSystemDarkModeRenderer darkModeRenderer + && darkModeRenderer.IsSystemDefaultAlternative) + { + base.OnRenderToolStripBackground(e); + + return; + } + ToolStrip toolStrip = e.ToolStrip; Graphics g = e.Graphics; Rectangle bounds = e.AffectedBounds; @@ -240,18 +252,18 @@ protected override void OnRenderToolStripBackground(ToolStripRenderEventArgs e) else if (DisplayInformation.LowResolution) { FillBackground(g, bounds, (toolStrip is ToolStripDropDown) - ? e.BackColor - : SystemColors.ControlLight); + ? SystemColors.ControlLight + : e.BackColor); } else if (toolStrip.IsDropDown) { - FillBackground(g, bounds, (!ToolStripManager.VisualStylesEnabled) + FillBackground(g, bounds, ToolStripManager.VisualStylesEnabled ? SystemColors.Menu : e.BackColor); } else if (toolStrip is MenuStrip) { - FillBackground(g, bounds, (!ToolStripManager.VisualStylesEnabled) + FillBackground(g, bounds, ToolStripManager.VisualStylesEnabled ? SystemColors.MenuBar : e.BackColor); } @@ -264,9 +276,9 @@ protected override void OnRenderToolStripBackground(ToolStripRenderEventArgs e) } else { - FillBackground(g, bounds, (!ToolStripManager.VisualStylesEnabled) - ? e.BackColor - : SystemColors.MenuBar); + FillBackground(g, bounds, ToolStripManager.VisualStylesEnabled + ? SystemColors.MenuBar + : e.BackColor); } } } From 25e40aec5d76becc48d60971d846933503924a13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Klaus=20L=C3=B6ffelmann?= Date: Thu, 22 May 2025 15:02:58 -0700 Subject: [PATCH 06/10] Refactor ToolStripRenderer classes and improve generic and GDIPlus Copilot Instruction files. --- .../System/Windows/Forms/Control.cs | 6 +- .../Controls/ToolStrips/ToolStripRenderer.cs | 2 + .../ToolStripSystemDarkModeRenderer.cs | 291 ++++++++---------- .../System/Windows/Forms/Form.cs | 10 + 4 files changed, 142 insertions(+), 167 deletions(-) diff --git a/src/System.Windows.Forms/System/Windows/Forms/Control.cs b/src/System.Windows.Forms/System/Windows/Forms/Control.cs index 27adec63d11..fd53c7d7dfb 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Control.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Control.cs @@ -279,13 +279,13 @@ public unsafe partial class Control : private Font? _scaledControlFont; private FontHandleWrapper? _scaledFontWrapper; + // Contains a collection of calculated fonts for various Dpi values of the control in the PerMonV2 mode. + private Dictionary? _dpiFonts; + // ContainerControls like 'PropertyGrid' scale their children when they resize. // no explicit scaling of children required in such cases. They have specific logic. internal bool _doNotScaleChildren; - // Contains a collection of calculated fonts for various Dpi values of the control in the PerMonV2 mode. - private Dictionary? _dpiFonts; - // Flag to signify whether any child controls necessitate the calculation of AnchorsInfo, // particularly in cases involving nested containers. internal bool _childControlsNeedAnchorLayout; diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStripRenderer.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStripRenderer.cs index 9603d2bad1d..f96041a3210 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStripRenderer.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStripRenderer.cs @@ -1130,12 +1130,14 @@ private protected static void OnRenderStatusStripSizingGrip( if (Environment.OSVersion.Version >= new Version(10, 0, 22000) && statusStrip.FindForm() is Form f) { +#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. cornerDef = f.FormCornerPreference switch { FormCornerPreference.Round => (4, new(1, 1, 2, 2)), FormCornerPreference.RoundSmall => (3, new(1, 1, 2, 2)), _ => (2, new(0, 0, 2, 2)) }; +#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. } return cornerDef; diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStripSystemDarkModeRenderer.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStripSystemDarkModeRenderer.cs index 5df04054885..dba67b0faad 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStripSystemDarkModeRenderer.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStripSystemDarkModeRenderer.cs @@ -13,7 +13,7 @@ namespace System.Windows.Forms; /// This renderer is designed to be used with the ToolStripSystemRenderer to provide dark mode /// styling while maintaining accessibility features. It inherits from ToolStripRenderer /// and overrides necessary methods to provide dark-themed rendering. -/// +/// /// internal class ToolStripSystemDarkModeRenderer : ToolStripRenderer { @@ -41,72 +41,43 @@ internal ToolStripSystemDarkModeRenderer(bool isSystemDefaultAlternative) /// /// The color to convert to a dark mode equivalent. /// A color suitable for dark mode. - private static Color GetDarkModeColor(Color color) - { - // Map system colors to some slightly different colors we would get - // form the actual system colors in dark mode, since the visual style - // renderer in light mode would also not "hit" (for contrast and styling - // reasons) the exact same palette settings as the system colors. - if (color == SystemColors.Control) - return Color.FromArgb(45, 45, 45); - if (color == SystemColors.ControlLight) - return Color.FromArgb(60, 60, 60); - if (color == SystemColors.ControlDark) - return Color.FromArgb(30, 30, 30); - if (color == SystemColors.ControlText) - return Color.FromArgb(240, 240, 240); - if (color == SystemColors.ButtonFace) - return Color.FromArgb(45, 45, 45); - if (color == SystemColors.Highlight) - return Color.FromArgb(0, 120, 215); - if (color == SystemColors.HighlightText) - return Color.White; - if (color == SystemColors.Window) - return Color.FromArgb(32, 32, 32); - if (color == SystemColors.WindowText) - return Color.FromArgb(240, 240, 240); - if (color == SystemColors.GrayText) - return Color.FromArgb(153, 153, 153); - if (color == SystemColors.InactiveBorder) - return Color.FromArgb(70, 70, 70); - if (color == SystemColors.ButtonHighlight) - return Color.FromArgb(80, 80, 80); - if (color == SystemColors.ButtonShadow) - return Color.FromArgb(20, 20, 20); - if (color == SystemColors.Menu) - return Color.FromArgb(45, 45, 45); - if (color == SystemColors.MenuText) - return Color.FromArgb(240, 240, 240); - - // For any other colors, darken them if they're bright - if (color.GetBrightness() > 0.5) + private static Color GetDarkModeColor(Color color) => + color switch { - // Create a darker version for light colors - return ControlPaint.Dark(color, 0.2f); - } - - return color; - } + Color c when c == SystemColors.Control => Color.FromArgb(45, 45, 45), + Color c when c == SystemColors.ControlLight => Color.FromArgb(60, 60, 60), + Color c when c == SystemColors.ControlDark => Color.FromArgb(30, 30, 30), + Color c when c == SystemColors.ControlText => Color.FromArgb(240, 240, 240), + Color c when c == SystemColors.ButtonFace => Color.FromArgb(45, 45, 45), + Color c when c == SystemColors.Highlight => Color.FromArgb(0, 120, 215), + Color c when c == SystemColors.HighlightText => Color.White, + Color c when c == SystemColors.Window => Color.FromArgb(32, 32, 32), + Color c when c == SystemColors.WindowText => Color.FromArgb(240, 240, 240), + Color c when c == SystemColors.GrayText => Color.FromArgb(153, 153, 153), + Color c when c == SystemColors.InactiveBorder => Color.FromArgb(70, 70, 70), + Color c when c == SystemColors.ButtonHighlight => Color.FromArgb(80, 80, 80), + Color c when c == SystemColors.ButtonShadow => Color.FromArgb(20, 20, 20), + Color c when c == SystemColors.Menu => Color.FromArgb(45, 45, 45), + Color c when c == SystemColors.MenuText => Color.FromArgb(240, 240, 240), + _ when color.GetBrightness() > 0.5 => ControlPaint.Dark(color, 0.2f), + _ => color + }; /// - /// Creates a dark mode compatible brush. Important: - /// Always do: `using var brush = GetDarkModeBrush(color)`, - /// since you're dealing with a cached brush => scope, really! + /// Creates a dark mode compatible brush. Always use 'using var brush = GetDarkModeBrush(color)'. /// /// The system color to convert. /// A brush with the dark mode color. - private static SolidBrushCache.Scope GetDarkModeBrush(Color color) - => GetDarkModeColor(color).GetCachedSolidBrushScope(); + private static SolidBrushCache.Scope GetDarkModeBrush(Color color) => + GetDarkModeColor(color).GetCachedSolidBrushScope(); /// - /// Creates a dark mode compatible pen. Important: - /// Always do: `using var somePen = GetDarkModePen(color)`, - /// since you're dealing with a cached pen => scope, really! + /// Creates a dark mode compatible pen. Always use 'using var pen = GetDarkModePen(color)'. /// /// The system color to convert. /// A pen with the dark mode color. - private static PenCache.Scope GetDarkModePen(Color color) - => GetDarkModeColor(color).GetCachedPenScope(); + private static PenCache.Scope GetDarkModePen(Color color) => + GetDarkModeColor(color).GetCachedPenScope(); /// /// Returns whether the background should be painted. @@ -125,10 +96,12 @@ private static bool ShouldPaintBackground(ToolStrip toolStrip) private static void FillBackground(Graphics g, Rectangle bounds, Color backColor) { if (bounds.Width <= 0 || bounds.Height <= 0) + { return; + } - // Use a dark mode color using var brush = GetDarkModeBrush(backColor); + g.FillRectangle(brush, bounds); } @@ -145,29 +118,31 @@ protected override void OnRenderToolStripBackground(ToolStripRenderEventArgs e) Rectangle bounds = e.AffectedBounds; if (!ShouldPaintBackground(toolStrip)) + { return; + } if (toolStrip is StatusStrip) { RenderStatusStripBackground(e); + + return; + } + + if (toolStrip.IsDropDown) + { + // Dark mode dropdown background + FillBackground(g, bounds, GetDarkModeColor(SystemColors.Menu)); + } + else if (toolStrip is MenuStrip) + { + // Dark mode menu background + FillBackground(g, bounds, GetDarkModeColor(SystemColors.Menu)); } else { - if (toolStrip.IsDropDown) - { - // Dark mode dropdown background - FillBackground(g, bounds, GetDarkModeColor(SystemColors.Menu)); - } - else if (toolStrip is MenuStrip) - { - // Dark mode menu background - FillBackground(g, bounds, GetDarkModeColor(SystemColors.Menu)); - } - else - { - // Standard ToolStrip background - FillBackground(g, bounds, GetDarkModeColor(e.BackColor)); - } + // Standard ToolStrip background + FillBackground(g, bounds, GetDarkModeColor(e.BackColor)); } } @@ -196,36 +171,33 @@ protected override void OnRenderToolStripBorder(ToolStripRenderEventArgs e) Graphics g = e.Graphics; Rectangle bounds = e.ToolStrip.ClientRectangle; + using var borderPen = GetDarkModePen(SystemColors.ControlDark); + if (toolStrip is StatusStrip) { RenderStatusStripBorder(e); - } - else if (toolStrip is ToolStripDropDown) - { - ToolStripDropDown? toolStripDropDown = toolStrip as ToolStripDropDown; - Debug.Assert(toolStripDropDown is not null, $"ToolStripDropDown cannot be null in {nameof(OnRenderToolStripBorder)}."); + return; + } + if (toolStrip is ToolStripDropDown toolStripDropDown) + { if (toolStripDropDown.DropShadowEnabled) { bounds.Width -= 1; bounds.Height -= 1; - using var borderPen = GetDarkModePen(SystemColors.ControlDark); g.DrawRectangle(borderPen, bounds); } else { - using var borderPen = GetDarkModePen(SystemColors.ControlDark); g.DrawRectangle(borderPen, bounds); } + + return; } - else - { - // Draw a subtle bottom border for toolstrips - using var borderPen = GetDarkModePen(SystemColors.ControlDark); - g.DrawLine(borderPen, 0, bounds.Bottom - 1, bounds.Width, bounds.Bottom - 1); - } + + g.DrawLine(borderPen, 0, bounds.Bottom - 1, bounds.Width, bounds.Bottom - 1); } /// @@ -237,8 +209,8 @@ private static void RenderStatusStripBorder(ToolStripRenderEventArgs e) Graphics g = e.Graphics; Rectangle bounds = e.ToolStrip.ClientRectangle; - // Dark mode StatusStrip border (usually top border only) using var borderPen = GetDarkModePen(SystemColors.ControlDark); + g.DrawLine(borderPen, 0, 0, bounds.Width, 0); } @@ -250,9 +222,8 @@ protected override void OnRenderItemBackground(ToolStripItemRenderEventArgs e) { ArgumentNullException.ThrowIfNull(e); - Rectangle bounds = new Rectangle(Point.Empty, e.Item.Size); + Rectangle bounds = new(Point.Empty, e.Item.Size); - // For items on dropdowns, adjust the bounds if (e.Item.IsOnDropDown) { bounds.X += 2; @@ -264,25 +235,28 @@ protected override void OnRenderItemBackground(ToolStripItemRenderEventArgs e) // Dark mode selection highlight using var highlightBrush = GetDarkModeBrush(SystemColors.Highlight); e.Graphics.FillRectangle(highlightBrush, bounds); + + return; } - else + + // Render background image if available + if (e.Item.BackgroundImage is not null) { - // Render background image if available - if (e.Item.BackgroundImage is not null) - { - ControlPaint.DrawBackgroundImage( - e.Graphics, - e.Item.BackgroundImage, - GetDarkModeColor(e.Item.BackColor), - e.Item.BackgroundImageLayout, - e.Item.ContentRectangle, - bounds); - } - else if (e.Item.BackColor != Color.Transparent && e.Item.BackColor != Color.Empty) - { - // Custom background color (apply dark mode transformation) - FillBackground(e.Graphics, bounds, e.Item.BackColor); - } + ControlPaint.DrawBackgroundImage( + g: e.Graphics, + backgroundImage: e.Item.BackgroundImage, + backColor: GetDarkModeColor(e.Item.BackColor), + backgroundImageLayout: e.Item.BackgroundImageLayout, + bounds: e.Item.ContentRectangle, + clipRect: bounds); + + return; + } + + if (e.Item.BackColor != Color.Transparent && e.Item.BackColor != Color.Empty) + { + // Custom background color (apply dark mode transformation) + FillBackground(e.Graphics, bounds, e.Item.BackColor); } } @@ -294,7 +268,7 @@ protected override void OnRenderButtonBackground(ToolStripItemRenderEventArgs e) { ArgumentNullException.ThrowIfNull(e); - Rectangle bounds = new Rectangle(Point.Empty, e.Item.Size); + Rectangle bounds = new(Point.Empty, e.Item.Size); bool isPressed; bool isSelected; @@ -325,7 +299,6 @@ protected override void OnRenderButtonBackground(ToolStripItemRenderEventArgs e) /// A ToolStripItemRenderEventArgs that contains the event data. protected override void OnRenderDropDownButtonBackground(ToolStripItemRenderEventArgs e) { - // Reuse button background drawing for dropdown buttons OnRenderButtonBackground(e); } @@ -342,7 +315,7 @@ protected override void OnRenderSplitButtonBackground(ToolStripItemRenderEventAr return; } - Rectangle bounds = new Rectangle(Point.Empty, e.Item.Size); + Rectangle bounds = new(Point.Empty, e.Item.Size); // Render the background based on state if (splitButton.Selected || splitButton.Pressed) @@ -359,18 +332,18 @@ protected override void OnRenderSplitButtonBackground(ToolStripItemRenderEventAr using var linePen = GetDarkModePen(SystemColors.ControlDark); e.Graphics.DrawLine( - linePen, - dropDownRect.Left - 1, - dropDownRect.Top + 2, - dropDownRect.Left - 1, - dropDownRect.Bottom - 2); + pen: linePen, + x1: dropDownRect.Left - 1, + y1: dropDownRect.Top + 2, + x2: dropDownRect.Left - 1, + y2: dropDownRect.Bottom - 2); DrawArrow(new ToolStripArrowRenderEventArgs( - e.Graphics, - e.Item, - dropDownRect, - SystemColors.ControlText, - ArrowDirection.Down)); + g: e.Graphics, + toolStripItem: e.Item, + arrowRectangle: dropDownRect, + arrowColor: SystemColors.ControlText, + arrowDirection: ArrowDirection.Down)); } /// @@ -412,6 +385,7 @@ protected override void OnRenderSeparator(ToolStripSeparatorRenderEventArgs e) g.DrawLine(leftPen, startX, bounds.Top, startX, bounds.Bottom); startX++; + using var rightPen = GetDarkModeColor(SystemColors.ControlDark).GetCachedPenScope(); g.DrawLine(rightPen, startX, bounds.Top, startX, bounds.Bottom); } @@ -421,14 +395,16 @@ protected override void OnRenderSeparator(ToolStripSeparatorRenderEventArgs e) // Horizontal separator if (bounds.Width >= 4) { - bounds.Inflate(-2, 0); // Scoot over 2px and start drawing + bounds.Inflate(-2, 0); } int startY = bounds.Height / 2; + using var foreColorPen = GetDarkModeColor(SystemColors.ControlDark).GetCachedPenScope(); g.DrawLine(foreColorPen, bounds.Left, startY, bounds.Right, startY); startY++; + using var darkModePen = GetDarkModeColor(SystemColors.ButtonShadow).GetCachedPenScope(); g.DrawLine(darkModePen, bounds.Left, startY, bounds.Right, startY); } @@ -443,9 +419,11 @@ protected override void OnRenderOverflowButtonBackground(ToolStripItemRenderEven ArgumentNullException.ThrowIfNull(e); if (e.Item is not ToolStripOverflowButton item) + { return; + } - Rectangle bounds = new Rectangle(Point.Empty, e.Item.Size); + Rectangle bounds = new(Point.Empty, e.Item.Size); // Render the background based on state if (item.Selected || item.Pressed) @@ -457,12 +435,11 @@ protected override void OnRenderOverflowButtonBackground(ToolStripItemRenderEven e.Graphics.FillRectangle(fillBrush, bounds); } - // Draw the overflow arrow Rectangle arrowRect = item.ContentRectangle; - Point middle = new Point( - arrowRect.Left + arrowRect.Width / 2, - arrowRect.Top + arrowRect.Height / 2); + Point middle = new( + x: arrowRect.Left + arrowRect.Width / 2, + y: arrowRect.Top + arrowRect.Height / 2); // Default to down arrow for overflow buttons ArrowDirection arrowDirection = ArrowDirection.Down; @@ -534,9 +511,10 @@ protected override void OnRenderGrip(ToolStripGripRenderEventArgs e) ArgumentNullException.ThrowIfNull(e); if (e.GripBounds.Width <= 0 || e.GripBounds.Height <= 0) + { return; + } - // Use dark mode colors for the grip dots using var darkColorBrush = GetDarkModeColor(SystemColors.ControlDark).GetCachedSolidBrushScope(); using var lightColorBrush = GetDarkModeColor(SystemColors.ControlLight).GetCachedSolidBrushScope(); @@ -547,25 +525,27 @@ protected override void OnRenderGrip(ToolStripGripRenderEventArgs e) // Draw grip dots if (toolStrip.Orientation == Orientation.Horizontal) { - // Draw vertical grip + // Draw horizontal grip int y = bounds.Top + 2; while (y < bounds.Bottom - 3) { g.FillRectangle(darkColorBrush, bounds.Left + 2, y, 1, 1); g.FillRectangle(lightColorBrush, bounds.Left + 3, y + 1, 1, 1); + y += 3; } } else { - // Draw horizontal grip + // Draw vertical grip int x = bounds.Left + 2; while (x < bounds.Right - 3) { g.FillRectangle(darkColorBrush, x, bounds.Top + 2, 1, 1); g.FillRectangle(lightColorBrush, x + 1, bounds.Top + 3, 1, 1); + x += 3; } } @@ -575,21 +555,14 @@ protected override void OnRenderGrip(ToolStripGripRenderEventArgs e) /// Raises the RenderArrow event. /// /// A ToolStripArrowRenderEventArgs that contains the event data. - /// - /// Raises the RenderArrow event in the derived class with dark mode support. - /// protected override void OnRenderArrow(ToolStripArrowRenderEventArgs e) { ArgumentNullException.ThrowIfNull(e); Debug.Assert(e.Item is not null, "The ToolStripItem should not be null on rendering the Arrow."); - Color arrowColor = GetDarkModeColor(e.ArrowColor); - - // Use white arrow for selected/highlighted items - if (e.Item.Selected || e.Item.Pressed) - { - arrowColor = GetDarkModeColor(SystemColors.HighlightText); - } + Color arrowColor = (e.Item.Selected || e.Item.Pressed) + ? GetDarkModeColor(SystemColors.HighlightText) + : GetDarkModeColor(e.ArrowColor); RenderArrowCore(e, arrowColor); } @@ -603,7 +576,7 @@ protected override void OnRenderImageMargin(ToolStripRenderEventArgs e) ArgumentNullException.ThrowIfNull(e); // Fill the image margin with a slightly different color than the background - using var marginColorBrush = GetDarkModeColor(SystemColors.ControlLight).GetCachedSolidBrushScope(); + using var marginColorBrush = GetDarkModeBrush(SystemColors.ControlLight); e.Graphics.FillRectangle(marginColorBrush, e.AffectedBounds); } @@ -616,31 +589,19 @@ protected override void OnRenderItemText(ToolStripItemTextRenderEventArgs e) { ArgumentNullException.ThrowIfNull(e); - // Set text color based on selection state - Color textColor; - - if (e.Item.Selected || e.Item.Pressed) - { - textColor = GetDarkModeColor(SystemColors.HighlightText); - } - else if (!e.Item.Enabled) - { - textColor = GetDarkModeColor(SystemColors.GrayText); - } - else - { - // Use the original text color but make sure it's dark mode compatible - textColor = GetDarkModeColor(e.TextColor); - } + Color textColor = (e.Item.Selected || e.Item.Pressed) + ? GetDarkModeColor(SystemColors.HighlightText) + : !e.Item.Enabled + ? GetDarkModeColor(SystemColors.GrayText) + : GetDarkModeColor(e.TextColor); - // Draw the text TextRenderer.DrawText( - e.Graphics, - e.Text, - e.TextFont, - e.TextRectangle, - textColor, - e.TextFormat); + dc: e.Graphics, + text: e.Text, + font: e.TextFont, + bounds: e.TextRectangle, + foreColor: textColor, + flags: e.TextFormat); } /// @@ -652,13 +613,15 @@ protected override void OnRenderItemImage(ToolStripItemImageRenderEventArgs e) ArgumentNullException.ThrowIfNull(e); if (e.Image is null) + { return; + } // DarkMode adjustments for the image are done by // the base class implementation already. - Image image = !e.Item.Enabled - ? CreateDisabledImage(e.Image) - : e.Image; + Image image = e.Item.Enabled + ? e.Image + : CreateDisabledImage(e.Image); e.Graphics.DrawImage(image, e.ImageRectangle); } diff --git a/src/System.Windows.Forms/System/Windows/Forms/Form.cs b/src/System.Windows.Forms/System/Windows/Forms/Form.cs index 92822eeb649..d0cb40856fe 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Form.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Form.cs @@ -2261,6 +2261,7 @@ protected override void SetVisibleCore(bool value) [SRDescription(nameof(SR.FormCornerPreferenceDescr))] [Browsable(false)] [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + [Experimental(DiagnosticIDs.ExperimentalDarkMode, UrlFormat = DiagnosticIDs.UrlFormat)] public FormCornerPreference FormCornerPreference { get => Properties.GetValueOrDefault(s_propFormCornerPreference, FormCornerPreference.Default); @@ -2298,6 +2299,7 @@ public FormCornerPreference FormCornerPreference /// /// An that contains the event data, in this case empty. /// + [Experimental(DiagnosticIDs.ExperimentalDarkMode, UrlFormat = DiagnosticIDs.UrlFormat)] protected virtual void OnFormCornerPreferenceChanged(EventArgs e) { if (Events[s_formCornerPreferenceChanged] is EventHandler eventHandler) @@ -2306,7 +2308,9 @@ protected virtual void OnFormCornerPreferenceChanged(EventArgs e) } } +#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. private unsafe void SetFormCornerPreferenceInternal(FormCornerPreference cornerPreference) +#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. { DWM_WINDOW_CORNER_PREFERENCE dwmCornerPreference = cornerPreference switch { @@ -2349,6 +2353,7 @@ private unsafe void SetFormCornerPreferenceInternal(FormCornerPreference cornerP [SRDescription(nameof(SR.FormBorderColorDescr))] [Browsable(false)] [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + [Experimental(DiagnosticIDs.ExperimentalDarkMode, UrlFormat = DiagnosticIDs.UrlFormat)] public Color FormBorderColor { get => Properties.GetValueOrDefault(s_propFormBorderColor, Color.Empty); @@ -2376,6 +2381,7 @@ public Color FormBorderColor /// /// An that contains the event data, in this case empty. /// + [Experimental(DiagnosticIDs.ExperimentalDarkMode, UrlFormat = DiagnosticIDs.UrlFormat)] protected virtual void OnFormBorderColorChanged(EventArgs e) { if (Events[s_formBorderColorChanged] is EventHandler eventHandler) @@ -2409,6 +2415,7 @@ protected virtual void OnFormBorderColorChanged(EventArgs e) [SRDescription(nameof(SR.FormCaptionBackColorDescr))] [Browsable(false)] [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + [Experimental(DiagnosticIDs.ExperimentalDarkMode, UrlFormat = DiagnosticIDs.UrlFormat)] public Color FormCaptionBackColor { get => Properties.GetValueOrDefault(s_propFormCaptionBackColor, Color.Empty); @@ -2437,6 +2444,7 @@ public Color FormCaptionBackColor /// /// An that contains the event data, in this case empty. /// + [Experimental(DiagnosticIDs.ExperimentalDarkMode, UrlFormat = DiagnosticIDs.UrlFormat)] protected virtual void OnFormCaptionBackColorChanged(EventArgs e) { if (Events[s_formCaptionBackColorChanged] is EventHandler eventHandler) @@ -2470,6 +2478,7 @@ protected virtual void OnFormCaptionBackColorChanged(EventArgs e) [SRDescription(nameof(SR.FormCaptionTextColorDescr))] [Browsable(false)] [DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)] + [Experimental(DiagnosticIDs.ExperimentalDarkMode, UrlFormat = DiagnosticIDs.UrlFormat)] public Color FormCaptionTextColor { get => Properties.GetValueOrDefault(s_propFormCaptionTextColor, Color.Empty); @@ -2498,6 +2507,7 @@ public Color FormCaptionTextColor /// /// An that contains the event data, in this case empty. /// + [Experimental(DiagnosticIDs.ExperimentalDarkMode, UrlFormat = DiagnosticIDs.UrlFormat)] protected virtual void OnFormCaptionTextColorChanged(EventArgs e) { if (Events[s_formCaptionTextColorChanged] is EventHandler eventHandler) From a6436aa9b39e5a711113e12cc8d183605b1157dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Klaus=20L=C3=B6ffelmann?= Date: Wed, 11 Jun 2025 23:22:03 -0700 Subject: [PATCH 07/10] Fix existing bug in GripSize calculation in HighDPI. --- .../Controls/ToolStrips/ToolStripRenderer.cs | 125 +++++++++--------- 1 file changed, 60 insertions(+), 65 deletions(-) diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStripRenderer.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStripRenderer.cs index f96041a3210..9c8b911cd0f 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStripRenderer.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStripRenderer.cs @@ -1052,95 +1052,90 @@ private protected static void OnRenderStatusStripSizingGrip( } Graphics g = eArgs.Graphics; - ReadOnlySpan baseRects = s_baseSizeGripRectangles; + ReadOnlySpan baseRectangles = s_baseSizeGripRectangles; - // Use device DPI for scaling - float dpiScale = 1.0f; + // Reference height for sizing grips at 96 DPI (standard sizing) + const int DefaultGripAreaHeight = 20; - if (statusStrip.DeviceDpi > 0 && ScaleHelper.IsThreadPerMonitorV2Aware) - { - dpiScale = statusStrip.DeviceDpi / 96f; - } + // Calculate scaling based on the almost half of the current height + // of the status strip's sizing grip area + float heightScale = 0.6f * ((float)sizeGripBounds.Height / DefaultGripAreaHeight); - // Create a buffer on the stack for the scaled rectangles - Span scaledRects = stackalloc Rectangle[baseRects.Length]; + // Save the current graphics state before transformations + GraphicsState originalState = g.Save(); - // Scale the base rectangles for the grip dots - for (int i = 0; i < baseRects.Length; i++) + try { - Rectangle r = baseRects[i]; - - scaledRects[i] = new Rectangle( - (int)(r.X * dpiScale), - (int)(r.Y * dpiScale), - Math.Max((int)(r.Width * dpiScale), 2), - Math.Max((int)(r.Height * dpiScale), 2)); - } + // Set anti-aliasing for smoother appearance + SmoothingMode oldSmoothing = g.SmoothingMode; + g.SmoothingMode = SmoothingMode.AntiAlias; - (int cornerOffset, Rectangle lastRect) = GetCornerOffset(statusStrip); - scaledRects[^1] = lastRect; + // Translate to the corner where we'll start drawing + bool isRtl = statusStrip.RightToLeft == RightToLeft.Yes; + int cornerOffset = GetCornerOffset(statusStrip); - SmoothingMode oldSmoothing = g.SmoothingMode; - g.SmoothingMode = SmoothingMode.AntiAlias; + // Set up the transform to scale from the bottom corner + if (isRtl) + { + g.TranslateTransform(sizeGripBounds.Left + cornerOffset, sizeGripBounds.Bottom - cornerOffset); + } + else + { + g.TranslateTransform(sizeGripBounds.Right - cornerOffset, sizeGripBounds.Bottom - cornerOffset); + } - // Optimize for RTL check by determining the calculation function once - bool isRtl = statusStrip.RightToLeft == RightToLeft.Yes; + // Apply scaling + g.ScaleTransform(heightScale, heightScale); - // Draw the grip dots, bottom-right aligned (mirrored for RTL) - Span workingRects = stackalloc Rectangle[3]; // actualRect, highlightRect, shadowRect + // Draw the sizing grip dots in the scaled context + foreach (Rectangle baseRect in baseRectangles) + { + Rectangle dotRect = new( + isRtl ? baseRect.X : -baseRect.X - baseRect.Width, + -baseRect.Y - baseRect.Height, + baseRect.Width, + baseRect.Height); + + // Highlight dot (top-left) + Rectangle highlightRect = dotRect; + highlightRect.Offset(-1, -1); + g.FillEllipse(highLightBrush, highlightRect); + + // Shadow dot (bottom-right) + Rectangle shadowRect = dotRect; + shadowRect.Offset(1, 1); + g.FillEllipse(shadowBrush, shadowRect); + } - for (int i = 0; i < scaledRects.Length; i++) + // Restore the original smoothing mode + g.SmoothingMode = oldSmoothing; + } + finally { - ref Rectangle dotRect = ref scaledRects[i]; - ref Rectangle actualRect = ref workingRects[0]; - ref Rectangle highlightRect = ref workingRects[1]; - ref Rectangle shadowRect = ref workingRects[2]; - - actualRect = isRtl - ? new Rectangle( - x: sizeGripBounds.Left + cornerOffset + dotRect.X, - y: sizeGripBounds.Bottom - cornerOffset - dotRect.Y - dotRect.Height, - width: dotRect.Width, - height: dotRect.Height) - : new Rectangle( - x: sizeGripBounds.Right - cornerOffset - dotRect.X - dotRect.Width, - y: sizeGripBounds.Bottom - cornerOffset - dotRect.Y - dotRect.Height, - width: dotRect.Width, - height: dotRect.Height); - - // Highlight dot (top-left) - highlightRect = actualRect; - highlightRect.Offset(-1, -1); - g.FillEllipse(highLightBrush, highlightRect); - - // Shadow dot (bottom-right) - shadowRect = actualRect; - shadowRect.Offset(1, 1); - g.FillEllipse(shadowBrush, shadowRect); + // Always restore the original graphics state + g.Restore(originalState); } - g.SmoothingMode = oldSmoothing; - - // We need to account for Windows 11+ AND whatever styled corners we have. - static (int cornerOffset, Rectangle rect) GetCornerOffset(StatusStrip statusStrip) + // Helper method to determine corner offset based on form corner preference + static int GetCornerOffset(StatusStrip statusStrip) { - // Default values - (int offset, Rectangle rect) cornerDef = (2, new(1, 1, 2, 2)); + // Default offset + int offset = 2; if (Environment.OSVersion.Version >= new Version(10, 0, 22000) && statusStrip.FindForm() is Form f) { #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. - cornerDef = f.FormCornerPreference switch + offset = f.FormCornerPreference switch { - FormCornerPreference.Round => (4, new(1, 1, 2, 2)), - FormCornerPreference.RoundSmall => (3, new(1, 1, 2, 2)), - _ => (2, new(0, 0, 2, 2)) + FormCornerPreference.Round => 4, + FormCornerPreference.RoundSmall => 3, + _ => 2 }; #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. } - return cornerDef; + return offset; } } From 933b263c8b9ce122b8f6aebcd04ed9106fb327bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Klaus=20L=C3=B6ffelmann?= Date: Tue, 29 Apr 2025 18:50:53 -0700 Subject: [PATCH 08/10] Fix bug where a dedicated Toolstrip Renderer would not be called for background painting. --- .../System/Windows/Forms/Controls/ToolStrips/ToolStrip.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStrip.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStrip.cs index cc5f21256ce..893d5ad4c31 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStrip.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStrip.cs @@ -3648,6 +3648,12 @@ protected override void OnPaintBackground(PaintEventArgs e) } } + if (Renderer.RendererOverride is ToolStripRenderer renderer) + { + renderer.DrawToolStripBackground(new ToolStripRenderEventArgs(g, this)); + return; + } + Renderer.DrawToolStripBackground(new ToolStripRenderEventArgs(g, this)); } finally From fa13f5e008615af4ec8841fe784aecc83d953a26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Klaus=20L=C3=B6ffelmann?= Date: Sat, 14 Jun 2025 21:50:55 -0700 Subject: [PATCH 09/10] Fix merge issues. --- .../System/Windows/Forms/Control.cs | 68 ++++++------------- 1 file changed, 22 insertions(+), 46 deletions(-) diff --git a/src/System.Windows.Forms/System/Windows/Forms/Control.cs b/src/System.Windows.Forms/System/Windows/Forms/Control.cs index fd53c7d7dfb..9300f40dcc6 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Control.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Control.cs @@ -279,13 +279,13 @@ public unsafe partial class Control : private Font? _scaledControlFont; private FontHandleWrapper? _scaledFontWrapper; - // Contains a collection of calculated fonts for various Dpi values of the control in the PerMonV2 mode. - private Dictionary? _dpiFonts; - // ContainerControls like 'PropertyGrid' scale their children when they resize. // no explicit scaling of children required in such cases. They have specific logic. internal bool _doNotScaleChildren; + // Contains a collection of calculated fonts for various Dpi values of the control in the PerMonV2 mode. + private Dictionary? _dpiFonts; + // Flag to signify whether any child controls necessitate the calculation of AnchorsInfo, // particularly in cases involving nested containers. internal bool _childControlsNeedAnchorLayout; @@ -814,7 +814,6 @@ public virtual object? DataContext { Properties.RemoveValue(s_dataContextProperty); OnDataContextChanged(EventArgs.Empty); - return; } @@ -7333,18 +7332,6 @@ protected virtual void OnHandleCreated(EventArgs e) SetWindowFont(); } -#pragma warning disable WFO5001 // Type is for evaluation purposes only and is subject to change or removal in future updates. - if (Application.IsDarkModeEnabled && GetStyle(ControlStyles.ApplyThemingImplicitly)) - { - HRESULT result = PInvoke.SetWindowTheme( - hwnd: HWND, - pszSubAppName: $"{DarkModeIdentifier}_{ExplorerThemeIdentifier}", - pszSubIdList: null); - - Debug.Assert(result.Succeeded, "SetWindowTheme failed with HRESULT: " + result); - } -#pragma warning restore WFO5001 - HandleHighDpi(); // Restore drag drop status. Ole Initialize happens when the ThreadContext in Application is created. @@ -10401,9 +10388,7 @@ protected virtual void SetVisibleCore(bool value) PrepareDarkMode(HWND, Application.IsDarkModeEnabled); } - PInvoke.ShowWindow(HWND, value - ? ShowParams - : SHOW_WINDOW_CMD.SW_HIDE); + PInvoke.ShowWindow(HWND, value ? ShowParams : SHOW_WINDOW_CMD.SW_HIDE); } } #pragma warning restore WFO5001 @@ -10415,7 +10400,6 @@ protected virtual void SetVisibleCore(bool value) SetState(States.Visible, value); fireChange = true; - try { if (value) @@ -10424,19 +10408,14 @@ protected virtual void SetVisibleCore(bool value) } PInvoke.SetWindowPos( - hWnd: this, - hWndInsertAfter: HWND.Null, - X: 0, - Y: 0, - cx: 0, - cy: 0, - uFlags: SET_WINDOW_POS_FLAGS.SWP_NOSIZE + this, + HWND.Null, + 0, 0, 0, 0, + SET_WINDOW_POS_FLAGS.SWP_NOSIZE | SET_WINDOW_POS_FLAGS.SWP_NOMOVE | SET_WINDOW_POS_FLAGS.SWP_NOZORDER | SET_WINDOW_POS_FLAGS.SWP_NOACTIVATE - | (value - ? SET_WINDOW_POS_FLAGS.SWP_SHOWWINDOW - : SET_WINDOW_POS_FLAGS.SWP_HIDEWINDOW)); + | (value ? SET_WINDOW_POS_FLAGS.SWP_SHOWWINDOW : SET_WINDOW_POS_FLAGS.SWP_HIDEWINDOW)); } catch { @@ -10490,30 +10469,27 @@ protected virtual void SetVisibleCore(bool value) if (IsHandleCreated) { PInvoke.SetWindowPos( - hWnd: this, - hWndInsertAfter: HWND.HWND_TOP, - X: 0, - Y: 0, - cx: 0, - cy: 0, - uFlags: SET_WINDOW_POS_FLAGS.SWP_NOSIZE + this, + HWND.HWND_TOP, + 0, 0, 0, 0, + SET_WINDOW_POS_FLAGS.SWP_NOSIZE | SET_WINDOW_POS_FLAGS.SWP_NOMOVE | SET_WINDOW_POS_FLAGS.SWP_NOZORDER | SET_WINDOW_POS_FLAGS.SWP_NOACTIVATE | (value ? SET_WINDOW_POS_FLAGS.SWP_SHOWWINDOW : SET_WINDOW_POS_FLAGS.SWP_HIDEWINDOW)); } } - } - private static unsafe void PrepareDarkMode(HWND hwnd, bool darkModeEnabled) - { - BOOL value = darkModeEnabled; + static unsafe void PrepareDarkMode(HWND hwnd, bool darkModeEnabled) + { + BOOL value = darkModeEnabled; - PInvoke.DwmSetWindowAttribute( - hwnd, - DWMWINDOWATTRIBUTE.DWMWA_USE_IMMERSIVE_DARK_MODE, - &value, - (uint)sizeof(BOOL)).AssertSuccess(); + PInvoke.DwmSetWindowAttribute( + hwnd, + DWMWINDOWATTRIBUTE.DWMWA_USE_IMMERSIVE_DARK_MODE, + &value, + (uint)sizeof(BOOL)).AssertSuccess(); + } } /// From 19fc0c42261128e435acac153b0732e7abc317cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Klaus=20L=C3=B6ffelmann?= Date: Sun, 15 Jun 2025 00:22:08 -0700 Subject: [PATCH 10/10] Address review suggestions. --- .github/copilot-instructions.md | 10 +++ .../Controls/ToolStrips/ToolStripRenderer.cs | 67 ++++++++++--------- .../ToolStripSystemDarkModeRenderer.cs | 18 ++--- .../ToolStrips/ToolStripSystemRenderer.cs | 2 +- 4 files changed, 54 insertions(+), 43 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index d062f76b3ab..405624f54ca 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -423,6 +423,16 @@ using Pen focusPen = new(focusColor) // Use 'using' with type name and omit type - **Don't XML-comment local functions**, as this is not supported. Instead, be more verbose in a comment if the local function's purpose is not immediately obvious. +- **Use `` for inherited members** to avoid duplication and ensure consistency in documentation. + ```csharp + /// + public override void OnClick(EventArgs e) + { + base.OnClick(e); + // Additional logic here + } + ``` + ### 1.9 File Structure and Formatting - **Use file-scoped namespaces** diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStripRenderer.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStripRenderer.cs index 9c8b911cd0f..814f43c9586 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStripRenderer.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStripRenderer.cs @@ -628,7 +628,7 @@ protected virtual void OnRenderArrow(ToolStripArrowRenderEventArgs e) /// The event arguments containing rendering information. /// The color to use for the arrow. /// The rendered arrow points. - private protected Point[] RenderArrowCore( + private protected void RenderArrowCore( ToolStripArrowRenderEventArgs e, Color arrowColor) { @@ -661,38 +661,39 @@ private protected Point[] RenderArrowCore( ? s_offset4X - Offset2X : Offset2X; - Point[] arrow = e.Direction switch + // Use stackalloc for the 3 arrow points + Span arrow = stackalloc Point[3]; + + // Fill the points based on arrow direction + switch (e.Direction) { - ArrowDirection.Up => - [ - new(middle.X - Offset2X, middle.Y + 1), - new(middle.X + Offset2X + 1, middle.Y + 1), - new(middle.X, middle.Y - Offset2Y) - ], - ArrowDirection.Left => - [ - new(middle.X + Offset2X, middle.Y - s_offset4Y), - new(middle.X + Offset2X, middle.Y + s_offset4Y), - new(middle.X - horizontalOffset, middle.Y) - ], - ArrowDirection.Right => - [ - new(middle.X - Offset2X, middle.Y - s_offset4Y), - new(middle.X - Offset2X, middle.Y + s_offset4Y), - new(middle.X + horizontalOffset, middle.Y) - ], - _ => - [ - new(middle.X - Offset2X, middle.Y - 1), - new(middle.X + Offset2X + 1, middle.Y - 1), - new(middle.X, middle.Y + Offset2Y) - ], - }; + case ArrowDirection.Up: + arrow[0] = new(middle.X - Offset2X, middle.Y + 1); + arrow[1] = new(middle.X + Offset2X + 1, middle.Y + 1); + arrow[2] = new(middle.X, middle.Y - Offset2Y); + break; + + case ArrowDirection.Left: + arrow[0] = new(middle.X + Offset2X, middle.Y - s_offset4Y); + arrow[1] = new(middle.X + Offset2X, middle.Y + s_offset4Y); + arrow[2] = new(middle.X - horizontalOffset, middle.Y); + break; + + case ArrowDirection.Right: + arrow[0] = new(middle.X - Offset2X, middle.Y - s_offset4Y); + arrow[1] = new(middle.X - Offset2X, middle.Y + s_offset4Y); + arrow[2] = new(middle.X + horizontalOffset, middle.Y); + break; + + default: // Down + arrow[0] = new(middle.X - Offset2X, middle.Y - 1); + arrow[1] = new(middle.X + Offset2X + 1, middle.Y - 1); + arrow[2] = new(middle.X, middle.Y + Offset2Y); + break; + } using var brush = arrowColor.GetCachedSolidBrushScope(); g.FillPolygon(brush, arrow); - - return arrow; } /// @@ -1029,17 +1030,17 @@ protected virtual void OnRenderStatusStripSizingGrip(ToolStripRenderEventArgs e) } OnRenderStatusStripSizingGrip( - eArgs: e, + e: e, highLightBrush: SystemBrushes.ButtonHighlight, shadowBrush: SystemBrushes.GrayText); } private protected static void OnRenderStatusStripSizingGrip( - ToolStripRenderEventArgs eArgs, + ToolStripRenderEventArgs e, Brush highLightBrush, Brush shadowBrush) { - if (eArgs.ToolStrip is not StatusStrip statusStrip) + if (e.ToolStrip is not StatusStrip statusStrip) { return; } @@ -1051,7 +1052,7 @@ private protected static void OnRenderStatusStripSizingGrip( return; } - Graphics g = eArgs.Graphics; + Graphics g = e.Graphics; ReadOnlySpan baseRectangles = s_baseSizeGripRectangles; // Reference height for sizing grips at 96 DPI (standard sizing) diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStripSystemDarkModeRenderer.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStripSystemDarkModeRenderer.cs index dba67b0faad..fbe8e89147f 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStripSystemDarkModeRenderer.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStripSystemDarkModeRenderer.cs @@ -114,7 +114,6 @@ protected override void OnRenderToolStripBackground(ToolStripRenderEventArgs e) ArgumentNullException.ThrowIfNull(e); ToolStrip toolStrip = e.ToolStrip; - Graphics g = e.Graphics; Rectangle bounds = e.AffectedBounds; if (!ShouldPaintBackground(toolStrip)) @@ -122,6 +121,8 @@ protected override void OnRenderToolStripBackground(ToolStripRenderEventArgs e) return; } + Graphics g = e.Graphics; + if (toolStrip is StatusStrip) { RenderStatusStripBackground(e); @@ -152,11 +153,10 @@ protected override void OnRenderToolStripBackground(ToolStripRenderEventArgs e) /// A ToolStripRenderEventArgs that contains the event data. internal static void RenderStatusStripBackground(ToolStripRenderEventArgs e) { - Graphics g = e.Graphics; Rectangle bounds = e.AffectedBounds; // Dark mode StatusStrip background - FillBackground(g, bounds, GetDarkModeColor(SystemColors.Control)); + FillBackground(e.Graphics, bounds, GetDarkModeColor(SystemColors.Control)); } /// @@ -168,18 +168,18 @@ protected override void OnRenderToolStripBorder(ToolStripRenderEventArgs e) ArgumentNullException.ThrowIfNull(e); ToolStrip toolStrip = e.ToolStrip; - Graphics g = e.Graphics; - Rectangle bounds = e.ToolStrip.ClientRectangle; - - using var borderPen = GetDarkModePen(SystemColors.ControlDark); if (toolStrip is StatusStrip) { RenderStatusStripBorder(e); - return; } + Graphics g = e.Graphics; + Rectangle bounds = e.ToolStrip.ClientRectangle; + + using var borderPen = GetDarkModePen(SystemColors.ControlDark); + if (toolStrip is ToolStripDropDown toolStripDropDown) { if (toolStripDropDown.DropShadowEnabled) @@ -648,7 +648,7 @@ protected override void OnRenderStatusStripSizingGrip(ToolStripRenderEventArgs e using var shadowBrush = GetDarkModeBrush(SystemColors.ButtonShadow); OnRenderStatusStripSizingGrip( - eArgs: e, + e: e, highLightBrush: highLightBrush, shadowBrush: shadowBrush); } diff --git a/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStripSystemRenderer.cs b/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStripSystemRenderer.cs index 4dfa8b522d3..fefaa59a575 100644 --- a/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStripSystemRenderer.cs +++ b/src/System.Windows.Forms/System/Windows/Forms/Controls/ToolStrips/ToolStripSystemRenderer.cs @@ -251,7 +251,7 @@ protected override void OnRenderToolStripBackground(ToolStripRenderEventArgs e) } else if (DisplayInformation.LowResolution) { - FillBackground(g, bounds, (toolStrip is ToolStripDropDown) + FillBackground(g, bounds, toolStrip is ToolStripDropDown ? SystemColors.ControlLight : e.BackColor); }