Программирование

Глобальный хук в Windows: перехват ввода с HID-устройства

В Windows можно перехватывать ввод с конкретного HID‑устройства, но только через Raw Input, а не глобальный хук. Узнайте, как зарегистрировать сканер и фильтровать события.

Можно ли в Windows, используя C#, глобально перехватывать ввод с конкретного HID-устройства (например, сканера) с помощью глобального хука?

Можно ли в Windows, используя C#, глобально перехватывать ввод с конкретного HID‑устройства (например, сканера) с помощью глобального хука?

Да, но только если под «глобальным хуком» понимать общий перехват клавиатурных событий, а затем фильтрацию по UID‑устройства. На самом деле, сам по себе WH_KEYBOARD_LL/WH_KEYBOARD возвращает только виртуальные коды клавиш без сведений об источнике, поэтому прямого способа «поймать только клавиши от конкретного сканера» у хука нет. Для специфической идентификации HID‑устройства в Windows используется API Raw Input, который позволяет регистрировать отдельные устройства и получать их данные в чистом виде.


Содержание


Что такое глобальный хук и его ограничения

Глобальный хук (WH_KEYBOARD_LL, WH_KEYBOARD) перехватывает все события клавиатуры на уровне пользователя. Он работает через вызов SetWindowsHookEx и обрабатывает виртуальные коды клавиш, но не предоставляет информацию о том, с какого именно устройства пришли данные. На самом деле, при наличии нескольких клавиатур или сканеров вы не сможете отфильтровать события по конкретному HID‑устройству без дополнительной логики.

Согласно ответу в Stack Overflow, «если приложение зарегистрировано на обработку Raw Input, нет необходимости ставить глобальный хук» – Raw Input уже умеет различать устройства. Stack Overflow – Can I use Global Hooks and Raw Input at the same time?


Почему Raw Input – лучший выбор для конкретного HID‑устройства

Raw Input – это низкоуровневая API Windows, позволяющая получать данные прямо от HID‑устройств. На самом деле, ключевые преимущества:

Особенность Что даёт
Регистрация конкретного TLC Вы указываете usUsagePage и usUsage, а также hWndTarget – окно, которое будет получать WM_INPUT.
Получение RAWINPUTDEVICE В сообщении WM_INPUT содержится дескриптор устройства (RAWINPUTHEADER.hDevice).
Проверка UID По дескриптору можно получить DeviceName и сравнить с VID/PID сканера.
Бесшовная работа Не требуется DLL‑hook, а приложение работает в своём процессе.

Официальная документация Microsoft подробно описывает весь процесс регистрации и обработки: Raw Input Overview.


Практический пример: регистрация сканера и фильтрация ввода

Ниже – упрощённый пример, демонстрирующий, как в WinForms‑приложении отследить ввод только от сканера с VID = 0x0C2E, PID = 0x0206.

csharp
using System;
using System.Runtime.InteropServices;
using System.Text;
using System.Windows.Forms;

public partial class MainForm : Form
{
    private const int RIDEV_INPUTSINK = 0x00000100;
    private const int WM_INPUT = 0x00FF;

    [StructLayout(LayoutKind.Sequential)]
    struct RAWINPUTDEVICE
    {
        public ushort usUsagePage;
        public ushort usUsage;
        public uint dwFlags;
        public IntPtr hwndTarget;
    }

    [DllImport("User32.dll")]
    static extern bool RegisterRawInputDevices(RAWINPUTDEVICE[] pRawInputDevices, uint uiNumDevices, uint cbSize);

    public MainForm()
    {
        InitializeComponent();
        RegisterScanner();
    }

    void RegisterScanner()
    {
        RAWINPUTDEVICE rid = new RAWINPUTDEVICE
        {
            usUsagePage = 1,              // Generic Desktop Controls
            usUsage = 6,                  // Keyboard
            dwFlags = RIDEV_INPUTSINK,
            hwndTarget = this.Handle
        };

        if (!RegisterRawInputDevices(new[] { rid }, 1, (uint)Marshal.SizeOf(typeof(RAWINPUTDEVICE))))
            MessageBox.Show("Failed to register scanner.");
    }

