Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ internal static partial class LocalAppContextSwitches
internal const string EnableMsoComponentManagerSwitchName = "Switch.System.Windows.Forms.EnableMsoComponentManager";
internal const string TreeNodeCollectionAddRangeRespectsSortOrderSwitchName = "System.Windows.Forms.TreeNodeCollectionAddRangeRespectsSortOrder";
internal const string MoveTreeViewTextLocationOnePixelSwitchName = "System.Windows.Forms.TreeView.MoveTreeViewTextLocationOnePixel";
internal const string DataGridViewDarkModeThemingSwitchName = "System.Windows.Forms.DataGridViewDarkModeTheming";

private static int s_scaleTopLevelFormMinMaxSizeForDpi;
private static int s_anchorLayoutV2;
Expand All @@ -39,6 +40,7 @@ internal static partial class LocalAppContextSwitches
private static int s_treeNodeCollectionAddRangeRespectsSortOrder;

private static int s_moveTreeViewTextLocationOnePixel;
private static int s_dataGridViewDarkModeTheming;

private static FrameworkName? s_targetFrameworkName;

Expand Down Expand Up @@ -134,6 +136,16 @@ private static bool GetSwitchDefaultValue(string switchName)
}
}

if (framework.Version.Major >= 10)
{
// Behavior changes added in .NET 10

if (switchName == DataGridViewDarkModeThemingSwitchName)
{
return true;
}
}

return false;
}

Expand Down Expand Up @@ -231,4 +243,15 @@ public static bool MoveTreeViewTextLocationOnePixel
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => GetCachedSwitchValue(MoveTreeViewTextLocationOnePixelSwitchName, ref s_moveTreeViewTextLocationOnePixel);
}

