- PVSM.RU - https://www.pvsm.ru -
Пожалуй, почти не осталось людей, не знающих, что такое 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» состоит из таких шагов:
Всю эту последовательность нужно было реализовать на 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].
Следующий шаг — перенести алгоритм разбора сообщения, который сводится к следующему:
Ниже представлена часть кода разбора данных от геймпада, который на выходе выдает список нажатых кнопок:
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
Нажмите здесь для печати.