    protected override void WndProc(ref Message m)
    {
        if (m.Msg == WM_INPUT)
        {
            uint dwSize = 0;
            GetRawInputData(m.LParam, RID_INPUT, IntPtr.Zero, ref dwSize, (uint)Marshal.SizeOf(typeof(RAWINPUTHEADER)));

            var buffer = Marshal.AllocHGlobal((int)dwSize);
            try
            {
                GetRawInputData(m.LParam, RID_INPUT, buffer, ref dwSize, (uint)Marshal.SizeOf(typeof(RAWINPUTHEADER)));
                var raw = Marshal.PtrToStructure<RAWINPUT>(buffer);
                // raw.header.hDevice contains handle to the device that sent the input
                var deviceName = GetDeviceName(raw.header.hDevice);
                if (deviceName.Contains("VID_0C2E&PID_0206"))
                {
                    // Process only scanner input
                    ProcessScannerInput(raw);
                }
            }
            finally
            {
                Marshal.FreeHGlobal(buffer);
            }
        }
        base.WndProc(ref m);
    }

    string GetDeviceName(IntPtr hDevice)
    {
        uint size = 0;
        GetRawInputDeviceInfo(hDevice, RIDI_DEVICENAME, IntPtr.Zero, ref size);
        var sb = new StringBuilder((int)size);
        GetRawInputDeviceInfo(hDevice, RIDI_DEVICENAME, sb, ref size);
        return sb.ToString();
    }

    void ProcessScannerInput(RAWINPUT raw)
    {
        // Пример: выводим ASCII‑код клавиши
        var key = raw.data.keyboard.VKey;
        Console.WriteLine($"Scanner key: 0x{key:X}");
    }

    /* P/Invoke declarations omitted for brevity */
}

Важные моменты:

  1. Регистрация – только клавиатурный HID, но можно указать конкретный usUsagePage/usUsage для сканера, если он использует другой класс.
  2. Проверка UIDGetDeviceName возвращает строку вида \\?\hid#vid_0c2e&pid_0206#.... Фильтруем по VID/PID.
  3. Обработка – в ProcessScannerInput можно преобразовать HID‑данные в символы, игнорировать клавиши, которые не нужны.

Для более удобной работы с HID‑данными можно использовать готовые библиотеки, например SharpLibHid или RawInput‑Sharp. На самом деле, они оборачивают API в более высокоуровневый интерфейс.


Альтернативные библиотеки и подходы

Библиотека Что делает Когда использовать
SharpLibHid Обрабатывает HID‑данные, включая сканеры, на уровне C#. Когда нужна высокая надёжность и поддержка сложных HID‑парсеров.
RawInput‑Sharp Пакет обёртки над Win32‑API Raw Input. Если нужен простой доступ к WM_INPUT без ручного P/Invoke.
HidLibrary Предоставляет объектно‑ориентированное API для HID‑устройств. Для упрощённого чтения отчётов (reports) через HID‑эндпоинты.
Global Keyboard Hook (SetWindowsHookEx) Позволяет перехватывать любые клавиатурные события. Когда нужно работать с весьма простыми клавишами, но без конкретного устройства.

Вопрос «можно ли использовать глобальный хук и Raw Input одновременно?» уже обсуждался на Stack Overflow, и ответ — «не нужно ставить хук, если вы регистрируете Raw Input» – так как Raw Input уже обеспечивает нужный фильтр. Stack Overflow – Can I use Global Hooks and Raw Input at the same time?


Итоги и рекомендации

  • Глобальные хуки (WH_KEYBOARD_LL, WH_KEYBOARD) не дают информации о конкретном HID‑устройстве, на самом деле, поэтому их использовать для фильтрации по конкретному сканеру нельзя.
  • Для перехвата ввода конкретного HID‑устройства в Windows лучше использовать Raw Input: регистрируйте устройство, обрабатывайте WM_INPUT, проверяйте дескриптор hDevice и UID.
  • Если вам нужна более «простой» обёртка над Raw Input, на самом деле, рассмотрите библиотеки SharpLibHid или RawInput‑Sharp.
  • При необходимости обработки данных из HID‑устройства без перехвата клавиатурных событий можно напрямую читать HID‑reports через HidLibrary.

Таким образом, глобальный хук сам по себе не подходит для изоляции ввода от конкретного HID‑устройства; вместо него следует применять Raw Input или специализированные HID‑библиотеки.


Источники

  1. Raw Input Overview – Microsoft Learn
  2. Can I use Global Hooks and Raw Input at the same time? – Stack Overflow
  3. SharpLibHid – GitHub
  4. RawInput‑Sharp – GitHub
  5. Low level keyboard hook & keystrokes from rawinput – Stack Overflow
Авторы
Проверено модерацией
Модерация