/// <summary>
/// Indicates whether dark mode theming is automatically applied to DataGridView
/// controls when the application is running in dark mode. Defaults to <see langword="true"/>
/// for .NET 10+ applications. Set to <see langword="false"/> to opt out.
/// </summary>
public static bool DataGridViewDarkModeTheming
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
get => GetCachedSwitchValue(DataGridViewDarkModeThemingSwitchName, ref s_dataGridViewDarkModeTheming);
}
}
2 changes: 2 additions & 0 deletions src/System.Windows.Forms/PublicAPI.Unshipped.txt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ static System.Windows.Forms.TaskDialog.ShowDialogAsync(nint hwndOwner, System.Wi
static System.Windows.Forms.TaskDialog.ShowDialogAsync(System.Windows.Forms.IWin32Window! owner, System.Windows.Forms.TaskDialogPage! page, System.Windows.Forms.TaskDialogStartupLocation startupLocation = System.Windows.Forms.TaskDialogStartupLocation.CenterOwner) -> System.Threading.Tasks.Task<System.Windows.Forms.TaskDialogButton!>!
static System.Windows.Forms.TaskDialog.ShowDialogAsync(System.Windows.Forms.TaskDialogPage! page, System.Windows.Forms.TaskDialogStartupLocation startupLocation = System.Windows.Forms.TaskDialogStartupLocation.CenterScreen) -> System.Threading.Tasks.Task<System.Windows.Forms.TaskDialogButton!>!
System.Windows.Forms.ControlStyles.ApplyThemingImplicitly = 524288 -> System.Windows.Forms.ControlStyles
System.Windows.Forms.DataGridViewCellStyle.SortGlyphColor.get -> System.Drawing.Color
System.Windows.Forms.DataGridViewCellStyle.SortGlyphColor.set -> void
System.Windows.Forms.Form.FormBorderColorChanged -> System.EventHandler?
System.Windows.Forms.Form.FormCaptionBackColorChanged -> System.EventHandler?
System.Windows.Forms.Form.FormCaptionTextColorChanged -> System.EventHandler?
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,51 @@ namespace System.Windows.Forms;

public partial class DataGridView
{
// A medium accent blue commonly used for selection highlights in dark UI.
private static readonly Color s_darkModeSelectionBackground = Color.FromArgb(0x33, 0x66, 0xCC);

/// <summary>
/// Applies dark mode theming to the DataGridView when the application is running
/// in dark mode. This is called automatically from <see cref="OnHandleCreated"/>
/// when the <c>System.Windows.Forms.DataGridViewDarkModeTheming</c> AppContext switch
/// is enabled (default for .NET 10+).
/// </summary>
private void ApplyDarkModeTheming()
{
Color surface = SystemColors.Window;
Color onSurface = SystemColors.WindowText;
Color headerBg = SystemColors.ControlDarkDark;
Color headerFg = SystemColors.ActiveCaptionText;
Color selectionBg = s_darkModeSelectionBackground;
Color selectionFg = Color.White;

// Disable the header's system theme so that our custom styles are not overridden by the system.
EnableHeadersVisualStyles = false;

// Table body
BackgroundColor = surface;
DefaultCellStyle.BackColor = surface;
DefaultCellStyle.ForeColor = onSurface;

// Column headers
ColumnHeadersDefaultCellStyle.BackColor = headerBg;
ColumnHeadersDefaultCellStyle.ForeColor = headerFg;

// Light-colored dividing line
GridColor = ControlPaint.Light(surface, 0.50f);

// Row headers
RowHeadersDefaultCellStyle.BackColor = headerBg;
RowHeadersDefaultCellStyle.ForeColor = headerFg;

// Selected state - use Color.Empty so header selection follows body selection
RowHeadersDefaultCellStyle.SelectionBackColor = Color.Empty;
ColumnHeadersDefaultCellStyle.SelectionBackColor = Color.Empty;

DefaultCellStyle.SelectionBackColor = selectionBg;
DefaultCellStyle.SelectionForeColor = selectionFg;
}

protected virtual void AccessibilityNotifyCurrentCellChanged(Point cellAddress)
{
if (cellAddress.X < 0 || cellAddress.X >= Columns.Count)
Expand Down Expand Up @@ -2853,6 +2898,20 @@ private void BuildInheritedColumnHeaderCellStyle(DataGridViewCellStyle inherited
inheritedCellStyle.SelectionForeColor = dataGridViewStyle.SelectionForeColor;
}

// Inherit SortGlyphColor for column header sort arrow
if (cellStyle is not null && !cellStyle.SortGlyphColor.IsEmpty)
{
inheritedCellStyle.SortGlyphColor = cellStyle.SortGlyphColor;
}
else if (!columnHeadersStyle.SortGlyphColor.IsEmpty)
{
inheritedCellStyle.SortGlyphColor = columnHeadersStyle.SortGlyphColor;
}
else if (!dataGridViewStyle.SortGlyphColor.IsEmpty)
{
inheritedCellStyle.SortGlyphColor = dataGridViewStyle.SortGlyphColor;
}

inheritedCellStyle.Font = cellStyle?.Font ?? columnHeadersStyle.Font ?? dataGridViewStyle.Font;

if (cellStyle is not null && !cellStyle.IsNullValueDefault)
Expand Down Expand Up @@ -15242,6 +15301,12 @@ protected override void OnHandleCreated(EventArgs e)
OnGlobalAutoSize();
}

// Automatically apply dark mode theming when enabled via quirk switch.
if (Application.IsDarkModeEnabled && AppContextSwitches.DataGridViewDarkModeTheming)
{
ApplyDarkModeTheming();
}

SystemEvents.UserPreferenceChanged += OnUserPreferenceChanged;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1435,6 +1435,18 @@ private static Bitmap GetBitmap(string bitmapName) =>
? ControlPaint.LightLight(baseline)
: SystemColors.ControlLightLight;
}
else if (Application.IsDarkModeEnabled && AppContextSwitches.DataGridViewDarkModeTheming)
{
// In Dark Mode, use higher contrast colors for better visibility.
// For dark backgrounds, we need lighter colors that stand out.
darkColor = darkDistance < ContrastThreshold
? ControlPaint.Light(baseline, 0.5f)
: SystemColors.ControlLight;

lightColor = lightDistance < ContrastThreshold
? ControlPaint.LightLight(baseline)
: SystemColors.ControlLightLight;
}
else
{
darkColor = darkDistance < ContrastThreshold
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ public class DataGridViewCellStyle : ICloneable
private static readonly int s_propPadding = PropertyStore.CreateKey();
private static readonly int s_propSelectionBackColor = PropertyStore.CreateKey();
private static readonly int s_propSelectionForeColor = PropertyStore.CreateKey();
private static readonly int s_propSortGlyphColor = PropertyStore.CreateKey();
private static readonly int s_propTag = PropertyStore.CreateKey();
private static readonly int s_propWrapMode = PropertyStore.CreateKey();
private DataGridView? _dataGridView;
Expand Down Expand Up @@ -59,6 +60,7 @@ public DataGridViewCellStyle(DataGridViewCellStyle dataGridViewCellStyle)
WrapModeInternal = dataGridViewCellStyle.WrapMode;
Tag = dataGridViewCellStyle.Tag;
PaddingInternal = dataGridViewCellStyle.Padding;
SortGlyphColor = dataGridViewCellStyle.SortGlyphColor;
}

[SRDescription(nameof(SR.DataGridViewCellStyleAlignmentDescr))]
Expand Down Expand Up @@ -331,6 +333,25 @@ public Color SelectionForeColor
}
}

