Files
LanMountainDesktop/LanMountainDesktop/Services/WindowPassthroughService.cs

380 lines
13 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Collections;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using Avalonia;
using Avalonia.Controls;
namespace LanMountainDesktop.Services;
/// <summary>
/// 窗口置底服务接口
/// </summary>
public interface IWindowBottomMostService
{
void SetupBottomMost(Window window);
void SendToBottom(Window window);
bool IsBottomMostSupported { get; }
}
/// <summary>
/// 区域级穿透服务接口 - 使用 WM_NCHITTEST 实现
/// </summary>
public interface IRegionPassthroughService
{
/// <summary>
/// 设置窗口的可交互区域
/// </summary>
void SetInteractiveRegions(Window window, IReadOnlyList<Rect> interactiveRegions);
/// <summary>
/// 清除所有可交互区域
/// </summary>
void ClearInteractiveRegions(Window window);
/// <summary>
/// 获取当前平台是否支持区域级穿透
/// </summary>
bool IsRegionPassthroughSupported { get; }
}
/// <summary>
/// 窗口置底服务工厂
/// </summary>
public static class WindowBottomMostServiceFactory
{
private static IWindowBottomMostService? _instance;
private static readonly object _lock = new();
public static IWindowBottomMostService GetOrCreate()
{
lock (_lock)
{
return _instance ??= OperatingSystem.IsWindows()
? new WindowsWindowBottomMostService()
: new NullWindowBottomMostService();
}
}
}
/// <summary>
/// 区域级穿透服务工厂
/// </summary>
public static class RegionPassthroughServiceFactory
{
private static IRegionPassthroughService? _instance;
private static readonly object _lock = new();
public static IRegionPassthroughService GetOrCreate()
{
lock (_lock)
{
return _instance ??= OperatingSystem.IsWindows()
? new WindowsRegionPassthroughService()
: new NullRegionPassthroughService();
}
}
}
/// <summary>
/// Windows 平台窗口置底服务
/// </summary>
internal sealed class WindowsWindowBottomMostService : IWindowBottomMostService
{
private const int GWL_EXSTYLE = -20;
private const int GWL_HWNDPARENT = -8;
private const int GWLP_WNDPROC = -4;
private const int WS_EX_TOOLWINDOW = 0x00000080;
private const int WS_EX_APPWINDOW = 0x00040000;
private const int WS_EX_NOACTIVATE = 0x08000000;
private const int WS_EX_LAYERED = 0x00080000;
private const uint SWP_NOSIZE = 0x0001;
private const uint SWP_NOMOVE = 0x0002;
private const uint SWP_NOACTIVATE = 0x0010;
private const int WM_WINDOWPOSCHANGING = 0x0046;
private const int WM_NCHITTEST = 0x0084;
private const int HTTRANSPARENT = -1;
private const int HTCLIENT = 1;
private static readonly IntPtr HWND_BOTTOM = new(1);
private static readonly Dictionary<IntPtr, bool> _bottomMostWindows = new();
private static readonly Dictionary<IntPtr, IntPtr> _originalWndProcs = new();
private static readonly Dictionary<IntPtr, List<Rect>> _interactiveRegions = new();
// 记录每个窗口的屏幕原点(窗口左上角的屏幕坐标),用于将 WM_NCHITTEST 屏幕坐标转成窗口相对坐标
private static readonly Dictionary<IntPtr, Point> _windowScreenOrigins = new();
private static readonly object _staticLock = new();
public bool IsBottomMostSupported => true;
public void SetupBottomMost(Window window)
{
if (!OperatingSystem.IsWindows()) return;
window.Opened += (s, e) =>
{
var handle = GetWindowHandle(window);
if (handle == IntPtr.Zero) return;
// 设置扩展样式
var exStyle = GetWindowLong(handle, GWL_EXSTYLE);
exStyle = (exStyle | WS_EX_TOOLWINDOW | WS_EX_NOACTIVATE | WS_EX_LAYERED) & ~WS_EX_APPWINDOW;
SetWindowLong(handle, GWL_EXSTYLE, exStyle);
// 设置为桌面子窗口
SetAsDesktopChild(handle);
// 注册置底状态 & 记录窗口屏幕原点
lock (_staticLock)
{
_bottomMostWindows[handle] = true;
_interactiveRegions[handle] = [];
UpdateWindowScreenOrigin(handle);
}
// 注入消息钩子
InstallMessageHook(handle);
// 初始置底
SendToBottomInternal(handle);
AppLogger.Info("WindowBottomMost", $"Window setup as bottom-most: {handle}");
};
window.Closed += (s, e) =>
{
var handle = GetWindowHandle(window);
if (handle != IntPtr.Zero)
{
lock (_staticLock)
{
_bottomMostWindows.Remove(handle);
_originalWndProcs.Remove(handle);
_interactiveRegions.Remove(handle);
_windowScreenOrigins.Remove(handle);
}
}
};
}
public void SendToBottom(Window window)
{
var handle = GetWindowHandle(window);
if (handle != IntPtr.Zero) SendToBottomInternal(handle);
}
private static IntPtr GetWindowHandle(Window window)
{
try { return window.TryGetPlatformHandle()?.Handle ?? IntPtr.Zero; }
catch { return IntPtr.Zero; }
}
private static void SendToBottomInternal(IntPtr handle)
{
SetWindowPos(handle, HWND_BOTTOM, 0, 0, 0, 0, SWP_NOSIZE | SWP_NOMOVE | SWP_NOACTIVATE);
}
private static void SetAsDesktopChild(IntPtr handle)
{
var windowHandles = new ArrayList();
EnumWindows(EnumWindowsCallback, windowHandles);
foreach (IntPtr h in windowHandles)
{
var hDefView = FindWindowEx(h, IntPtr.Zero, "SHELLDLL_DefView", null);
if (hDefView != IntPtr.Zero)
{
SetWindowLong(handle, GWL_HWNDPARENT, hDefView.ToInt32());
break;
}
}
}
private static bool EnumWindowsCallback(IntPtr handle, ArrayList handles)
{
handles.Add(handle);
return true;
}
private static void InstallMessageHook(IntPtr handle)
{
var originalWndProc = GetWindowLongPtr(handle, GWLP_WNDPROC);
if (originalWndProc == IntPtr.Zero) return;
lock (_staticLock)
{
_originalWndProcs[handle] = originalWndProc;
}
SetWindowLongPtr(handle, GWLP_WNDPROC, Marshal.GetFunctionPointerForDelegate<WndProcDelegate>(SubclassWndProc));
}
private static IntPtr SubclassWndProc(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam)
{
// 处理 WM_WINDOWPOSCHANGING - 保持置底
if (msg == WM_WINDOWPOSCHANGING)
{
lock (_staticLock)
{
if (_bottomMostWindows.TryGetValue(hWnd, out var isBottomMost) && isBottomMost)
{
SendToBottomInternal(hWnd);
}
}
}
// 处理 WM_NCHITTEST - 区域级穿透
if (msg == WM_NCHITTEST)
{
// WM_NCHITTEST 的鼠标坐标在 lParam低16位=X高16位=Y且为屏幕坐标
var screenX = (short)(lParam.ToInt64() & 0xFFFF);
var screenY = (short)((lParam.ToInt64() >> 16) & 0xFFFF);
lock (_staticLock)
{
if (_interactiveRegions.TryGetValue(hWnd, out var regions) && regions.Count > 0)
{
// 将屏幕坐标转为窗口相对坐标_interactiveRegions 存的是窗口内坐标)
_windowScreenOrigins.TryGetValue(hWnd, out var origin);
var clientX = screenX - origin.X;
var clientY = screenY - origin.Y;
var point = new Point(clientX, clientY);
foreach (var region in regions)
{
if (region.Contains(point))
{
// 在可交互区域内,返回 HTCLIENT
return (IntPtr)HTCLIENT;
}
}
}
}
// 不在可交互区域内,返回 HTTRANSPARENT 让事件穿透
return (IntPtr)HTTRANSPARENT;
}
// 调用原始窗口过程
IntPtr originalWndProc;
lock (_staticLock)
{
if (!_originalWndProcs.TryGetValue(hWnd, out originalWndProc))
{
return DefWindowProc(hWnd, msg, wParam, lParam);
}
}
return CallWindowProc(originalWndProc, hWnd, msg, wParam, lParam);
}
/// <summary>
/// 设置窗口的可交互区域(供 WindowsRegionPassthroughService 调用)
/// </summary>
internal static void SetInteractiveRegionsInternal(IntPtr handle, List<Rect> regions)
{
lock (_staticLock)
{
_interactiveRegions[handle] = regions;
// 同步刷新屏幕原点DPI 缩放可能影响坐标,每次更新区域时一并刷新)
UpdateWindowScreenOrigin(handle);
}
}
/// <summary>
/// 更新指定窗口的屏幕左上角坐标缓存(用于将 WM_NCHITTEST 屏幕坐标转为窗口相对坐标)
/// </summary>
private static void UpdateWindowScreenOrigin(IntPtr handle)
{
if (GetWindowRect(handle, out var rect))
{
_windowScreenOrigins[handle] = new Point(rect.Left, rect.Top);
}
}
[StructLayout(LayoutKind.Sequential)]
private struct RECT { public int Left, Top, Right, Bottom; }
[DllImport("user32.dll")]
private static extern bool GetWindowRect(IntPtr hWnd, out RECT lpRect);
private delegate IntPtr WndProcDelegate(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam);
[DllImport("user32.dll", SetLastError = true)]
private static extern int GetWindowLong(IntPtr hWnd, int nIndex);
[DllImport("user32.dll")]
private static extern int SetWindowLong(IntPtr hWnd, int nIndex, int dwNewLong);
[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 FindWindowEx(IntPtr hParent, IntPtr hChildAfter, string? lpszClass, string? lpszWindow);
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, ArrayList lParam);
private delegate bool EnumWindowsProc(IntPtr handle, ArrayList handles);
[DllImport("user32.dll")]
private static extern IntPtr CallWindowProc(IntPtr lpPrevWndFunc, IntPtr hWnd, int Msg, IntPtr wParam, IntPtr lParam);
[DllImport("user32.dll")]
private static extern IntPtr DefWindowProc(IntPtr hWnd, int uMsg, IntPtr wParam, IntPtr lParam);
}
/// <summary>
/// Windows 平台区域级穿透服务 - 使用 WM_NCHITTEST
/// </summary>
internal sealed class WindowsRegionPassthroughService : IRegionPassthroughService
{
public bool IsRegionPassthroughSupported => true;
public void SetInteractiveRegions(Window window, IReadOnlyList<Rect> interactiveRegions)
{
var handle = GetWindowHandle(window);
if (handle == IntPtr.Zero) return;
WindowsWindowBottomMostService.SetInteractiveRegionsInternal(handle, new List<Rect>(interactiveRegions));
AppLogger.Info("RegionPassthrough", $"Set {interactiveRegions.Count} interactive regions.");
}
public void ClearInteractiveRegions(Window window)
{
var handle = GetWindowHandle(window);
if (handle == IntPtr.Zero) return;
WindowsWindowBottomMostService.SetInteractiveRegionsInternal(handle, []);
}
private static IntPtr GetWindowHandle(Window window)
{
try { return window.TryGetPlatformHandle()?.Handle ?? IntPtr.Zero; }
catch { return IntPtr.Zero; }
}
}
/// <summary>
/// 空实现
/// </summary>
internal sealed class NullWindowBottomMostService : IWindowBottomMostService
{
public bool IsBottomMostSupported => false;
public void SetupBottomMost(Window window) { }
public void SendToBottom(Window window) { }
}
internal sealed class NullRegionPassthroughService : IRegionPassthroughService
{
public bool IsRegionPassthroughSupported => false;
public void SetInteractiveRegions(Window window, IReadOnlyList<Rect> interactiveRegions) { }
public void ClearInteractiveRegions(Window window) { }
}