12

I have a WinForms application which automatically adjusts to the dark/light theme on Windows 10. My problem is that the title bar of my window always stays white, regardless which theme the user selects.

enter image description here
Top is current, bottom is how I want it (simulated with Photoshop)

See explorer for example. That is not an UWP app, however it uses a dark title bar on Windows 1903 and newer (when a dark theme is selected).

How can I achieve the same thing? I do not want to use any custom titlebar as I want the application to look and behave like any native application on older Windows versions as well.

TylerH
  • 20,799
  • 66
  • 75
  • 101
Jonas Kohl
  • 1,018
  • 1
  • 11
  • 28
  • https://stackoverflow.com/questions/11862315/changing-the-color-of-the-title-bar-in-winform – User81772 Jul 21 '19 at 20:19
  • @User81772 The accepted answer (and all others) do not seem to work on modern versions of Windows – Jonas Kohl Jul 21 '19 at 20:36
  • 1
    You will need to hide the top bar and make your own. –  Jul 22 '19 at 14:16
  • cmd.exe also has a dark titlebar when using the dark theme these days. In the past, I would have expected there to be a window style or API call to make it happen and maintain standard rendering (making the app future-proof when the Windows style changes). With today's MS, I wouldn't be surprised if both File Explorer and cmd.exe use custom hacks instead of them improving the actual OS and APIs for everything. :( – Leo Davidson Aug 05 '19 at 09:38

3 Answers3

31

So after some long searching, I have finally found the answer for this. The trick is to use dwmapi.dll's DwmSetWindowAttribute and passing the undocumented constant DWMWA_USE_IMMERSIVE_DARK_MODE into the function. In C#, the code for this looks a little something like this (works with both WinForms and WPF):

/*
using System.Runtime.InteropServices;
*/

[DllImport("dwmapi.dll")]
private static extern int DwmSetWindowAttribute(IntPtr hwnd, int attr, ref int attrValue, int attrSize);

private const int DWMWA_USE_IMMERSIVE_DARK_MODE_BEFORE_20H1 = 19;
private const int DWMWA_USE_IMMERSIVE_DARK_MODE = 20;

private static bool UseImmersiveDarkMode(IntPtr handle, bool enabled)
{
    if (IsWindows10OrGreater(17763))
    {
        var attribute = DWMWA_USE_IMMERSIVE_DARK_MODE_BEFORE_20H1;
        if (IsWindows10OrGreater(18985))
        {
            attribute = DWMWA_USE_IMMERSIVE_DARK_MODE;
        }

        int useImmersiveDarkMode = enabled ? 1 : 0;
        return DwmSetWindowAttribute(handle, (int)attribute, ref useImmersiveDarkMode, sizeof(int)) == 0;
    }

    return false;
}

private static bool IsWindows10OrGreater(int build = -1)
{
    return Environment.OSVersion.Version.Major >= 10 && Environment.OSVersion.Version.Build >= build;
}
Jonas Kohl
  • 1,018
  • 1
  • 11
  • 28
  • 1
    In my case, I had to additionally hide/show window for this change to take effect. – Alexander Dyagilev Aug 29 '21 at 10:34
  • 1
    Would be nice to know what other call is needed to force change it. But one way to do it without flashing the window or changing focus is to change the window opacity very slightly +/- relative to the current opacity by a small fraction like 0.0000000001 then return it back. This makes the required call that forces it to repaint. – Pk King X11 Aug 30 '21 at 10:45
  • 1
    @PkKingX11 After looking at the [reference source](https://referencesource.microsoft.com/#System.Windows.Forms/winforms/Managed/System/WinForms/Control.cs,2895b1373643ef53) it seems that a call to SetWindowPos like follows seem to be needed: `SetWindowPos(Handle, IntPtr.Zero, 0, 0, 0, 0, SWP_DRAWFRAME | SWP_NOACTIVATE | SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER);` Although I have not tried this yet – Jonas Kohl Oct 14 '21 at 08:15
  • 4
    Great, it works! Hint: Add `UseImmersiveDarkMode(this.Handle, true);` right after the `InitializeComponent();` call in your form. – Matt Oct 29 '21 at 14:12
  • 3
    The constant is no longer undocumented since I just opened this PR for it: https://github.com/MicrosoftDocs/sdk-api/pull/966/files. Also to force the window to repaint, one can call Invalidate() on the entire form or call SendMessageW with WM_NCPAINT to force an NCPAINT to happen again. – PSXGamerPro1 Nov 18 '21 at 06:13
8

The fastest way:

[DllImport("DwmApi")] //System.Runtime.InteropServices
private static extern int DwmSetWindowAttribute(IntPtr hwnd, int attr, int[] attrValue, int attrSize);

protected override void OnHandleCreated(EventArgs e)
{
    if (DwmSetWindowAttribute(Handle, 19, new[] { 1 }, 4) != 0)
        DwmSetWindowAttribute(Handle, 20, new[] { 1 }, 4);
}
Lorenzo Lotti
  • 81
  • 1
  • 3
  • 3
    You may want to give a bit more details what those values mean exactly, or better yet use the functions that map literals to object identifiers. – StarShine Nov 20 '20 at 12:15
3

For the solution from Jonas Kohl, remember that for .net fw 4.8.1 and prior, the version returned is not ok, fixed in .Net6, here a snippet (.Net 5 is not managed):

    private static bool IsWindows10OrGreater(int build = -1)
    {
        return WindowsVersion() >= 10 && WindowsBuildNumber() >= build;
    }

    public static int WindowsVersion()
    {
     //for .Net4.8 and Minor
     #if NETFRAMEWORK
        int result = 10;
        var reg = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows NT\CurrentVersion");
        string[] productName = reg.GetValue("ProductName").ToString().Split((char)32);
        int.TryParse(productName[1], out result);
        return result;
     #else
        //fixed in .Net6
        return System.Environment.OSVersion.Version.Major;
     #endif
    }

    public static int WindowsBuildNumber()
    {
        //for .Net4.8 and Minor
    #if NETFRAMEWORK
        int result = 22000;
        var reg = Registry.LocalMachine.OpenSubKey(@"SOFTWARE\Microsoft\Windows NT\CurrentVersion");
        string buildNumber = reg.GetValue("CurrentBuildNumber").ToString();
        int.TryParse(buildNumber, out result);
        return result;
    #endif

    #if NET
        //fixed in .Net6
        return System.Environment.OSVersion.Version.Build;
    #endif
    }
Angelo Cresta
  • 104
  • 1
  • 7
  • 2
    If you add an `app.manifest` with the tag `` you will also get back the correct value for `Environment.OSVersion.Version` – Jonas Kohl Dec 12 '22 at 14:06