winforms icon indicating copy to clipboard operation
winforms copied to clipboard

[Dark Mode] Some toolstrip colors don't change when switching color mode

Open cyanfish opened this issue 1 year ago • 8 comments
trafficstars

.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): image

Incorrect (when starting the app in light mode, switching to dark mode, and re-creating the form): image

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

  1. Start the application with Application.SetColorMode(SystemColorMode.Classic)
  2. Call Application.SetColorMode(SystemColorMode.Dark)
  3. Close and re-open a form with a toolbar
  4. Observe that ToolStripSeparator and dropdown arrows on ToolStripSplitButton have the wrong color

cyanfish avatar Aug 31 '24 22:08 cyanfish

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 avatar Aug 31 '24 22:08 cyanfish

@cyanfish, cannot reproduce the issue based on your description as below screenshot. could you please have a check if there have any wrong steps. GH12027

Zheng-Li01 avatar Sep 02 '24 08:09 Zheng-Li01

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:

  1. Set the Windows color mode to Light
  2. Start the program
  3. Open Form2
  4. Close Form2
  5. Switch the Windows color mode to Dark
  6. 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 avatar Sep 02 '24 18:09 cyanfish

@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); image NotWorking

This works: Task.Delay(100).ContinueWith(_ => Application.SetColorMode(newValue ? SystemColorMode.Dark : SystemColorMode.Classic)); image Working

Zheng-Li01 avatar Sep 03 '24 01:09 Zheng-Li01

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

memoarfaa avatar Jun 02 '25 06:06 memoarfaa

@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.

memoarfaa avatar Jun 02 '25 06:06 memoarfaa

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

Olina-Zhang avatar Jun 19 '25 03:06 Olina-Zhang