fix: dark mode checkboxes in CustomTreeView

- Added ThemeManager.DrawDarkCheckBox to render dark-themed rounded checkboxes
- Fixed TreeView and Proxy checkboxes showing white backgrounds in dark mode due to CheckBoxRenderer drawing opaque system-themed glyphs. > Still need to fix the actual checkmark to match the top checkboxes.
- Disabled DrawDefault for checkbox tree nodes in dark mode and manually draw the row background, text, and checkbox glyphs.
- Checkbox glyph sizing now uses CheckBoxRenderer.GetGlyphSize for proper DPI scaling. (Probably)
This commit is contained in:
Frog
2026-05-25 14:54:51 -07:00
parent b7067c2621
commit 558612f098
2 changed files with 108 additions and 9 deletions
+44 -9
View File
@@ -75,11 +75,14 @@ internal sealed class CustomTreeView : TreeView
private void DrawTreeNode(object sender, DrawTreeNodeEventArgs e)
{
e.DrawDefault = true;
TreeNode node = e.Node;
if (node is not { IsVisible: true })
{
e.DrawDefault = true;
return;
}
bool dark = Program.DarkModeEnabled;
bool highlighted = (e.State & TreeNodeStates.Selected) == TreeNodeStates.Selected && Focused;
Graphics graphics = e.Graphics;
@@ -103,12 +106,43 @@ internal sealed class CustomTreeView : TreeView
}
}
Form form = FindForm();
if (dark && CheckBoxes)
{
// In dark mode we take full ownership of the row so the system never
// gets a chance to paint a light-background checkbox.
e.DrawDefault = false;
// Row background
Rectangle rowRect = new(0, node.Bounds.Top, ClientSize.Width, node.Bounds.Height);
graphics.FillRectangle(highlighted ? selectionBrush : backBrush, rowRect);
// Node text
Font nodeFont = node.NodeFont ?? Font;
Color textColor = Enabled ? ForeColor : SystemColors.GrayText;
TextRenderer.DrawText(graphics, node.Text, nodeFont,
new Point(node.Bounds.Left, node.Bounds.Top + 1), textColor, TextFormatFlags.Default);
// Checkbox glyph pure GDI so it matches the dark-themed CheckBox controls
CheckBoxState cbState = node.Checked
? (Enabled ? CheckBoxState.CheckedNormal : CheckBoxState.CheckedDisabled)
: (Enabled ? CheckBoxState.UncheckedNormal : CheckBoxState.UncheckedDisabled);
Size cbSize = CheckBoxRenderer.GetGlyphSize(graphics, cbState);
int cbX = node.Bounds.Left - cbSize.Width - 2;
int cbY = node.Bounds.Top + node.Bounds.Height / 2 - cbSize.Height / 2;
ThemeManager.DrawDarkCheckBox(graphics, new Point(cbX, cbY), cbSize, node.Checked, Enabled);
}
else
{
e.DrawDefault = true;
}
Font font = node.NodeFont ?? Font;
Brush brush = highlighted ? (Brush)selectionBrush : backBrush;
Rectangle bounds = node.Bounds;
Rectangle selectionBounds = bounds;
Form form = FindForm();
if (form is not SelectForm and not SelectDialogForm)
return;
@@ -168,18 +202,19 @@ internal sealed class CustomTreeView : TreeView
graphics.FillRectangle(brush, bounds);
}
CheckBoxState checkBoxState = selection.UseProxy
? Enabled ? CheckBoxState.CheckedPressed : CheckBoxState.CheckedDisabled
: Enabled
? CheckBoxState.UncheckedPressed
: CheckBoxState.UncheckedDisabled;
size = CheckBoxRenderer.GetGlyphSize(graphics, checkBoxState);
CheckBoxState proxyState = selection.UseProxy
? (Enabled ? CheckBoxState.CheckedNormal : CheckBoxState.CheckedDisabled)
: (Enabled ? CheckBoxState.UncheckedNormal : CheckBoxState.UncheckedDisabled);
size = CheckBoxRenderer.GetGlyphSize(graphics, proxyState);
bounds = bounds with { X = bounds.X + bounds.Width, Width = size.Width };
selectionBounds = new(selectionBounds.Location, selectionBounds.Size + bounds.Size with { Height = 0 });
Rectangle checkBoxBounds = bounds;
graphics.FillRectangle(backBrush, bounds);
point = new(bounds.Left, bounds.Top + bounds.Height / 2 - size.Height / 2 - 1);
CheckBoxRenderer.DrawCheckBox(graphics, point, checkBoxState);
if (dark)
ThemeManager.DrawDarkCheckBox(graphics, point, size, selection.UseProxy, Enabled);
else
CheckBoxRenderer.DrawCheckBox(graphics, point, proxyState);
text = ProxyToggleString;
size = TextRenderer.MeasureText(graphics, text, font);
+64
View File
@@ -422,6 +422,70 @@ internal static class ThemeManager
// button is centralized here so theming resides in ThemeManager.
// -----------------------------------------------------------------
// Dark checkbox colors matched to how the system renders the "All" CheckBox control
// in dark mode: dark fill, mid-gray border, light foreground tick.
private static readonly Color DarkCbBorder = ColorTranslator.FromHtml("#6B6B6B");
private static readonly Color DarkCbDisabledBorder = ColorTranslator.FromHtml("#454545");
/// <summary>
/// Draws a checkbox glyph in pure GDI that matches the appearance of a dark-themed
/// WinForms CheckBox control (same background, border, tick colors, and rounded corners).
/// Use this in owner-draw contexts where CheckBoxRenderer always paints a white background.
/// </summary>
internal static void DrawDarkCheckBox(Graphics g, Point point, Size glyphSize, bool isChecked, bool enabled = true)
{
if (g is null) return;
int w = glyphSize.Width;
int h = glyphSize.Height;
Rectangle box = new(point.X, point.Y, w - 1, h - 1);
int radius = Math.Max(2, w / 5); // rounded corner radius proportional to glyph size
// Build rounded rectangle path
using System.Drawing.Drawing2D.GraphicsPath path = RoundedRect(box, radius);
// Fill
using SolidBrush fillBrush = new(DarkBackAlt);
g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.AntiAlias;
g.FillPath(fillBrush, path);
// Border
using Pen borderPen = new(enabled ? DarkCbBorder : DarkCbDisabledBorder);
g.DrawPath(borderPen, path);
if (isChecked)
{
Color tickColor = enabled ? DarkFore : DarkForeDim;
using Pen tickPen = new(tickColor, 1.7f)
{
StartCap = System.Drawing.Drawing2D.LineCap.Round,
EndCap = System.Drawing.Drawing2D.LineCap.Round,
LineJoin = System.Drawing.Drawing2D.LineJoin.Round,
};
// Scale tick proportionally to the glyph size
float scaleX = w / 13f;
float scaleY = h / 13f;
g.DrawLines(tickPen, new PointF[]
{
new(point.X + 2 * scaleX, point.Y + 6 * scaleY),
new(point.X + 5 * scaleX, point.Y + 9 * scaleY),
new(point.X + 10 * scaleX, point.Y + 3 * scaleY),
});
}
g.SmoothingMode = System.Drawing.Drawing2D.SmoothingMode.Default;
}
private static System.Drawing.Drawing2D.GraphicsPath RoundedRect(Rectangle r, int radius)
{
int d = radius * 2;
System.Drawing.Drawing2D.GraphicsPath path = new();
path.AddArc(r.Left, r.Top, d, d, 180, 90);
path.AddArc(r.Right - d, r.Top, d, d, 270, 90);
path.AddArc(r.Right - d, r.Bottom - d, d, d, 0, 90);
path.AddArc(r.Left, r.Bottom - d, d, d, 90, 90);
path.CloseFigure();
return path;
}
/// <summary>
/// Draws the themed combobox area (background, border and text) used in CustomTreeView.
/// This centralizes colors and rendering for light/dark modes.