using System.Runtime.InteropServices; using XFEEDToolBox.Core.Session; namespace XFEEDToolBox.Services; public sealed class WindowsGlobalHotkeys : IHotkeyRegistry, IDisposable { private readonly nint window; private readonly SubclassProc callback; private readonly GlobalHotkeyBindings bindings; private readonly Action action; private bool disposed; private static int nextSubclass = 0x584600; private readonly nuint subclassId = (nuint)Interlocked.Increment(ref nextSubclass); public WindowsGlobalHotkeys(nint window, Action action) { this.window = window; this.action = action; bindings = new(this); callback = OnMessage; if (!SetWindowSubclass(window, callback, subclassId, 0)) throw new InvalidOperationException("无法在工具箱窗口接收全局快捷键。"); } public HotkeySettings Settings => bindings.Settings; public void Apply(HotkeySettings settings) { ObjectDisposedException.ThrowIf(disposed, this); bindings.Apply(settings); } void IHotkeyRegistry.Register(int id, HotkeyGesture gesture) { if (!RegisterHotKey(window, id, gesture.Modifiers | 0x4000, gesture.Key)) throw new InvalidOperationException($"快捷键 {gesture} 无法注册,可能已被其他程序占用({Marshal.GetLastWin32Error()})。"); } void IHotkeyRegistry.Unregister(int id) => UnregisterHotKey(window, id); private nint OnMessage(nint hwnd, uint message, nuint wParam, nint lParam, nuint subclass, nuint data) { if (message == 0x0312 && !disposed && bindings.Resolve(unchecked((int)wParam)) is { } operation) { action(operation); return 0; } return DefSubclassProc(hwnd, message, wParam, lParam); } public void Dispose() { if (disposed) return; disposed = true; bindings.Dispose(); RemoveWindowSubclass(window, callback, subclassId); GC.KeepAlive(callback); } /// Hidden-window QA: posts only to our own window and uses a counting callback, never a game command. public static async Task ProbeAsync(nint window) { var received = new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); var gesture = HotkeyGesture.Parse("Ctrl+Alt+Shift+F24"); using var probe = new WindowsGlobalHotkeys(window, operation => received.TrySetResult(operation)); try { probe.Apply(new(true, Restart: gesture.ToString())); var id = probe.bindings.IdFor(SessionOperation.Restart)!.Value; if (!PostMessage(window, 0x0312, (nuint)id, (nint)((gesture.Key << 16) | gesture.Modifiers))) throw new InvalidOperationException("无法向工具箱测试窗口投递快捷键消息。"); var action = await received.Task.WaitAsync(TimeSpan.FromSeconds(2)); probe.Dispose(); var released = RegisterHotKey(window, 0x5FFD, gesture.Modifiers | 0x4000, gesture.Key); if (released) UnregisterHotKey(window, 0x5FFD); return new { registered = true, messageDispatched = action == SessionOperation.Restart, released, gameActionInvoked = false }; } catch (Exception e) { return new { registered = false, error = e.Message, gameActionInvoked = false }; } } private delegate nint SubclassProc(nint hwnd, uint msg, nuint wParam, nint lParam, nuint id, nuint data); [DllImport("user32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool RegisterHotKey(nint hwnd, int id, uint modifiers, uint key); [DllImport("user32.dll")] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool UnregisterHotKey(nint hwnd, int id); [DllImport("user32.dll", SetLastError = true)] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool PostMessage(nint hwnd, uint msg, nuint wParam, nint lParam); [DllImport("comctl32.dll")] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool SetWindowSubclass(nint hwnd, SubclassProc proc, nuint id, nuint data); [DllImport("comctl32.dll")] [return: MarshalAs(UnmanagedType.Bool)] private static extern bool RemoveWindowSubclass(nint hwnd, SubclassProc proc, nuint id); [DllImport("comctl32.dll")] private static extern nint DefSubclassProc(nint hwnd, uint msg, nuint wParam, nint lParam); }