using System.Runtime.InteropServices; namespace CursorLang.Interop; /// /// Системный перехватчик клавиатуры (WH_KEYBOARD_LL): видит нажатия во всех /// приложениях и умеет не пропускать их дальше. /// /// /// Ставится только низкоуровневый хук: обычный WH_KEYBOARD требует внедрения /// DLL в чужие процессы, что для managed-кода невозможно. Обратный вызов /// приходит на тот поток, который поставил хук, и поток обязан крутить цикл /// сообщений — отсюда требование ставить хук из потока пользовательского /// интерфейса. Возврат управления затягивать нельзя: по истечении системного /// тайм-аута Windows молча снимает хук. /// internal sealed class LowLevelKeyboardHook : IDisposable { /// /// Обработчик события клавиши. Возвращает true, если событие нужно /// проглотить — тогда приложение переднего плана его не увидит. /// internal delegate bool KeyFilter(int virtualKey, bool isKeyDown); private const int WhKeyboardLowLevel = 13; private const int HcAction = 0; private const int WmKeyDown = 0x0100; private const int WmKeyUp = 0x0101; private const int WmSysKeyDown = 0x0104; private const int WmSysKeyUp = 0x0105; /// Событие пришло не от живого нажатия, а из SendInput. private const uint LowLevelKeyHookFlagInjected = 0x10; private readonly KeyFilter _filter; // Делегат живёт в поле не для удобства: ссылку на него держит только Win32, // о которой сборщик мусора не знает, и без поля хук перестаёт работать // через случайное время private readonly HookProc _callback; private IntPtr _handle; internal LowLevelKeyboardHook(KeyFilter filter) { _filter = filter; _callback = OnHookEvent; } private delegate IntPtr HookProc(int code, IntPtr wParam, IntPtr lParam); internal bool IsInstalled => _handle != IntPtr.Zero; /// /// Ставит хук. Возвращает false, если система отказала. /// internal bool Install() { if (_handle != IntPtr.Zero) { return true; } _handle = SetWindowsHookEx(WhKeyboardLowLevel, _callback, GetModuleHandle(null), 0); return _handle != IntPtr.Zero; } internal void Uninstall() { if (_handle == IntPtr.Zero) { return; } UnhookWindowsHookEx(_handle); _handle = IntPtr.Zero; } public void Dispose() => Uninstall(); [DllImport("user32.dll", SetLastError = true)] private static extern IntPtr SetWindowsHookEx(int idHook, HookProc lpfn, IntPtr hMod, uint dwThreadId); [DllImport("user32.dll", SetLastError = true)] private static extern bool UnhookWindowsHookEx(IntPtr hhk); [DllImport("user32.dll")] private static extern IntPtr CallNextHookEx(IntPtr hhk, int code, IntPtr wParam, IntPtr lParam); [DllImport("kernel32.dll", CharSet = CharSet.Unicode)] private static extern IntPtr GetModuleHandle(string? lpModuleName); private IntPtr OnHookEvent(int code, IntPtr wParam, IntPtr lParam) { if (code != HcAction) { return CallNextHookEx(_handle, code, wParam, lParam); } var message = (int)wParam; bool isKeyDown = message is WmKeyDown or WmSysKeyDown; bool isKeyUp = message is WmKeyUp or WmSysKeyUp; var data = Marshal.PtrToStructure(lParam); // Синтетический ввод присылают экранные клавиатуры, программы // автозамены и средства автоматизации: подменять их работу не наше дело bool injected = (data.flags & LowLevelKeyHookFlagInjected) != 0; if ((isKeyDown || isKeyUp) && !injected && _filter((int)data.vkCode, isKeyDown)) { // Ненулевой результат вместо CallNextHookEx обрывает цепочку: событие // не дойдёт ни до приложения, ни до обработчика регистра в Windows return 1; } return CallNextHookEx(_handle, code, wParam, lParam); } [StructLayout(LayoutKind.Sequential)] private struct KeyboardHookData { public uint vkCode; public uint scanCode; public uint flags; public uint time; public IntPtr dwExtraInfo; } }