refactor window controller

This commit is contained in:
DismissedLight
2023-09-21 22:45:40 +08:00
parent 2821f7f2af
commit b72b5ddf91
11 changed files with 178 additions and 216 deletions

View File

@@ -0,0 +1,45 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using System.Runtime.CompilerServices;
using Windows.Graphics;
namespace Snap.Hutao.Core.Windowing;
internal readonly struct CompactRect
{
private readonly short x;
private readonly short y;
private readonly short width;
private readonly short height;
private CompactRect(int x, int y, int width, int height)
{
this.x = (short)x;
this.y = (short)y;
this.width = (short)width;
this.height = (short)height;
}
public static implicit operator RectInt32(CompactRect rect)
{
return new(rect.x, rect.y, rect.width, rect.height);
}
public static explicit operator CompactRect(RectInt32 rect)
{
return new(rect.X, rect.Y, rect.Width, rect.Height);
}
public static unsafe explicit operator CompactRect(ulong value)
{
Unsafe.SkipInit(out CompactRect rect);
*(ulong*)&rect = value;
return rect;
}
public static unsafe implicit operator ulong(CompactRect rect)
{
return *(ulong*)▭
}
}

View File

@@ -0,0 +1,27 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Windows.Win32.Foundation;
using static Windows.Win32.PInvoke;
namespace Snap.Hutao.Core.Windowing;
internal static class HotKey
{
private const int DefaultId = 100000;
public static bool Register(in HWND hwnd)
{
return RegisterHotKey(hwnd, DefaultId, default, (uint)Windows.Win32.UI.Input.KeyboardAndMouse.VIRTUAL_KEY.VK_F8);
}
public static bool Unregister(in HWND hwnd)
{
return UnregisterHotKey(hwnd, DefaultId);
}
public static bool OnHotKeyPressed()
{
return true;
}
}

View File

@@ -1,113 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml;
using Snap.Hutao.Core.Setting;
using Snap.Hutao.Win32;
using System.Runtime.CompilerServices;
using Windows.Graphics;
using Windows.Win32.UI.WindowsAndMessaging;
using static Windows.Win32.PInvoke;
namespace Snap.Hutao.Core.Windowing;
/// <summary>
/// 窗体持久化
/// </summary>
[HighQuality]
internal static class Persistence
{
/// <summary>
/// 设置窗体位置
/// </summary>
/// <typeparam name="TWindow">窗体类型</typeparam>
/// <param name="window">选项窗口</param>
public static void RecoverOrInit<TWindow>(TWindow window)
where TWindow : Window, IWindowOptionsSource
{
WindowOptions options = window.WindowOptions;
// Set first launch size
double scale = options.GetWindowScale();
SizeInt32 transformedSize = options.InitSize.Scale(scale);
RectInt32 rect = StructMarshal.RectInt32(transformedSize);
if (options.PersistSize)
{
RectInt32 persistedRect = (CompactRect)LocalSetting.Get(SettingKeys.WindowRect, (CompactRect)rect);
if (persistedRect.Size() >= options.InitSize.Size())
{
rect = persistedRect;
}
}
TransformToCenterScreen(ref rect);
window.AppWindow.MoveAndResize(rect);
}
/// <summary>
/// 保存窗体的位置
/// </summary>
/// <param name="window">窗口</param>
/// <typeparam name="TWindow">窗体类型</typeparam>
public static void Save<TWindow>(TWindow window)
where TWindow : Window, IWindowOptionsSource
{
WINDOWPLACEMENT windowPlacement = StructMarshal.WINDOWPLACEMENT();
GetWindowPlacement(window.WindowOptions.Hwnd, ref windowPlacement);
// prevent save value when we are maximized.
if (!windowPlacement.showCmd.HasFlag(SHOW_WINDOW_CMD.SW_SHOWMAXIMIZED))
{
LocalSetting.Set(SettingKeys.WindowRect, (CompactRect)window.AppWindow.GetRect());
}
}
private static void TransformToCenterScreen(ref RectInt32 rect)
{
DisplayArea displayArea = DisplayArea.GetFromRect(rect, DisplayAreaFallback.Primary);
RectInt32 workAreaRect = displayArea.WorkArea;
rect.X = workAreaRect.X + ((workAreaRect.Width - rect.Width) / 2);
rect.Y = workAreaRect.Y + ((workAreaRect.Height - rect.Height) / 2);
}
private readonly struct CompactRect
{
private readonly short x;
private readonly short y;
private readonly short width;
private readonly short height;
private CompactRect(int x, int y, int width, int height)
{
this.x = (short)x;
this.y = (short)y;
this.width = (short)width;
this.height = (short)height;
}
public static implicit operator RectInt32(CompactRect rect)
{
return new(rect.x, rect.y, rect.width, rect.height);
}
public static explicit operator CompactRect(RectInt32 rect)
{
return new(rect.X, rect.Y, rect.Width, rect.Height);
}
public static unsafe explicit operator CompactRect(ulong value)
{
Unsafe.SkipInit(out CompactRect rect);
*(ulong*)&rect = value;
return rect;
}
public static unsafe implicit operator ulong(CompactRect rect)
{
return *(ulong*)&rect;
}
}
}

