我正在编写一个受Contexts启发的应用程序切换器,有点有趣,但陷入了对全局键盘快捷键的反应。我正在使用 .NET 8 + Avalonia。
到目前为止我得到了什么:
using System;
using System.Diagnostics;
using System.Reactive;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using System.Runtime.InteropServices;
namespace WindowSwitcher.Services.Keyboard;
public class KeyboardInterceptor2 : IKeyboardInterceptor
{
private const int WhKeyboardLl = 13;
private const int WmKeydown = 0x0100;
private const int WmSyskeydown = 0x0104;
private const int WmKeyup = 0x0101;
private const int WmSyskeyup = 0x0105;
private readonly Subject<Unit> _signalSubject = new();
private readonly IntPtr _hookId;
private bool _consumeNextWinKeyUp;
public IObservable<Unit> Signal => _signalSubject.AsObservable();
public KeyboardInterceptor2()
{
_hookId = SetHook(HookCallback);
}
private IntPtr SetHook(LowLevelKeyboardProc proc)
{
using var curProcess = Process.GetCurrentProcess();
using var curModule = curProcess.MainModule!;
return SetWindowsHookEx(WhKeyboardLl, proc, GetModuleHandle(curModule.ModuleName), 0);
}
private IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam)
{
if (nCode >= 0)
{
int vkCode = Marshal.ReadInt32(lParam);
if (wParam == WmKeydown || wParam == WmSyskeydown)
{
if (vkCode == (int)VirtualKeyStates.VkS)
{
if ((GetAsyncKeyState(VirtualKeyStates.VkLwin) & 0x8000) != 0 ||
(GetAsyncKeyState(VirtualKeyStates.VkRwin) & 0x8000) != 0)
{
_signalSubject.OnNext(Unit.Default);
_consumeNextWinKeyUp = true;
return 1; // Consume the S key press when Windows key is pressed
}
}
}
else if (wParam == WmKeyup || wParam == WmSyskeyup)
{
if ((vkCode == (int)VirtualKeyStates.VkLwin || vkCode == (int)VirtualKeyStates.VkRwin) && _consumeNextWinKeyUp)
{
_consumeNextWinKeyUp = false;
return 1; // Consume the Windows key up event
}
}
}
return CallNextHookEx(_hookId, nCode, wParam, lParam);
}
public void Dispose()
{
UnhookWindowsHookEx(_hookId);
_signalSubject.Dispose();
}
private delegate IntPtr LowLevelKeyboardProc(int nCode, IntPtr wParam, IntPtr lParam);
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern IntPtr SetWindowsHookEx(int idHook, LowLevelKeyboardProc lpfn, IntPtr hMod, uint dwThreadId);
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool UnhookWindowsHookEx(IntPtr hhk);
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam);
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
private static extern IntPtr GetModuleHandle(string lpModuleName);
[DllImport("user32.dll")]
private static extern short GetAsyncKeyState(VirtualKeyStates nVirtKey);
private enum VirtualKeyStates
{
VkLwin = 0x5B,
VkRwin = 0x5C,
VkS = 0x53
}
}
基本上,我想要的只是按 Windows + S 来激活我的程序,并且它可以工作,只是不是每次都有效,大多数时候都会弹出菜单启动。对我来说这看起来不错,但也许存在一些奇怪的同步问题?
我有一个想法,尝试仅使用两个将分别更新的可观察量(每个键一个),如果它们都已设置,我将触发信号。
我也尝试过 GlobalHotKey,但是对于 .NET 8,
Key
类不可用 - 我什至将 TargetFramework
设置为 net8.0-windows
,但它没有做任何事情。
最好的方法是什么? Avalonia 有内置的东西吗?
InputManager
被标记为内部,因此不包含在内。
在克劳德的帮助下,我得出了以下结论:
using System;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Reactive;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using System.Runtime.InteropServices;
namespace WindowSwitcher.Services.Keyboard
{
public class HotKeyInterceptor : IKeyboardInterceptor
{
private const int WmHotkey = 0x0312;
const int ModControl = 0x0002;
const int ModWin = 0x0008;
const int VkTab = 0x09;
private readonly Subject<Unit> _signalSubject = new Subject<Unit>();
private readonly int _hotkeyId = 213769420;
private readonly MessageWindow _messageWindow;
public IObservable<Unit> Signal => _signalSubject.AsObservable();
[RequiresAssemblyFiles()]
public HotKeyInterceptor()
{
_messageWindow = new MessageWindow();
_messageWindow.HotKeyPressed += OnHotKeyPressed;
UnregisterHotKey(_messageWindow.Handle, _hotkeyId);
if (!RegisterHotKey(_messageWindow.Handle, _hotkeyId, ModWin | ModControl, VkTab))
{
int error = Marshal.GetLastWin32Error();
string errorMessage = GetErrorMessage(error);
throw new Win32Exception(error, $"Could not register the hot key. {errorMessage}");
}
}
private void OnHotKeyPressed(object? sender, EventArgs e)
{
_signalSubject.OnNext(Unit.Default);
}
public void Dispose()
{
UnregisterHotKey(_messageWindow.Handle, _hotkeyId);
_messageWindow.Dispose();
_signalSubject.Dispose();
}
private string GetErrorMessage(int errorCode)
{
switch (errorCode)
{
case 1409:
return "The hotkey is already registered by another application.";
case 1400:
return "The window handle is not valid.";
case 87:
return "An invalid parameter was passed to the function.";
default:
return $"Unknown error occurred. Error code: {errorCode}";
}
}
[DllImport("user32.dll", SetLastError = true)]
private static extern bool RegisterHotKey(IntPtr hWnd, int id, int fsModifiers, int vk);
[DllImport("user32.dll")]
private static extern bool UnregisterHotKey(IntPtr hWnd, int id);
private class MessageWindow : IDisposable
{
private const int WsExToolwindow = 0x80;
private const int WsPopup = unchecked((int)0x80000000);
public event EventHandler? HotKeyPressed;
private readonly IntPtr _hwnd;
public IntPtr Handle => _hwnd;
[RequiresAssemblyFiles("Calls System.Runtime.InteropServices.Marshal.GetHINSTANCE(Module)")]
public MessageWindow()
{
var wndClass = new WindowClass
{
lpfnWndProc = Marshal.GetFunctionPointerForDelegate(WndProc),
hInstance = Marshal.GetHINSTANCE(typeof(MessageWindow).Module),
lpszClassName = "MessageWindowClass"
};
var classAtom = RegisterClass(ref wndClass);
if (classAtom == 0)
throw new InvalidOperationException("Failed to register window class");
_hwnd = CreateWindowEx(
WsExToolwindow,
classAtom,
"MessageWindow",
WsPopup,
0, 0, 0, 0,
IntPtr.Zero,
IntPtr.Zero,
wndClass.hInstance,
IntPtr.Zero);
if (_hwnd == IntPtr.Zero)
throw new InvalidOperationException("Failed to create message window");
}
private IntPtr WndProc(IntPtr hwnd, uint msg, IntPtr wParam, IntPtr lParam)
{
if (msg == WmHotkey)
{
HotKeyPressed?.Invoke(this, EventArgs.Empty);
return IntPtr.Zero;
}
return DefWindowProc(hwnd, msg, wParam, lParam);
}
public void Dispose()
{
if (_hwnd != IntPtr.Zero)
{
DestroyWindow(_hwnd);
}
}
[DllImport("user32.dll")]
private static extern ushort RegisterClass(ref WindowClass lpWndClass);
[DllImport("user32.dll")]
private static extern IntPtr CreateWindowEx(
int dwExStyle,
ushort classAtom,
string lpWindowName,
int dwStyle,
int x, int y,
int nWidth, int nHeight,
IntPtr hWndParent,
IntPtr hMenu,
IntPtr hInstance,
IntPtr lpParam);
[DllImport("user32.dll")]
private static extern IntPtr DefWindowProc(IntPtr hWnd, uint uMsg, IntPtr wParam, IntPtr lParam);
[DllImport("user32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool DestroyWindow(IntPtr hwnd);
[StructLayout(LayoutKind.Sequential)]
private struct WindowClass
{
public int style;
public IntPtr lpfnWndProc;
public int cbClsExtra;
public int cbWndExtra;
public IntPtr hInstance;
public IntPtr hIcon;
public IntPtr hCursor;
public IntPtr hbrBackground;
[MarshalAs(UnmanagedType.LPStr)]
public string lpszMenuName;
[MarshalAs(UnmanagedType.LPStr)]
public string lpszClassName;
}
}
}
}
每次按快捷键都会起作用,所以我想问题已经解决了。我还必须选择 ctrl+win+tab 作为快捷方式,因为 Win+S 只是打开开始菜单。