Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Detect system theme change in WPF

Tags:

c#

.net

wpf

winapi

dwm

I need, for my WPF app, to detect when the DWM is turned on/off or when the system theme changes.
There is such an event in WinForms, but I can't see any in WPF.

like image 523
Vercas Avatar asked Jun 15 '11 15:06

Vercas


3 Answers

I haven't heard of a WinForms event that fires when a WinForms window receives messages from the system, however it has its own WndProc() method that you can override. You're probably confusing window messages for form events. Ah, so it's the StyleChanged event that gets invoked in WinForms windows. The rest of my answer still stands though.

WPF isn't closely tied to the Windows API either as it's a high-level technology that invests a lot of abstraction away from the internals. For one, it draws everything in a window by itself, and doesn't ask the system to do the drawing for it (EDIT: which is why WPF lacks such a StyleChanged event). That said, Windows sends messages to all windows when the DWM is toggled and when the theme changes, and you can still drill down into the low level from the WPF layer to access these messages and manipulate your WPF controls accordingly.

Attach a window procedure to your WPF window's HWND (window handle) as part of your window's SourceInitialized event. In your window procedure, handle the WM_DWMCOMPOSITIONCHANGED and WM_THEMECHANGED window messages respectively.

Here's a quick example (with boilerplate code adapted from this question of mine):

private IntPtr hwnd;
private HwndSource hsource;

private const int WM_DWMCOMPOSITIONCHANGED= 0x31E;
private const int WM_THEMECHANGED = 0x31A;

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 IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
{
    switch (msg)
    {
        case WM_DWMCOMPOSITIONCHANGED: 
        case WM_THEMECHANGED:         

            // Respond to DWM being enabled/disabled or system theme being changed

            return IntPtr.Zero;

        default:
            return IntPtr.Zero;
    }
}
like image 71
BoltClock Avatar answered Sep 30 '22 16:09

BoltClock


Unfortunately the accepted solution does not work with Aero color theme changes, and the WM message hex numbers are mixed up - but I agree that it is very useful if you want to catch WM messages in WPF. I've been trying to find a solution to this problem for a while, and I think I've got it solved for all possible cases (for aero and classic themes).

The Aero color change triggers the WM_DWMCOLORIZATIONCOLORCHANGED message.

To detect when the color theme changes you have to use multiple methods. The Form.StyleChanged event is going to detect all theme change, except for Aero color changes. Here is an alternative solution to StyleChanged. (Ok, I know this is WinForms, but you've got the idea. The WPF equivalent is in the accepted answer anyway.)

    private const int WM_DWMCOLORIZATIONCOLORCHANGED = 0x320;
    private const int WM_DWMCOMPOSITIONCHANGED = 0x31E;
    private const int WM_THEMECHANGED = 0x031A;

    protected override void WndProc(ref Message m)
    {
        switch(m.Msg)
        {
            case WM_DWMCOLORIZATIONCOLORCHANGED:
            case WM_DWMCOMPOSITIONCHANGED:
            case WM_THEMECHANGED:
                // you code here
                break;
            default:
                break;
        }
        base.WndProc(ref m);
    }

For Aero color themes, the SystemEvents.UserPreferenceChanged event works too (thanks sees!):

    Microsoft.Win32.SystemEvents.UserPreferenceChanged += SystemEvents_UserPreferenceChanged;

    private void SystemEvents_UserPreferenceChanged(object sender, Microsoft.Win32.UserPreferenceChangedEventArgs e)
    {
        if (e.Category == Microsoft.Win32.UserPreferenceCategory.General)
        {
            // your code here, compare saved theme color with current one
        }
    }

As you can see above, it is far from intuitive. Aero color change triggers a 'General' preference change event, even though there are many more suitable ones for this, like 'VisualStyle', etc...

If you want to be thorough, you should compare the saved DWM color to the current DWM color, to make sure it was indeed an Aero color theme that triggered this event (using the DwmGetColorizationParameters API call), and not something else. See these answers on how Aero colors can be retrieved: Get the active color of Windows 8 automatic color theme

like image 27
Peter Bulyaki Avatar answered Sep 30 '22 14:09

Peter Bulyaki


The event SystemEvents.UserPreferenceChanged also does the trick. UserPreferenceChanged(in Japaense)

like image 23
sees Avatar answered Sep 30 '22 14:09

sees