View File

@@ -1,13 +1,12 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.UI;
using Microsoft.UI.Composition.SystemBackdrops;
using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Media;
using Snap.Hutao.Message;
using Snap.Hutao.Core.Setting;
using Snap.Hutao.Service;
using Snap.Hutao.Win32;
using System.IO;
@@ -15,59 +14,48 @@ using Windows.Graphics;
using Windows.UI;
using Windows.Win32.Foundation;
using Windows.Win32.Graphics.Dwm;
using Windows.Win32.UI.WindowsAndMessaging;
using static Windows.Win32.PInvoke;
namespace Snap.Hutao.Core.Windowing;
/// <summary>
/// 扩展窗口
/// </summary>
/// <typeparam name="TWindow">窗体类型</typeparam>
[SuppressMessage("", "CA1001")]
internal sealed class ExtendedWindow<TWindow> : IRecipient<FlyoutStateChangedMessage>
where TWindow : Window, IWindowOptionsSource
internal sealed class WindowController
{
private readonly TWindow window;
private readonly Window window;
private readonly WindowOptions options;
private readonly IServiceProvider serviceProvider;
private readonly WindowSubclass<TWindow> subclass;
private readonly WindowSubclass subclass;
private ExtendedWindow(TWindow window, IServiceProvider serviceProvider)
public WindowController(Window window, in WindowOptions options, IServiceProvider serviceProvider)
{
this.window = window;
this.options = options;
this.serviceProvider = serviceProvider;
subclass = new(window);
subclass = new(window, options);
InitializeWindow();
InitializeCore();
}
/// <summary>
/// 初始化
/// </summary>
/// <param name="window">窗口</param>
/// <param name="serviceProvider">服务提供器</param>
/// <returns>实例</returns>
public static ExtendedWindow<TWindow> Initialize(TWindow window, IServiceProvider serviceProvider)
private static void TransformToCenterScreen(ref RectInt32 rect)
{
return new(window, serviceProvider);
DisplayArea displayArea = DisplayArea.GetFromRect(rect, DisplayAreaFallback.Primary);
RectInt32 workAreaRect = displayArea.WorkArea;
rect.X = workAreaRect.X + ((workAreaRect.Width - rect.Width) / 2);
rect.Y = workAreaRect.Y + ((workAreaRect.Height - rect.Height) / 2);
}
/// <inheritdoc/>
public void Receive(FlyoutStateChangedMessage message)
{
UpdateDragRectangles(message.IsOpen);
}
private void InitializeWindow()
private void InitializeCore()
{
RuntimeOptions hutaoOptions = serviceProvider.GetRequiredService<RuntimeOptions>();
WindowOptions options = window.WindowOptions;
window.AppWindow.Title = SH.AppNameAndVersion.Format(hutaoOptions.Version);
window.AppWindow.SetIcon(Path.Combine(hutaoOptions.InstalledLocation, "Assets/Logo.ico"));
ExtendsContentIntoTitleBar();
Persistence.RecoverOrInit(window);
RecoverOrInitWindowSize();
UpdateImmersiveDarkMode(options.TitleBar, default!);
// appWindow.Show(true);
@@ -81,12 +69,47 @@ internal sealed class ExtendedWindow<TWindow> : IRecipient<FlyoutStateChangedMes
subclass.Initialize();
serviceProvider.GetRequiredService<IMessenger>().Register(this);
window.Closed += OnWindowClosed;
options.TitleBar.ActualThemeChanged += UpdateImmersiveDarkMode;
}
private void RecoverOrInitWindowSize()
{
// Set first launch size
double scale = options.GetWindowScale();
SizeInt32 transformedSize = options.InitSize.Scale(scale);
RectInt32 rect = StructMarshal.RectInt32(transformedSize);
if (options.PersistSize)
{
RectInt32 persistedRect = (CompactRect)LocalSetting.Get(SettingKeys.WindowRect, (CompactRect)rect);
if (persistedRect.Size() >= options.InitSize.Size())
{
rect = persistedRect;
}
}
TransformToCenterScreen(ref rect);
window.AppWindow.MoveAndResize(rect);
}
private void SaveOrSkipWindowSize()
{
if (!options.PersistSize)
{
return;
}
WINDOWPLACEMENT windowPlacement = StructMarshal.WINDOWPLACEMENT();
GetWindowPlacement(options.Hwnd, ref windowPlacement);
// prevent save value when we are maximized.
if (!windowPlacement.showCmd.HasFlag(SHOW_WINDOW_CMD.SW_SHOWMAXIMIZED))
{
LocalSetting.Set(SettingKeys.WindowRect, (CompactRect)window.AppWindow.GetRect());
}
}
private void OnOptionsPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(AppOptions.BackdropType))
@@ -98,17 +121,12 @@ internal sealed class ExtendedWindow<TWindow> : IRecipient<FlyoutStateChangedMes
private void OnWindowClosed(object sender, WindowEventArgs args)
{
if (window.WindowOptions.PersistSize)
{
Persistence.Save(window);
}
SaveOrSkipWindowSize();
subclass?.Dispose();
}
private void ExtendsContentIntoTitleBar()
{
WindowOptions options = window.WindowOptions;
if (options.UseLegacyDragBarImplementation)
{
// use normal Window method to extend.
@@ -168,31 +186,22 @@ internal sealed class ExtendedWindow<TWindow> : IRecipient<FlyoutStateChangedMes
private unsafe void UpdateImmersiveDarkMode(FrameworkElement titleBar, object discard)
{
BOOL isDarkMode = Control.Theme.ThemeHelper.IsDarkMode(titleBar.ActualTheme);
DwmSetWindowAttribute(window.WindowOptions.Hwnd, DWMWINDOWATTRIBUTE.DWMWA_USE_IMMERSIVE_DARK_MODE, &isDarkMode, unchecked((uint)sizeof(BOOL)));
DwmSetWindowAttribute(options.Hwnd, DWMWINDOWATTRIBUTE.DWMWA_USE_IMMERSIVE_DARK_MODE, &isDarkMode, unchecked((uint)sizeof(BOOL)));
}
private void UpdateDragRectangles(bool isFlyoutOpened = false)
private void UpdateDragRectangles()
{
AppWindowTitleBar appTitleBar = window.AppWindow.TitleBar;
if (isFlyoutOpened)
{
// set to 0
appTitleBar.SetDragRectangles(default(RectInt32).ToArray());
}
else
{
WindowOptions options = window.WindowOptions;
double scale = options.GetWindowScale();
double scale = options.GetWindowScale();
// 48 is the navigation button leftInset
RectInt32 dragRect = StructMarshal.RectInt32(48, 0, options.TitleBar.ActualSize).Scale(scale);
appTitleBar.SetDragRectangles(dragRect.ToArray());
// 48 is the navigation button leftInset
RectInt32 dragRect = StructMarshal.RectInt32(48, 0, options.TitleBar.ActualSize).Scale(scale);
appTitleBar.SetDragRectangles(dragRect.ToArray());
// workaround for https://github.com/microsoft/WindowsAppSDK/issues/2976
SizeInt32 size = window.AppWindow.ClientSize;
size.Height -= (int)(31 * scale);
window.AppWindow.ResizeClient(size);
}
// workaround for https://github.com/microsoft/WindowsAppSDK/issues/2976
SizeInt32 size = window.AppWindow.ClientSize;
size.Height -= (int)(31 * scale);
window.AppWindow.ResizeClient(size);
}
}

View File

@@ -0,0 +1,19 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
using Microsoft.UI.Xaml;
using System.Runtime.CompilerServices;
namespace Snap.Hutao.Core.Windowing;
internal static class WindowExtension
{
private static readonly ConditionalWeakTable<Window, WindowController> WindowControllers = new();
public static void InitializeController<TWindow>(this TWindow window, IServiceProvider serviceProvider)
where TWindow : Window, IWindowOptionsSource
{
WindowController windowController = new(window, window.WindowOptions, serviceProvider);
WindowControllers.Add(window, windowController);
}
}

View File

@@ -12,27 +12,23 @@ namespace Snap.Hutao.Core.Windowing;
/// <summary>
/// 窗体子类管理器
/// </summary>
/// <typeparam name="TWindow">窗体类型</typeparam>
[HighQuality]
internal sealed class WindowSubclass<TWindow> : IDisposable
where TWindow : Window, IWindowOptionsSource
internal sealed class WindowSubclass : IDisposable
{
private const int WindowSubclassId = 101;
private const int DragBarSubclassId = 102;
private readonly TWindow window;
private readonly Window window;
private readonly WindowOptions options;
// We have to explicitly hold a reference to SUBCLASSPROC
private SUBCLASSPROC? windowProc;
private SUBCLASSPROC? legacyDragBarProc;
/// <summary>
/// 构造一个新的窗体子类管理器
/// </summary>
/// <param name="window">窗口</param>
public WindowSubclass(TWindow window)
public WindowSubclass(Window window, in WindowOptions options)
{
this.window = window;
this.options = options;
}
/// <summary>
@@ -41,10 +37,9 @@ internal sealed class WindowSubclass<TWindow> : IDisposable
/// <returns>是否设置成功</returns>
public bool Initialize()
{
WindowOptions options = window.WindowOptions;
windowProc = OnSubclassProcedure;
bool windowHooked = SetWindowSubclass(options.Hwnd, windowProc, WindowSubclassId, 0);
HotKey.Register(options.Hwnd);
bool titleBarHooked = true;
@@ -71,8 +66,6 @@ internal sealed class WindowSubclass<TWindow> : IDisposable
/// <inheritdoc/>
public void Dispose()
{
WindowOptions options = window.WindowOptions;
RemoveWindowSubclass(options.Hwnd, windowProc, WindowSubclassId);
windowProc = null;
@@ -81,6 +74,7 @@ internal sealed class WindowSubclass<TWindow> : IDisposable
return;
}
HotKey.Unregister(options.Hwnd);
RemoveWindowSubclass(options.Hwnd, legacyDragBarProc, DragBarSubclassId);
legacyDragBarProc = null;
}
@@ -94,7 +88,7 @@ internal sealed class WindowSubclass<TWindow> : IDisposable
{
uint dpi = GetDpiForWindow(hwnd);
double scalingFactor = Math.Round(dpi / 96D, 2, MidpointRounding.AwayFromZero);
window.ProcessMinMaxInfo((MINMAXINFO*)lParam.Value, scalingFactor);
((IWindowOptionsSource)window).ProcessMinMaxInfo((MINMAXINFO*)lParam.Value, scalingFactor);
break;
}
@@ -103,6 +97,12 @@ internal sealed class WindowSubclass<TWindow> : IDisposable
{
return (LRESULT)(nint)WM_NULL;
}
case WM_HOTKEY:
{
System.Diagnostics.Debug.WriteLine($"Hot key pressed, wParam: {wParam.Value}, lParam: {lParam.Value}");
break;
}
}
return DefSubclassProc(hwnd, uMsg, wParam, lParam);

View File

@@ -25,7 +25,7 @@ internal sealed partial class GuideWindow : Window, IWindowOptionsSource
{
InitializeComponent();
windowOptions = new(this, DragableGrid, new(MinWidth, MinHeight));
ExtendedWindow<GuideWindow>.Initialize(this, Ioc.Default);
this.InitializeController(serviceProvider);
}
WindowOptions IWindowOptionsSource.WindowOptions { get => windowOptions; }

View File

@@ -34,7 +34,7 @@ internal sealed partial class LaunchGameWindow : Window, IDisposable, IWindowOpt
scope = serviceProvider.CreateScope();
windowOptions = new(this, DragableGrid, new(MaxWidth, MaxHeight));
ExtendedWindow<LaunchGameWindow>.Initialize(this, scope.ServiceProvider);
this.InitializeController(serviceProvider);
RootGrid.DataContext = scope.ServiceProvider.GetRequiredService<LaunchGameViewModel>();
}

