diff --git a/LanMountainDesktop.Platform.Abstractions/IMainWindowDesktopLayerService.cs b/LanMountainDesktop.Platform.Abstractions/IMainWindowDesktopLayerService.cs new file mode 100644 index 0000000..056743b --- /dev/null +++ b/LanMountainDesktop.Platform.Abstractions/IMainWindowDesktopLayerService.cs @@ -0,0 +1,30 @@ +using Avalonia.Controls; + +namespace LanMountainDesktop.Platform.Abstractions; + +/// +/// 主窗口桌面层服务接口。将主窗口嵌入系统桌面图标层(仅 Windows 支持)。 +/// +public interface IMainWindowDesktopLayerService +{ + bool IsSupported { get; } + void EnableOrRefresh(Window window); + void Disable(Window window); +} + +/// +/// 无操作实现。用于不支持桌面层嵌入的平台(Linux/macOS/移动端)。 +/// +public sealed class NullMainWindowDesktopLayerService : IMainWindowDesktopLayerService +{ + public bool IsSupported => false; + + public void EnableOrRefresh(Window window) + { + PlatformLog.Info("MainWindowDesktopLayer", "Desktop layer requested on an unsupported platform."); + } + + public void Disable(Window window) + { + } +} diff --git a/LanMountainDesktop.Platform.Abstractions/IPowerManagementService.cs b/LanMountainDesktop.Platform.Abstractions/IPowerManagementService.cs new file mode 100644 index 0000000..dc93c50 --- /dev/null +++ b/LanMountainDesktop.Platform.Abstractions/IPowerManagementService.cs @@ -0,0 +1,48 @@ +namespace LanMountainDesktop.Platform.Abstractions; + +/// +/// 电源管理服务接口。桌面平台提供关机/重启/注销/锁定/睡眠能力; +/// 移动平台不提供此能力(使用 )。 +/// +public interface IPowerManagementService +{ + bool IsShutdownSupported { get; } + bool IsRestartSupported { get; } + bool IsLogoutSupported { get; } + bool IsLockSupported { get; } + bool IsSleepSupported { get; } + + Task ShutdownAsync(); + Task RestartAsync(); + Task LogoutAsync(); + Task LockAsync(); + Task SleepAsync(); + + void ShowNativePowerUI(PowerAction action); +} + +public enum PowerAction +{ + Shutdown, + Restart +} + +/// +/// 无操作实现。用于不支持电源管理的平台(如移动端)。 +/// +public sealed class NullPowerManagementService : IPowerManagementService +{ + public bool IsShutdownSupported => false; + public bool IsRestartSupported => false; + public bool IsLogoutSupported => false; + public bool IsLockSupported => false; + public bool IsSleepSupported => false; + + public Task ShutdownAsync() => Task.CompletedTask; + public Task RestartAsync() => Task.CompletedTask; + public Task LogoutAsync() => Task.CompletedTask; + public Task LockAsync() => Task.CompletedTask; + public Task SleepAsync() => Task.CompletedTask; + + public void ShowNativePowerUI(PowerAction action) { } +} diff --git a/LanMountainDesktop.Platform.Abstractions/IWindowPassthroughServices.cs b/LanMountainDesktop.Platform.Abstractions/IWindowPassthroughServices.cs new file mode 100644 index 0000000..f81f92d --- /dev/null +++ b/LanMountainDesktop.Platform.Abstractions/IWindowPassthroughServices.cs @@ -0,0 +1,68 @@ +using Avalonia; +using Avalonia.Controls; + +namespace LanMountainDesktop.Platform.Abstractions; + +/// +/// 窗口置底服务接口。使组件窗口保持在桌面层(Z 序最底)。 +/// +public interface IWindowBottomMostService +{ + void SetupBottomMost(Window window); + void SendToBottom(Window window); + PixelPoint GetScreenPosition(Window window); + bool SetScreenPosition(Window window, PixelPoint position, bool queueOnFailure = false); + bool IsBottomMostSupported { get; } +} + +/// +/// 窗口交互区域定义。区域外的点击穿透到桌面。 +/// +public readonly record struct WindowInteractiveRegion( + Rect Bounds, + double CornerRadius, + Matrix? ClientToRegionTransform = null, + Rect? ClientClipBounds = null, + double ClientClipCornerRadius = 0d); + +/// +/// 区域点击穿透服务接口。 +/// +public interface IRegionPassthroughService +{ + void SetInteractiveRegions(Window window, IReadOnlyList interactiveRegions); + void ClearInteractiveRegions(Window window); + bool IsRegionPassthroughSupported { get; } +} + +/// +/// 无操作实现:非 Windows 平台窗口置底不可用。 +/// +public sealed class NullWindowBottomMostService : IWindowBottomMostService +{ + public bool IsBottomMostSupported => false; + + public void SetupBottomMost(Window window) { } + + public void SendToBottom(Window window) { } + + public PixelPoint GetScreenPosition(Window window) => window.Position; + + public bool SetScreenPosition(Window window, PixelPoint position, bool queueOnFailure = false) + { + window.Position = position; + return true; + } +} + +/// +/// 无操作实现:非 Windows 平台区域穿透不可用。 +/// +public sealed class NullRegionPassthroughService : IRegionPassthroughService +{ + public bool IsRegionPassthroughSupported => false; + + public void SetInteractiveRegions(Window window, IReadOnlyList interactiveRegions) { } + + public void ClearInteractiveRegions(Window window) { } +} diff --git a/LanMountainDesktop.Platform.Abstractions/LanMountainDesktop.Platform.Abstractions.csproj b/LanMountainDesktop.Platform.Abstractions/LanMountainDesktop.Platform.Abstractions.csproj new file mode 100644 index 0000000..0a8d1e2 --- /dev/null +++ b/LanMountainDesktop.Platform.Abstractions/LanMountainDesktop.Platform.Abstractions.csproj @@ -0,0 +1,11 @@ + + + net10.0 + enable + enable + + + + + + diff --git a/LanMountainDesktop.Platform.Abstractions/PlatformLog.cs b/LanMountainDesktop.Platform.Abstractions/PlatformLog.cs new file mode 100644 index 0000000..3c612e8 --- /dev/null +++ b/LanMountainDesktop.Platform.Abstractions/PlatformLog.cs @@ -0,0 +1,35 @@ +namespace LanMountainDesktop.Platform.Abstractions; + +/// +/// 平台层日志桥。平台实现项目不引用宿主, +/// 宿主在启动时通过 接入自己的日志系统(AppLogger)。 +/// 未接入时日志静默丢弃。 +/// +public static class PlatformLog +{ + private static IPlatformLogSink? _sink; + + public static void SetSink(IPlatformLogSink sink) + { + ArgumentNullException.ThrowIfNull(sink); + _sink = sink; + } + + public static void Info(string category, string message) => _sink?.Info(category, message); + + public static void Warn(string category, string message, Exception? exception = null) => + _sink?.Warn(category, message, exception); + + public static void Error(string category, string message, Exception? exception = null) => + _sink?.Error(category, message, exception); +} + +/// +/// 平台层日志输出目标。 +/// +public interface IPlatformLogSink +{ + void Info(string category, string message); + void Warn(string category, string message, Exception? exception = null); + void Error(string category, string message, Exception? exception = null); +} diff --git a/LanMountainDesktop.Platform.Android/LanMountainDesktop.Platform.Android.csproj b/LanMountainDesktop.Platform.Android/LanMountainDesktop.Platform.Android.csproj new file mode 100644 index 0000000..04040b9 --- /dev/null +++ b/LanMountainDesktop.Platform.Android/LanMountainDesktop.Platform.Android.csproj @@ -0,0 +1,17 @@ + + + net10.0-android + enable + enable + 24 + + + + + + + + + + + diff --git a/LanMountainDesktop.Platform.MacOS/LanMountainDesktop.Platform.MacOS.csproj b/LanMountainDesktop.Platform.MacOS/LanMountainDesktop.Platform.MacOS.csproj new file mode 100644 index 0000000..48063b7 --- /dev/null +++ b/LanMountainDesktop.Platform.MacOS/LanMountainDesktop.Platform.MacOS.csproj @@ -0,0 +1,15 @@ + + + net10.0 + enable + enable + + + + + + + + + + diff --git a/LanMountainDesktop/Services/MacIconService.cs b/LanMountainDesktop.Platform.MacOS/MacIconService.cs similarity index 98% rename from LanMountainDesktop/Services/MacIconService.cs rename to LanMountainDesktop.Platform.MacOS/MacIconService.cs index 991d03f..5727b60 100644 --- a/LanMountainDesktop/Services/MacIconService.cs +++ b/LanMountainDesktop.Platform.MacOS/MacIconService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Diagnostics; using System.Drawing; using System.Drawing.Imaging; @@ -6,10 +6,10 @@ using System.IO; using System.Runtime.InteropServices; using System.Runtime.Versioning; -namespace LanMountainDesktop.Services; +namespace LanMountainDesktop.Platform.MacOS; [SupportedOSPlatform("macos")] -internal static class MacIconService +public static class MacIconService { private const int IconSize = 256; diff --git a/LanMountainDesktop.Platform.Windows/LanMountainDesktop.Platform.Windows.csproj b/LanMountainDesktop.Platform.Windows/LanMountainDesktop.Platform.Windows.csproj new file mode 100644 index 0000000..522c0c8 --- /dev/null +++ b/LanMountainDesktop.Platform.Windows/LanMountainDesktop.Platform.Windows.csproj @@ -0,0 +1,17 @@ + + + net10.0 + enable + enable + + + + + + + + + + + + diff --git a/LanMountainDesktop/Services/UwpManifestIconResolver.cs b/LanMountainDesktop.Platform.Windows/UwpManifestIconResolver.cs similarity index 99% rename from LanMountainDesktop/Services/UwpManifestIconResolver.cs rename to LanMountainDesktop.Platform.Windows/UwpManifestIconResolver.cs index 7537449..ce2cb7d 100644 --- a/LanMountainDesktop/Services/UwpManifestIconResolver.cs +++ b/LanMountainDesktop.Platform.Windows/UwpManifestIconResolver.cs @@ -10,10 +10,10 @@ using System.Text; using System.Text.RegularExpressions; using System.Xml.Linq; -namespace LanMountainDesktop.Services; +namespace LanMountainDesktop.Platform.Windows; [SupportedOSPlatform("windows")] -internal static class UwpManifestIconResolver +public static class UwpManifestIconResolver { private const int ErrorSuccess = 0; private const int ErrorInsufficientBuffer = 122; diff --git a/LanMountainDesktop.Platform.Windows/WindowsDwmInterop.cs b/LanMountainDesktop.Platform.Windows/WindowsDwmInterop.cs new file mode 100644 index 0000000..399b63f --- /dev/null +++ b/LanMountainDesktop.Platform.Windows/WindowsDwmInterop.cs @@ -0,0 +1,45 @@ +using System.Runtime.InteropServices; + +namespace LanMountainDesktop.Platform.Windows; + +/// +/// DWM(桌面窗口管理器)互操作封装。 +/// +public static class WindowsDwmInterop +{ + public const int WindowAttributeBorderColor = 34; + public const uint ColorNone = 0xFFFFFFFE; + + /// + /// 移除窗口原生边框颜色(Windows 11 22000+)。 + /// 失败静默忽略(DWM 属性为尽力而为语义)。 + /// + public static void TryDisableWindowBorder(IntPtr windowHandle) + { + if (windowHandle == IntPtr.Zero || !OperatingSystem.IsWindowsVersionAtLeast(10, 0, 22000)) + { + return; + } + + try + { + var borderColor = ColorNone; + _ = DwmSetWindowAttribute( + windowHandle, + WindowAttributeBorderColor, + ref borderColor, + sizeof(uint)); + } + catch + { + // DWM attributes are best-effort and unavailable on older/unsupported Windows builds. + } + } + + [DllImport("dwmapi.dll")] + private static extern int DwmSetWindowAttribute( + IntPtr windowHandle, + int attribute, + ref uint attributeValue, + int attributeSize); +} diff --git a/LanMountainDesktop/Services/WindowsIconService.cs b/LanMountainDesktop.Platform.Windows/WindowsIconService.cs similarity index 99% rename from LanMountainDesktop/Services/WindowsIconService.cs rename to LanMountainDesktop.Platform.Windows/WindowsIconService.cs index 888faf9..7d08806 100644 --- a/LanMountainDesktop/Services/WindowsIconService.cs +++ b/LanMountainDesktop.Platform.Windows/WindowsIconService.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Drawing; using System.Drawing.Drawing2D; using System.Drawing.Imaging; @@ -9,10 +9,10 @@ using System.Runtime.Versioning; using System.Text; using System.Text.RegularExpressions; -namespace LanMountainDesktop.Services; +namespace LanMountainDesktop.Platform.Windows; [SupportedOSPlatform("windows")] -internal static class WindowsIconService +public static class WindowsIconService { private const int HighResolutionIconSize = 256; private const int MaxShellPath = 1024; diff --git a/LanMountainDesktop.Platform.Windows/WindowsMainWindowDesktopLayerService.cs b/LanMountainDesktop.Platform.Windows/WindowsMainWindowDesktopLayerService.cs new file mode 100644 index 0000000..57b3901 --- /dev/null +++ b/LanMountainDesktop.Platform.Windows/WindowsMainWindowDesktopLayerService.cs @@ -0,0 +1,238 @@ +using System.Runtime.InteropServices; +using Avalonia.Controls; +using LanMountainDesktop.Platform.Abstractions; + +namespace LanMountainDesktop.Platform.Windows; + +/// +/// Windows 桌面层实现:将窗口设为桌面图标宿主(SHELLDLL_DefView)的子窗口。 +/// 自 LanMountainDesktop.Services.MainWindowDesktopLayerService 迁移,行为不变。 +/// +public sealed class WindowsMainWindowDesktopLayerService : IMainWindowDesktopLayerService +{ + private const int GWL_STYLE = -16; + private const int GWL_EXSTYLE = -20; + + private const long WS_CHILD = 0x40000000L; + private const long WS_POPUP = 0x80000000L; + private const long WS_CAPTION = 0x00C00000L; + private const long WS_THICKFRAME = 0x00040000L; + private const long WS_MINIMIZEBOX = 0x00020000L; + private const long WS_MAXIMIZEBOX = 0x00010000L; + private const long WS_SYSMENU = 0x00080000L; + + private const uint SWP_NOSIZE = 0x0001; + private const uint SWP_NOMOVE = 0x0002; + private const uint SWP_NOACTIVATE = 0x0010; + private const uint SWP_SHOWWINDOW = 0x0040; + private const uint SWP_FRAMECHANGED = 0x0020; + + private static readonly IntPtr HWND_TOP = IntPtr.Zero; + private static readonly IntPtr HWND_BOTTOM = new(1); + + private readonly object _gate = new(); + private readonly Dictionary _restoreStates = []; + + public bool IsSupported => true; + + public void EnableOrRefresh(Window window) + { + ArgumentNullException.ThrowIfNull(window); + + var handle = GetWindowHandle(window); + if (handle == IntPtr.Zero) + { + window.Opened -= OnDeferredOpened; + window.Opened += OnDeferredOpened; + return; + } + + EnableOrRefresh(handle); + } + + public void Disable(Window window) + { + ArgumentNullException.ThrowIfNull(window); + window.Opened -= OnDeferredOpened; + + var handle = GetWindowHandle(window); + if (handle == IntPtr.Zero) + { + return; + } + + WindowRestoreState? restoreState; + lock (_gate) + { + if (!_restoreStates.Remove(handle, out restoreState)) + { + return; + } + } + + try + { + _ = SetParent(handle, restoreState.Parent); + SetWindowLongPtr(handle, GWL_STYLE, restoreState.Style); + SetWindowLongPtr(handle, GWL_EXSTYLE, restoreState.ExStyle); + _ = SetWindowPos( + handle, + HWND_TOP, + 0, + 0, + 0, + 0, + SWP_NOSIZE | SWP_NOMOVE | SWP_NOACTIVATE | SWP_FRAMECHANGED | SWP_SHOWWINDOW); + PlatformLog.Info("MainWindowDesktopLayer", $"Disabled desktop layer. Window={handle}."); + } + catch (Exception ex) + { + PlatformLog.Warn("MainWindowDesktopLayer", $"Failed to disable desktop layer. Window={handle}.", ex); + } + } + + private void OnDeferredOpened(object? sender, EventArgs e) + { + if (sender is not Window window) + { + return; + } + + window.Opened -= OnDeferredOpened; + EnableOrRefresh(window); + } + + private void EnableOrRefresh(IntPtr handle) + { + if (handle == IntPtr.Zero || !IsWindow(handle)) + { + return; + } + + SaveRestoreStateIfNeeded(handle); + var desktopHost = ResolveDesktopIconHost(); + if (desktopHost != IntPtr.Zero && IsWindow(desktopHost)) + { + ApplyDesktopChildStyle(handle); + if (GetParent(handle) != desktopHost) + { + _ = SetParent(handle, desktopHost); + } + + _ = SetWindowPos( + handle, + HWND_TOP, + 0, + 0, + 0, + 0, + SWP_NOSIZE | SWP_NOMOVE | SWP_NOACTIVATE | SWP_FRAMECHANGED | SWP_SHOWWINDOW); + PlatformLog.Info("MainWindowDesktopLayer", $"Enabled desktop layer. Window={handle}; Host={desktopHost}."); + return; + } + + _ = SetWindowPos(handle, HWND_BOTTOM, 0, 0, 0, 0, SWP_NOSIZE | SWP_NOMOVE | SWP_NOACTIVATE | SWP_SHOWWINDOW); + PlatformLog.Warn("MainWindowDesktopLayer", $"Desktop icon host not found. Falling back to HWND_BOTTOM. Window={handle}."); + } + + private void SaveRestoreStateIfNeeded(IntPtr handle) + { + lock (_gate) + { + if (_restoreStates.ContainsKey(handle)) + { + return; + } + + _restoreStates[handle] = new WindowRestoreState( + GetParent(handle), + GetWindowLongPtr(handle, GWL_STYLE), + GetWindowLongPtr(handle, GWL_EXSTYLE)); + } + } + + private static void ApplyDesktopChildStyle(IntPtr handle) + { + var style = GetWindowLongPtr(handle, GWL_STYLE).ToInt64(); + style |= WS_CHILD; + style &= ~(WS_POPUP | WS_CAPTION | WS_THICKFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX | WS_SYSMENU); + SetWindowLongPtr(handle, GWL_STYLE, new IntPtr(style)); + } + + private static IntPtr ResolveDesktopIconHost() + { + var topLevelWindows = new List(); + EnumWindows((handle, _) => + { + topLevelWindows.Add(handle); + return true; + }, IntPtr.Zero); + + foreach (var topLevelWindow in topLevelWindows) + { + var worker = FindWindowEx(topLevelWindow, IntPtr.Zero, "WorkerW", null); + if (worker == IntPtr.Zero) + { + continue; + } + + var defView = FindWindowEx(worker, IntPtr.Zero, "SHELLDLL_DefView", null); + if (defView != IntPtr.Zero) + { + return defView; + } + } + + foreach (var topLevelWindow in topLevelWindows) + { + var defView = FindWindowEx(topLevelWindow, IntPtr.Zero, "SHELLDLL_DefView", null); + if (defView != IntPtr.Zero) + { + return defView; + } + } + + return IntPtr.Zero; + } + + private static IntPtr GetWindowHandle(Window window) + { + try + { + return window.TryGetPlatformHandle()?.Handle ?? IntPtr.Zero; + } + catch + { + return IntPtr.Zero; + } + } + + private sealed record WindowRestoreState(IntPtr Parent, IntPtr Style, IntPtr ExStyle); + + private delegate bool EnumWindowsProc(IntPtr handle, IntPtr lParam); + + [DllImport("user32.dll", EntryPoint = "GetWindowLongPtr")] + private static extern IntPtr GetWindowLongPtr(IntPtr hWnd, int nIndex); + + [DllImport("user32.dll", EntryPoint = "SetWindowLongPtr")] + private static extern IntPtr SetWindowLongPtr(IntPtr hWnd, int nIndex, IntPtr dwNewLong); + + [DllImport("user32.dll")] + private static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int x, int y, int cx, int cy, uint flags); + + [DllImport("user32.dll", SetLastError = true)] + private static extern IntPtr SetParent(IntPtr hWndChild, IntPtr hWndNewParent); + + [DllImport("user32.dll")] + private static extern IntPtr GetParent(IntPtr hWnd); + + [DllImport("user32.dll")] + private static extern bool IsWindow(IntPtr hWnd); + + [DllImport("user32.dll", SetLastError = true)] + private static extern IntPtr FindWindowEx(IntPtr hParent, IntPtr hChildAfter, string? lpszClass, string? lpszWindow); + + [DllImport("user32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam); +} diff --git a/LanMountainDesktop.Platform.Windows/WindowsNativeDialogs.cs b/LanMountainDesktop.Platform.Windows/WindowsNativeDialogs.cs new file mode 100644 index 0000000..549ea51 --- /dev/null +++ b/LanMountainDesktop.Platform.Windows/WindowsNativeDialogs.cs @@ -0,0 +1,30 @@ +using System.Runtime.InteropServices; + +namespace LanMountainDesktop.Platform.Windows; + +/// +/// Windows 原生消息框(MessageBoxW)封装。 +/// 供宿主在 UI 框架尚不可用(启动早期)时显示诊断信息。 +/// +public static class WindowsNativeDialogs +{ + public const uint Ok = 0x00000000; + public const uint IconInformation = 0x00000040; + public const uint IconWarning = 0x00000030; + + /// + /// 显示原生消息框。仅在 Windows 上有效,其他平台为空操作。 + /// + public static void Show(string caption, string message, uint type) + { + if (!OperatingSystem.IsWindows()) + { + return; + } + + _ = MessageBoxW(IntPtr.Zero, message, caption, type); + } + + [DllImport("user32.dll", EntryPoint = "MessageBoxW", CharSet = CharSet.Unicode, SetLastError = true)] + private static extern int MessageBoxW(IntPtr hWnd, string text, string caption, uint type); +} diff --git a/LanMountainDesktop.Platform.Windows/WindowsPackageIdentity.cs b/LanMountainDesktop.Platform.Windows/WindowsPackageIdentity.cs new file mode 100644 index 0000000..55fd69d --- /dev/null +++ b/LanMountainDesktop.Platform.Windows/WindowsPackageIdentity.cs @@ -0,0 +1,43 @@ +using System.Runtime.InteropServices; +using System.Text; + +namespace LanMountainDesktop.Platform.Windows; + +/// +/// Windows 包标识查询(MSIX/UWP 打包检测)。 +/// +public static class WindowsPackageIdentity +{ + private const int AppmodelErrorNoPackage = 15700; + + /// + /// 检测当前进程是否具有包标识(以 MSIX 打包运行)。 + /// 非 Windows 平台返回 false。 + /// + public static bool HasPackageIdentity() + { + if (!OperatingSystem.IsWindows()) + { + return false; + } + + var length = 0; + var hr = GetCurrentPackageFullName(ref length, null); + if (hr == AppmodelErrorNoPackage) + { + return false; + } + + if (length <= 0) + { + return hr == 0; + } + + var builder = new StringBuilder(length); + hr = GetCurrentPackageFullName(ref length, builder); + return hr == 0; + } + + [DllImport("kernel32.dll", CharSet = CharSet.Unicode)] + private static extern int GetCurrentPackageFullName(ref int packageFullNameLength, StringBuilder? packageFullName); +} diff --git a/LanMountainDesktop.Platform.Windows/WindowsPowerManagementService.cs b/LanMountainDesktop.Platform.Windows/WindowsPowerManagementService.cs new file mode 100644 index 0000000..7468f26 --- /dev/null +++ b/LanMountainDesktop.Platform.Windows/WindowsPowerManagementService.cs @@ -0,0 +1,106 @@ +using System.Diagnostics; +using System.Runtime.InteropServices; +using LanMountainDesktop.Platform.Abstractions; + +namespace LanMountainDesktop.Platform.Windows; + +/// +/// Windows 电源管理实现(P/Invoke user32/powrprof)。 +/// 自 LanMountainDesktop.Services.PowerManagementService 迁移,行为不变。 +/// +public sealed class WindowsPowerManagementService : IPowerManagementService +{ + public bool IsShutdownSupported => true; + public bool IsRestartSupported => true; + public bool IsLogoutSupported => true; + public bool IsLockSupported => true; + public bool IsSleepSupported => true; + + public async Task ShutdownAsync() + { + await Task.Run(() => + { + Process.Start(new ProcessStartInfo + { + FileName = "shutdown", + Arguments = "/s /t 0", + UseShellExecute = true, + WindowStyle = ProcessWindowStyle.Hidden + }); + }); + } + + public async Task RestartAsync() + { + await Task.Run(() => + { + Process.Start(new ProcessStartInfo + { + FileName = "shutdown", + Arguments = "/r /t 0", + UseShellExecute = true, + WindowStyle = ProcessWindowStyle.Hidden + }); + }); + } + + public async Task LogoutAsync() + { + await Task.Run(() => + { + ExitWindowsEx(0, 0); + }); + } + + public async Task LockAsync() + { + await Task.Run(() => + { + LockWorkStation(); + }); + } + + public async Task SleepAsync() + { + await Task.Run(() => + { + SetSuspendState(false, false, false); + }); + } + + public void ShowNativePowerUI(PowerAction action) + { + // SlideToShutDown.exe 只支持关机,不支持重启 + // 重启操作应该通过 RestartAsync() 使用 shutdown /r 命令 + if (action != PowerAction.Shutdown) + return; + + var slideToShutDownPath = Environment.ExpandEnvironmentVariables(@"%windir%\System32\SlideToShutDown.exe"); + if (File.Exists(slideToShutDownPath)) + { + Process.Start(new ProcessStartInfo + { + FileName = slideToShutDownPath, + UseShellExecute = true + }); + return; + } + + // 回退到标准关机命令 + Process.Start(new ProcessStartInfo + { + FileName = "shutdown", + Arguments = "/s /t 5 /c \"LanMountainDesktop: Shutting down...\"", + UseShellExecute = true + }); + } + + [DllImport("user32.dll", SetLastError = true)] + private static extern bool ExitWindowsEx(uint uFlags, uint dwReason); + + [DllImport("user32.dll")] + private static extern void LockWorkStation(); + + [DllImport("powrprof.dll", SetLastError = true)] + private static extern bool SetSuspendState(bool hibernate, bool forceCritical, bool disableWakeEvent); +} diff --git a/LanMountainDesktop.Platform.Windows/WindowsWindowPassthroughServices.cs b/LanMountainDesktop.Platform.Windows/WindowsWindowPassthroughServices.cs new file mode 100644 index 0000000..ff8e362 --- /dev/null +++ b/LanMountainDesktop.Platform.Windows/WindowsWindowPassthroughServices.cs @@ -0,0 +1,1324 @@ +using System.Runtime.InteropServices; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Threading; +using LanMountainDesktop.Platform.Abstractions; + +namespace LanMountainDesktop.Platform.Windows; +public sealed class WindowsWindowBottomMostService : IWindowBottomMostService +{ + private const int GWL_STYLE = -16; + private const int GWL_EXSTYLE = -20; + private const int GWLP_HWNDPARENT = -8; + + private const uint WS_CHILD = 0x40000000U; + private const uint WS_POPUP = 0x80000000U; + private const uint WS_CAPTION = 0x00C00000U; + private const uint WS_THICKFRAME = 0x00040000U; + private const uint WS_MINIMIZEBOX = 0x00020000U; + private const uint WS_MAXIMIZEBOX = 0x00010000U; + private const uint WS_SYSMENU = 0x00080000U; + + private const uint WS_EX_TOOLWINDOW = 0x00000080U; + private const uint WS_EX_APPWINDOW = 0x00040000U; + private const uint WS_EX_NOACTIVATE = 0x08000000U; + private const uint WS_EX_NOREDIRECTIONBITMAP = 0x00200000U; + private const uint AVALONIA_COMPOSITION_EXSTYLE_MASK = WS_EX_NOREDIRECTIONBITMAP; + + private const uint SWP_NOSIZE = 0x0001; + private const uint SWP_NOMOVE = 0x0002; + private const uint SWP_NOZORDER = 0x0004; + private const uint SWP_NOACTIVATE = 0x0010; + private const uint SWP_FRAMECHANGED = 0x0020; + private const uint SWP_SHOWWINDOW = 0x0040; + private const uint SWP_HIDEWINDOW = 0x0080; + + private const uint WM_NCHITTEST = 0x0084; + private const int HTTRANSPARENT = -1; + private const int HTCLIENT = 1; + + private const int DWMWA_WINDOW_CORNER_PREFERENCE = 33; + private const int DWMWA_BORDER_COLOR = 34; + private const uint DWMWCP_DONOTROUND = 1; + private const uint DWMWA_COLOR_NONE = 0xFFFFFFFEU; + + private static readonly IntPtr HWND_TOP = IntPtr.Zero; + private static readonly IntPtr HWND_BOTTOM = new(1); + private static readonly object StaticLock = new(); + private static readonly object TimerLock = new(); + + private static readonly Dictionary WindowStates = new(); + + private static System.Timers.Timer? _desktopHostMonitorTimer; + private static IntPtr _lastResolvedDesktopHost; + private static int _monitorDispatchPending; + + public bool IsBottomMostSupported => true; + + public void SetupBottomMost(Window window) + { + ArgumentNullException.ThrowIfNull(window); + if (!OperatingSystem.IsWindows()) + { + return; + } + + DesktopWindowState state; + lock (StaticLock) + { + if (WindowStates.TryGetValue(window, out state!)) + { + return; + } + + state = new DesktopWindowState(window); + WindowStates[window] = state; + } + + Win32Properties.SetWindowCornerPreference(window, Win32Properties.WindowCornerPreference.DoNotRound); + Win32Properties.AddWindowStylesCallback(window, state.WindowStylesCallback); + Win32Properties.AddWndProcHookCallback(window, state.WndProcHookCallback); + + window.Closed += OnWindowClosed; + + var handle = GetWindowHandle(window); + if (handle == IntPtr.Zero) + { + window.Opened += OnWindowOpened; + return; + } + + RunOnUiThread(() => InitializeAndAttach(state, handle, logSuccess: true)); + } + + public void SendToBottom(Window window) + { + ArgumentNullException.ThrowIfNull(window); + if (!TryGetWindowState(window, out var state)) + { + SetupBottomMost(window); + return; + } + + RunOnUiThread(() => + { + var handle = GetWindowHandle(window); + if (handle == IntPtr.Zero || !IsWindow(handle)) + { + return; + } + + RegisterHandle(state, handle); + if (state.NeedsNativeRepair && + !ShouldAttemptNativeRepair( + false, + DateTime.UtcNow, + state.NextNativeRepairAttemptUtc)) + { + return; + } + + var desktopHost = ResolveDesktopIconHost(); + if (!state.NeedsNativeRepair && + state.IsDesktopAttached && + state.HasStableDesktopAttachment && + desktopHost != IntPtr.Zero && + state.DesktopHost == desktopHost && + GetParent(handle) == desktopHost && + state.OriginalState is { } originalState && + HasExpectedDesktopRoleStyles(handle, originalState)) + { + _ = SetWindowPos( + handle, + HWND_TOP, + 0, + 0, + 0, + 0, + SWP_NOSIZE | SWP_NOMOVE | SWP_NOACTIVATE | SWP_SHOWWINDOW); + return; + } + + ApplyDesktopAttachment( + state, + desktopHost, + logSuccess: false, + "explicit refresh", + allowRetryFailedHost: true); + }); + } + + public PixelPoint GetScreenPosition(Window window) + { + ArgumentNullException.ThrowIfNull(window); + var handle = GetWindowHandle(window); + return handle != IntPtr.Zero && GetWindowRect(handle, out var rect) + ? new PixelPoint(rect.Left, rect.Top) + : window.Position; + } + + public bool SetScreenPosition( + Window window, + PixelPoint position, + bool queueOnFailure = false) + { + ArgumentNullException.ThrowIfNull(window); + TryGetWindowState(window, out var state); + var handle = GetWindowHandle(window); + if (handle == IntPtr.Zero || !IsWindow(handle)) + { + window.Position = position; + if (state is not null) + { + state.PendingScreenPosition = null; + state.HasLoggedPositionFailure = false; + } + + return true; + } + + var nativePosition = new POINT(position.X, position.Y); + var style = ReadWindowStyle(handle, GWL_STYLE); + var nativeParent = GetParent(handle); + if (state is not null) + { + if (state.NeedsNativeRepair) + { + if (queueOnFailure) + { + state.PendingScreenPosition = position; + } + + return false; + } + + if (state.IsDesktopAttached) + { + if (state.OriginalState is not { } originalState || + nativeParent != state.DesktopHost || + !HasExpectedDesktopRoleStyles(handle, originalState)) + { + LogPositionFailureOnce( + state, + $"Refusing to move a desktop window with invalid native attachment state. " + + $"Window={handle}; Parent={nativeParent}; ExpectedHost={state.DesktopHost}."); + if (queueOnFailure) + { + state.PendingScreenPosition = position; + } + + return false; + } + + nativeParent = state.DesktopHost; + } + } + + if (OriginalWindowUsesParentClientCoordinates(style) && + (nativeParent == IntPtr.Zero || !ScreenToClient(nativeParent, ref nativePosition))) + { + if (state is not null) + { + LogPositionFailureOnce( + state, + $"Could not translate screen position to child-window coordinates. " + + $"Window={handle}; Parent={nativeParent}."); + if (queueOnFailure) + { + state.PendingScreenPosition = position; + } + } + return false; + } + + if (!SetWindowPos( + handle, + IntPtr.Zero, + nativePosition.X, + nativePosition.Y, + 0, + 0, + SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE)) + { + if (state is not null) + { + LogPositionFailureOnce( + state, + $"Could not set screen position. Window={handle}; Position={position}; " + + $"Error={Marshal.GetLastWin32Error()}."); + if (queueOnFailure) + { + state.PendingScreenPosition = position; + } + } + return false; + } + + if (state is not null) + { + state.PendingScreenPosition = null; + state.HasLoggedPositionFailure = false; + } + + return true; + } + + private static void LogPositionFailureOnce(DesktopWindowState state, string message) + { + if (state.HasLoggedPositionFailure) + { + return; + } + + PlatformLog.Warn("WindowBottomMost", message); + state.HasLoggedPositionFailure = true; + } + + private static void TryApplyPendingScreenPosition(DesktopWindowState state) + { + if (state.NeedsNativeRepair || state.PendingScreenPosition is not { } pendingPosition) + { + return; + } + + _ = new WindowsWindowBottomMostService().SetScreenPosition( + state.Window, + pendingPosition, + queueOnFailure: true); + } + + internal static void SetInteractiveRegionsInternal( + Window window, + IReadOnlyList regions) + { + if (!TryGetWindowState(window, out var state)) + { + return; + } + + var snapshot = new WindowInteractiveRegion[regions.Count]; + for (var i = 0; i < regions.Count; i++) + { + snapshot[i] = regions[i]; + } + + state.InteractiveRegions = snapshot; + } + + internal static (uint Style, uint ExStyle) CreateDesktopChildStyles(uint style, uint exStyle) + { + style |= WS_CHILD; + style &= ~(WS_POPUP | WS_CAPTION | WS_THICKFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX | WS_SYSMENU); + return (style, ApplyDesktopRoleExtendedStyles(exStyle)); + } + + internal static bool IsPointInsideRegion(WindowInteractiveRegion region, Point point) + { + if (region.ClientClipBounds is { } clientClipBounds && + !IsPointInsideRoundedBounds(clientClipBounds, region.ClientClipCornerRadius, point)) + { + return false; + } + + if (region.ClientToRegionTransform is { } clientToRegionTransform) + { + point = clientToRegionTransform.Transform(point); + } + + return IsPointInsideRoundedBounds(region.Bounds, region.CornerRadius, point); + } + + private static bool IsPointInsideRoundedBounds(Rect bounds, double cornerRadius, Point point) + { + if (bounds.Width <= 0 || bounds.Height <= 0 || !bounds.Contains(point)) + { + return false; + } + + var radius = Math.Clamp(cornerRadius, 0, Math.Min(bounds.Width, bounds.Height) / 2); + if (radius <= 0) + { + return true; + } + + var localX = point.X - bounds.X; + var localY = point.Y - bounds.Y; + if (localX >= radius && localX <= bounds.Width - radius || + localY >= radius && localY <= bounds.Height - radius) + { + return true; + } + + var centerX = localX < radius ? radius : bounds.Width - radius; + var centerY = localY < radius ? radius : bounds.Height - radius; + var deltaX = localX - centerX; + var deltaY = localY - centerY; + return deltaX * deltaX + deltaY * deltaY <= radius * radius; + } + + internal static bool OriginalWindowUsesParentClientCoordinates(uint style) + { + return (style & WS_CHILD) != 0; + } + + internal static bool ShouldAttemptNativeRepair( + bool hostChanged, + DateTime utcNow, + DateTime nextRepairAttemptUtc) + { + return hostChanged || utcNow >= nextRepairAttemptUtc; + } + + internal static bool ShouldAttemptDesktopAttachment( + bool hostChanged, + IntPtr attachedHost, + IntPtr currentHost, + bool parentMismatch) + { + return parentMismatch || (hostChanged && attachedHost != currentHost); + } + + private static uint ApplyDesktopRoleExtendedStyles(uint exStyle) + { + // Preserve every compositor-managed bit from Avalonia. In particular, this method must + // never opt the window into a second, legacy alpha-composition path. + return (exStyle | WS_EX_TOOLWINDOW | WS_EX_NOACTIVATE) & ~WS_EX_APPWINDOW; + } + + private static void OnWindowOpened(object? sender, EventArgs e) + { + if (sender is not Window window || !TryGetWindowState(window, out var state)) + { + return; + } + + window.Opened -= OnWindowOpened; + var handle = GetWindowHandle(window); + if (handle != IntPtr.Zero) + { + RunOnUiThread(() => InitializeAndAttach(state, handle, logSuccess: true)); + } + } + + private static void OnWindowClosed(object? sender, EventArgs e) + { + if (sender is Window window && TryGetWindowState(window, out var state)) + { + CleanupWindow(state, restoreNativeState: true); + } + } + + private static void InitializeAndAttach(DesktopWindowState state, IntPtr handle, bool logSuccess) + { + if (handle == IntPtr.Zero || !IsWindow(handle)) + { + return; + } + + RegisterHandle(state, handle); + ConfigureDwmAppearance(handle); + ApplyDesktopAttachment(state, ResolveDesktopIconHost(), logSuccess, "initial setup"); + } + + private static void RegisterHandle(DesktopWindowState state, IntPtr handle) + { + lock (StaticLock) + { + if (state.Handle != IntPtr.Zero && state.Handle != handle) + { + state.OriginalState = null; + state.DesktopHost = IntPtr.Zero; + state.IsDesktopAttached = false; + state.HasStableDesktopAttachment = false; + ResetNativeRepairState(state); + state.AttachToCurrentHostAfterRepair = false; + state.HasLoggedFallback = false; + state.HasLoggedPositionFailure = false; + } + + state.Handle = handle; + state.OriginalState ??= new NativeWindowState( + GetParent(handle), + ReadWindowStyle(handle, GWL_STYLE), + ReadWindowStyle(handle, GWL_EXSTYLE)); + } + } + + private static void ApplyDesktopAttachment( + DesktopWindowState state, + IntPtr desktopHost, + bool logSuccess, + string reason, + bool allowRetryFailedHost = false) + { + var handle = state.Handle; + if (handle == IntPtr.Zero || !IsWindow(handle) || state.OriginalState is not { } originalState) + { + return; + } + + var screenPosition = GetNativeScreenPosition(handle, state.Window.Position); + if (state.NeedsNativeRepair) + { + var failedHost = state.FailedDesktopHost; + var attachAfterRepair = state.AttachToCurrentHostAfterRepair; + FallBackToBottom( + state, + screenPosition, + "repairing an incomplete native rollback before attachment", + logSuccess: false, + failedHost); + if (state.NeedsNativeRepair) + { + StartDesktopHostMonitorTimer(desktopHost); + return; + } + + state.AttachToCurrentHostAfterRepair = false; + if (!attachAfterRepair && desktopHost == failedHost && !allowRetryFailedHost) + { + // The failed host has not changed. Stay in the safe top-level fallback until + // Explorer changes or the caller explicitly requests another attachment. + StartDesktopHostMonitorTimer(desktopHost); + return; + } + } + + var beforeStyle = ReadWindowStyle(handle, GWL_STYLE); + var beforeExStyle = ReadWindowStyle(handle, GWL_EXSTYLE); + + if (desktopHost == IntPtr.Zero || !IsWindow(desktopHost)) + { + FallBackToBottom( + state, + screenPosition, + "desktop icon host is unavailable", + logSuccess, + desktopHost); + StartDesktopHostMonitorTimer(desktopHost); + return; + } + + if (state.IsDesktopAttached) + { + if (state.DesktopHost == desktopHost && GetParent(handle) == desktopHost) + { + if (HasExpectedDesktopRoleStyles(handle, originalState)) + { + StartDesktopHostMonitorTimer(desktopHost); + return; + } + + FallBackToBottom( + state, + screenPosition, + "desktop child style validation failed", + logSuccess, + desktopHost); + StartDesktopHostMonitorTimer(desktopHost); + return; + } + + var hostBeingDetached = state.DesktopHost; + if (!TryRestoreNativeState(state, screenPosition, showWindow: true)) + { + FallBackToBottom( + state, + screenPosition, + "failed to restore before remount", + logSuccess, + hostBeingDetached); + if (state.NeedsNativeRepair && desktopHost != hostBeingDetached) + { + state.AttachToCurrentHostAfterRepair = true; + } + + if (state.NeedsNativeRepair || + (desktopHost == hostBeingDetached && !allowRetryFailedHost)) + { + StartDesktopHostMonitorTimer(desktopHost); + return; + } + } + + beforeStyle = ReadWindowStyle(handle, GWL_STYLE); + beforeExStyle = ReadWindowStyle(handle, GWL_EXSTYLE); + } + + beforeExStyle |= originalState.ExStyle & AVALONIA_COMPOSITION_EXSTYLE_MASK; + var (expectedStyle, expectedExStyle) = CreateDesktopChildStyles(beforeStyle, beforeExStyle); + WriteWindowStyle(handle, GWL_STYLE, expectedStyle); + WriteWindowStyle(handle, GWL_EXSTYLE, expectedExStyle); + + _ = SetParent(handle, desktopHost); + var setParentError = Marshal.GetLastWin32Error(); + if (GetParent(handle) != desktopHost) + { + FallBackToBottom( + state, + screenPosition, + $"SetParent failed with error {setParentError}", + logSuccess, + desktopHost); + StartDesktopHostMonitorTimer(desktopHost); + return; + } + + state.DesktopHost = desktopHost; + state.IsDesktopAttached = true; + state.HasStableDesktopAttachment = false; + + var childPosition = new POINT(screenPosition.X, screenPosition.Y); + if (!ScreenToClient(desktopHost, ref childPosition) || + !SetWindowPos( + handle, + HWND_TOP, + childPosition.X, + childPosition.Y, + 0, + 0, + SWP_NOSIZE | SWP_NOACTIVATE | SWP_FRAMECHANGED | SWP_SHOWWINDOW) || + !HasExpectedDesktopStyles(handle, expectedStyle, expectedExStyle) || + GetParent(handle) != desktopHost) + { + var error = Marshal.GetLastWin32Error(); + FallBackToBottom( + state, + screenPosition, + $"post-attachment validation failed with error {error}", + logSuccess, + desktopHost); + StartDesktopHostMonitorTimer(desktopHost); + return; + } + + ResetNativeRepairState(state); + state.AttachToCurrentHostAfterRepair = false; + state.HasLoggedFallback = false; + state.HasStableDesktopAttachment = true; + ConfigureDwmAppearance(handle); + if (logSuccess) + { + var afterStyle = ReadWindowStyle(handle, GWL_STYLE); + var afterExStyle = ReadWindowStyle(handle, GWL_EXSTYLE); + PlatformLog.Info( + "WindowBottomMost", + $"Mounted window to desktop icon host. Window={handle}; Host={desktopHost}; Reason={reason}; " + + $"Style=0x{beforeStyle:X8}->0x{afterStyle:X8}; ExStyle=0x{beforeExStyle:X8}->0x{afterExStyle:X8}; " + + $"NoRedirectionBitmap={((afterExStyle & WS_EX_NOREDIRECTIONBITMAP) != 0)}."); + } + + StartDesktopHostMonitorTimer(desktopHost); + } + + private static void FallBackToBottom( + DesktopWindowState state, + PixelPoint screenPosition, + string reason, + bool logSuccess, + IntPtr failedDesktopHost) + { + var handle = state.Handle; + var wasAttached = state.IsDesktopAttached; + var wasStablyAttached = state.HasStableDesktopAttachment; + var restored = true; + if (state.OriginalState is { } originalState && + (state.NeedsNativeRepair || + wasAttached || + GetParent(handle) != originalState.Parent || + ReadWindowStyle(handle, GWL_STYLE) != originalState.Style || + ReadWindowStyle(handle, GWL_EXSTYLE) != originalState.ExStyle)) + { + restored = TryRestoreNativeState(state, screenPosition, showWindow: true); + } + + if (!restored) + { + MarkNativeRepairPending(state, failedDesktopHost); + if (logSuccess || !state.HasLoggedRepairFailure) + { + PlatformLog.Warn( + "WindowBottomMost", + $"Native rollback is incomplete; keeping the window in repair state. " + + $"Window={handle}; FailedHost={failedDesktopHost}; Reason={reason}."); + state.HasLoggedRepairFailure = true; + } + + return; + } + + state.DesktopHost = IntPtr.Zero; + state.IsDesktopAttached = false; + state.HasStableDesktopAttachment = false; + + if (IsWindow(handle) && + !SetWindowPos( + handle, + HWND_BOTTOM, + 0, + 0, + 0, + 0, + SWP_NOSIZE | SWP_NOMOVE | SWP_NOACTIVATE | SWP_SHOWWINDOW)) + { + MarkNativeRepairPending(state, failedDesktopHost); + if (logSuccess || !state.HasLoggedRepairFailure) + { + PlatformLog.Warn( + "WindowBottomMost", + $"Native state was restored, but HWND_BOTTOM fallback positioning failed. " + + $"Window={handle}; FailedHost={failedDesktopHost}; Reason={reason}; " + + $"Error={Marshal.GetLastWin32Error()}."); + state.HasLoggedRepairFailure = true; + } + + return; + } + + ResetNativeRepairState(state); + + if (logSuccess || wasStablyAttached || !state.HasLoggedFallback) + { + PlatformLog.Warn( + "WindowBottomMost", + $"Using HWND_BOTTOM fallback. Window={handle}; Reason={reason}; NativeStateRestored={restored}."); + state.HasLoggedFallback = true; + } + } + + private static void MarkNativeRepairPending(DesktopWindowState state, IntPtr failedDesktopHost) + { + state.FailedDesktopHost = failedDesktopHost; + state.NativeRepairAttemptCount = Math.Min(state.NativeRepairAttemptCount + 1, 30); + var exponent = Math.Min(state.NativeRepairAttemptCount - 1, 5); + var delaySeconds = Math.Min(60, 2 * (1 << exponent)); + state.NextNativeRepairAttemptUtc = DateTime.UtcNow.AddSeconds(delaySeconds); + state.NeedsNativeRepair = true; + + if (state.Handle != IntPtr.Zero && IsWindow(state.Handle)) + { + _ = SetWindowPos( + state.Handle, + IntPtr.Zero, + 0, + 0, + 0, + 0, + SWP_NOSIZE | SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE | SWP_HIDEWINDOW); + } + } + + private static void ResetNativeRepairState(DesktopWindowState state) + { + state.NeedsNativeRepair = false; + state.FailedDesktopHost = IntPtr.Zero; + state.HasLoggedRepairFailure = false; + state.NativeRepairAttemptCount = 0; + state.NextNativeRepairAttemptUtc = DateTime.MinValue; + } + + private static bool TryRestoreNativeState( + DesktopWindowState state, + PixelPoint screenPosition, + bool showWindow) + { + var handle = state.Handle; + if (handle == IntPtr.Zero || !IsWindow(handle) || state.OriginalState is not { } originalState) + { + return false; + } + + var previousDesktopHost = state.DesktopHost; + var wasDesktopAttached = state.IsDesktopAttached; + var wasStablyDesktopAttached = state.HasStableDesktopAttachment; + state.IsDesktopAttached = false; + state.HasStableDesktopAttachment = false; + state.DesktopHost = IntPtr.Zero; + + var restoreParentOrOwner = originalState.Parent != IntPtr.Zero && !IsWindow(originalState.Parent) + ? IntPtr.Zero + : originalState.Parent; + var restorePosition = new POINT(screenPosition.X, screenPosition.Y); + + if (OriginalWindowUsesParentClientCoordinates(originalState.Style)) + { + _ = SetParent(handle, restoreParentOrOwner); + WriteWindowStyle(handle, GWL_STYLE, originalState.Style); + WriteWindowStyle(handle, GWL_EXSTYLE, originalState.ExStyle); + if (restoreParentOrOwner != IntPtr.Zero && + !ScreenToClient(restoreParentOrOwner, ref restorePosition)) + { + restorePosition = new POINT(screenPosition.X, screenPosition.Y); + } + } + else + { + // GetParent returns an owner for a top-level popup. It is not a child-coordinate + // parent: first leave the desktop child hierarchy, restore the popup styles, then + // restore the owner through GWLP_HWNDPARENT and keep SetWindowPos in screen pixels. + _ = SetParent(handle, IntPtr.Zero); + WriteWindowStyle(handle, GWL_STYLE, originalState.Style); + WriteWindowStyle(handle, GWL_EXSTYLE, originalState.ExStyle); + _ = SetWindowLongPtr(handle, GWLP_HWNDPARENT, restoreParentOrOwner); + } + + var flags = SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED; + if (showWindow) + { + flags |= SWP_SHOWWINDOW; + } + + var positioned = SetWindowPos( + handle, + IntPtr.Zero, + restorePosition.X, + restorePosition.Y, + 0, + 0, + flags); + + var hierarchyAndStylesRestored = + GetParent(handle) == restoreParentOrOwner && + ReadWindowStyle(handle, GWL_STYLE) == originalState.Style && + ReadWindowStyle(handle, GWL_EXSTYLE) == originalState.ExStyle; + if (!hierarchyAndStylesRestored) + { + // Preserve the logical attachment state until a later repair attempt succeeds. + // This prevents the monitor from treating a half-converted child HWND as a valid + // top-level fallback. + state.DesktopHost = previousDesktopHost; + state.IsDesktopAttached = wasDesktopAttached; + state.HasStableDesktopAttachment = wasStablyDesktopAttached; + } + + return hierarchyAndStylesRestored && positioned; + } + + private static bool HasExpectedDesktopStyles(IntPtr handle, uint style, uint exStyle) + { + return ReadWindowStyle(handle, GWL_STYLE) == style && + ReadWindowStyle(handle, GWL_EXSTYLE) == exStyle; + } + + private static bool HasExpectedDesktopRoleStyles(IntPtr handle, NativeWindowState originalState) + { + var style = ReadWindowStyle(handle, GWL_STYLE); + var exStyle = ReadWindowStyle(handle, GWL_EXSTYLE); + var expected = CreateDesktopChildStyles(style, exStyle); + var requiredAvaloniaBits = originalState.ExStyle & AVALONIA_COMPOSITION_EXSTYLE_MASK; + return style == expected.Style && + exStyle == expected.ExStyle && + (exStyle & requiredAvaloniaBits) == requiredAvaloniaBits; + } + + private static void ConfigureDwmAppearance(IntPtr handle) + { + if (handle == IntPtr.Zero || !IsWindow(handle)) + { + return; + } + + try + { + var cornerPreference = DWMWCP_DONOTROUND; + _ = DwmSetWindowAttribute( + handle, + DWMWA_WINDOW_CORNER_PREFERENCE, + ref cornerPreference, + sizeof(uint)); + + var borderColor = DWMWA_COLOR_NONE; + _ = DwmSetWindowAttribute(handle, DWMWA_BORDER_COLOR, ref borderColor, sizeof(uint)); + } + catch (Exception ex) when (ex is DllNotFoundException or EntryPointNotFoundException) + { + // These attributes are best-effort and unavailable on older Windows versions. + } + } + + private static IntPtr ResolveDesktopIconHost() + { + var topLevelWindows = new List(); + EnumWindows((handle, _) => + { + topLevelWindows.Add(handle); + return true; + }, IntPtr.Zero); + + foreach (var topLevelWindow in topLevelWindows) + { + var worker = FindWindowEx(topLevelWindow, IntPtr.Zero, "WorkerW", null); + if (worker == IntPtr.Zero) + { + continue; + } + + var defView = FindWindowEx(worker, IntPtr.Zero, "SHELLDLL_DefView", null); + if (defView != IntPtr.Zero) + { + return defView; + } + } + + foreach (var topLevelWindow in topLevelWindows) + { + var defView = FindWindowEx(topLevelWindow, IntPtr.Zero, "SHELLDLL_DefView", null); + if (defView != IntPtr.Zero) + { + return defView; + } + } + + return IntPtr.Zero; + } + + private static void StartDesktopHostMonitorTimer(IntPtr currentHost) + { + lock (TimerLock) + { + if (_desktopHostMonitorTimer != null) + { + return; + } + + _lastResolvedDesktopHost = currentHost; + _desktopHostMonitorTimer = new System.Timers.Timer(TimeSpan.FromSeconds(2)) + { + AutoReset = true + }; + _desktopHostMonitorTimer.Elapsed += (_, _) => MonitorDesktopHostAttachments(); + _desktopHostMonitorTimer.Start(); + } + } + + private static void MonitorDesktopHostAttachments() + { + var desktopHost = ResolveDesktopIconHost(); + var hostChanged = false; + lock (TimerLock) + { + if (desktopHost != _lastResolvedDesktopHost) + { + _lastResolvedDesktopHost = desktopHost; + hostChanged = true; + } + } + + List states; + lock (StaticLock) + { + states = [.. WindowStates.Values]; + } + + var requiresCleanupOrRepair = false; + var now = DateTime.UtcNow; + foreach (var state in states) + { + if ((state.NeedsNativeRepair && now >= state.NextNativeRepairAttemptUtc) || + (!state.NeedsNativeRepair && state.PendingScreenPosition.HasValue) || + state.Handle != IntPtr.Zero && !IsWindow(state.Handle) || + state.IsDesktopAttached && + (GetParent(state.Handle) != state.DesktopHost || + state.OriginalState is not { } originalState || + !HasExpectedDesktopRoleStyles(state.Handle, originalState))) + { + requiresCleanupOrRepair = true; + break; + } + } + + if (!hostChanged && !requiresCleanupOrRepair || + Interlocked.Exchange(ref _monitorDispatchPending, 1) != 0) + { + return; + } + + Dispatcher.UIThread.Post(() => + { + try + { + var currentHost = ResolveDesktopIconHost(); + var effectiveHostChanged = hostChanged || currentHost != desktopHost; + List currentStates; + lock (StaticLock) + { + currentStates = [.. WindowStates.Values]; + } + + foreach (var state in currentStates) + { + if (state.Handle == IntPtr.Zero) + { + // A window can be registered before Avalonia creates its native handle. + continue; + } + + if (!IsWindow(state.Handle)) + { + CleanupWindow(state, restoreNativeState: false); + continue; + } + + if (state.NeedsNativeRepair) + { + if (!ShouldAttemptNativeRepair( + effectiveHostChanged, + DateTime.UtcNow, + state.NextNativeRepairAttemptUtc)) + { + continue; + } + + var failedHost = state.FailedDesktopHost; + var attachAfterRepair = state.AttachToCurrentHostAfterRepair; + var screenPosition = GetNativeScreenPosition(state.Handle, state.Window.Position); + FallBackToBottom( + state, + screenPosition, + "retrying incomplete native rollback", + logSuccess: false, + failedHost); + if (state.NeedsNativeRepair) + { + continue; + } + + state.AttachToCurrentHostAfterRepair = false; + if (currentHost != IntPtr.Zero && + (attachAfterRepair || + effectiveHostChanged && currentHost != failedHost)) + { + ApplyDesktopAttachment( + state, + currentHost, + logSuccess: true, + "desktop host changed while native state was being repaired"); + } + + TryApplyPendingScreenPosition(state); + continue; + } + + var attachmentDrift = state.IsDesktopAttached && + (GetParent(state.Handle) != state.DesktopHost || + state.OriginalState is not { } originalState || + !HasExpectedDesktopRoleStyles(state.Handle, originalState)); + if (ShouldAttemptDesktopAttachment( + effectiveHostChanged, + state.DesktopHost, + currentHost, + attachmentDrift)) + { + ApplyDesktopAttachment(state, currentHost, logSuccess: true, "desktop host changed"); + } + + TryApplyPendingScreenPosition(state); + } + + lock (TimerLock) + { + _lastResolvedDesktopHost = currentHost; + } + } + finally + { + Interlocked.Exchange(ref _monitorDispatchPending, 0); + } + }, DispatcherPriority.Background); + } + + private static void CleanupWindow(DesktopWindowState state, bool restoreNativeState) + { + state.Window.Opened -= OnWindowOpened; + state.Window.Closed -= OnWindowClosed; + Win32Properties.RemoveWindowStylesCallback(state.Window, state.WindowStylesCallback); + Win32Properties.RemoveWndProcHookCallback(state.Window, state.WndProcHookCallback); + + if (restoreNativeState && state.Handle != IntPtr.Zero && IsWindow(state.Handle)) + { + var screenPosition = GetNativeScreenPosition(state.Handle, state.Window.Position); + _ = TryRestoreNativeState(state, screenPosition, showWindow: false); + } + + lock (StaticLock) + { + WindowStates.Remove(state.Window); + } + + StopDesktopHostMonitorTimerIfIdle(); + } + + private static void StopDesktopHostMonitorTimerIfIdle() + { + lock (StaticLock) + { + if (WindowStates.Count > 0) + { + return; + } + } + + lock (TimerLock) + { + _desktopHostMonitorTimer?.Stop(); + _desktopHostMonitorTimer?.Dispose(); + _desktopHostMonitorTimer = null; + _lastResolvedDesktopHost = IntPtr.Zero; + } + } + + private static IntPtr HandleWindowMessage( + DesktopWindowState state, + IntPtr hWnd, + uint message, + IntPtr wParam, + IntPtr lParam, + ref bool handled) + { + if (message != WM_NCHITTEST) + { + return IntPtr.Zero; + } + + if (state.NeedsNativeRepair) + { + handled = true; + return (IntPtr)HTTRANSPARENT; + } + + var screenPoint = new POINT( + unchecked((short)(lParam.ToInt64() & 0xFFFF)), + unchecked((short)((lParam.ToInt64() >> 16) & 0xFFFF))); + if (!ScreenToClient(hWnd, ref screenPoint)) + { + handled = true; + return (IntPtr)HTTRANSPARENT; + } + + var point = ConvertPhysicalClientPointToDip( + new Point(screenPoint.X, screenPoint.Y), + GetWindowDpiScale(hWnd)); + var regions = state.InteractiveRegions; + foreach (var region in regions) + { + if (IsPointInsideRegion(region, point)) + { + handled = true; + return (IntPtr)HTCLIENT; + } + } + + handled = true; + return (IntPtr)HTTRANSPARENT; + } + + internal static Point ConvertPhysicalClientPointToDip(Point physicalPoint, double dpiScale) + { + var scale = double.IsFinite(dpiScale) ? Math.Max(0.1, dpiScale) : 1d; + return new Point(physicalPoint.X / scale, physicalPoint.Y / scale); + } + + private static double GetWindowDpiScale(IntPtr handle) + { + try + { + var dpi = GetDpiForWindow(handle); + return dpi > 0 ? dpi / 96.0 : 1.0; + } + catch (EntryPointNotFoundException) + { + return 1.0; + } + } + + private static PixelPoint GetNativeScreenPosition(IntPtr handle, PixelPoint fallback) + { + return GetWindowRect(handle, out var rect) + ? new PixelPoint(rect.Left, rect.Top) + : fallback; + } + + private static bool TryGetWindowState(Window window, out DesktopWindowState state) + { + lock (StaticLock) + { + return WindowStates.TryGetValue(window, out state!); + } + } + + private static void RunOnUiThread(Action action) + { + if (Dispatcher.UIThread.CheckAccess()) + { + action(); + } + else + { + Dispatcher.UIThread.Post(action); + } + } + + private static uint ReadWindowStyle(IntPtr handle, int index) + { + return unchecked((uint)GetWindowLongPtr(handle, index).ToInt64()); + } + + private static void WriteWindowStyle(IntPtr handle, int index, uint value) + { + _ = SetWindowLongPtr(handle, index, new IntPtr(unchecked((int)value))); + } + + private static IntPtr GetWindowHandle(Window window) + { + try + { + return window.TryGetPlatformHandle()?.Handle ?? IntPtr.Zero; + } + catch + { + return IntPtr.Zero; + } + } + + [StructLayout(LayoutKind.Sequential)] + private struct RECT + { + public int Left; + public int Top; + public int Right; + public int Bottom; + } + + [StructLayout(LayoutKind.Sequential)] + private struct POINT(int x, int y) + { + public int X = x; + public int Y = y; + } + + private sealed class DesktopWindowState + { + public DesktopWindowState(Window window) + { + Window = window; + WindowStylesCallback = ApplyWindowStyles; + WndProcHookCallback = ProcessWindowMessage; + } + + public Window Window { get; } + public IntPtr Handle { get; set; } + public NativeWindowState? OriginalState { get; set; } + public IntPtr DesktopHost { get; set; } + public volatile bool IsDesktopAttached; + public volatile bool HasStableDesktopAttachment; + public volatile bool NeedsNativeRepair; + public IntPtr FailedDesktopHost { get; set; } + public bool AttachToCurrentHostAfterRepair { get; set; } + public int NativeRepairAttemptCount { get; set; } + public DateTime NextNativeRepairAttemptUtc { get; set; } + public PixelPoint? PendingScreenPosition { get; set; } + public bool HasLoggedPositionFailure { get; set; } + public bool HasLoggedFallback { get; set; } + public bool HasLoggedRepairFailure { get; set; } + public volatile WindowInteractiveRegion[] InteractiveRegions = []; + public Win32Properties.CustomWindowStylesCallback WindowStylesCallback { get; } + public Win32Properties.CustomWndProcHookCallback WndProcHookCallback { get; } + + private (uint style, uint exStyle) ApplyWindowStyles(uint style, uint exStyle) + { + return IsDesktopAttached + ? CreateDesktopChildStyles(style, exStyle) + : (style, ApplyDesktopRoleExtendedStyles(exStyle)); + } + + private IntPtr ProcessWindowMessage( + IntPtr hWnd, + uint message, + IntPtr wParam, + IntPtr lParam, + ref bool handled) + { + return HandleWindowMessage(this, hWnd, message, wParam, lParam, ref handled); + } + } + + private readonly record struct NativeWindowState(IntPtr Parent, uint Style, uint ExStyle); + + private delegate bool EnumWindowsProc(IntPtr handle, IntPtr lParam); + + [DllImport("user32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect); + + [DllImport("user32.dll", EntryPoint = "GetWindowLongPtr", SetLastError = true)] + private static extern IntPtr GetWindowLongPtr(IntPtr hWnd, int nIndex); + + [DllImport("user32.dll", EntryPoint = "SetWindowLongPtr", SetLastError = true)] + private static extern IntPtr SetWindowLongPtr(IntPtr hWnd, int nIndex, IntPtr dwNewLong); + + [DllImport("user32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool SetWindowPos( + IntPtr hWnd, + IntPtr hWndInsertAfter, + int x, + int y, + int cx, + int cy, + uint flags); + + [DllImport("user32.dll", SetLastError = true)] + private static extern IntPtr SetParent(IntPtr hWndChild, IntPtr hWndNewParent); + + [DllImport("user32.dll")] + private static extern IntPtr GetParent(IntPtr hWnd); + + [DllImport("user32.dll")] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool IsWindow(IntPtr hWnd); + + [DllImport("user32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool ScreenToClient(IntPtr hWnd, ref POINT lpPoint); + + [DllImport("user32.dll", SetLastError = true)] + private static extern IntPtr FindWindowEx( + IntPtr hParent, + IntPtr hChildAfter, + string? lpszClass, + string? lpszWindow); + + [DllImport("user32.dll", SetLastError = true)] + [return: MarshalAs(UnmanagedType.Bool)] + private static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam); + + [DllImport("user32.dll")] + private static extern uint GetDpiForWindow(IntPtr hWnd); + + [DllImport("dwmapi.dll")] + private static extern int DwmSetWindowAttribute( + IntPtr hWnd, + int dwAttribute, + ref uint pvAttribute, + int cbAttribute); +} + +public sealed class WindowsRegionPassthroughService : IRegionPassthroughService +{ + public bool IsRegionPassthroughSupported => true; + + public void SetInteractiveRegions( + Window window, + IReadOnlyList interactiveRegions) + { + ArgumentNullException.ThrowIfNull(window); + ArgumentNullException.ThrowIfNull(interactiveRegions); + WindowsWindowBottomMostService.SetInteractiveRegionsInternal(window, interactiveRegions); + } + + public void ClearInteractiveRegions(Window window) + { + ArgumentNullException.ThrowIfNull(window); + WindowsWindowBottomMostService.SetInteractiveRegionsInternal(window, []); + } +} diff --git a/LanMountainDesktop.Tests/WindowPassthroughServiceTests.cs b/LanMountainDesktop.Tests/WindowPassthroughServiceTests.cs index be91727..069a697 100644 --- a/LanMountainDesktop.Tests/WindowPassthroughServiceTests.cs +++ b/LanMountainDesktop.Tests/WindowPassthroughServiceTests.cs @@ -1,6 +1,7 @@ -using System.Reflection; +using System.Reflection; using Avalonia; -using LanMountainDesktop.Services; +using LanMountainDesktop.Platform.Abstractions; +using LanMountainDesktop.Platform.Windows; using Xunit; namespace LanMountainDesktop.Tests; @@ -52,7 +53,7 @@ public sealed class WindowPassthroughServiceTests [Fact] public void NativeIntegration_UsesAvaloniaCallbacksWithoutManualWndProcSubclassing() { - var source = ReadRepositoryFile("LanMountainDesktop", "Services", "WindowPassthroughService.cs"); + var source = ReadRepositoryFile("LanMountainDesktop.Platform.Windows", "WindowsWindowPassthroughServices.cs"); Assert.Contains("Win32Properties.AddWindowStylesCallback", source); Assert.Contains("Win32Properties.AddWndProcHookCallback", source); @@ -158,7 +159,7 @@ public sealed class WindowPassthroughServiceTests Assert.False(Invoke("OriginalWindowUsesParentClientCoordinates", WsPopup)); Assert.False(Invoke("OriginalWindowUsesParentClientCoordinates", 0U)); - var source = ReadRepositoryFile("LanMountainDesktop", "Services", "WindowPassthroughService.cs"); + var source = ReadRepositoryFile("LanMountainDesktop.Platform.Windows", "WindowsWindowPassthroughServices.cs"); Assert.Contains("GWLP_HWNDPARENT", source); Assert.Contains("restore the owner through GWLP_HWNDPARENT", source); } @@ -213,7 +214,7 @@ public sealed class WindowPassthroughServiceTests [Fact] public void HitTestCoordinates_AreResolvedFromTheLiveWindowState() { - var source = ReadRepositoryFile("LanMountainDesktop", "Services", "WindowPassthroughService.cs"); + var source = ReadRepositoryFile("LanMountainDesktop.Platform.Windows", "WindowsWindowPassthroughServices.cs"); Assert.Contains("ScreenToClient(hWnd, ref screenPoint)", source); Assert.Contains("GetDpiForWindow(handle)", source); @@ -229,9 +230,8 @@ public sealed class WindowPassthroughServiceTests private static T Invoke(string methodName, params object[] arguments) { - var serviceType = typeof(IWindowBottomMostService).Assembly.GetType( - "LanMountainDesktop.Services.WindowsWindowBottomMostService", - throwOnError: true)!; + // Windows 实现已迁移至 Platform.Windows(跨平台重构),类型现为 public。 + var serviceType = typeof(WindowsWindowBottomMostService); var method = serviceType.GetMethod( methodName, BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public); diff --git a/LanMountainDesktop.slnx b/LanMountainDesktop.slnx index f143eac..f607885 100644 --- a/LanMountainDesktop.slnx +++ b/LanMountainDesktop.slnx @@ -1,4 +1,8 @@ + + + + diff --git a/LanMountainDesktop/App.axaml.cs b/LanMountainDesktop/App.axaml.cs index 90043c3..a7a6f92 100644 --- a/LanMountainDesktop/App.axaml.cs +++ b/LanMountainDesktop/App.axaml.cs @@ -18,6 +18,7 @@ using Avalonia.Threading; using LanMountainDesktop.ComponentSystem; using LanMountainDesktop.DesktopHost; using LanMountainDesktop.Models; +using LanMountainDesktop.Platform.Abstractions; using LanMountainDesktop.PluginSdk; using LanMountainDesktop.Services; using LanMountainDesktop.Services.ExternalIpc; diff --git a/LanMountainDesktop/LanMountainDesktop.csproj b/LanMountainDesktop/LanMountainDesktop.csproj index 6cb23c7..d4497df 100644 --- a/LanMountainDesktop/LanMountainDesktop.csproj +++ b/LanMountainDesktop/LanMountainDesktop.csproj @@ -30,6 +30,9 @@ + + + diff --git a/LanMountainDesktop/Services/FusedDesktopManagerService.cs b/LanMountainDesktop/Services/FusedDesktopManagerService.cs index 3598253..0e43221 100644 --- a/LanMountainDesktop/Services/FusedDesktopManagerService.cs +++ b/LanMountainDesktop/Services/FusedDesktopManagerService.cs @@ -10,6 +10,7 @@ using LanMountainDesktop.ComponentSystem; using LanMountainDesktop.DesktopEditing; using LanMountainDesktop.Host.Abstractions; using LanMountainDesktop.Models; +using LanMountainDesktop.Platform.Abstractions; using LanMountainDesktop.PluginSdk; using LanMountainDesktop.Services.Settings; using LanMountainDesktop.Views; diff --git a/LanMountainDesktop/Services/MainWindowDesktopLayerService.cs b/LanMountainDesktop/Services/MainWindowDesktopLayerService.cs index 5ace9ca..220e977 100644 --- a/LanMountainDesktop/Services/MainWindowDesktopLayerService.cs +++ b/LanMountainDesktop/Services/MainWindowDesktopLayerService.cs @@ -1,17 +1,14 @@ using System; -using System.Collections.Generic; -using System.Runtime.InteropServices; -using Avalonia.Controls; +using LanMountainDesktop.Platform.Abstractions; +using LanMountainDesktop.Platform.Windows; namespace LanMountainDesktop.Services; -public interface IMainWindowDesktopLayerService -{ - bool IsSupported { get; } - void EnableOrRefresh(Window window); - void Disable(Window window); -} - +/// +/// 桌面层服务工厂。接口与实现位于平台层: +/// 接口 在 Platform.Abstractions, +/// Windows 实现(P/Invoke)在 Platform.Windows。 +/// public static class MainWindowDesktopLayerServiceFactory { private static readonly object Gate = new(); @@ -21,252 +18,15 @@ public static class MainWindowDesktopLayerServiceFactory { lock (Gate) { - return _instance ??= OperatingSystem.IsWindows() - ? new WindowsMainWindowDesktopLayerService() - : new NullMainWindowDesktopLayerService(); + if (_instance is null) + { + PlatformLogBridge.Install(); + _instance = OperatingSystem.IsWindows() + ? new WindowsMainWindowDesktopLayerService() + : new NullMainWindowDesktopLayerService(); + } + + return _instance; } } } - -internal sealed class WindowsMainWindowDesktopLayerService : IMainWindowDesktopLayerService -{ - private const int GWL_STYLE = -16; - private const int GWL_EXSTYLE = -20; - - private const long WS_CHILD = 0x40000000L; - private const long WS_POPUP = 0x80000000L; - private const long WS_CAPTION = 0x00C00000L; - private const long WS_THICKFRAME = 0x00040000L; - private const long WS_MINIMIZEBOX = 0x00020000L; - private const long WS_MAXIMIZEBOX = 0x00010000L; - private const long WS_SYSMENU = 0x00080000L; - - private const uint SWP_NOSIZE = 0x0001; - private const uint SWP_NOMOVE = 0x0002; - private const uint SWP_NOACTIVATE = 0x0010; - private const uint SWP_SHOWWINDOW = 0x0040; - private const uint SWP_FRAMECHANGED = 0x0020; - - private static readonly IntPtr HWND_TOP = IntPtr.Zero; - private static readonly IntPtr HWND_BOTTOM = new(1); - - private readonly object _gate = new(); - private readonly Dictionary _restoreStates = []; - - public bool IsSupported => true; - - public void EnableOrRefresh(Window window) - { - ArgumentNullException.ThrowIfNull(window); - - var handle = GetWindowHandle(window); - if (handle == IntPtr.Zero) - { - window.Opened -= OnDeferredOpened; - window.Opened += OnDeferredOpened; - return; - } - - EnableOrRefresh(handle); - } - - public void Disable(Window window) - { - ArgumentNullException.ThrowIfNull(window); - window.Opened -= OnDeferredOpened; - - var handle = GetWindowHandle(window); - if (handle == IntPtr.Zero) - { - return; - } - - WindowRestoreState? restoreState; - lock (_gate) - { - if (!_restoreStates.Remove(handle, out restoreState)) - { - return; - } - } - - try - { - _ = SetParent(handle, restoreState.Parent); - SetWindowLongPtr(handle, GWL_STYLE, restoreState.Style); - SetWindowLongPtr(handle, GWL_EXSTYLE, restoreState.ExStyle); - _ = SetWindowPos( - handle, - HWND_TOP, - 0, - 0, - 0, - 0, - SWP_NOSIZE | SWP_NOMOVE | SWP_NOACTIVATE | SWP_FRAMECHANGED | SWP_SHOWWINDOW); - AppLogger.Info("MainWindowDesktopLayer", $"Disabled desktop layer. Window={handle}."); - } - catch (Exception ex) - { - AppLogger.Warn("MainWindowDesktopLayer", $"Failed to disable desktop layer. Window={handle}.", ex); - } - } - - private void OnDeferredOpened(object? sender, EventArgs e) - { - if (sender is not Window window) - { - return; - } - - window.Opened -= OnDeferredOpened; - EnableOrRefresh(window); - } - - private void EnableOrRefresh(IntPtr handle) - { - if (handle == IntPtr.Zero || !IsWindow(handle)) - { - return; - } - - SaveRestoreStateIfNeeded(handle); - var desktopHost = ResolveDesktopIconHost(); - if (desktopHost != IntPtr.Zero && IsWindow(desktopHost)) - { - ApplyDesktopChildStyle(handle); - if (GetParent(handle) != desktopHost) - { - _ = SetParent(handle, desktopHost); - } - - _ = SetWindowPos( - handle, - HWND_TOP, - 0, - 0, - 0, - 0, - SWP_NOSIZE | SWP_NOMOVE | SWP_NOACTIVATE | SWP_FRAMECHANGED | SWP_SHOWWINDOW); - AppLogger.Info("MainWindowDesktopLayer", $"Enabled desktop layer. Window={handle}; Host={desktopHost}."); - return; - } - - _ = SetWindowPos(handle, HWND_BOTTOM, 0, 0, 0, 0, SWP_NOSIZE | SWP_NOMOVE | SWP_NOACTIVATE | SWP_SHOWWINDOW); - AppLogger.Warn("MainWindowDesktopLayer", $"Desktop icon host not found. Falling back to HWND_BOTTOM. Window={handle}."); - } - - private void SaveRestoreStateIfNeeded(IntPtr handle) - { - lock (_gate) - { - if (_restoreStates.ContainsKey(handle)) - { - return; - } - - _restoreStates[handle] = new WindowRestoreState( - GetParent(handle), - GetWindowLongPtr(handle, GWL_STYLE), - GetWindowLongPtr(handle, GWL_EXSTYLE)); - } - } - - private static void ApplyDesktopChildStyle(IntPtr handle) - { - var style = GetWindowLongPtr(handle, GWL_STYLE).ToInt64(); - style |= WS_CHILD; - style &= ~(WS_POPUP | WS_CAPTION | WS_THICKFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX | WS_SYSMENU); - SetWindowLongPtr(handle, GWL_STYLE, new IntPtr(style)); - } - - private static IntPtr ResolveDesktopIconHost() - { - var topLevelWindows = new List(); - EnumWindows((handle, _) => - { - topLevelWindows.Add(handle); - return true; - }, IntPtr.Zero); - - foreach (var topLevelWindow in topLevelWindows) - { - var worker = FindWindowEx(topLevelWindow, IntPtr.Zero, "WorkerW", null); - if (worker == IntPtr.Zero) - { - continue; - } - - var defView = FindWindowEx(worker, IntPtr.Zero, "SHELLDLL_DefView", null); - if (defView != IntPtr.Zero) - { - return defView; - } - } - - foreach (var topLevelWindow in topLevelWindows) - { - var defView = FindWindowEx(topLevelWindow, IntPtr.Zero, "SHELLDLL_DefView", null); - if (defView != IntPtr.Zero) - { - return defView; - } - } - - return IntPtr.Zero; - } - - private static IntPtr GetWindowHandle(Window window) - { - try - { - return window.TryGetPlatformHandle()?.Handle ?? IntPtr.Zero; - } - catch - { - return IntPtr.Zero; - } - } - - private sealed record WindowRestoreState(IntPtr Parent, IntPtr Style, IntPtr ExStyle); - - private delegate bool EnumWindowsProc(IntPtr handle, IntPtr lParam); - - [DllImport("user32.dll", EntryPoint = "GetWindowLongPtr")] - private static extern IntPtr GetWindowLongPtr(IntPtr hWnd, int nIndex); - - [DllImport("user32.dll", EntryPoint = "SetWindowLongPtr")] - private static extern IntPtr SetWindowLongPtr(IntPtr hWnd, int nIndex, IntPtr dwNewLong); - - [DllImport("user32.dll")] - private static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int x, int y, int cx, int cy, uint flags); - - [DllImport("user32.dll", SetLastError = true)] - private static extern IntPtr SetParent(IntPtr hWndChild, IntPtr hWndNewParent); - - [DllImport("user32.dll")] - private static extern IntPtr GetParent(IntPtr hWnd); - - [DllImport("user32.dll")] - private static extern bool IsWindow(IntPtr hWnd); - - [DllImport("user32.dll", SetLastError = true)] - private static extern IntPtr FindWindowEx(IntPtr hParent, IntPtr hChildAfter, string? lpszClass, string? lpszWindow); - - [DllImport("user32.dll", SetLastError = true)] - [return: MarshalAs(UnmanagedType.Bool)] - private static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam); -} - -internal sealed class NullMainWindowDesktopLayerService : IMainWindowDesktopLayerService -{ - public bool IsSupported => false; - - public void EnableOrRefresh(Window window) - { - AppLogger.Info("MainWindowDesktopLayer", "Desktop layer requested on an unsupported platform."); - } - - public void Disable(Window window) - { - } -} diff --git a/LanMountainDesktop/Services/PlatformLogBridge.cs b/LanMountainDesktop/Services/PlatformLogBridge.cs new file mode 100644 index 0000000..cb022d1 --- /dev/null +++ b/LanMountainDesktop/Services/PlatformLogBridge.cs @@ -0,0 +1,35 @@ +using System; +using LanMountainDesktop.Platform.Abstractions; + +namespace LanMountainDesktop.Services; + +/// +/// 将平台层日志(PlatformLog)接入宿主 AppLogger。 +/// 在应用启动早期调用 一次。 +/// +internal static class PlatformLogBridge +{ + private static bool _installed; + + public static void Install() + { + if (_installed) + { + return; + } + + _installed = true; + PlatformLog.SetSink(new AppLoggerSink()); + } + + private sealed class AppLoggerSink : IPlatformLogSink + { + public void Info(string category, string message) => AppLogger.Info(category, message); + + public void Warn(string category, string message, Exception? exception = null) => + AppLogger.Warn(category, message, exception); + + public void Error(string category, string message, Exception? exception = null) => + AppLogger.Error(category, message, exception); + } +} diff --git a/LanMountainDesktop/Services/PowerManagementService.cs b/LanMountainDesktop/Services/PowerManagementService.cs index 2adc836..831cb76 100644 --- a/LanMountainDesktop/Services/PowerManagementService.cs +++ b/LanMountainDesktop/Services/PowerManagementService.cs @@ -1,33 +1,17 @@ using System; using System.Diagnostics; -using System.Runtime.InteropServices; using System.Threading.Tasks; +using LanMountainDesktop.Platform.Abstractions; +using LanMountainDesktop.Platform.Windows; namespace LanMountainDesktop.Services; -public interface IPowerManagementService -{ - bool IsShutdownSupported { get; } - bool IsRestartSupported { get; } - bool IsLogoutSupported { get; } - bool IsLockSupported { get; } - bool IsSleepSupported { get; } - - Task ShutdownAsync(); - Task RestartAsync(); - Task LogoutAsync(); - Task LockAsync(); - Task SleepAsync(); - - void ShowNativePowerUI(PowerAction action); -} - -public enum PowerAction -{ - Shutdown, - Restart -} - +/// +/// 电源管理服务工厂。接口与实现位于平台层: +/// 接口 在 Platform.Abstractions, +/// Windows 实现(P/Invoke)在 Platform.Windows。 +/// Linux 实现基于 systemctl/loginctl 命令行,无平台专属 API,保留在宿主内。 +/// public static class PowerManagementServiceFactory { private static IPowerManagementService? _instance; @@ -51,103 +35,6 @@ public static class PowerManagementServiceFactory } } -internal sealed class WindowsPowerManagementService : IPowerManagementService -{ - public bool IsShutdownSupported => true; - public bool IsRestartSupported => true; - public bool IsLogoutSupported => true; - public bool IsLockSupported => true; - public bool IsSleepSupported => true; - - public async Task ShutdownAsync() - { - await Task.Run(() => - { - Process.Start(new ProcessStartInfo - { - FileName = "shutdown", - Arguments = "/s /t 0", - UseShellExecute = true, - WindowStyle = ProcessWindowStyle.Hidden - }); - }); - } - - public async Task RestartAsync() - { - await Task.Run(() => - { - Process.Start(new ProcessStartInfo - { - FileName = "shutdown", - Arguments = "/r /t 0", - UseShellExecute = true, - WindowStyle = ProcessWindowStyle.Hidden - }); - }); - } - - public async Task LogoutAsync() - { - await Task.Run(() => - { - ExitWindowsEx(0, 0); - }); - } - - public async Task LockAsync() - { - await Task.Run(() => - { - LockWorkStation(); - }); - } - - public async Task SleepAsync() - { - await Task.Run(() => - { - SetSuspendState(false, false, false); - }); - } - - public void ShowNativePowerUI(PowerAction action) - { - // SlideToShutDown.exe 只支持关机,不支持重启 - // 重启操作应该通过 RestartAsync() 使用 shutdown /r 命令 - if (action != PowerAction.Shutdown) - return; - - var slideToShutDownPath = Environment.ExpandEnvironmentVariables(@"%windir%\System32\SlideToShutDown.exe"); - if (System.IO.File.Exists(slideToShutDownPath)) - { - Process.Start(new ProcessStartInfo - { - FileName = slideToShutDownPath, - UseShellExecute = true - }); - return; - } - - // 回退到标准关机命令 - Process.Start(new ProcessStartInfo - { - FileName = "shutdown", - Arguments = "/s /t 5 /c \"LanMountainDesktop: Shutting down...\"", - UseShellExecute = true - }); - } - - [DllImport("user32.dll", SetLastError = true)] - private static extern bool ExitWindowsEx(uint uFlags, uint dwReason); - - [DllImport("user32.dll")] - private static extern void LockWorkStation(); - - [DllImport("powrprof.dll", SetLastError = true)] - private static extern bool SetSuspendState(bool hibernate, bool forceCritical, bool disableWakeEvent); -} - internal sealed class LinuxPowerManagementService : IPowerManagementService { public bool IsShutdownSupported => true; @@ -226,20 +113,3 @@ internal sealed class LinuxPowerManagementService : IPowerManagementService }); } } - -internal sealed class NullPowerManagementService : IPowerManagementService -{ - public bool IsShutdownSupported => false; - public bool IsRestartSupported => false; - public bool IsLogoutSupported => false; - public bool IsLockSupported => false; - public bool IsSleepSupported => false; - - public Task ShutdownAsync() => Task.CompletedTask; - public Task RestartAsync() => Task.CompletedTask; - public Task LogoutAsync() => Task.CompletedTask; - public Task LockAsync() => Task.CompletedTask; - public Task SleepAsync() => Task.CompletedTask; - - public void ShowNativePowerUI(PowerAction action) { } -} diff --git a/LanMountainDesktop/Services/WindowPassthroughService.cs b/LanMountainDesktop/Services/WindowPassthroughService.cs index 745b20f..d6473d6 100644 --- a/LanMountainDesktop/Services/WindowPassthroughService.cs +++ b/LanMountainDesktop/Services/WindowPassthroughService.cs @@ -1,36 +1,14 @@ using System; -using System.Collections.Generic; -using System.Runtime.InteropServices; -using System.Threading; -using Avalonia; -using Avalonia.Controls; -using Avalonia.Threading; +using LanMountainDesktop.Platform.Abstractions; +using LanMountainDesktop.Platform.Windows; namespace LanMountainDesktop.Services; -public interface IWindowBottomMostService -{ - void SetupBottomMost(Window window); - void SendToBottom(Window window); - PixelPoint GetScreenPosition(Window window); - bool SetScreenPosition(Window window, PixelPoint position, bool queueOnFailure = false); - bool IsBottomMostSupported { get; } -} - -public readonly record struct WindowInteractiveRegion( - Rect Bounds, - double CornerRadius, - Matrix? ClientToRegionTransform = null, - Rect? ClientClipBounds = null, - double ClientClipCornerRadius = 0d); - -public interface IRegionPassthroughService -{ - void SetInteractiveRegions(Window window, IReadOnlyList interactiveRegions); - void ClearInteractiveRegions(Window window); - bool IsRegionPassthroughSupported { get; } -} - +/// +/// 窗口置底/区域穿透服务工厂。接口与实现位于平台层: +/// 接口(IWindowBottomMostService / IRegionPassthroughService / WindowInteractiveRegion) +/// 在 Platform.Abstractions,Windows 实现(P/Invoke)在 Platform.Windows。 +/// public static class WindowBottomMostServiceFactory { private static IWindowBottomMostService? _instance; @@ -40,9 +18,15 @@ public static class WindowBottomMostServiceFactory { lock (_lock) { - return _instance ??= OperatingSystem.IsWindows() - ? new WindowsWindowBottomMostService() - : new NullWindowBottomMostService(); + if (_instance is null) + { + PlatformLogBridge.Install(); + _instance = OperatingSystem.IsWindows() + ? new WindowsWindowBottomMostService() + : new NullWindowBottomMostService(); + } + + return _instance; } } } @@ -56,1350 +40,15 @@ public static class RegionPassthroughServiceFactory { lock (_lock) { - return _instance ??= OperatingSystem.IsWindows() - ? new WindowsRegionPassthroughService() - : new NullRegionPassthroughService(); + if (_instance is null) + { + PlatformLogBridge.Install(); + _instance = OperatingSystem.IsWindows() + ? new WindowsRegionPassthroughService() + : new NullRegionPassthroughService(); + } + + return _instance; } } } - -internal sealed class WindowsWindowBottomMostService : IWindowBottomMostService -{ - private const int GWL_STYLE = -16; - private const int GWL_EXSTYLE = -20; - private const int GWLP_HWNDPARENT = -8; - - private const uint WS_CHILD = 0x40000000U; - private const uint WS_POPUP = 0x80000000U; - private const uint WS_CAPTION = 0x00C00000U; - private const uint WS_THICKFRAME = 0x00040000U; - private const uint WS_MINIMIZEBOX = 0x00020000U; - private const uint WS_MAXIMIZEBOX = 0x00010000U; - private const uint WS_SYSMENU = 0x00080000U; - - private const uint WS_EX_TOOLWINDOW = 0x00000080U; - private const uint WS_EX_APPWINDOW = 0x00040000U; - private const uint WS_EX_NOACTIVATE = 0x08000000U; - private const uint WS_EX_NOREDIRECTIONBITMAP = 0x00200000U; - private const uint AVALONIA_COMPOSITION_EXSTYLE_MASK = WS_EX_NOREDIRECTIONBITMAP; - - private const uint SWP_NOSIZE = 0x0001; - private const uint SWP_NOMOVE = 0x0002; - private const uint SWP_NOZORDER = 0x0004; - private const uint SWP_NOACTIVATE = 0x0010; - private const uint SWP_FRAMECHANGED = 0x0020; - private const uint SWP_SHOWWINDOW = 0x0040; - private const uint SWP_HIDEWINDOW = 0x0080; - - private const uint WM_NCHITTEST = 0x0084; - private const int HTTRANSPARENT = -1; - private const int HTCLIENT = 1; - - private const int DWMWA_WINDOW_CORNER_PREFERENCE = 33; - private const int DWMWA_BORDER_COLOR = 34; - private const uint DWMWCP_DONOTROUND = 1; - private const uint DWMWA_COLOR_NONE = 0xFFFFFFFEU; - - private static readonly IntPtr HWND_TOP = IntPtr.Zero; - private static readonly IntPtr HWND_BOTTOM = new(1); - private static readonly object StaticLock = new(); - private static readonly object TimerLock = new(); - - private static readonly Dictionary WindowStates = new(); - - private static System.Timers.Timer? _desktopHostMonitorTimer; - private static IntPtr _lastResolvedDesktopHost; - private static int _monitorDispatchPending; - - public bool IsBottomMostSupported => true; - - public void SetupBottomMost(Window window) - { - ArgumentNullException.ThrowIfNull(window); - if (!OperatingSystem.IsWindows()) - { - return; - } - - DesktopWindowState state; - lock (StaticLock) - { - if (WindowStates.TryGetValue(window, out state!)) - { - return; - } - - state = new DesktopWindowState(window); - WindowStates[window] = state; - } - - Win32Properties.SetWindowCornerPreference(window, Win32Properties.WindowCornerPreference.DoNotRound); - Win32Properties.AddWindowStylesCallback(window, state.WindowStylesCallback); - Win32Properties.AddWndProcHookCallback(window, state.WndProcHookCallback); - - window.Closed += OnWindowClosed; - - var handle = GetWindowHandle(window); - if (handle == IntPtr.Zero) - { - window.Opened += OnWindowOpened; - return; - } - - RunOnUiThread(() => InitializeAndAttach(state, handle, logSuccess: true)); - } - - public void SendToBottom(Window window) - { - ArgumentNullException.ThrowIfNull(window); - if (!TryGetWindowState(window, out var state)) - { - SetupBottomMost(window); - return; - } - - RunOnUiThread(() => - { - var handle = GetWindowHandle(window); - if (handle == IntPtr.Zero || !IsWindow(handle)) - { - return; - } - - RegisterHandle(state, handle); - if (state.NeedsNativeRepair && - !ShouldAttemptNativeRepair( - false, - DateTime.UtcNow, - state.NextNativeRepairAttemptUtc)) - { - return; - } - - var desktopHost = ResolveDesktopIconHost(); - if (!state.NeedsNativeRepair && - state.IsDesktopAttached && - state.HasStableDesktopAttachment && - desktopHost != IntPtr.Zero && - state.DesktopHost == desktopHost && - GetParent(handle) == desktopHost && - state.OriginalState is { } originalState && - HasExpectedDesktopRoleStyles(handle, originalState)) - { - _ = SetWindowPos( - handle, - HWND_TOP, - 0, - 0, - 0, - 0, - SWP_NOSIZE | SWP_NOMOVE | SWP_NOACTIVATE | SWP_SHOWWINDOW); - return; - } - - ApplyDesktopAttachment( - state, - desktopHost, - logSuccess: false, - "explicit refresh", - allowRetryFailedHost: true); - }); - } - - public PixelPoint GetScreenPosition(Window window) - { - ArgumentNullException.ThrowIfNull(window); - var handle = GetWindowHandle(window); - return handle != IntPtr.Zero && GetWindowRect(handle, out var rect) - ? new PixelPoint(rect.Left, rect.Top) - : window.Position; - } - - public bool SetScreenPosition( - Window window, - PixelPoint position, - bool queueOnFailure = false) - { - ArgumentNullException.ThrowIfNull(window); - TryGetWindowState(window, out var state); - var handle = GetWindowHandle(window); - if (handle == IntPtr.Zero || !IsWindow(handle)) - { - window.Position = position; - if (state is not null) - { - state.PendingScreenPosition = null; - state.HasLoggedPositionFailure = false; - } - - return true; - } - - var nativePosition = new POINT(position.X, position.Y); - var style = ReadWindowStyle(handle, GWL_STYLE); - var nativeParent = GetParent(handle); - if (state is not null) - { - if (state.NeedsNativeRepair) - { - if (queueOnFailure) - { - state.PendingScreenPosition = position; - } - - return false; - } - - if (state.IsDesktopAttached) - { - if (state.OriginalState is not { } originalState || - nativeParent != state.DesktopHost || - !HasExpectedDesktopRoleStyles(handle, originalState)) - { - LogPositionFailureOnce( - state, - $"Refusing to move a desktop window with invalid native attachment state. " + - $"Window={handle}; Parent={nativeParent}; ExpectedHost={state.DesktopHost}."); - if (queueOnFailure) - { - state.PendingScreenPosition = position; - } - - return false; - } - - nativeParent = state.DesktopHost; - } - } - - if (OriginalWindowUsesParentClientCoordinates(style) && - (nativeParent == IntPtr.Zero || !ScreenToClient(nativeParent, ref nativePosition))) - { - if (state is not null) - { - LogPositionFailureOnce( - state, - $"Could not translate screen position to child-window coordinates. " + - $"Window={handle}; Parent={nativeParent}."); - if (queueOnFailure) - { - state.PendingScreenPosition = position; - } - } - return false; - } - - if (!SetWindowPos( - handle, - IntPtr.Zero, - nativePosition.X, - nativePosition.Y, - 0, - 0, - SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE)) - { - if (state is not null) - { - LogPositionFailureOnce( - state, - $"Could not set screen position. Window={handle}; Position={position}; " + - $"Error={Marshal.GetLastWin32Error()}."); - if (queueOnFailure) - { - state.PendingScreenPosition = position; - } - } - return false; - } - - if (state is not null) - { - state.PendingScreenPosition = null; - state.HasLoggedPositionFailure = false; - } - - return true; - } - - private static void LogPositionFailureOnce(DesktopWindowState state, string message) - { - if (state.HasLoggedPositionFailure) - { - return; - } - - AppLogger.Warn("WindowBottomMost", message); - state.HasLoggedPositionFailure = true; - } - - private static void TryApplyPendingScreenPosition(DesktopWindowState state) - { - if (state.NeedsNativeRepair || state.PendingScreenPosition is not { } pendingPosition) - { - return; - } - - _ = new WindowsWindowBottomMostService().SetScreenPosition( - state.Window, - pendingPosition, - queueOnFailure: true); - } - - internal static void SetInteractiveRegionsInternal( - Window window, - IReadOnlyList regions) - { - if (!TryGetWindowState(window, out var state)) - { - return; - } - - var snapshot = new WindowInteractiveRegion[regions.Count]; - for (var i = 0; i < regions.Count; i++) - { - snapshot[i] = regions[i]; - } - - state.InteractiveRegions = snapshot; - } - - internal static (uint Style, uint ExStyle) CreateDesktopChildStyles(uint style, uint exStyle) - { - style |= WS_CHILD; - style &= ~(WS_POPUP | WS_CAPTION | WS_THICKFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX | WS_SYSMENU); - return (style, ApplyDesktopRoleExtendedStyles(exStyle)); - } - - internal static bool IsPointInsideRegion(WindowInteractiveRegion region, Point point) - { - if (region.ClientClipBounds is { } clientClipBounds && - !IsPointInsideRoundedBounds(clientClipBounds, region.ClientClipCornerRadius, point)) - { - return false; - } - - if (region.ClientToRegionTransform is { } clientToRegionTransform) - { - point = clientToRegionTransform.Transform(point); - } - - return IsPointInsideRoundedBounds(region.Bounds, region.CornerRadius, point); - } - - private static bool IsPointInsideRoundedBounds(Rect bounds, double cornerRadius, Point point) - { - if (bounds.Width <= 0 || bounds.Height <= 0 || !bounds.Contains(point)) - { - return false; - } - - var radius = Math.Clamp(cornerRadius, 0, Math.Min(bounds.Width, bounds.Height) / 2); - if (radius <= 0) - { - return true; - } - - var localX = point.X - bounds.X; - var localY = point.Y - bounds.Y; - if (localX >= radius && localX <= bounds.Width - radius || - localY >= radius && localY <= bounds.Height - radius) - { - return true; - } - - var centerX = localX < radius ? radius : bounds.Width - radius; - var centerY = localY < radius ? radius : bounds.Height - radius; - var deltaX = localX - centerX; - var deltaY = localY - centerY; - return deltaX * deltaX + deltaY * deltaY <= radius * radius; - } - - internal static bool OriginalWindowUsesParentClientCoordinates(uint style) - { - return (style & WS_CHILD) != 0; - } - - internal static bool ShouldAttemptNativeRepair( - bool hostChanged, - DateTime utcNow, - DateTime nextRepairAttemptUtc) - { - return hostChanged || utcNow >= nextRepairAttemptUtc; - } - - internal static bool ShouldAttemptDesktopAttachment( - bool hostChanged, - IntPtr attachedHost, - IntPtr currentHost, - bool parentMismatch) - { - return parentMismatch || (hostChanged && attachedHost != currentHost); - } - - private static uint ApplyDesktopRoleExtendedStyles(uint exStyle) - { - // Preserve every compositor-managed bit from Avalonia. In particular, this method must - // never opt the window into a second, legacy alpha-composition path. - return (exStyle | WS_EX_TOOLWINDOW | WS_EX_NOACTIVATE) & ~WS_EX_APPWINDOW; - } - - private static void OnWindowOpened(object? sender, EventArgs e) - { - if (sender is not Window window || !TryGetWindowState(window, out var state)) - { - return; - } - - window.Opened -= OnWindowOpened; - var handle = GetWindowHandle(window); - if (handle != IntPtr.Zero) - { - RunOnUiThread(() => InitializeAndAttach(state, handle, logSuccess: true)); - } - } - - private static void OnWindowClosed(object? sender, EventArgs e) - { - if (sender is Window window && TryGetWindowState(window, out var state)) - { - CleanupWindow(state, restoreNativeState: true); - } - } - - private static void InitializeAndAttach(DesktopWindowState state, IntPtr handle, bool logSuccess) - { - if (handle == IntPtr.Zero || !IsWindow(handle)) - { - return; - } - - RegisterHandle(state, handle); - ConfigureDwmAppearance(handle); - ApplyDesktopAttachment(state, ResolveDesktopIconHost(), logSuccess, "initial setup"); - } - - private static void RegisterHandle(DesktopWindowState state, IntPtr handle) - { - lock (StaticLock) - { - if (state.Handle != IntPtr.Zero && state.Handle != handle) - { - state.OriginalState = null; - state.DesktopHost = IntPtr.Zero; - state.IsDesktopAttached = false; - state.HasStableDesktopAttachment = false; - ResetNativeRepairState(state); - state.AttachToCurrentHostAfterRepair = false; - state.HasLoggedFallback = false; - state.HasLoggedPositionFailure = false; - } - - state.Handle = handle; - state.OriginalState ??= new NativeWindowState( - GetParent(handle), - ReadWindowStyle(handle, GWL_STYLE), - ReadWindowStyle(handle, GWL_EXSTYLE)); - } - } - - private static void ApplyDesktopAttachment( - DesktopWindowState state, - IntPtr desktopHost, - bool logSuccess, - string reason, - bool allowRetryFailedHost = false) - { - var handle = state.Handle; - if (handle == IntPtr.Zero || !IsWindow(handle) || state.OriginalState is not { } originalState) - { - return; - } - - var screenPosition = GetNativeScreenPosition(handle, state.Window.Position); - if (state.NeedsNativeRepair) - { - var failedHost = state.FailedDesktopHost; - var attachAfterRepair = state.AttachToCurrentHostAfterRepair; - FallBackToBottom( - state, - screenPosition, - "repairing an incomplete native rollback before attachment", - logSuccess: false, - failedHost); - if (state.NeedsNativeRepair) - { - StartDesktopHostMonitorTimer(desktopHost); - return; - } - - state.AttachToCurrentHostAfterRepair = false; - if (!attachAfterRepair && desktopHost == failedHost && !allowRetryFailedHost) - { - // The failed host has not changed. Stay in the safe top-level fallback until - // Explorer changes or the caller explicitly requests another attachment. - StartDesktopHostMonitorTimer(desktopHost); - return; - } - } - - var beforeStyle = ReadWindowStyle(handle, GWL_STYLE); - var beforeExStyle = ReadWindowStyle(handle, GWL_EXSTYLE); - - if (desktopHost == IntPtr.Zero || !IsWindow(desktopHost)) - { - FallBackToBottom( - state, - screenPosition, - "desktop icon host is unavailable", - logSuccess, - desktopHost); - StartDesktopHostMonitorTimer(desktopHost); - return; - } - - if (state.IsDesktopAttached) - { - if (state.DesktopHost == desktopHost && GetParent(handle) == desktopHost) - { - if (HasExpectedDesktopRoleStyles(handle, originalState)) - { - StartDesktopHostMonitorTimer(desktopHost); - return; - } - - FallBackToBottom( - state, - screenPosition, - "desktop child style validation failed", - logSuccess, - desktopHost); - StartDesktopHostMonitorTimer(desktopHost); - return; - } - - var hostBeingDetached = state.DesktopHost; - if (!TryRestoreNativeState(state, screenPosition, showWindow: true)) - { - FallBackToBottom( - state, - screenPosition, - "failed to restore before remount", - logSuccess, - hostBeingDetached); - if (state.NeedsNativeRepair && desktopHost != hostBeingDetached) - { - state.AttachToCurrentHostAfterRepair = true; - } - - if (state.NeedsNativeRepair || - (desktopHost == hostBeingDetached && !allowRetryFailedHost)) - { - StartDesktopHostMonitorTimer(desktopHost); - return; - } - } - - beforeStyle = ReadWindowStyle(handle, GWL_STYLE); - beforeExStyle = ReadWindowStyle(handle, GWL_EXSTYLE); - } - - beforeExStyle |= originalState.ExStyle & AVALONIA_COMPOSITION_EXSTYLE_MASK; - var (expectedStyle, expectedExStyle) = CreateDesktopChildStyles(beforeStyle, beforeExStyle); - WriteWindowStyle(handle, GWL_STYLE, expectedStyle); - WriteWindowStyle(handle, GWL_EXSTYLE, expectedExStyle); - - _ = SetParent(handle, desktopHost); - var setParentError = Marshal.GetLastWin32Error(); - if (GetParent(handle) != desktopHost) - { - FallBackToBottom( - state, - screenPosition, - $"SetParent failed with error {setParentError}", - logSuccess, - desktopHost); - StartDesktopHostMonitorTimer(desktopHost); - return; - } - - state.DesktopHost = desktopHost; - state.IsDesktopAttached = true; - state.HasStableDesktopAttachment = false; - - var childPosition = new POINT(screenPosition.X, screenPosition.Y); - if (!ScreenToClient(desktopHost, ref childPosition) || - !SetWindowPos( - handle, - HWND_TOP, - childPosition.X, - childPosition.Y, - 0, - 0, - SWP_NOSIZE | SWP_NOACTIVATE | SWP_FRAMECHANGED | SWP_SHOWWINDOW) || - !HasExpectedDesktopStyles(handle, expectedStyle, expectedExStyle) || - GetParent(handle) != desktopHost) - { - var error = Marshal.GetLastWin32Error(); - FallBackToBottom( - state, - screenPosition, - $"post-attachment validation failed with error {error}", - logSuccess, - desktopHost); - StartDesktopHostMonitorTimer(desktopHost); - return; - } - - ResetNativeRepairState(state); - state.AttachToCurrentHostAfterRepair = false; - state.HasLoggedFallback = false; - state.HasStableDesktopAttachment = true; - ConfigureDwmAppearance(handle); - if (logSuccess) - { - var afterStyle = ReadWindowStyle(handle, GWL_STYLE); - var afterExStyle = ReadWindowStyle(handle, GWL_EXSTYLE); - AppLogger.Info( - "WindowBottomMost", - $"Mounted window to desktop icon host. Window={handle}; Host={desktopHost}; Reason={reason}; " + - $"Style=0x{beforeStyle:X8}->0x{afterStyle:X8}; ExStyle=0x{beforeExStyle:X8}->0x{afterExStyle:X8}; " + - $"NoRedirectionBitmap={((afterExStyle & WS_EX_NOREDIRECTIONBITMAP) != 0)}."); - } - - StartDesktopHostMonitorTimer(desktopHost); - } - - private static void FallBackToBottom( - DesktopWindowState state, - PixelPoint screenPosition, - string reason, - bool logSuccess, - IntPtr failedDesktopHost) - { - var handle = state.Handle; - var wasAttached = state.IsDesktopAttached; - var wasStablyAttached = state.HasStableDesktopAttachment; - var restored = true; - if (state.OriginalState is { } originalState && - (state.NeedsNativeRepair || - wasAttached || - GetParent(handle) != originalState.Parent || - ReadWindowStyle(handle, GWL_STYLE) != originalState.Style || - ReadWindowStyle(handle, GWL_EXSTYLE) != originalState.ExStyle)) - { - restored = TryRestoreNativeState(state, screenPosition, showWindow: true); - } - - if (!restored) - { - MarkNativeRepairPending(state, failedDesktopHost); - if (logSuccess || !state.HasLoggedRepairFailure) - { - AppLogger.Warn( - "WindowBottomMost", - $"Native rollback is incomplete; keeping the window in repair state. " + - $"Window={handle}; FailedHost={failedDesktopHost}; Reason={reason}."); - state.HasLoggedRepairFailure = true; - } - - return; - } - - state.DesktopHost = IntPtr.Zero; - state.IsDesktopAttached = false; - state.HasStableDesktopAttachment = false; - - if (IsWindow(handle) && - !SetWindowPos( - handle, - HWND_BOTTOM, - 0, - 0, - 0, - 0, - SWP_NOSIZE | SWP_NOMOVE | SWP_NOACTIVATE | SWP_SHOWWINDOW)) - { - MarkNativeRepairPending(state, failedDesktopHost); - if (logSuccess || !state.HasLoggedRepairFailure) - { - AppLogger.Warn( - "WindowBottomMost", - $"Native state was restored, but HWND_BOTTOM fallback positioning failed. " + - $"Window={handle}; FailedHost={failedDesktopHost}; Reason={reason}; " + - $"Error={Marshal.GetLastWin32Error()}."); - state.HasLoggedRepairFailure = true; - } - - return; - } - - ResetNativeRepairState(state); - - if (logSuccess || wasStablyAttached || !state.HasLoggedFallback) - { - AppLogger.Warn( - "WindowBottomMost", - $"Using HWND_BOTTOM fallback. Window={handle}; Reason={reason}; NativeStateRestored={restored}."); - state.HasLoggedFallback = true; - } - } - - private static void MarkNativeRepairPending(DesktopWindowState state, IntPtr failedDesktopHost) - { - state.FailedDesktopHost = failedDesktopHost; - state.NativeRepairAttemptCount = Math.Min(state.NativeRepairAttemptCount + 1, 30); - var exponent = Math.Min(state.NativeRepairAttemptCount - 1, 5); - var delaySeconds = Math.Min(60, 2 * (1 << exponent)); - state.NextNativeRepairAttemptUtc = DateTime.UtcNow.AddSeconds(delaySeconds); - state.NeedsNativeRepair = true; - - if (state.Handle != IntPtr.Zero && IsWindow(state.Handle)) - { - _ = SetWindowPos( - state.Handle, - IntPtr.Zero, - 0, - 0, - 0, - 0, - SWP_NOSIZE | SWP_NOMOVE | SWP_NOZORDER | SWP_NOACTIVATE | SWP_HIDEWINDOW); - } - } - - private static void ResetNativeRepairState(DesktopWindowState state) - { - state.NeedsNativeRepair = false; - state.FailedDesktopHost = IntPtr.Zero; - state.HasLoggedRepairFailure = false; - state.NativeRepairAttemptCount = 0; - state.NextNativeRepairAttemptUtc = DateTime.MinValue; - } - - private static bool TryRestoreNativeState( - DesktopWindowState state, - PixelPoint screenPosition, - bool showWindow) - { - var handle = state.Handle; - if (handle == IntPtr.Zero || !IsWindow(handle) || state.OriginalState is not { } originalState) - { - return false; - } - - var previousDesktopHost = state.DesktopHost; - var wasDesktopAttached = state.IsDesktopAttached; - var wasStablyDesktopAttached = state.HasStableDesktopAttachment; - state.IsDesktopAttached = false; - state.HasStableDesktopAttachment = false; - state.DesktopHost = IntPtr.Zero; - - var restoreParentOrOwner = originalState.Parent != IntPtr.Zero && !IsWindow(originalState.Parent) - ? IntPtr.Zero - : originalState.Parent; - var restorePosition = new POINT(screenPosition.X, screenPosition.Y); - - if (OriginalWindowUsesParentClientCoordinates(originalState.Style)) - { - _ = SetParent(handle, restoreParentOrOwner); - WriteWindowStyle(handle, GWL_STYLE, originalState.Style); - WriteWindowStyle(handle, GWL_EXSTYLE, originalState.ExStyle); - if (restoreParentOrOwner != IntPtr.Zero && - !ScreenToClient(restoreParentOrOwner, ref restorePosition)) - { - restorePosition = new POINT(screenPosition.X, screenPosition.Y); - } - } - else - { - // GetParent returns an owner for a top-level popup. It is not a child-coordinate - // parent: first leave the desktop child hierarchy, restore the popup styles, then - // restore the owner through GWLP_HWNDPARENT and keep SetWindowPos in screen pixels. - _ = SetParent(handle, IntPtr.Zero); - WriteWindowStyle(handle, GWL_STYLE, originalState.Style); - WriteWindowStyle(handle, GWL_EXSTYLE, originalState.ExStyle); - _ = SetWindowLongPtr(handle, GWLP_HWNDPARENT, restoreParentOrOwner); - } - - var flags = SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED; - if (showWindow) - { - flags |= SWP_SHOWWINDOW; - } - - var positioned = SetWindowPos( - handle, - IntPtr.Zero, - restorePosition.X, - restorePosition.Y, - 0, - 0, - flags); - - var hierarchyAndStylesRestored = - GetParent(handle) == restoreParentOrOwner && - ReadWindowStyle(handle, GWL_STYLE) == originalState.Style && - ReadWindowStyle(handle, GWL_EXSTYLE) == originalState.ExStyle; - if (!hierarchyAndStylesRestored) - { - // Preserve the logical attachment state until a later repair attempt succeeds. - // This prevents the monitor from treating a half-converted child HWND as a valid - // top-level fallback. - state.DesktopHost = previousDesktopHost; - state.IsDesktopAttached = wasDesktopAttached; - state.HasStableDesktopAttachment = wasStablyDesktopAttached; - } - - return hierarchyAndStylesRestored && positioned; - } - - private static bool HasExpectedDesktopStyles(IntPtr handle, uint style, uint exStyle) - { - return ReadWindowStyle(handle, GWL_STYLE) == style && - ReadWindowStyle(handle, GWL_EXSTYLE) == exStyle; - } - - private static bool HasExpectedDesktopRoleStyles(IntPtr handle, NativeWindowState originalState) - { - var style = ReadWindowStyle(handle, GWL_STYLE); - var exStyle = ReadWindowStyle(handle, GWL_EXSTYLE); - var expected = CreateDesktopChildStyles(style, exStyle); - var requiredAvaloniaBits = originalState.ExStyle & AVALONIA_COMPOSITION_EXSTYLE_MASK; - return style == expected.Style && - exStyle == expected.ExStyle && - (exStyle & requiredAvaloniaBits) == requiredAvaloniaBits; - } - - private static void ConfigureDwmAppearance(IntPtr handle) - { - if (handle == IntPtr.Zero || !IsWindow(handle)) - { - return; - } - - try - { - var cornerPreference = DWMWCP_DONOTROUND; - _ = DwmSetWindowAttribute( - handle, - DWMWA_WINDOW_CORNER_PREFERENCE, - ref cornerPreference, - sizeof(uint)); - - var borderColor = DWMWA_COLOR_NONE; - _ = DwmSetWindowAttribute(handle, DWMWA_BORDER_COLOR, ref borderColor, sizeof(uint)); - } - catch (Exception ex) when (ex is DllNotFoundException or EntryPointNotFoundException) - { - // These attributes are best-effort and unavailable on older Windows versions. - } - } - - private static IntPtr ResolveDesktopIconHost() - { - var topLevelWindows = new List(); - EnumWindows((handle, _) => - { - topLevelWindows.Add(handle); - return true; - }, IntPtr.Zero); - - foreach (var topLevelWindow in topLevelWindows) - { - var worker = FindWindowEx(topLevelWindow, IntPtr.Zero, "WorkerW", null); - if (worker == IntPtr.Zero) - { - continue; - } - - var defView = FindWindowEx(worker, IntPtr.Zero, "SHELLDLL_DefView", null); - if (defView != IntPtr.Zero) - { - return defView; - } - } - - foreach (var topLevelWindow in topLevelWindows) - { - var defView = FindWindowEx(topLevelWindow, IntPtr.Zero, "SHELLDLL_DefView", null); - if (defView != IntPtr.Zero) - { - return defView; - } - } - - return IntPtr.Zero; - } - - private static void StartDesktopHostMonitorTimer(IntPtr currentHost) - { - lock (TimerLock) - { - if (_desktopHostMonitorTimer != null) - { - return; - } - - _lastResolvedDesktopHost = currentHost; - _desktopHostMonitorTimer = new System.Timers.Timer(TimeSpan.FromSeconds(2)) - { - AutoReset = true - }; - _desktopHostMonitorTimer.Elapsed += (_, _) => MonitorDesktopHostAttachments(); - _desktopHostMonitorTimer.Start(); - } - } - - private static void MonitorDesktopHostAttachments() - { - var desktopHost = ResolveDesktopIconHost(); - var hostChanged = false; - lock (TimerLock) - { - if (desktopHost != _lastResolvedDesktopHost) - { - _lastResolvedDesktopHost = desktopHost; - hostChanged = true; - } - } - - List states; - lock (StaticLock) - { - states = [.. WindowStates.Values]; - } - - var requiresCleanupOrRepair = false; - var now = DateTime.UtcNow; - foreach (var state in states) - { - if ((state.NeedsNativeRepair && now >= state.NextNativeRepairAttemptUtc) || - (!state.NeedsNativeRepair && state.PendingScreenPosition.HasValue) || - state.Handle != IntPtr.Zero && !IsWindow(state.Handle) || - state.IsDesktopAttached && - (GetParent(state.Handle) != state.DesktopHost || - state.OriginalState is not { } originalState || - !HasExpectedDesktopRoleStyles(state.Handle, originalState))) - { - requiresCleanupOrRepair = true; - break; - } - } - - if (!hostChanged && !requiresCleanupOrRepair || - Interlocked.Exchange(ref _monitorDispatchPending, 1) != 0) - { - return; - } - - Dispatcher.UIThread.Post(() => - { - try - { - var currentHost = ResolveDesktopIconHost(); - var effectiveHostChanged = hostChanged || currentHost != desktopHost; - List currentStates; - lock (StaticLock) - { - currentStates = [.. WindowStates.Values]; - } - - foreach (var state in currentStates) - { - if (state.Handle == IntPtr.Zero) - { - // A window can be registered before Avalonia creates its native handle. - continue; - } - - if (!IsWindow(state.Handle)) - { - CleanupWindow(state, restoreNativeState: false); - continue; - } - - if (state.NeedsNativeRepair) - { - if (!ShouldAttemptNativeRepair( - effectiveHostChanged, - DateTime.UtcNow, - state.NextNativeRepairAttemptUtc)) - { - continue; - } - - var failedHost = state.FailedDesktopHost; - var attachAfterRepair = state.AttachToCurrentHostAfterRepair; - var screenPosition = GetNativeScreenPosition(state.Handle, state.Window.Position); - FallBackToBottom( - state, - screenPosition, - "retrying incomplete native rollback", - logSuccess: false, - failedHost); - if (state.NeedsNativeRepair) - { - continue; - } - - state.AttachToCurrentHostAfterRepair = false; - if (currentHost != IntPtr.Zero && - (attachAfterRepair || - effectiveHostChanged && currentHost != failedHost)) - { - ApplyDesktopAttachment( - state, - currentHost, - logSuccess: true, - "desktop host changed while native state was being repaired"); - } - - TryApplyPendingScreenPosition(state); - continue; - } - - var attachmentDrift = state.IsDesktopAttached && - (GetParent(state.Handle) != state.DesktopHost || - state.OriginalState is not { } originalState || - !HasExpectedDesktopRoleStyles(state.Handle, originalState)); - if (ShouldAttemptDesktopAttachment( - effectiveHostChanged, - state.DesktopHost, - currentHost, - attachmentDrift)) - { - ApplyDesktopAttachment(state, currentHost, logSuccess: true, "desktop host changed"); - } - - TryApplyPendingScreenPosition(state); - } - - lock (TimerLock) - { - _lastResolvedDesktopHost = currentHost; - } - } - finally - { - Interlocked.Exchange(ref _monitorDispatchPending, 0); - } - }, DispatcherPriority.Background); - } - - private static void CleanupWindow(DesktopWindowState state, bool restoreNativeState) - { - state.Window.Opened -= OnWindowOpened; - state.Window.Closed -= OnWindowClosed; - Win32Properties.RemoveWindowStylesCallback(state.Window, state.WindowStylesCallback); - Win32Properties.RemoveWndProcHookCallback(state.Window, state.WndProcHookCallback); - - if (restoreNativeState && state.Handle != IntPtr.Zero && IsWindow(state.Handle)) - { - var screenPosition = GetNativeScreenPosition(state.Handle, state.Window.Position); - _ = TryRestoreNativeState(state, screenPosition, showWindow: false); - } - - lock (StaticLock) - { - WindowStates.Remove(state.Window); - } - - StopDesktopHostMonitorTimerIfIdle(); - } - - private static void StopDesktopHostMonitorTimerIfIdle() - { - lock (StaticLock) - { - if (WindowStates.Count > 0) - { - return; - } - } - - lock (TimerLock) - { - _desktopHostMonitorTimer?.Stop(); - _desktopHostMonitorTimer?.Dispose(); - _desktopHostMonitorTimer = null; - _lastResolvedDesktopHost = IntPtr.Zero; - } - } - - private static IntPtr HandleWindowMessage( - DesktopWindowState state, - IntPtr hWnd, - uint message, - IntPtr wParam, - IntPtr lParam, - ref bool handled) - { - if (message != WM_NCHITTEST) - { - return IntPtr.Zero; - } - - if (state.NeedsNativeRepair) - { - handled = true; - return (IntPtr)HTTRANSPARENT; - } - - var screenPoint = new POINT( - unchecked((short)(lParam.ToInt64() & 0xFFFF)), - unchecked((short)((lParam.ToInt64() >> 16) & 0xFFFF))); - if (!ScreenToClient(hWnd, ref screenPoint)) - { - handled = true; - return (IntPtr)HTTRANSPARENT; - } - - var point = ConvertPhysicalClientPointToDip( - new Point(screenPoint.X, screenPoint.Y), - GetWindowDpiScale(hWnd)); - var regions = state.InteractiveRegions; - foreach (var region in regions) - { - if (IsPointInsideRegion(region, point)) - { - handled = true; - return (IntPtr)HTCLIENT; - } - } - - handled = true; - return (IntPtr)HTTRANSPARENT; - } - - internal static Point ConvertPhysicalClientPointToDip(Point physicalPoint, double dpiScale) - { - var scale = double.IsFinite(dpiScale) ? Math.Max(0.1, dpiScale) : 1d; - return new Point(physicalPoint.X / scale, physicalPoint.Y / scale); - } - - private static double GetWindowDpiScale(IntPtr handle) - { - try - { - var dpi = GetDpiForWindow(handle); - return dpi > 0 ? dpi / 96.0 : 1.0; - } - catch (EntryPointNotFoundException) - { - return 1.0; - } - } - - private static PixelPoint GetNativeScreenPosition(IntPtr handle, PixelPoint fallback) - { - return GetWindowRect(handle, out var rect) - ? new PixelPoint(rect.Left, rect.Top) - : fallback; - } - - private static bool TryGetWindowState(Window window, out DesktopWindowState state) - { - lock (StaticLock) - { - return WindowStates.TryGetValue(window, out state!); - } - } - - private static void RunOnUiThread(Action action) - { - if (Dispatcher.UIThread.CheckAccess()) - { - action(); - } - else - { - Dispatcher.UIThread.Post(action); - } - } - - private static uint ReadWindowStyle(IntPtr handle, int index) - { - return unchecked((uint)GetWindowLongPtr(handle, index).ToInt64()); - } - - private static void WriteWindowStyle(IntPtr handle, int index, uint value) - { - _ = SetWindowLongPtr(handle, index, new IntPtr(unchecked((int)value))); - } - - private static IntPtr GetWindowHandle(Window window) - { - try - { - return window.TryGetPlatformHandle()?.Handle ?? IntPtr.Zero; - } - catch - { - return IntPtr.Zero; - } - } - - [StructLayout(LayoutKind.Sequential)] - private struct RECT - { - public int Left; - public int Top; - public int Right; - public int Bottom; - } - - [StructLayout(LayoutKind.Sequential)] - private struct POINT(int x, int y) - { - public int X = x; - public int Y = y; - } - - private sealed class DesktopWindowState - { - public DesktopWindowState(Window window) - { - Window = window; - WindowStylesCallback = ApplyWindowStyles; - WndProcHookCallback = ProcessWindowMessage; - } - - public Window Window { get; } - public IntPtr Handle { get; set; } - public NativeWindowState? OriginalState { get; set; } - public IntPtr DesktopHost { get; set; } - public volatile bool IsDesktopAttached; - public volatile bool HasStableDesktopAttachment; - public volatile bool NeedsNativeRepair; - public IntPtr FailedDesktopHost { get; set; } - public bool AttachToCurrentHostAfterRepair { get; set; } - public int NativeRepairAttemptCount { get; set; } - public DateTime NextNativeRepairAttemptUtc { get; set; } - public PixelPoint? PendingScreenPosition { get; set; } - public bool HasLoggedPositionFailure { get; set; } - public bool HasLoggedFallback { get; set; } - public bool HasLoggedRepairFailure { get; set; } - public volatile WindowInteractiveRegion[] InteractiveRegions = []; - public Win32Properties.CustomWindowStylesCallback WindowStylesCallback { get; } - public Win32Properties.CustomWndProcHookCallback WndProcHookCallback { get; } - - private (uint style, uint exStyle) ApplyWindowStyles(uint style, uint exStyle) - { - return IsDesktopAttached - ? CreateDesktopChildStyles(style, exStyle) - : (style, ApplyDesktopRoleExtendedStyles(exStyle)); - } - - private IntPtr ProcessWindowMessage( - IntPtr hWnd, - uint message, - IntPtr wParam, - IntPtr lParam, - ref bool handled) - { - return HandleWindowMessage(this, hWnd, message, wParam, lParam, ref handled); - } - } - - private readonly record struct NativeWindowState(IntPtr Parent, uint Style, uint ExStyle); - - private delegate bool EnumWindowsProc(IntPtr handle, IntPtr lParam); - - [DllImport("user32.dll")] - [return: MarshalAs(UnmanagedType.Bool)] - private static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect); - - [DllImport("user32.dll", EntryPoint = "GetWindowLongPtr", SetLastError = true)] - private static extern IntPtr GetWindowLongPtr(IntPtr hWnd, int nIndex); - - [DllImport("user32.dll", EntryPoint = "SetWindowLongPtr", SetLastError = true)] - private static extern IntPtr SetWindowLongPtr(IntPtr hWnd, int nIndex, IntPtr dwNewLong); - - [DllImport("user32.dll", SetLastError = true)] - [return: MarshalAs(UnmanagedType.Bool)] - private static extern bool SetWindowPos( - IntPtr hWnd, - IntPtr hWndInsertAfter, - int x, - int y, - int cx, - int cy, - uint flags); - - [DllImport("user32.dll", SetLastError = true)] - private static extern IntPtr SetParent(IntPtr hWndChild, IntPtr hWndNewParent); - - [DllImport("user32.dll")] - private static extern IntPtr GetParent(IntPtr hWnd); - - [DllImport("user32.dll")] - [return: MarshalAs(UnmanagedType.Bool)] - private static extern bool IsWindow(IntPtr hWnd); - - [DllImport("user32.dll", SetLastError = true)] - [return: MarshalAs(UnmanagedType.Bool)] - private static extern bool ScreenToClient(IntPtr hWnd, ref POINT lpPoint); - - [DllImport("user32.dll", SetLastError = true)] - private static extern IntPtr FindWindowEx( - IntPtr hParent, - IntPtr hChildAfter, - string? lpszClass, - string? lpszWindow); - - [DllImport("user32.dll", SetLastError = true)] - [return: MarshalAs(UnmanagedType.Bool)] - private static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam); - - [DllImport("user32.dll")] - private static extern uint GetDpiForWindow(IntPtr hWnd); - - [DllImport("dwmapi.dll")] - private static extern int DwmSetWindowAttribute( - IntPtr hWnd, - int dwAttribute, - ref uint pvAttribute, - int cbAttribute); -} - -internal sealed class WindowsRegionPassthroughService : IRegionPassthroughService -{ - public bool IsRegionPassthroughSupported => true; - - public void SetInteractiveRegions( - Window window, - IReadOnlyList interactiveRegions) - { - ArgumentNullException.ThrowIfNull(window); - ArgumentNullException.ThrowIfNull(interactiveRegions); - WindowsWindowBottomMostService.SetInteractiveRegionsInternal(window, interactiveRegions); - } - - public void ClearInteractiveRegions(Window window) - { - ArgumentNullException.ThrowIfNull(window); - WindowsWindowBottomMostService.SetInteractiveRegionsInternal(window, []); - } -} - -internal sealed class NullWindowBottomMostService : IWindowBottomMostService -{ - public bool IsBottomMostSupported => false; - public void SetupBottomMost(Window window) { } - public void SendToBottom(Window window) { } - public PixelPoint GetScreenPosition(Window window) => window.Position; - public bool SetScreenPosition( - Window window, - PixelPoint position, - bool queueOnFailure = false) - { - window.Position = position; - return true; - } -} - -internal sealed class NullRegionPassthroughService : IRegionPassthroughService -{ - public bool IsRegionPassthroughSupported => false; - public void SetInteractiveRegions(Window window, IReadOnlyList interactiveRegions) { } - public void ClearInteractiveRegions(Window window) { } -} diff --git a/LanMountainDesktop/Services/WindowsNativeDialogService.cs b/LanMountainDesktop/Services/WindowsNativeDialogService.cs index 02edc02..bf13d7a 100644 --- a/LanMountainDesktop/Services/WindowsNativeDialogService.cs +++ b/LanMountainDesktop/Services/WindowsNativeDialogService.cs @@ -1,41 +1,32 @@ using System; -using System.Runtime.InteropServices; +using LanMountainDesktop.Platform.Windows; namespace LanMountainDesktop.Services; +/// +/// 原生对话框门面。P/Invoke 实现位于 Platform.Windows(WindowsNativeDialogs)。 +/// internal static class WindowsNativeDialogService { - private const uint Ok = 0x00000000; - private const uint IconInformation = 0x00000040; - private const uint IconWarning = 0x00000030; - public static void ShowInformation(string caption, string message) { - Show(caption, message, Ok | IconInformation, "NativeDialog"); + Show(caption, message, WindowsNativeDialogs.Ok | WindowsNativeDialogs.IconInformation, "NativeDialog"); } public static void ShowWarning(string caption, string message) { - Show(caption, message, Ok | IconWarning, "StartupDiagnostics"); + Show(caption, message, WindowsNativeDialogs.Ok | WindowsNativeDialogs.IconWarning, "StartupDiagnostics"); } private static void Show(string caption, string message, uint type, string logCategory) { - if (!OperatingSystem.IsWindows()) - { - return; - } - try { - _ = MessageBoxW(IntPtr.Zero, message, caption, type); + WindowsNativeDialogs.Show(caption, message, type); } catch (Exception ex) { AppLogger.Warn(logCategory, "Failed to show native dialog.", ex); } } - - [DllImport("user32.dll", EntryPoint = "MessageBoxW", CharSet = CharSet.Unicode, SetLastError = true)] - private static extern int MessageBoxW(IntPtr hWnd, string text, string caption, uint type); } diff --git a/LanMountainDesktop/Services/WindowsNotificationListener.cs b/LanMountainDesktop/Services/WindowsNotificationListener.cs index 6e9fdbd..5f80b5c 100644 --- a/LanMountainDesktop/Services/WindowsNotificationListener.cs +++ b/LanMountainDesktop/Services/WindowsNotificationListener.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -8,6 +8,7 @@ using System.Text; using System.Threading; using System.Threading.Tasks; using LanMountainDesktop.Models; +using LanMountainDesktop.Platform.Windows; namespace LanMountainDesktop.Services; @@ -320,26 +321,7 @@ internal sealed class WindowsNotificationListener : IPlatformNotificationListene private static bool HasPackageIdentity() { - if (!OperatingSystem.IsWindows()) - { - return false; - } - - var length = 0; - var hr = GetCurrentPackageFullName(ref length, null); - if (hr == AppmodelErrorNoPackage) - { - return false; - } - - if (length <= 0) - { - return hr == 0; - } - - var builder = new StringBuilder(length); - hr = GetCurrentPackageFullName(ref length, builder); - return hr == 0; + return WindowsPackageIdentity.HasPackageIdentity(); } private static string TryReadPackageFamilyName(object? appInfo) @@ -497,8 +479,4 @@ internal sealed class WindowsNotificationListener : IPlatformNotificationListene _cts.Dispose(); } - private const int AppmodelErrorNoPackage = 15700; - - [DllImport("kernel32.dll", CharSet = CharSet.Unicode)] - private static extern int GetCurrentPackageFullName(ref int packageFullNameLength, StringBuilder? packageFullName); } diff --git a/LanMountainDesktop/Services/WindowsStartMenuService.cs b/LanMountainDesktop/Services/WindowsStartMenuService.cs index 10a89f9..2214c3d 100644 --- a/LanMountainDesktop/Services/WindowsStartMenuService.cs +++ b/LanMountainDesktop/Services/WindowsStartMenuService.cs @@ -4,6 +4,7 @@ using System.Globalization; using System.IO; using System.Linq; using LanMountainDesktop.Models; +using LanMountainDesktop.Platform.Windows; namespace LanMountainDesktop.Services; diff --git a/LanMountainDesktop/Views/Components/FileManagerWidget.axaml.cs b/LanMountainDesktop/Views/Components/FileManagerWidget.axaml.cs index 620cd70..b6f568b 100644 --- a/LanMountainDesktop/Views/Components/FileManagerWidget.axaml.cs +++ b/LanMountainDesktop/Views/Components/FileManagerWidget.axaml.cs @@ -14,6 +14,8 @@ using Avalonia.Platform; using FluentIcons.Avalonia; using LanMountainDesktop.ComponentSystem; using LanMountainDesktop.Models; +using LanMountainDesktop.Platform.MacOS; +using LanMountainDesktop.Platform.Windows; using LanMountainDesktop.PluginSdk; using LanMountainDesktop.Services; diff --git a/LanMountainDesktop/Views/Components/ShortcutWidget.axaml.cs b/LanMountainDesktop/Views/Components/ShortcutWidget.axaml.cs index 4992960..638b86f 100644 --- a/LanMountainDesktop/Views/Components/ShortcutWidget.axaml.cs +++ b/LanMountainDesktop/Views/Components/ShortcutWidget.axaml.cs @@ -11,6 +11,8 @@ using Avalonia.Media.Imaging; using FluentIcons.Avalonia; using LanMountainDesktop.ComponentSystem; using LanMountainDesktop.Models; +using LanMountainDesktop.Platform.MacOS; +using LanMountainDesktop.Platform.Windows; using LanMountainDesktop.PluginSdk; using LanMountainDesktop.Services; diff --git a/LanMountainDesktop/Views/DesktopWidgetWindow.axaml.cs b/LanMountainDesktop/Views/DesktopWidgetWindow.axaml.cs index fb8f70f..a1a99a7 100644 --- a/LanMountainDesktop/Views/DesktopWidgetWindow.axaml.cs +++ b/LanMountainDesktop/Views/DesktopWidgetWindow.axaml.cs @@ -7,6 +7,7 @@ using Avalonia.Media; using Avalonia.Threading; using LanMountainDesktop.DesktopEditing; using LanMountainDesktop.Models; +using LanMountainDesktop.Platform.Abstractions; using LanMountainDesktop.Services; using LanMountainDesktop.Services.Settings; diff --git a/LanMountainDesktop/Views/FusedDesktopComponentLibraryWindow.axaml.cs b/LanMountainDesktop/Views/FusedDesktopComponentLibraryWindow.axaml.cs index d8112a3..99c5645 100644 --- a/LanMountainDesktop/Views/FusedDesktopComponentLibraryWindow.axaml.cs +++ b/LanMountainDesktop/Views/FusedDesktopComponentLibraryWindow.axaml.cs @@ -1,11 +1,11 @@ using System; -using System.Runtime.InteropServices; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Input; using Avalonia.Interactivity; using LanMountainDesktop.Appearance; +using LanMountainDesktop.Platform.Windows; using LanMountainDesktop.Services; using LanMountainDesktop.Services.Settings; using LanMountainDesktop.Settings.Core; @@ -14,9 +14,6 @@ namespace LanMountainDesktop.Views; public partial class FusedDesktopComponentLibraryWindow : Window { - private const int DwmWindowAttributeBorderColor = 34; - private const uint DwmColorNone = 0xFFFFFFFE; - private static readonly LocalizationService LocalizationService = new(); public FusedDesktopComponentLibraryWindow() @@ -124,30 +121,8 @@ public partial class FusedDesktopComponentLibraryWindow : Window private void TryDisableNativeWindowBorder() { - if (!OperatingSystem.IsWindowsVersionAtLeast(10, 0, 22000)) - { - return; - } - - try - { - var handle = TryGetPlatformHandle()?.Handle ?? IntPtr.Zero; - if (handle == IntPtr.Zero) - { - return; - } - - var borderColor = DwmColorNone; - _ = DwmSetWindowAttribute( - handle, - DwmWindowAttributeBorderColor, - ref borderColor, - sizeof(uint)); - } - catch - { - // DWM attributes are best-effort and unavailable on older/unsupported Windows builds. - } + var handle = TryGetPlatformHandle()?.Handle ?? IntPtr.Zero; + WindowsDwmInterop.TryDisableWindowBorder(handle); } protected override void OnClosed(EventArgs e) @@ -163,11 +138,4 @@ public partial class FusedDesktopComponentLibraryWindow : Window var mainWindow = (Application.Current?.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime)?.MainWindow as MainWindow; mainWindow?.UnregisterFusedLibraryWindow(this); } - - [DllImport("dwmapi.dll")] - private static extern int DwmSetWindowAttribute( - IntPtr windowHandle, - int attribute, - ref uint attributeValue, - int attributeSize); } diff --git a/LanMountainDesktop/Views/MainWindow.ComponentSystem.cs b/LanMountainDesktop/Views/MainWindow.ComponentSystem.cs index a6da705..286bfc4 100644 --- a/LanMountainDesktop/Views/MainWindow.ComponentSystem.cs +++ b/LanMountainDesktop/Views/MainWindow.ComponentSystem.cs @@ -18,6 +18,7 @@ using LanMountainDesktop.ComponentSystem; using LanMountainDesktop.DesktopEditing; using LanMountainDesktop.Host.Abstractions; using LanMountainDesktop.Models; +using LanMountainDesktop.Platform.Abstractions; using LanMountainDesktop.Services; using LanMountainDesktop.Settings.Core; using LanMountainDesktop.Theme; diff --git a/LanMountainDesktop/Views/MainWindow.DesktopPaging.cs b/LanMountainDesktop/Views/MainWindow.DesktopPaging.cs index a676328..7b515f2 100644 --- a/LanMountainDesktop/Views/MainWindow.DesktopPaging.cs +++ b/LanMountainDesktop/Views/MainWindow.DesktopPaging.cs @@ -16,6 +16,7 @@ using Avalonia.Threading; using Avalonia.VisualTree; using FluentAvalonia.UI.Controls; using LanMountainDesktop.Models; +using LanMountainDesktop.Platform.Windows; using LanMountainDesktop.PluginSdk; using LanMountainDesktop.Services; using LanMountainDesktop.Theme;