/// <summary>
/// Gets or sets the color used to draw the sort glyph (arrow) in column headers.
/// When set to <see cref="Color.Empty"/>, the glyph color is automatically
/// calculated based on the background color for optimal contrast.
/// </summary>
[SRCategory(nameof(SR.CatAppearance))]
public Color SortGlyphColor
{
get => Properties.GetValueOrDefault<Color>(s_propSortGlyphColor);
set
{
Color previous = Properties.AddOrRemoveValue(s_propSortGlyphColor, value);
if (!previous.Equals(value))
{
OnPropertyChanged(DataGridViewCellStylePropertyInternal.Color);
}
}
}

[Browsable(false)]
[DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)]
public object? Tag
Expand Down Expand Up @@ -439,6 +460,11 @@ public virtual void ApplyStyle(DataGridViewCellStyle dataGridViewCellStyle)
{
PaddingInternal = dataGridViewCellStyle.Padding;
}

if (!dataGridViewCellStyle.SortGlyphColor.IsEmpty)
{
SortGlyphColor = dataGridViewCellStyle.SortGlyphColor;
}
}

public virtual DataGridViewCellStyle Clone() => new(this);
Expand All @@ -465,7 +491,8 @@ internal DataGridViewCellStyleDifferences GetDifferencesFrom(DataGridViewCellSty
dgvcs.BackColor != BackColor ||
dgvcs.ForeColor != ForeColor ||
dgvcs.SelectionBackColor != SelectionBackColor ||
dgvcs.SelectionForeColor != SelectionForeColor);
dgvcs.SelectionForeColor != SelectionForeColor ||
dgvcs.SortGlyphColor != SortGlyphColor);

if (preferredSizeAffectingPropDifferent)
{
Expand All @@ -492,6 +519,7 @@ public override int GetHashCode()
hash.Add(ForeColor);
hash.Add(SelectionBackColor);
hash.Add(SelectionForeColor);
hash.Add(SortGlyphColor);
hash.Add(Font);
hash.Add(NullValue);
hash.Add(DataSourceNullValue);
Expand Down Expand Up @@ -530,6 +558,8 @@ internal void RemoveScope(DataGridViewCellStyleScopes scope)

private bool ShouldSerializeSelectionForeColor() => Properties.ContainsKey(s_propSelectionForeColor);

private bool ShouldSerializeSortGlyphColor() => Properties.ContainsKey(s_propSortGlyphColor);

public override string ToString()
{
StringBuilder sb = new(128);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1240,10 +1240,11 @@ private Rectangle PaintPrivate(
{
if (paint && PaintContentForeground(paintParts))
{
DataGridViewCheckBoxCellRenderer.DrawCheckBox(
CheckBoxRenderer.DrawCheckBoxWithVisualStyles(
g,
new Rectangle(checkBoxX, checkBoxY, checkBoxSize.Width, checkBoxSize.Height),
(int)themeCheckBoxState);
new Point(checkBoxX, checkBoxY),
themeCheckBoxState,
DataGridView.HWNDInternal);
}

resultBounds = new Rectangle(checkBoxX, checkBoxY, checkBoxSize.Width, checkBoxSize.Height);
Expand Down
Loading
Loading