View File

@@ -31,7 +31,7 @@ internal sealed partial class MainWindow : Window, IWindowOptionsSource
{
InitializeComponent();
windowOptions = new(this, TitleBarView.DragArea, new(1200, 741), true);
ExtendedWindow<MainWindow>.Initialize(this, serviceProvider);
this.InitializeController(serviceProvider);
logger = serviceProvider.GetRequiredService<ILogger<MainWindow>>();
closedEventHander = OnClosed;

View File

@@ -1,29 +0,0 @@
// Copyright (c) DGP Studio. All rights reserved.
// Licensed under the MIT license.
namespace Snap.Hutao.Message;
/// <summary>
/// Flyout开启关闭消息
/// </summary>
[HighQuality]
internal sealed class FlyoutStateChangedMessage
{
/// <summary>
/// 构造一个新的Flyout开启关闭消息
/// </summary>
/// <param name="isOpen">是否为开启状态</param>
public FlyoutStateChangedMessage(bool isOpen)
{
IsOpen = isOpen;
}
public static FlyoutStateChangedMessage Open { get; } = new(true);
public static FlyoutStateChangedMessage Close { get; } = new(false);
/// <summary>
/// 是否为开启状态
/// </summary>
public bool IsOpen { get; }
}

View File

@@ -39,7 +39,10 @@ GetForegroundWindow
GetWindowPlacement
GetWindowThreadProcessId
ReleaseDC
RegisterHotKey
SendInput
SetForegroundWindow
UnregisterHotKey
// COM
IPersistFile
@@ -53,6 +56,7 @@ IMemoryBufferByteAccess
// Const value
INFINITE
WM_GETMINMAXINFO
WM_HOTKEY
WM_NCRBUTTONDOWN
WM_NCRBUTTONUP
WM_NULL