winforms
winforms copied to clipboard
[Dark Mode] Some toolstrip colors don't change when switching color mode
.NET version
9.0.100-rc.2.24430.10
Did it work in .NET Framework?
No
Did it work in any of the earlier releases of .NET Core or .NET 5+?
Dark mode is a new feature in net9
Issue description
Some toolstrip colors don't change when the SystemColorMode is changed while the app is running, even after re-creating the form. (It works fine when setting the SystemColorMode at app start.)
Correct (when starting the app in dark mode):
Incorrect (when starting the app in light mode, switching to dark mode, and re-creating the form):
I believe the root cause of the issue is here, where there is a static cache of brushes with an index based on the KnownColor enum (e.g. KnownColor.ControlText). I imagine the solution involves purging these caches when the color mode is changed.
https://github.com/dotnet/winforms/blob/b6695d6cd48019890b785ea32684df47297bc2a5/src/System.Drawing.Common/src/System/Drawing/SystemBrushes.cs#L53
Also here has a similar problem, though I haven't evaluated if there are further impacts from these static caches. https://github.com/dotnet/winforms/blob/b6695d6cd48019890b785ea32684df47297bc2a5/src/System.Drawing.Common/src/System/Drawing/SystemPens.cs#L54
Steps to reproduce
- Start the application with
Application.SetColorMode(SystemColorMode.Classic) - Call
Application.SetColorMode(SystemColorMode.Dark) - Close and re-open a form with a toolbar
- Observe that ToolStripSeparator and dropdown arrows on ToolStripSplitButton have the wrong color
Calling this from the main thread after switching color modes seems to be a working hack:
private void ClearCachedBrushesAndPens()
{
var threadData = (IDictionary<object, object?>) typeof(SystemBrushes).Assembly.GetType("System.Drawing.Gdip")!
.GetProperty("ThreadData", BindingFlags.Static | BindingFlags.NonPublic)!
.GetValue(null)!;
var systemBrushesKey = typeof(SystemBrushes)
.GetField("s_systemBrushesKey", BindingFlags.Static | BindingFlags.NonPublic)!
.GetValue(null)!;
var systemPensKey = typeof(SystemPens)
.GetField("s_systemPensKey", BindingFlags.Static | BindingFlags.NonPublic)!
.GetValue(null)!;
threadData[systemBrushesKey] = null;
threadData[systemPensKey] = null;
}
@cyanfish, cannot reproduce the issue based on your description as below screenshot. could you please have a check if there have any wrong steps.
Sorry, the trigger is actually much more obscure than I thought. It seems to be some kind of race condition when calling SetColorMode in response to the UserPreferenceChanged event for the system switching between light and dark mode.
Specific repro steps with the below code:
- Set the Windows color mode to Light
- Start the program
- Open Form2
- Close Form2
- Switch the Windows color mode to Dark
- Open Form2
But as noted in the code with any kind of delay to the Application.SetColorMode call the issue goes away.
Repro Code
using Microsoft.Win32;
namespace WinFormsApp1;
static class Program
{
[STAThread]
static void Main()
{
ApplicationConfiguration.Initialize();
#pragma warning disable WFO5001
Application.SetColorMode(SystemColorMode.System);
#pragma warning restore WFO5001
Application.Run(new Form1());
}
}
public class Form1 : Form
{
private static DarkModeManager _dmm;
public Form1()
{
_dmm = new DarkModeManager();
var btn = new Button { Text = "Form2", Location = new Point(0, 50) };
btn.Click += (_, _) => new Form2().Show();
Controls.Add(btn);
}
}
public class Form2 : Form
{
public Form2()
{
var tsc = new ToolStripContainer();
var ts = new ToolStrip();
ts.Items.Add(new ToolStripSplitButton("A"));
ts.Items.Add(new ToolStripSeparator());
ts.Items.Add(new ToolStripSplitButton("B"));
tsc.TopToolStripPanel.Controls.Add(ts);
Controls.Add(tsc);
}
}
public class DarkModeManager
{
private bool? _value;
public DarkModeManager()
{
SystemEvents.UserPreferenceChanged += OnUserPreferenceChanged;
}
private bool ReadDarkMode()
{
try
{
using var key =
Registry.CurrentUser.OpenSubKey(@"SOFTWARE\Microsoft\Windows\CurrentVersion\Themes\Personalize");
return Equals(key?.GetValue("AppsUseLightTheme"), 0);
}
catch (Exception)
{
return false;
}
}
private void OnUserPreferenceChanged(object sender, UserPreferenceChangedEventArgs e)
{
var newValue = ReadDarkMode();
if (newValue != _value)
{
_value = newValue;
#pragma warning disable WFO5001
// This does not work:
Application.SetColorMode(newValue ? SystemColorMode.Dark : SystemColorMode.Classic);
// This works:
// Task.Delay(100).ContinueWith(_ =>
// Application.SetColorMode(newValue ? SystemColorMode.Dark : SystemColorMode.Classic));
#pragma warning restore WFO5001
}
}
}
@cyanfish thanks for your response, yes. the issue can be reproduced with your code as below screenshot. 12027.zip
This does not work:
Application.SetColorMode(newValue ? SystemColorMode.Dark : SystemColorMode.Classic);
This works:
Task.Delay(100).ContinueWith(_ =>
Application.SetColorMode(newValue ? SystemColorMode.Dark : SystemColorMode.Classic));
This issue is now marked as "help wanted", and we’re looking for a community volunteer to work on this issue. If we receive no interest in 180 days, we will close the issue. To learn more about how we handle feature requests, please see our documentation.
Happy Coding!
@cyanfish @Zheng-Li01
Is this what is required? This took some effort but the appearance change at runtime is required.
https://github.com/user-attachments/assets/b9713b31-991f-42a4-bd7c-fe114cc4f39c
@KlausLoeffelmann @merriemcgaw @JeremyKuhne I am almost done and I will open a discussion to clarify the backgrounds as you requested. There is no public api but I will clarify why some Methods need reimplementation.
Verified in the latest .NET 10 SDK build: 10.0.100-preview.7.25318.104, issue still repro as the details, reopen it. @KlausLoeffelmann, can you please look into it again?
https://github.com/user-attachments/assets/c1a37105-40f1-4f2b-8cc2-b64d3fce1db0