Skip to content

Fix issue 14198: [Dark Mode] Improve visual contrast of the Group Headers in ListView control in dark#14406

Open
SimonZhao888 wants to merge 6 commits into
dotnet:mainfrom
SimonZhao888:fix_issue_14198
Open

Fix issue 14198: [Dark Mode] Improve visual contrast of the Group Headers in ListView control in dark#14406
SimonZhao888 wants to merge 6 commits into
dotnet:mainfrom
SimonZhao888:fix_issue_14198

Conversation

@SimonZhao888
Copy link
Copy Markdown
Member

@SimonZhao888 SimonZhao888 commented Mar 18, 2026

Fixes #14198

Proposed changes

  • In Dark Mode, the ListView's internal theme has been switched from "Explorer" to "ItemsView" to ensure alignment with the column headers.
  • After the Handle is created, call the new method UpdateDarkModeGroupTextColors() to set the group header and footer text colors using LVM_SETGROUPMETRICS.
  • Add CDRF_NOTIFYITEMDRAW to enable the native control to send CDDS_ITEMPREPAINT notifications for group items.
  • Added a 'group' branch to CDDS_ITEMPREPAINT: In dark mode, return CDRF_SKIPDEFAULT to skip native group painting, and cache clrTextBk and uItemState in a dictionary for use during the overlay phase.
  • In Dark Mode, force the HDC text color to equal ForeColor, and set the group text of the header control to use clrTextBk.

Customer Impact

  • Improve visual contrast of the Group Headers in ListView control in dark.

Regression?

  • No

Risk

  • Min

Screenshots

Before

image

After

2026-05-15.103056.mp4

Test environment(s)

  • 11.0.100-preview.5.26227.104

@SimonZhao888 SimonZhao888 requested a review from Copilot March 18, 2026 08:26
@SimonZhao888 SimonZhao888 requested a review from a team as a code owner March 18, 2026 08:26
@github-actions github-actions Bot added the area-DarkMode Issues relating to Dark Mode feature label Mar 18, 2026
@SimonZhao888 SimonZhao888 changed the title Fix issue 14198: [Dark Mode] Improve visual contrast of the Group Hea… Fix issue 14198: [Dark Mode] Improve visual contrast of the Group Headers in ListView control in dark Mar 18, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR targets WinForms ListView dark mode rendering by improving the visual contrast of group headers (and related group text) when groups are enabled.

Changes:

  • Adds custom dark-mode rendering logic for group header text, chevron, subtitle, and footer.
  • Adjusts custom-draw flags and text color handling to receive group-related draw notifications and control group text colors.
  • Updates the applied dark-mode theme identifier for the ListView/ColumnHeader to use the ItemsView theme.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/System.Windows.Forms/System/Windows/Forms/Controls/ListView/ListView.cs Outdated
@SimonZhao888 SimonZhao888 requested a review from Copilot March 19, 2026 02:20
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Improves ListView group header readability in dark mode by adjusting theming/custom-draw behavior and adding an overlay render pass to redraw group header/subtitle/footer with higher-contrast styling.

Changes:

  • Add dark-mode overlay drawing for group header/subtitle/footer and a custom chevron/underline rendering path.
  • Adjust ListView custom draw flags/colors so group custom-draw notifications are raised and native group text is suppressed in dark mode.
  • Switch the applied dark-mode theme identifier to DarkMode_ItemsView and attempt to configure group text colors via LVM_SETGROUPMETRICS.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/System.Windows.Forms/System/Windows/Forms/Controls/ListView/ListView.cs Outdated
@LeafShi1
Copy link
Copy Markdown
Member

Please consider Copilot's suggestion and complete the PR description

@SimonZhao888
Copy link
Copy Markdown
Member Author

Please consider Copilot's suggestion and complete the PR description

I'm trying to add a selected overlay to a Group Item in Dark Mode.

Simon Zhao (BEYONDSOFT CONSULTING INC) added 3 commits March 26, 2026 13:51
@SimonZhao888 SimonZhao888 marked this pull request as draft March 26, 2026 08:47
@dotnet-policy-service dotnet-policy-service Bot added the draft draft PR label Mar 26, 2026
@SimonZhao888 SimonZhao888 marked this pull request as ready for review May 15, 2026 02:40
@SimonZhao888
Copy link
Copy Markdown
Member Author

/azp run

@azure-pipelines
Copy link
Copy Markdown

Azure Pipelines successfully started running 1 pipeline(s).

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 6 comments.

Comment on lines +443 to +467
int centerX = headerRect.Right - 9;
int centerY = headerTextRect.Top + (headerTextRect.Height / 2) + 1;

Rectangle nativeChevronRect = new(
headerRect.Right - 28,
headerRect.Top,
28,
headerRect.Height);
using (Brush backgroundBrush = new SolidBrush(backgroundColor))
{
g.FillRectangle(backgroundBrush, nativeChevronRect);
}

Point[] points = collapsed
? [new Point(centerX - 3, centerY - 5), new Point(centerX + 2, centerY), new Point(centerX - 3, centerY + 5)]
: [new Point(centerX - 5, centerY - 2), new Point(centerX, centerY + 3), new Point(centerX + 5, centerY - 2)];

using Pen chevronPen = new(chevronColor, 2.6f)
{
StartCap = Drawing.Drawing2D.LineCap.Round,
EndCap = Drawing.Drawing2D.LineCap.Round,
LineJoin = Drawing.Drawing2D.LineJoin.Round
};

