diff --git a/src/WinUIEx.Tests/WindowManagerTests.cs b/src/WinUIEx.Tests/WindowManagerTests.cs index 2e8b75f3..9a8f25d7 100644 --- a/src/WinUIEx.Tests/WindowManagerTests.cs +++ b/src/WinUIEx.Tests/WindowManagerTests.cs @@ -1,4 +1,5 @@ using Microsoft.UI.Windowing; +using System.Collections.Generic; namespace WinUIUnitTests { @@ -234,6 +235,94 @@ public async Task PersistenceId() => await UITestHelper.RunWindowTest(async (win newWindow.Close(); } }); + + [TestMethod] + public async Task PersistenceIgnoresFullScreenBounds() => await UITestHelper.RunWindowTest(async (window) => + { + var previousStorage = WindowManager.PersistenceStorage; + WindowManager.PersistenceStorage = new Dictionary(); + try + { + window.Content = new Grid(); + var manager = WindowManager.Get(window); + manager.PersistenceId = nameof(PersistenceIgnoresFullScreenBounds); + await window.Content.LoadAsync(); + window.AppWindow.MoveAndResize(new Windows.Graphics.RectInt32(45, 67, 456, 345)); + await Task.Delay(150); + window.AppWindow.SetPresenter(AppWindowPresenterKind.FullScreen); + await Task.Delay(150); + Assert.AreEqual(AppWindowPresenterKind.FullScreen, window.AppWindow.Presenter.Kind); + window.Close(); + + Window newWindow = new Window(); + var manager2 = WindowManager.Get(newWindow); + manager2.PersistenceId = manager.PersistenceId; + newWindow.Content = new Grid(); + try + { + newWindow.Activate(); + await newWindow.Content.LoadAsync(); + Assert.AreEqual(AppWindowPresenterKind.Overlapped, newWindow.AppWindow.Presenter.Kind); + Assert.AreEqual(45, newWindow.AppWindow.Position.X); + Assert.AreEqual(67, newWindow.AppWindow.Position.Y); + Assert.AreEqual(456, newWindow.AppWindow.Size.Width); + Assert.AreEqual(345, newWindow.AppWindow.Size.Height); + } + finally + { + newWindow.Close(); + } + } + finally + { + WindowManager.PersistenceStorage = previousStorage; + } + }); + + [TestMethod] + public async Task PersistenceRetainsMaximizedStateBeforeFullScreen() => await UITestHelper.RunWindowTest(async (window) => + { + var previousStorage = WindowManager.PersistenceStorage; + WindowManager.PersistenceStorage = new Dictionary(); + try + { + window.Content = new Grid(); + var manager = WindowManager.Get(window); + manager.PersistenceId = nameof(PersistenceRetainsMaximizedStateBeforeFullScreen); + await window.Content.LoadAsync(); + window.AppWindow.MoveAndResize(new Windows.Graphics.RectInt32(45, 67, 456, 345)); + ((OverlappedPresenter)window.AppWindow.Presenter).Maximize(); + await Task.Delay(150); + window.AppWindow.SetPresenter(AppWindowPresenterKind.FullScreen); + await Task.Delay(150); + window.Close(); + + Window newWindow = new Window(); + var manager2 = WindowManager.Get(newWindow); + manager2.PersistenceId = manager.PersistenceId; + newWindow.Content = new Grid(); + try + { + newWindow.Activate(); + await newWindow.Content.LoadAsync(); + var presenter = (OverlappedPresenter)newWindow.AppWindow.Presenter; + Assert.AreEqual(OverlappedPresenterState.Maximized, presenter.State); + presenter.Restore(); + Assert.AreEqual(45, newWindow.AppWindow.Position.X); + Assert.AreEqual(67, newWindow.AppWindow.Position.Y); + Assert.AreEqual(456, newWindow.AppWindow.Size.Width); + Assert.AreEqual(345, newWindow.AppWindow.Size.Height); + } + finally + { + newWindow.Close(); + } + } + finally + { + WindowManager.PersistenceStorage = previousStorage; + } + }); } } diff --git a/src/WinUIEx/WinUIEx.csproj b/src/WinUIEx/WinUIEx.csproj index d1263ff3..da21796f 100644 --- a/src/WinUIEx/WinUIEx.csproj +++ b/src/WinUIEx/WinUIEx.csproj @@ -25,6 +25,7 @@ WinUIEx WinUI Extensions + - Preserve the pre-fullscreen window size, position, and maximized state when using `WindowManager` persistence (Issue #253). - Add SVG file support to `TrayIcon.SetIcon(string)`, including relative path resolution and smart `.ico`/`.svg` detection. diff --git a/src/WinUIEx/WindowManager.cs b/src/WinUIEx/WindowManager.cs index ca31e1d4..5826d5b4 100644 --- a/src/WinUIEx/WindowManager.cs +++ b/src/WinUIEx/WindowManager.cs @@ -22,6 +22,7 @@ public partial class WindowManager : IDisposable private readonly WindowMessageMonitor _monitor; private readonly Window _window; private OverlappedPresenter overlappedPresenter; + private readonly Microsoft.UI.Dispatching.DispatcherQueueTimer _persistenceTimer; private readonly static Dictionary> managers = new Dictionary>(); private bool _isInitialized; // Set to true on first activation. Used to track persistence restore @@ -75,6 +76,11 @@ private WindowManager(Window window, WindowMessageMonitor monitor) AppWindow.Changed += AppWindow_Changed; AppWindow.Destroying += AppWindow_Destroying; + _persistenceTimer = _window.DispatcherQueue.CreateTimer(); + // Presenter changes are reported after their resize, so wait until the resize burst settles. + _persistenceTimer.Interval = TimeSpan.FromMilliseconds(100); + _persistenceTimer.IsRepeating = false; + _persistenceTimer.Tick += PersistenceTimer_Tick; overlappedPresenter = AppWindow.Presenter as OverlappedPresenter ?? Microsoft.UI.Windowing.OverlappedPresenter.Create(); managers[window.GetWindowHandle()] = new WeakReference(this); switch (overlappedPresenter.State) @@ -103,6 +109,7 @@ private void Window_Activated(object sender, WindowActivatedEventArgs args) private void Window_Closed(object sender, WindowEventArgs args) { CleanUpBackdrop(); + _persistenceTimer.Stop(); SavePersistence(); _trayIcon?.Dispose(); _trayIcon = null; @@ -134,6 +141,8 @@ private void Dispose(bool disposing) _window.Activated -= Window_Activated; _window.Closed -= Window_Closed; _window.VisibilityChanged -= Window_VisibilityChanged; + _persistenceTimer.Stop(); + _persistenceTimer.Tick -= PersistenceTimer_Tick; _monitor.WindowMessageReceived -= OnWindowMessage; _monitor.Dispose(); } @@ -267,6 +276,8 @@ private unsafe void OnWindowMessage(object? sender, Messaging.WindowMessageEvent { _isInitialized = true; LoadPersistence(); + if (AppWindow.Presenter.Kind == AppWindowPresenterKind.Overlapped) + CacheOverlappedWindowPlacement(); _window.DispatcherQueue.TryEnqueue(Microsoft.UI.Dispatching.DispatcherQueuePriority.High, () => { InitBackdrop(); @@ -404,6 +415,20 @@ public WindowState WindowState public string? PersistenceId { get; set; } private bool _restoringPersistence; // Flag used to avoid WinUI DPI adjustment + private WINDOWPLACEMENT? _overlappedWindowPlacement; + + private void CacheOverlappedWindowPlacement() + { + var placement = new WINDOWPLACEMENT(); + Windows.Win32.PInvoke.GetWindowPlacement(new Windows.Win32.Foundation.HWND(_window.GetWindowHandle()), ref placement); + _overlappedWindowPlacement = placement; + } + + private void PersistenceTimer_Tick(Microsoft.UI.Dispatching.DispatcherQueueTimer sender, object args) + { + if (AppWindow.Presenter.Kind == AppWindowPresenterKind.Overlapped) + CacheOverlappedWindowPlacement(); + } /// /// Gets or sets the persistence storage for maintaining window settings across application settings. @@ -508,8 +533,16 @@ private void SavePersistence() sw.Write(monitor.RectMonitor.Right); sw.Write(monitor.RectMonitor.Bottom); } - var placement = new WINDOWPLACEMENT(); - Windows.Win32.PInvoke.GetWindowPlacement(new Windows.Win32.Foundation.HWND(_window.GetWindowHandle()), ref placement); + WINDOWPLACEMENT placement; + if (AppWindow.Presenter.Kind != AppWindowPresenterKind.Overlapped && _overlappedWindowPlacement.HasValue) + { + placement = _overlappedWindowPlacement.Value; + } + else + { + placement = new WINDOWPLACEMENT(); + Windows.Win32.PInvoke.GetWindowPlacement(new Windows.Win32.Foundation.HWND(_window.GetWindowHandle()), ref placement); + } int structSize = Marshal.SizeOf(typeof(WINDOWPLACEMENT)); IntPtr buffer = Marshal.AllocHGlobal(structSize); @@ -527,6 +560,11 @@ private void SavePersistence() private void AppWindow_Changed(Microsoft.UI.Windowing.AppWindow sender, Microsoft.UI.Windowing.AppWindowChangedEventArgs args) { + if ((args.DidPositionChange || args.DidSizeChange) && !string.IsNullOrEmpty(PersistenceId)) + { + _persistenceTimer.Stop(); + _persistenceTimer.Start(); + } if (args.DidPositionChange) PositionChanged?.Invoke(this, sender.Position); if (args.DidPresenterChange) @@ -536,6 +574,8 @@ private void AppWindow_Changed(Microsoft.UI.Windowing.AppWindow sender, Microsof overlappedPresenter = op; _IsTitleBarVisible = op.HasTitleBar; } + if (sender.Presenter.Kind == AppWindowPresenterKind.Overlapped) + CacheOverlappedWindowPlacement(); PresenterChanged?.Invoke(this, sender.Presenter); } if(args.DidZOrderChange)