mirror of
https://github.com/wwiinnddyy/LanMountainDesktop.git
synced 2026-08-18 15:33:33 +08:00
refactor: 新建 Platform 平台差异层,主工程 P/Invoke 全部迁出
- 新增 Platform.Abstractions(接口+NoOp+PlatformLog 日志桥)、 Platform.Windows、Platform.MacOS、Platform.Android 四个项目 - 迁移电源管理、原生对话框、桌面层嵌入、窗口置底/区域穿透、 DWM 互操作、图标服务、包标识查询等全部 Windows P/Invoke 实现 - 主工程保留静态工厂门面,调用点不变;DllImport 扫描为零 - 更新 WindowPassthroughServiceTests 指向新位置(20/20 通过)
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace LanMountainDesktop.Platform.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// 主窗口桌面层服务接口。将主窗口嵌入系统桌面图标层(仅 Windows 支持)。
|
||||
/// </summary>
|
||||
public interface IMainWindowDesktopLayerService
|
||||
{
|
||||
bool IsSupported { get; }
|
||||
void EnableOrRefresh(Window window);
|
||||
void Disable(Window window);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 无操作实现。用于不支持桌面层嵌入的平台(Linux/macOS/移动端)。
|
||||
/// </summary>
|
||||
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)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
namespace LanMountainDesktop.Platform.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// 电源管理服务接口。桌面平台提供关机/重启/注销/锁定/睡眠能力;
|
||||
/// 移动平台不提供此能力(使用 <see cref="NullPowerManagementService"/>)。
|
||||
/// </summary>
|
||||
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
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 无操作实现。用于不支持电源管理的平台(如移动端)。
|
||||
/// </summary>
|
||||
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) { }
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
|
||||
namespace LanMountainDesktop.Platform.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// 窗口置底服务接口。使组件窗口保持在桌面层(Z 序最底)。
|
||||
/// </summary>
|
||||
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; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 窗口交互区域定义。区域外的点击穿透到桌面。
|
||||
/// </summary>
|
||||
public readonly record struct WindowInteractiveRegion(
|
||||
Rect Bounds,
|
||||
double CornerRadius,
|
||||
Matrix? ClientToRegionTransform = null,
|
||||
Rect? ClientClipBounds = null,
|
||||
double ClientClipCornerRadius = 0d);
|
||||
|
||||
/// <summary>
|
||||
/// 区域点击穿透服务接口。
|
||||
/// </summary>
|
||||
public interface IRegionPassthroughService
|
||||
{
|
||||
void SetInteractiveRegions(Window window, IReadOnlyList<WindowInteractiveRegion> interactiveRegions);
|
||||
void ClearInteractiveRegions(Window window);
|
||||
bool IsRegionPassthroughSupported { get; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 无操作实现:非 Windows 平台窗口置底不可用。
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 无操作实现:非 Windows 平台区域穿透不可用。
|
||||
/// </summary>
|
||||
public sealed class NullRegionPassthroughService : IRegionPassthroughService
|
||||
{
|
||||
public bool IsRegionPassthroughSupported => false;
|
||||
|
||||
public void SetInteractiveRegions(Window window, IReadOnlyList<WindowInteractiveRegion> interactiveRegions) { }
|
||||
|
||||
public void ClearInteractiveRegions(Window window) { }
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Avalonia" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
35
LanMountainDesktop.Platform.Abstractions/PlatformLog.cs
Normal file
35
LanMountainDesktop.Platform.Abstractions/PlatformLog.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
namespace LanMountainDesktop.Platform.Abstractions;
|
||||
|
||||
/// <summary>
|
||||
/// 平台层日志桥。平台实现项目不引用宿主,
|
||||
/// 宿主在启动时通过 <see cref="SetSink"/> 接入自己的日志系统(AppLogger)。
|
||||
/// 未接入时日志静默丢弃。
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 平台层日志输出目标。
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0-android</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<SupportedOSPlatformVersion>24</SupportedOSPlatformVersion>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\LanMountainDesktop.Platform.Abstractions\LanMountainDesktop.Platform.Abstractions.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Avalonia" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,15 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\LanMountainDesktop.Platform.Abstractions\LanMountainDesktop.Platform.Abstractions.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="System.Drawing.Common" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -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;
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\LanMountainDesktop.Platform.Abstractions\LanMountainDesktop.Platform.Abstractions.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Avalonia" />
|
||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" />
|
||||
<PackageReference Include="System.Drawing.Common" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -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;
|
||||
45
LanMountainDesktop.Platform.Windows/WindowsDwmInterop.cs
Normal file
45
LanMountainDesktop.Platform.Windows/WindowsDwmInterop.cs
Normal file
@@ -0,0 +1,45 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace LanMountainDesktop.Platform.Windows;
|
||||
|
||||
/// <summary>
|
||||
/// DWM(桌面窗口管理器)互操作封装。
|
||||
/// </summary>
|
||||
public static class WindowsDwmInterop
|
||||
{
|
||||
public const int WindowAttributeBorderColor = 34;
|
||||
public const uint ColorNone = 0xFFFFFFFE;
|
||||
|
||||
/// <summary>
|
||||
/// 移除窗口原生边框颜色(Windows 11 22000+)。
|
||||
/// 失败静默忽略(DWM 属性为尽力而为语义)。
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
@@ -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;
|
||||
@@ -0,0 +1,238 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using Avalonia.Controls;
|
||||
using LanMountainDesktop.Platform.Abstractions;
|
||||
|
||||
namespace LanMountainDesktop.Platform.Windows;
|
||||
|
||||
/// <summary>
|
||||
/// Windows 桌面层实现:将窗口设为桌面图标宿主(SHELLDLL_DefView)的子窗口。
|
||||
/// 自 LanMountainDesktop.Services.MainWindowDesktopLayerService 迁移,行为不变。
|
||||
/// </summary>
|
||||
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<IntPtr, WindowRestoreState> _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<IntPtr>();
|
||||
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);
|
||||
}
|
||||
30
LanMountainDesktop.Platform.Windows/WindowsNativeDialogs.cs
Normal file
30
LanMountainDesktop.Platform.Windows/WindowsNativeDialogs.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace LanMountainDesktop.Platform.Windows;
|
||||
|
||||
/// <summary>
|
||||
/// Windows 原生消息框(MessageBoxW)封装。
|
||||
/// 供宿主在 UI 框架尚不可用(启动早期)时显示诊断信息。
|
||||
/// </summary>
|
||||
public static class WindowsNativeDialogs
|
||||
{
|
||||
public const uint Ok = 0x00000000;
|
||||
public const uint IconInformation = 0x00000040;
|
||||
public const uint IconWarning = 0x00000030;
|
||||
|
||||
/// <summary>
|
||||
/// 显示原生消息框。仅在 Windows 上有效,其他平台为空操作。
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
|
||||
namespace LanMountainDesktop.Platform.Windows;
|
||||
|
||||
/// <summary>
|
||||
/// Windows 包标识查询(MSIX/UWP 打包检测)。
|
||||
/// </summary>
|
||||
public static class WindowsPackageIdentity
|
||||
{
|
||||
private const int AppmodelErrorNoPackage = 15700;
|
||||
|
||||
/// <summary>
|
||||
/// 检测当前进程是否具有包标识(以 MSIX 打包运行)。
|
||||
/// 非 Windows 平台返回 false。
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
using LanMountainDesktop.Platform.Abstractions;
|
||||
|
||||
namespace LanMountainDesktop.Platform.Windows;
|
||||
|
||||
/// <summary>
|
||||
/// Windows 电源管理实现(P/Invoke user32/powrprof)。
|
||||
/// 自 LanMountainDesktop.Services.PowerManagementService 迁移,行为不变。
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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<bool>("OriginalWindowUsesParentClientCoordinates", WsPopup));
|
||||
Assert.False(Invoke<bool>("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<T>(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);
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
<Solution>
|
||||
<Project Path="LanMountainDesktop.Platform.Abstractions/LanMountainDesktop.Platform.Abstractions.csproj" />
|
||||
<Project Path="LanMountainDesktop.Platform.Windows/LanMountainDesktop.Platform.Windows.csproj" />
|
||||
<Project Path="LanMountainDesktop.Platform.Android/LanMountainDesktop.Platform.Android.csproj" />
|
||||
<Project Path="LanMountainDesktop.Platform.MacOS/LanMountainDesktop.Platform.MacOS.csproj" />
|
||||
<Project Path="LanMountainDesktop.Host.Abstractions/LanMountainDesktop.Host.Abstractions.csproj" />
|
||||
<Project Path="LanMountainDesktop.Shared.Contracts/LanMountainDesktop.Shared.Contracts.csproj" />
|
||||
<Project Path="LanMountainDesktop.Shared.IPC/LanMountainDesktop.Shared.IPC.csproj" />
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -30,6 +30,9 @@
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\LanMountainDesktop.Platform.Abstractions\LanMountainDesktop.Platform.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\LanMountainDesktop.Platform.Windows\LanMountainDesktop.Platform.Windows.csproj" />
|
||||
<ProjectReference Include="..\LanMountainDesktop.Platform.MacOS\LanMountainDesktop.Platform.MacOS.csproj" />
|
||||
<ProjectReference Include="..\LanMountainDesktop.Host.Abstractions\LanMountainDesktop.Host.Abstractions.csproj" />
|
||||
<ProjectReference Include="..\LanMountainDesktop.Shared.Contracts\LanMountainDesktop.Shared.Contracts.csproj" />
|
||||
<ProjectReference Include="..\LanMountainDesktop.Shared.IPC\LanMountainDesktop.Shared.IPC.csproj" />
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 桌面层服务工厂。接口与实现位于平台层:
|
||||
/// 接口 <see cref="IMainWindowDesktopLayerService"/> 在 Platform.Abstractions,
|
||||
/// Windows 实现(P/Invoke)在 Platform.Windows。
|
||||
/// </summary>
|
||||
public static class MainWindowDesktopLayerServiceFactory
|
||||
{
|
||||
private static readonly object Gate = new();
|
||||
@@ -21,252 +18,15 @@ public static class MainWindowDesktopLayerServiceFactory
|
||||
{
|
||||
lock (Gate)
|
||||
{
|
||||
return _instance ??= OperatingSystem.IsWindows()
|
||||
if (_instance is null)
|
||||
{
|
||||
PlatformLogBridge.Install();
|
||||
_instance = OperatingSystem.IsWindows()
|
||||
? new WindowsMainWindowDesktopLayerService()
|
||||
: new NullMainWindowDesktopLayerService();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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<IntPtr, WindowRestoreState> _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<IntPtr>();
|
||||
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)
|
||||
{
|
||||
|
||||
return _instance;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
35
LanMountainDesktop/Services/PlatformLogBridge.cs
Normal file
35
LanMountainDesktop/Services/PlatformLogBridge.cs
Normal file
@@ -0,0 +1,35 @@
|
||||
using System;
|
||||
using LanMountainDesktop.Platform.Abstractions;
|
||||
|
||||
namespace LanMountainDesktop.Services;
|
||||
|
||||
/// <summary>
|
||||
/// 将平台层日志(PlatformLog)接入宿主 AppLogger。
|
||||
/// 在应用启动早期调用 <see cref="Install"/> 一次。
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 电源管理服务工厂。接口与实现位于平台层:
|
||||
/// 接口 <see cref="IPowerManagementService"/> 在 Platform.Abstractions,
|
||||
/// Windows 实现(P/Invoke)在 Platform.Windows。
|
||||
/// Linux 实现基于 systemctl/loginctl 命令行,无平台专属 API,保留在宿主内。
|
||||
/// </summary>
|
||||
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) { }
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,41 +1,32 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using LanMountainDesktop.Platform.Windows;
|
||||
|
||||
namespace LanMountainDesktop.Services;
|
||||
|
||||
/// <summary>
|
||||
/// 原生对话框门面。P/Invoke 实现位于 Platform.Windows(WindowsNativeDialogs)。
|
||||
/// </summary>
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using LanMountainDesktop.Models;
|
||||
using LanMountainDesktop.Platform.Windows;
|
||||
|
||||
namespace LanMountainDesktop.Services;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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()
|
||||
@@ -123,31 +120,9 @@ 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.
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user