g.DrawLines(chevronPen, points);
Comment on lines +5156 to +5166
COLORREF textColor = (COLORREF)ColorTranslator.ToWin32(ForeColor);
COLORREF headerColor = (COLORREF)ColorTranslator.ToWin32(BackColor);
LVGROUPMETRICS groupMetrics = new()
{
cbSize = (uint)sizeof(LVGROUPMETRICS),
mask = LVGMF_TEXTCOLOR,
crHeader = headerColor,
crFooter = textColor
};

PInvokeCore.SendMessage(this, LVM_SETGROUPMETRICS, (WPARAM)0, ref groupMetrics);
Comment on lines +246 to +247
? Color.FromArgb(64, 90, 150, 255)
: (isHovered ? Color.FromArgb(28, 255, 255, 255) : Color.Empty);
Comment on lines +267 to +287
int interactionBottom = headerRect.Bottom;
if (!string.IsNullOrEmpty(group.Subtitle))
{
interactionBottom = headerRect.Top + Font.Height + verticalSpacing + Font.Height + headerVerticalPadding;
}

if (!string.IsNullOrEmpty(group.Footer) && (collapsed || group.Items.Count == 0))
{
int subtitleOffset = string.IsNullOrEmpty(group.Subtitle) ? 1 : 2;
int collapsedFooterY = headerRect.Top + (Font.Height * subtitleOffset) + footerCollapsedAdditionalOffset;
interactionBottom = Math.Min(interactionBottom, Math.Max(headerRect.Top, collapsedFooterY - LogicalToDeviceUnits(1)));
}

Rectangle interactionRect = Rectangle.Intersect(
groupRect,
new Rectangle(
groupRect.Left,
headerRect.Top,
groupRect.Width,
Math.Max(0, interactionBottom - headerRect.Top)));

Comment on lines +7680 to +7687
int oldHoveredGroupId = _darkModeHoveredGroupId;
int hoveredGroupId = GetDarkModeGroupHeaderHitId();
if (_darkModeHoveredGroupId != hoveredGroupId)
{
_darkModeHoveredGroupId = hoveredGroupId;
InvalidateDarkModeGroupHeader(oldHoveredGroupId);
InvalidateDarkModeGroupHeader(_darkModeHoveredGroupId);
}
Comment on lines +7689 to +7700
// Suppress native hover processing in dark mode group overlay mode.
// Native hover invalidates group headers while mouse moves over grouped content,
// causing repaint jitter.
LVHITTESTINFO lvhi = SetupHitTestInfo();
bool isMouseOverItem = (int)PInvokeCore.SendMessage(this, PInvoke.LVM_HITTEST, (WPARAM)0, ref lvhi) >= 0;
bool allowNativeHover = isMouseOverItem || HoverSelection || HotTracking;

if (!allowNativeHover)
{
Capture = false;
return;
}
Copy link
Copy Markdown
Member

@LeafShi1 LeafShi1 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please consider Copilot's suggestions and the test case updates.

@dotnet-policy-service dotnet-policy-service Bot removed the draft draft PR label May 15, 2026
Copy link
Copy Markdown
Member

@LeafShi1 LeafShi1 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Several potential issues found

public COLORREF crHeader;
public COLORREF crFooter;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hardcoded colors are not theme-aware

Color.FromArgb(120, 180, 255) (header), Color.FromArgb(80, 170, 255) (chevron), Color.FromArgb(64, 90, 150, 255) (selected overlay), and Color.FromArgb(28, 255, 255, 255) (hover overlay) are all hardcoded magic values. These should at minimum be extracted as named static readonly constants for maintainability. Ideally they should come from system/theme colors or be configurable — if the system dark mode palette changes in a future Windows version, these will look wrong.

HWND header = (HWND)PInvokeCore.SendMessage(this, PInvoke.LVM_GETHEADER);
if (!header.IsNull)
{
PInvokeCore.GetWindowRect(header, out RECT headerRect);
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Chevron drawing is not DPI-aware

In this method, several pixel values are not scaled via LogicalToDeviceUnits():

  • centerX = headerRect.Right - 9 and centerY offset +1
  • The native chevron cover rect uses a hardcoded width of 28
  • Chevron point offsets (3, 5, 2) and pen width (2.6f) are raw pixel values
  • dividerTextSpacing = 6 is unscaled

The parent method (DrawDarkModeGroupSubtitleAndFooterOverlay) correctly uses LogicalToDeviceUnits() for padding/spacing. On high-DPI displays (200%+), the chevron will appear undersized and mispositioned relative to the DPI-scaled header text.

{
return background;
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicated layout logic between drawing and hit-testing

The interaction rect computation here (headerVerticalPadding, verticalSpacing, footerCollapsedAdditionalOffset, interactionBottom calculation) is duplicated nearly identically in TryGetDarkModeGroupInteractionRect further below. If any layout constant or formula changes, both locations must be updated in sync — this is fragile. Consider extracting a shared helper that computes the layout metrics once.

{
Capture = false;
return;
}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suppressing base.WndProc for WM_MOUSEMOVE may have side effects

When !allowNativeHover, the code sets Capture = false and returns, completely bypassing base.WndProc(ref m). This means other mouse-move processing (accessibility notifications, tooltip tracking, drag detection) will be skipped. Could this cause subtle regressions for drag-and-drop or accessibility scenarios? It might be safer to only suppress the specific hover invalidation rather than skipping the entire base message processing.

textColor = nmlvcd->clrTextBk;
}
else
{
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using clrTextBk as text color is semantically confusing

textColor = nmlvcd->clrTextBk;

This reads the background color field and uses it as the text color. Even if this works due to how the native control populates these fields for group items, it's surprising to readers and fragile if the native behavior changes. A comment explaining why the background color field contains the desired text color value would help future maintainers.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-DarkMode Issues relating to Dark Mode feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Dark Mode] Improve visual contrast of the Group Headers in ListView control in dark

3 participants