- PVSM.RU - https://www.pvsm.ru -

Слушаем пользовательский ввод с помощью “Raw Input API” для управления фоновым приложением

Слушаем пользовательский ввод с помощью “Raw Input API” для управления фоновым приложением - 1

Пожалуй, почти не осталось людей, не знающих, что такое Ctrl+C и Ctrl+V. Более опытные пользователи знают горячие клавиши часто используемых приложений. Есть те, кто использует более сложные комбинации: например, для управления плеером, находящимся в фоне. Для разработчиков реализация подобной функциональности обычно не вызывает больших трудностей, т.к. эта задача широко распространена, а о её решении уже многое написано. Но как быть, если надо в свернутом состоянии слушать пользовательский ввод с джойстика или презентера, да к тому же ещё и разбираться, от какого именно устройства пришло событие? Скажем честно, для нас эта задача оказалась чем-то новым. Под катом мы расскажем, как мы её решили на C# в WPF приложении с помощью "Raw Input API [1]".

Предыстория

Наше приложение Jalinga Studio используют для проведения съемок видео, вебинаров и онлайн трансляций, а управляют им преимущественно с помощью презентера или джойстика (зачем нам это понадобилось, можно почитать в нашей предыдущей статье “Как мы оживляем презентацию” [2]). Большинство презентеров сделано для работы с Power Point, поэтому они генерируют обычный клавиатурный ввод: F5, Page Up, Page Down и т.д. В WPF есть стандартный механизм для работы с клавиатурным вводом. Им мы первое время и пользовались, пока не наткнулись на существенный для нас недостаток. Дело в том, что этот механизм работает только тогда, когда приложение активно (находится на переднем плане), но некоторые наши клиенты хотели бы параллельно иметь доступ, например, к браузеру или другой программе, что непременно лишает нас получения клавиатурного ввода. Сначала мы пытались обойти эту проблему путем создания дополнительного небольшого окна на переднем плане, подобно тому, как это сделано в Skype. На этом окне отображается статус работы программы и несколько кнопок для управления, если пользователю удобнее управлять мышкой. Этот подход оказался не самым удобным — окно управления нужно активировать. Если пользователь забывал переключить фокус, то клавиатурный ввод с презентера уходил текущему активному приложению. Например, F5 или Page Down в браузер. Вдобавок к этому всему, в какой-то момент нам стало не хватать кнопок  на презентере, и мы решили использовать джойстики, которые стандартный механизм WPF не поддерживает.

Поиск решения

Сначала мы сформулировали требования к новому механизму:

  • получение информации о пользовательском вводе, если приложение работает в фоне;
  • возможность работы с презентерами, джойстиками, геймпадами;
  • наличие способа различать устройства ввода.

Первое, что пришло в голову — это хуки, которые можно поставить с помощью функции SetWindowsHookEx. Но тут всё равно остаётся открытым вопрос поддержки джойстиков и геймпадов. Я уж молчу про антивирусы, которые могут принять нас за кейлоггера, влияние хуков на работу чужих программ, создание 32-х и 64-х битных dll-ек, взаимодействие с нашим приложением из другого процесса и общую сложность поддержки.

Рассмотрели вариант использования DirectInput или XInput. DirectInput устарел, Microsoft вместо него рекомендуют использовать XInput [3]. С помощью них можно получать пользовательский ввод с джойстиков даже в фоновом режиме, если с помощью метода SetCooperativeLevel поставить флаг Background. Но XInput не поддерживает клавиатуры и мыши. Еще не понравилась pull-модель использования, из-за которой придется с некоторой периодичностью опрашивать интересующие нас устройства.

Продолжили копать дальше. Один хороший друг из Parallels посоветовал посмотреть в сторону "Raw Input API [1]". Проанализировав возможности этого API, мы поняли, что все, что нам нужно, там есть — и работа с различными устройствами HID класса, и возможность получать ввод, не являясь активным окном, и доступный ID устройства, от которого пришло событие. Ограничения тоже есть — если ввод осуществляется в админский процесс, а наш процесс не является админским, то события ввода не придут. Но нам это и не нужно. В крайнем случае всегда можно запустить наше приложение с правами администратора.

Реализация

Обобщенно процесс получения пользовательского ввода с помощью «Raw Input API» состоит из таких шагов:

  1. Регистрируем типы устройств, от которых будем получать события ввода, с помощью RegisterRawInputDevices.
  2. Слушаем события WM_INPUT в оконной процедуре [4].
  3. Разбираем пришедшие события с помощью GetRawInputData.
  4. Определяем тип события (RAWMOUSE, RAWKEYBOARD, RAWHID) и разбираем его в соответствии с его типом.

