Get the active color of Windows 8 automatic color theme

In Windows 8, I have set the color scheme to automatic and configured my wallpaper to change after x minutes. The color scheme changes according to the active wallpaper.

I'm developing a WPF application and would like to have my gradients change when Windows changes the color scheme to match the current wallpaper.

Is there a way get the current/actual color scheme and be notified of the change in C#?


Solution 1:

Yes, it's possible. However be warned: this encompasses quite a bit of Win32 interop (this means P/Invokes into native DLLs from managed code), and is only doable with certain undocumented APIs. Although, the only undocumented features involved are for obtaining the window color scheme (or as the DWM calls it, the window colorization color), which is covered in this other question:

Vista/7: How to get glass color?

In my own project, I make use of a call to DwmGetColorizationParameters():

internal static class NativeMethods
{
    [DllImport("dwmapi.dll", EntryPoint="#127")]
    internal static extern void DwmGetColorizationParameters(ref DWMCOLORIZATIONPARAMS params);
}

public struct DWMCOLORIZATIONPARAMS
{
    public uint ColorizationColor, 
        ColorizationAfterglow, 
        ColorizationColorBalance, 
        ColorizationAfterglowBalance, 
        ColorizationBlurBalance, 
        ColorizationGlassReflectionIntensity, 
        ColorizationOpaqueBlend;
}

I've tested it and it works great with Windows 8 and its automatic window colorization feature. As suggested in the link above, you can look in the registry for the color values as an alternative to a P/Invoke, but I haven't tested that method, and as stated these are undocumented and not guaranteed to be stable.

Once you obtain the color for drawing your gradient brushes, the brushes won't update when the window color scheme changes, whether manually or automatically by Windows. Thankfully, Windows broadcasts the WM_DWMCOLORIZATIONCOLORCHANGED window message whenever that happens, so you simply need to listen for that message and update your colors whenever it's sent. You do this by hooking onto the window procedure (WndProc()).

The value of WM_DWMCOLORIZATIONCOLORCHANGED is 0x320; you'll want to define that as a constant somewhere so you can use it in code.

Also, unlike WinForms, WPF windows don't have a virtual WndProc() method to override, so you have to create and hook one in as a delegate to their associated window handles (HWNDs).

Taking some example code from these answers of mine:

  • How do I make a WPF window movable by dragging the extended window frame?
  • Detect system theme change in WPF

We have:

const int WM_DWMCOLORIZATIONCOLORCHANGED = 0x320;

private IntPtr hwnd;
private HwndSource hsource;

private void Window_SourceInitialized(object sender, EventArgs e)
{
    if ((hwnd = new WindowInteropHelper(this).Handle) == IntPtr.Zero)
    {
        throw new InvalidOperationException("Could not get window handle.");
    }

    hsource = HwndSource.FromHwnd(hwnd);
    hsource.AddHook(WndProc);
}

private static Color GetWindowColorizationColor(bool opaque)
{
    var params = NativeMethods.DwmGetColorizationParameters();

    return Color.FromArgb(
        (byte)(opaque ? 255 : params.ColorizationColor >> 24), 
        (byte)(params.ColorizationColor >> 16), 
        (byte)(params.ColorizationColor >> 8), 
        (byte) params.ColorizationColor
    );
}

private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
{
    switch (msg)
    {
        case WM_DWMCOLORIZATIONCOLORCHANGED:

            /* 
             * Update gradient brushes with new color information from
             * NativeMethods.DwmGetColorizationParams() or the registry.
             */

            return IntPtr.Zero;

        default:
            return IntPtr.Zero;
    }
}

When Windows transitions the color change, WM_DWMCOLORIZATIONCOLORCHANGED is dispatched at every keyframe in the transition, so you'll receive numerous messages at a short burst during the color change. This is normal; just update your gradient brushes as usual and you'll notice that when Windows transitions the window color scheme, your gradients will transition smoothly along with the rest of the window frames as well.

Remember that you may need to account for situations where the DWM isn't available, such as when running on Windows XP, or when running on Windows Vista or later with desktop composition disabled. You'll also want to ensure you don't overuse this, or you may incur a significant performance hit and slow down your app.

Solution 2:

This can be done in .NET 4.5 and later without P/Invokes. The SystemParameters class now has static WindowGlassBrush and WindowGlassColor properties along with a StaticPropertyChanged event.

From XAML, you can bind to the WindowGlassBrush property like:

<Grid Background="{x:Static SystemParameters.WindowGlassBrush}">

However, with this assignment the Background color won't get updated automatically when Windows changes its colors. Unfortunately, SystemParameters does not provide WindowGlassBrushKey or WindowGlassColorKey properties to use as ResourceKeys with DynamicResource, so getting change notifications requires code behind to handle the StaticPropertyChanged event.

public partial class MainWindow : Window
{
    public MainWindow()
    {
        this.InitializeComponent();
        SystemParameters.StaticPropertyChanged += this.SystemParameters_StaticPropertyChanged;

        // Call this if you haven't set Background in XAML.
        this.SetBackgroundColor();
    }

    protected override void OnClosed(EventArgs e)
    {
        SystemParameters.StaticPropertyChanged -= this.SystemParameters_StaticPropertyChanged;
        base.OnClosed(e);
    }

    private void SetBackgroundColor()
    {
        this.Background = SystemParameters.WindowGlassBrush;
    }

    private void SystemParameters_StaticPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
    {
        if (e.PropertyName == "WindowGlassBrush")
        {
            this.SetBackgroundColor();
        }
    }
}