From 9b61717549ab6f94831cd6fcfc69a4483d074498 Mon Sep 17 00:00:00 2001 From: Koichi Kobayashi Date: Sun, 14 Dec 2025 16:55:31 +0900 Subject: [PATCH 1/3] Fix theme and accent color reset after Windows session unlock - Add session switch event handling to restore theme and accent color after unlock - Store current theme and accent color before session lock - Ignore WM_WININICHANGE messages during unlock restore period (5 seconds) - Preserve stored theme when it differs from system theme - Optimize code by removing unnecessary operations and using switch expressions - Remove debug output statements --- src/Wpf.Ui/Appearance/SystemThemeWatcher.cs | 185 +++++++++++++++++--- 1 file changed, 162 insertions(+), 23 deletions(-) diff --git a/src/Wpf.Ui/Appearance/SystemThemeWatcher.cs b/src/Wpf.Ui/Appearance/SystemThemeWatcher.cs index 841a6b4bd..1b091eb73 100644 --- a/src/Wpf.Ui/Appearance/SystemThemeWatcher.cs +++ b/src/Wpf.Ui/Appearance/SystemThemeWatcher.cs @@ -3,6 +3,7 @@ // Copyright (C) Leszek Pomianowski and WPF UI Contributors. // All Rights Reserved. +using Microsoft.Win32; using Windows.Win32; using Wpf.Ui.Controls; using Wpf.Ui.Interop; @@ -27,6 +28,12 @@ namespace Wpf.Ui.Appearance; public static class SystemThemeWatcher { private static readonly List _observedWindows = []; + private static readonly TimeSpan _unlockIgnoreDuration = TimeSpan.FromSeconds(5); + private static ApplicationTheme? _lastAppliedTheme; + private static Color? _lastAppliedAccentColor; + private static bool _sessionSwitchHandlerRegistered; + private static DateTime _lastUnlockTime = DateTime.MinValue; + private static bool _isRestoringTheme = false; /// /// Watches the and applies the background effect and theme according to the system theme. @@ -56,11 +63,15 @@ public static void Watch( if (_observedWindows.Count == 0) { - System.Diagnostics.Debug.WriteLine( - $"INFO | {typeof(SystemThemeWatcher)} changed the app theme on initialization.", - nameof(SystemThemeWatcher) - ); ApplicationThemeManager.ApplySystemTheme(updateAccents); + StoreCurrentThemeAndAccent(); + } + + if (!_sessionSwitchHandlerRegistered) + { + SystemEvents.SessionSwitch += OnSessionSwitch; + ApplicationThemeManager.Changed += OnThemeChanged; + _sessionSwitchHandlerRegistered = true; } } @@ -105,10 +116,6 @@ private static void ObserveLoadedHandle(ObservedWindow observedWindow) { if (!observedWindow.HasHook) { - System.Diagnostics.Debug.WriteLine( - $"INFO | {observedWindow.Handle} ({observedWindow.RootVisual?.Title}) registered as watched window.", - nameof(SystemThemeWatcher) - ); observedWindow.AddHook(WndProc); _observedWindows.Add(observedWindow); } @@ -151,9 +158,13 @@ public static void UnWatch(Window? window) /// private static IntPtr WndProc(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled) { - if (msg == (int)PInvoke.WM_WININICHANGE) + if (msg == (int)PInvoke.WM_WININICHANGE && !_isRestoringTheme) { - UpdateObservedWindow(hWnd); + DateTime now = DateTime.Now; + if (now - _lastUnlockTime > _unlockIgnoreDuration) + { + UpdateObservedWindow(hWnd); + } } return IntPtr.Zero; @@ -167,27 +178,155 @@ private static void UpdateObservedWindow(nint hWnd) } ObservedWindow? observedWindow = _observedWindows.FirstOrDefault(x => x.Handle == hWnd); - if (observedWindow is null) { return; } - ApplicationThemeManager.ApplySystemTheme(observedWindow.UpdateAccents); - ApplicationTheme currentApplicationTheme = ApplicationThemeManager.GetAppTheme(); + SystemTheme currentSystemTheme = ApplicationThemeManager.GetSystemTheme(); + ApplicationTheme systemThemeAsAppTheme = ConvertSystemThemeToApplicationTheme(currentSystemTheme); + + // Check if stored theme is different from what system theme would be + if (_lastAppliedTheme.HasValue && + _lastAppliedTheme.Value != ApplicationTheme.Unknown && + _lastAppliedTheme.Value != systemThemeAsAppTheme) + { + // Preserve stored theme + WindowBackdropType backdrop = _observedWindows.Count > 0 + ? _observedWindows[0].Backdrop + : WindowBackdropType.Mica; - System.Diagnostics.Debug.WriteLine( - $"INFO | {observedWindow.Handle} ({observedWindow.RootVisual?.Title}) triggered the application theme change to {ApplicationThemeManager.GetSystemTheme()}.", - nameof(SystemThemeWatcher) - ); + ApplicationThemeManager.Apply(_lastAppliedTheme.Value, backdrop, false); + + if (_lastAppliedAccentColor.HasValue && _lastAppliedAccentColor.Value != Colors.Transparent) + { + ApplicationAccentColorManager.Apply(_lastAppliedAccentColor.Value, _lastAppliedTheme.Value, false); + } + + if (observedWindow.RootVisual is { } window) + { + WindowBackgroundManager.UpdateBackground(window, _lastAppliedTheme.Value, observedWindow.Backdrop); + } + } + else + { + // Normal system theme update + ApplicationThemeManager.ApplySystemTheme(observedWindow.UpdateAccents); + StoreCurrentThemeAndAccent(); + + ApplicationTheme newAppTheme = ApplicationThemeManager.GetAppTheme(); + if (observedWindow.RootVisual is { } window) + { + WindowBackgroundManager.UpdateBackground(window, newAppTheme, observedWindow.Backdrop); + } + } + } + + /// + /// Converts a system theme to an application theme. + /// + private static ApplicationTheme ConvertSystemThemeToApplicationTheme(SystemTheme systemTheme) + { + return systemTheme switch + { + SystemTheme.Dark or SystemTheme.CapturedMotion or SystemTheme.Glow => ApplicationTheme.Dark, + SystemTheme.HC1 or SystemTheme.HC2 or SystemTheme.HCBlack or SystemTheme.HCWhite => ApplicationTheme.HighContrast, + _ => ApplicationTheme.Light + }; + } - if (observedWindow.RootVisual is not null) + /// + /// Stores the current theme and accent color for restoration after session unlock. + /// + private static void StoreCurrentThemeAndAccent() + { + _lastAppliedTheme = ApplicationThemeManager.GetAppTheme(); + _lastAppliedAccentColor = ApplicationAccentColorManager.SystemAccent; + } + + /// + /// Handles session switch events (lock/unlock) to restore theme and accent color. + /// + private static void OnSessionSwitch(object sender, SessionSwitchEventArgs e) + { + if (e.Reason == SessionSwitchReason.SessionUnlock) + { + _lastUnlockTime = DateTime.Now; + + // Restore theme if we have a stored value + if (_lastAppliedTheme.HasValue && _lastAppliedTheme.Value != ApplicationTheme.Unknown) + { + _isRestoringTheme = true; + + // Use the backdrop from the first observed window, or default to Mica + WindowBackdropType backdrop = _observedWindows.Count > 0 + ? _observedWindows[0].Backdrop + : WindowBackdropType.Mica; + + ApplicationThemeManager.Apply( + _lastAppliedTheme.Value, + backdrop, + false // Don't update accent yet, we'll do it separately + ); + + // Restore accent color if we have a stored value + if (_lastAppliedAccentColor.HasValue && _lastAppliedAccentColor.Value != Colors.Transparent) + { + ApplicationAccentColorManager.Apply( + _lastAppliedAccentColor.Value, + _lastAppliedTheme.Value, + false + ); + } + else + { + // Fallback to system accent color + ApplicationAccentColorManager.ApplySystemAccent(); + } + + // Update all observed windows + foreach (ObservedWindow observedWindow in _observedWindows) + { + if (observedWindow.RootVisual is { } window) + { + WindowBackgroundManager.UpdateBackground(window, _lastAppliedTheme.Value, observedWindow.Backdrop); + } + } + + // Store the restored theme to prevent it from being overwritten + StoreCurrentThemeAndAccent(); + + // Give the UI time to update before allowing system theme changes + _ = System.Threading.Tasks.Task.Run(async () => + { + await System.Threading.Tasks.Task.Delay(100); + _isRestoringTheme = false; + }); + } + else + { + // If we don't have a stored theme, apply system theme + ApplicationThemeManager.ApplySystemTheme(true); + StoreCurrentThemeAndAccent(); + } + } + else if (e.Reason == SessionSwitchReason.SessionLock) + { + // Store current theme and accent before lock + StoreCurrentThemeAndAccent(); + } + } + + /// + /// Handles theme change events to keep the stored theme up to date. + /// + private static void OnThemeChanged(ApplicationTheme theme, Color accentColor) + { + // Update stored theme if we're restoring or if we're past the unlock ignore period + if (_isRestoringTheme || DateTime.Now - _lastUnlockTime > _unlockIgnoreDuration) { - WindowBackgroundManager.UpdateBackground( - observedWindow.RootVisual, - currentApplicationTheme, - observedWindow.Backdrop - ); + _lastAppliedTheme = theme; + _lastAppliedAccentColor = accentColor; } } } From 44e3a70e09e028df95e6789c5c4d1113c7a99467 Mon Sep 17 00:00:00 2001 From: Koichi Kobayashi Date: Mon, 15 Dec 2025 00:05:38 +0900 Subject: [PATCH 2/3] Revert "Fix theme and accent color reset after Windows session unlock" This reverts commit 9b61717549ab6f94831cd6fcfc69a4483d074498. --- src/Wpf.Ui/Appearance/SystemThemeWatcher.cs | 185 +++----------------- 1 file changed, 23 insertions(+), 162 deletions(-) diff --git a/src/Wpf.Ui/Appearance/SystemThemeWatcher.cs b/src/Wpf.Ui/Appearance/SystemThemeWatcher.cs index 1b091eb73..841a6b4bd 100644 --- a/src/Wpf.Ui/Appearance/SystemThemeWatcher.cs +++ b/src/Wpf.Ui/Appearance/SystemThemeWatcher.cs @@ -3,7 +3,6 @@ // Copyright (C) Leszek Pomianowski and WPF UI Contributors. // All Rights Reserved. -using Microsoft.Win32; using Windows.Win32; using Wpf.Ui.Controls; using Wpf.Ui.Interop; @@ -28,12 +27,6 @@ namespace Wpf.Ui.Appearance; public static class SystemThemeWatcher { private static readonly List _observedWindows = []; - private static readonly TimeSpan _unlockIgnoreDuration = TimeSpan.FromSeconds(5); - private static ApplicationTheme? _lastAppliedTheme; - private static Color? _lastAppliedAccentColor; - private static bool _sessionSwitchHandlerRegistered; - private static DateTime _lastUnlockTime = DateTime.MinValue; - private static bool _isRestoringTheme = false; /// /// Watches the and applies the background effect and theme according to the system theme. @@ -63,15 +56,11 @@ public static void Watch( if (_observedWindows.Count == 0) { + System.Diagnostics.Debug.WriteLine( + $"INFO | {typeof(SystemThemeWatcher)} changed the app theme on initialization.", + nameof(SystemThemeWatcher) + ); ApplicationThemeManager.ApplySystemTheme(updateAccents); - StoreCurrentThemeAndAccent(); - } - - if (!_sessionSwitchHandlerRegistered) - { - SystemEvents.SessionSwitch += OnSessionSwitch; - ApplicationThemeManager.Changed += OnThemeChanged; - _sessionSwitchHandlerRegistered = true; } } @@ -116,6 +105,10 @@ private static void ObserveLoadedHandle(ObservedWindow observedWindow) { if (!observedWindow.HasHook) { + System.Diagnostics.Debug.WriteLine( + $"INFO | {observedWindow.Handle} ({observedWindow.RootVisual?.Title}) registered as watched window.", + nameof(SystemThemeWatcher) + ); observedWindow.AddHook(WndProc); _observedWindows.Add(observedWindow); } @@ -158,13 +151,9 @@ public static void UnWatch(Window? window) /// private static IntPtr WndProc(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled) { - if (msg == (int)PInvoke.WM_WININICHANGE && !_isRestoringTheme) + if (msg == (int)PInvoke.WM_WININICHANGE) { - DateTime now = DateTime.Now; - if (now - _lastUnlockTime > _unlockIgnoreDuration) - { - UpdateObservedWindow(hWnd); - } + UpdateObservedWindow(hWnd); } return IntPtr.Zero; @@ -178,155 +167,27 @@ private static void UpdateObservedWindow(nint hWnd) } ObservedWindow? observedWindow = _observedWindows.FirstOrDefault(x => x.Handle == hWnd); + if (observedWindow is null) { return; } - SystemTheme currentSystemTheme = ApplicationThemeManager.GetSystemTheme(); - ApplicationTheme systemThemeAsAppTheme = ConvertSystemThemeToApplicationTheme(currentSystemTheme); - - // Check if stored theme is different from what system theme would be - if (_lastAppliedTheme.HasValue && - _lastAppliedTheme.Value != ApplicationTheme.Unknown && - _lastAppliedTheme.Value != systemThemeAsAppTheme) - { - // Preserve stored theme - WindowBackdropType backdrop = _observedWindows.Count > 0 - ? _observedWindows[0].Backdrop - : WindowBackdropType.Mica; + ApplicationThemeManager.ApplySystemTheme(observedWindow.UpdateAccents); + ApplicationTheme currentApplicationTheme = ApplicationThemeManager.GetAppTheme(); - ApplicationThemeManager.Apply(_lastAppliedTheme.Value, backdrop, false); - - if (_lastAppliedAccentColor.HasValue && _lastAppliedAccentColor.Value != Colors.Transparent) - { - ApplicationAccentColorManager.Apply(_lastAppliedAccentColor.Value, _lastAppliedTheme.Value, false); - } - - if (observedWindow.RootVisual is { } window) - { - WindowBackgroundManager.UpdateBackground(window, _lastAppliedTheme.Value, observedWindow.Backdrop); - } - } - else - { - // Normal system theme update - ApplicationThemeManager.ApplySystemTheme(observedWindow.UpdateAccents); - StoreCurrentThemeAndAccent(); - - ApplicationTheme newAppTheme = ApplicationThemeManager.GetAppTheme(); - if (observedWindow.RootVisual is { } window) - { - WindowBackgroundManager.UpdateBackground(window, newAppTheme, observedWindow.Backdrop); - } - } - } - - /// - /// Converts a system theme to an application theme. - /// - private static ApplicationTheme ConvertSystemThemeToApplicationTheme(SystemTheme systemTheme) - { - return systemTheme switch - { - SystemTheme.Dark or SystemTheme.CapturedMotion or SystemTheme.Glow => ApplicationTheme.Dark, - SystemTheme.HC1 or SystemTheme.HC2 or SystemTheme.HCBlack or SystemTheme.HCWhite => ApplicationTheme.HighContrast, - _ => ApplicationTheme.Light - }; - } + System.Diagnostics.Debug.WriteLine( + $"INFO | {observedWindow.Handle} ({observedWindow.RootVisual?.Title}) triggered the application theme change to {ApplicationThemeManager.GetSystemTheme()}.", + nameof(SystemThemeWatcher) + ); - /// - /// Stores the current theme and accent color for restoration after session unlock. - /// - private static void StoreCurrentThemeAndAccent() - { - _lastAppliedTheme = ApplicationThemeManager.GetAppTheme(); - _lastAppliedAccentColor = ApplicationAccentColorManager.SystemAccent; - } - - /// - /// Handles session switch events (lock/unlock) to restore theme and accent color. - /// - private static void OnSessionSwitch(object sender, SessionSwitchEventArgs e) - { - if (e.Reason == SessionSwitchReason.SessionUnlock) - { - _lastUnlockTime = DateTime.Now; - - // Restore theme if we have a stored value - if (_lastAppliedTheme.HasValue && _lastAppliedTheme.Value != ApplicationTheme.Unknown) - { - _isRestoringTheme = true; - - // Use the backdrop from the first observed window, or default to Mica - WindowBackdropType backdrop = _observedWindows.Count > 0 - ? _observedWindows[0].Backdrop - : WindowBackdropType.Mica; - - ApplicationThemeManager.Apply( - _lastAppliedTheme.Value, - backdrop, - false // Don't update accent yet, we'll do it separately - ); - - // Restore accent color if we have a stored value - if (_lastAppliedAccentColor.HasValue && _lastAppliedAccentColor.Value != Colors.Transparent) - { - ApplicationAccentColorManager.Apply( - _lastAppliedAccentColor.Value, - _lastAppliedTheme.Value, - false - ); - } - else - { - // Fallback to system accent color - ApplicationAccentColorManager.ApplySystemAccent(); - } - - // Update all observed windows - foreach (ObservedWindow observedWindow in _observedWindows) - { - if (observedWindow.RootVisual is { } window) - { - WindowBackgroundManager.UpdateBackground(window, _lastAppliedTheme.Value, observedWindow.Backdrop); - } - } - - // Store the restored theme to prevent it from being overwritten - StoreCurrentThemeAndAccent(); - - // Give the UI time to update before allowing system theme changes - _ = System.Threading.Tasks.Task.Run(async () => - { - await System.Threading.Tasks.Task.Delay(100); - _isRestoringTheme = false; - }); - } - else - { - // If we don't have a stored theme, apply system theme - ApplicationThemeManager.ApplySystemTheme(true); - StoreCurrentThemeAndAccent(); - } - } - else if (e.Reason == SessionSwitchReason.SessionLock) - { - // Store current theme and accent before lock - StoreCurrentThemeAndAccent(); - } - } - - /// - /// Handles theme change events to keep the stored theme up to date. - /// - private static void OnThemeChanged(ApplicationTheme theme, Color accentColor) - { - // Update stored theme if we're restoring or if we're past the unlock ignore period - if (_isRestoringTheme || DateTime.Now - _lastUnlockTime > _unlockIgnoreDuration) + if (observedWindow.RootVisual is not null) { - _lastAppliedTheme = theme; - _lastAppliedAccentColor = accentColor; + WindowBackgroundManager.UpdateBackground( + observedWindow.RootVisual, + currentApplicationTheme, + observedWindow.Backdrop + ); } } } From e8b80c02c8661d490d3ab710570a457f1d00e4bc Mon Sep 17 00:00:00 2001 From: Koichi Kobayashi Date: Mon, 15 Dec 2025 00:28:37 +0900 Subject: [PATCH 3/3] Changes to the Conditions for Executing UpdateObservedWindow --- src/Wpf.Ui/Appearance/SystemThemeWatcher.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Wpf.Ui/Appearance/SystemThemeWatcher.cs b/src/Wpf.Ui/Appearance/SystemThemeWatcher.cs index 841a6b4bd..3bc005284 100644 --- a/src/Wpf.Ui/Appearance/SystemThemeWatcher.cs +++ b/src/Wpf.Ui/Appearance/SystemThemeWatcher.cs @@ -151,7 +151,9 @@ public static void UnWatch(Window? window) /// private static IntPtr WndProc(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled) { - if (msg == (int)PInvoke.WM_WININICHANGE) + if (msg == (int)PInvoke.WM_DWMCOLORIZATIONCOLORCHANGED || + msg == (int)PInvoke.WM_THEMECHANGED || + msg == (int)PInvoke.WM_SYSCOLORCHANGE) { UpdateObservedWindow(hWnd); }