Всю эту последовательность нужно было реализовать на C# в WPF приложении. Чтобы не писать самостоятельно большого количества оберток Win API-шных функций, было решено использовать SharpDX.RawInput.

Вот так выглядит упрощенный код на C#, если вы используете Windows.Forms:

public class RawInputListener
{
    public void Init(IntPtr hWnd)
    {
        Device.RegisterDevice(UsagePage.Generic, UsageId.GenericGamepad,
            DeviceFlags.InputSink, hWnd);
        Device.RegisterDevice(UsagePage.Generic, UsageId.GenericKeyboard,
            DeviceFlags.InputSink, hWnd);
        Device.RawInput += OnRawInput;
        Device.KeyboardInput += OnKeyboardInput;
    }

    public void Clear()
    {
        Device.RawInput -= OnRawInput;
        Device.KeyboardInput -= OnKeyboardInput;
    }

    private void OnKeyboardInput(object sender, KeyboardInputEventArgs e)
    {
    }

    private void OnRawInput(object sender, RawInputEventArgs e)
    {
    }
}

Флаг DeviceFlags.InputSink нужен для того, чтобы приложение получало сообщение, даже если оно не находится на переднем плане. При использовании этого флага обязательно нужно указать hWnd.

Если вы используете WPF, то в таком виде методы OnRawInput и OnKeyboardInput вызываться не будут, т.к. внутри класса Device реализуется интерфейс IMessageFilter [5]из Windows.Forms. Если заглянуть в исходный код Device [6], то там можно увидеть, что в  методе PreFilterMessage вызывается HandleMessage.

Упрощенный код на C#, если вы используете WPF:

public class RawInputListener
{
    private const int WM_INPUT = 0x00FF;
    private HwndSource _hwndSource;

    public void Init(IntPtr hWnd)
    {
        if (_hwndSource != null)
        {
            return;
        }

        _hwndSource = HwndSource.FromHwnd(hWnd);
        if (_hwndSource != null)
            _hwndSource.AddHook(WndProc);

        Device.RegisterDevice(UsagePage.Generic, UsageId.GenericGamepad,
            DeviceFlags.InputSink, hWnd);
        Device.RegisterDevice(UsagePage.Generic, UsageId.GenericKeyboard,
            DeviceFlags.InputSink, hWnd);

        Device.RawInput += OnRawInput;
        Device.KeyboardInput += OnKeyboardInput;
    }

    public void Clear()
    {
        Device.RawInput -= OnRawInput;
        Device.KeyboardInput -= OnKeyboardInput;
    }

    private IntPtr WndProc(IntPtr hWnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
    {
        if (msg == WM_INPUT)
        {
            Device.HandleMessage(lParam, hWnd);
        }

        return IntPtr.Zero;
    }

    private void OnKeyboardInput(object sender, KeyboardInputEventArgs e)
    {
    }

    private void OnRawInput(object sender, RawInputEventArgs e)
    {
    }
}

Чтобы разобраться, от какого устройства пришло событие, можно использовать свойство Device класса RawInputEventArgs, с помощью которого можно получить DeviceName вида:

"\?HID#{00001124-0000-1000-8000-00805f9b34fb}_VID&000205ac_PID&3232&Col02#8&26f2f425&7&0001#{884b96c3-56ef-11d1-bc8c-00a0c91405dd}"

Vendor ID и Product ID можно найти в этом имени или воспользоваться функциями GetRawInputDeviceInfo или HidD_GetAttributes. Подробнее о VID и PID можно почитать тут [7]и тут [8].

Перейдем теперь к разбору событий. С клавиатурой все оказалось просто: информация уже приходит в разобранном виде, описанном в классе KeyboardInputEventArgs. А вот с геймпадом все оказалось сложнее. В OnRawInput приходит аргумент базового типа RawInputEventArgs. Этот аргумент нужно привести к типу HidInputEventArgs и, если получилось, дальше работать с ним. В HidInputEventArgs есть только массив байт, пришедший от устройства, причем количество байт в этом массиве у разных джойстиков и геймпадов отличается.

К сожалению, удалось найти мало документации, рассказывающей о том, как разбирать эти данные, да и та встречалась обычно в кратком виде (даже в MSDN сплошные недосказанности по этому вопросу). Самым полезным оказался вот этот проект на C [9]. Его пришлось сначала довести до рабочего состояния, но это уже был отличный старт. После оставалось перенести нужные части на C#.

Первым делом пришлось обернуть нативные функции для вызова их из C# кода. Тут, как всегда, помог pinvoke.net [10], кое-что понадобилось описать самому. Обзорно про Marshal, PInvoke и небезопасный код в C# можно почитать тут [11].

Следующий шаг — перенести алгоритм разбора сообщения, который сводится к следующему:

  1. получить PreparsedData устройства (GetRawInputDeviceInfo или HidD_GetPreparsedData);
  2. узнать о возможностях устройства (HidP_GetCaps);
  3. узнать о кнопках устройства (HidP_GetButtonCaps);
  4. получить список нажатых кнопок (HidP_GetUsages).

Ниже представлена часть кода разбора данных от геймпада, который на выходе выдает список нажатых кнопок:

public static class RawInputParser
{
    public static bool Parse(HidInputEventArgs hidInput, out List<ushort> pressedButtons)
    {
        var preparsedData = IntPtr.Zero;
        pressedButtons = new List<ushort>();

        try
        {
            preparsedData = GetPreparsedData(hidInput.Device);
            if (preparsedData == IntPtr.Zero)
                return false;

            HIDP_CAPS hidCaps;
            CheckError(HidP_GetCaps(preparsedData, out hidCaps));

            pressedButtons = GetPressedButtons(hidCaps, preparsedData, hidInput.RawData);
        }
        catch (Win32Exception e)
        {
            return false;
        }
        finally
        {
            if (preparsedData != IntPtr.Zero)
            {
                Marshal.FreeHGlobal(preparsedData);
            }
        }

        return true;
    }
}

PreparsedData легче получать с помощью GetRawInputDeviceInfo, т.к. нужный хэндл устройства уже есть в RawInputEventArgs. Функция HidD_GetPreparsedData этот хэндл не принимает, для него требуется хэндл, который можно получить с помощью CreateFile.

Для получения значений дискретных кнопок процедура аналогичная, только вместо HidP_GetButtonCaps нужно сначала вызвать HidP_GetValueCaps, а потом HidP_GetUsageValue, чтобы получить дискретное значение кнопки.

Теперь, когда набор байт из HidInputEventArgs преобразован в данные о том, какие кнопки нажаты, можно сделать механизм, аналогичный тому, что есть в WPF для работы с клавиатурой и мышкой.

Полный код приложения, разбирающего пользовательский ввод от геймпадов и клавиатуры, можно посмотреть в проекте RawInputWPF [12]на GitHub.

Итог

Вот таким образом с помощью «Raw Input API» можно получить пользовательский ввод с клавиатуры, мышки, джойстика, геймпада или другого устройства пользовательского ввода, даже если ваше приложение находится в фоне.

А что делать с данными о нажатых кнопках, решать вам.

Автор: ЦифраЛаб

Источник [13]


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/c-2/234090

Ссылки в тексте:

[1] Raw Input API: https://msdn.microsoft.com/ru-ru/library/windows/desktop/ms645536(v=vs.85).aspx

[2] “Как мы оживляем презентацию”: https://habrahabr.ru/company/jalinga/blog/317082/

[3] DirectInput устарел, Microsoft вместо него рекомендуют использовать XInput: https://msdn.microsoft.com/en-us/library/windows/desktop/ee417014(v=vs.85).aspx

[4] оконной процедуре: https://msdn.microsoft.com/ru-ru/library/windows/desktop/ms632593(v=vs.85).aspx

[5] IMessageFilter : https://msdn.microsoft.com/ru-ru/library/system.windows.forms.imessagefilter(v=vs.110).aspx

[6] исходный код Device: https://github.com/sharpdx/SharpDX/blob/8dc1f64a9424c6b0dc8b9009f70e144ded03f1d7/Source/SharpDX.RawInput/Device.cs

[7] тут : https://msdn.microsoft.com/en-us/windows/hardware/drivers/bringup/device-management-namespace-objects

[8] тут: https://msdn.microsoft.com/en-us/library/windows/hardware/ff536681(v=vs.85).aspx

[9] вот этот проект на C: https://www.codeproject.com/Articles/185522/Using-the-Raw-Input-API-to-Process-Joystick-Input

[10] pinvoke.net: http://www.pinvoke.net/

[11] тут: http://www.cyberforum.ru/csharp-net/thread342135.html

[12] RawInputWPF : https://github.com/antshil/RawInputWPF

[13] Источник: https://habrahabr.ru/post/319452/?utm_source=habrahabr&utm_medium=rss&utm_campaign=best