はじめに
USB-HID(Human Interface Device)は、キーボードやマウス、ゲームパッドなどの入力の標準規格です。
これらのデバイスが送信するイベントをモニターすることで、特定の入力を検出したり、
独自のアクションを実行したりできます。
本記事では、C# で RawInput API を使用した USB-HIDデバイスのモニター方法を解説します。
ゲーム実況やデバイスの動作確認、カスタム入力の実装に役立つと思います。
RawInput API とは?
RawInput は、キーボードやマウス、ジョイスティック、タッチスクリーンなど、
さまざまな入力デバイスからの生データを直接アプリケーションで受け取るための API です。
USB プロトコルでは、デバイス側(入力デバイスなど)は、
ホスト側(PCなど)からの IN パケットに応答する形でしか OUT データを送信できないため、
1ms 間隔で常にデータのやり取りが行われます。
RawInput API は、このような高速なデータ通信に対応できるように設計された低レイヤーの API です。
RawInput API の使い方
入力監視するデバイスの登録
まず、入力を監視したいデバイスを登録します。
今回はゲームコントローラとして、ジョイスティックを登録します。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
private void RegisterGamepad(IntPtr hwnd) { RAWINPUTDEVICE[] devices = new RAWINPUTDEVICE[1]; devices[0].usUsagePage = 0x01; // Generic Desktop Controls devices[0].usUsage = 0x04; // Joystick devices[0].dwFlags = 0x00000100; // RIDEV_INPUTSINKフラグを指定 devices[0].hwndTarget = hwnd; if (!RegisterRawInputDevices(devices, (uint)devices.Length, (uint)Marshal.SizeOf(typeof(RAWINPUTDEVICE)))) { int errorCode = Marshal.GetLastWin32Error(); throw new ApplicationException($"Raw Inputデバイスの登録に失敗しました。エラーコード:{errorCode}"); } } |
- usUsagePage
デスクトップを制御することができる入力デバイスを指定(0x01) - usUsage
0x01:キーボード、0x02:マウス、0x04:ジョイスティック - dwFlags
ウィンドウがアクティブでない時でも入力を受信できる RIDEV_INPUTSINK を指定 - hwndTarget
紐づけるウィンドウのハンドルを指定
GUIのメッセージループと紐づけ
Form などのウィンドウを持つクラス内で this.Handle でウィンドウハンドルを取得できます。
先ほどの関数にハンドルを渡すことで GUI のメッセージループに紐づけることができます。
|
1 2 3 4 5 6 |
public Form1() { InitializeComponent(); RegisterGamepad(this.Handle); } |
入力デバイスを登録すると、WM_INPUT のメッセージを受け取れるようになります。
WndProc をオーバーライドしてメッセージを解析します。
|
1 2 3 4 5 6 7 8 9 10 |
protected override void WndProc(ref Message m) { if (m.Msg == WM_INPUT) { // 受信した入力データの処理 } // 基底クラスの WndProc を呼び出す base.WndProc(ref m); } |
入力データの取得
さまざまな入力デバイスに対応するため、HID から入力されるデータは可変長になっています。
そのため、まずヘッダーを読み出してデータサイズを取得した後、ペイロードを読み出します。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 |
private byte[] ReadHidData(ref Message m) { uint rawInputSize = 0; uint rawInputHeaderSize = (uint)Marshal.SizeOf<RAWINPUTHEADER>(); if (GetRawInputData(m.LParam, RID_INPUT, IntPtr.Zero, ref rawInputSize, rawInputHeaderSize) != 0) { int errorCode = Marshal.GetLastWin32Error(); throw new ApplicationException($"GetRawInputDataサイズ取得失敗。エラーコード:{errorCode}"); } RAWINPUT rawInput; byte[] rawHidRawData; IntPtr rawInputPointer = IntPtr.Zero; try { rawInputPointer = Marshal.AllocHGlobal((int)rawInputSize); if (GetRawInputData(m.LParam, RID_INPUT, rawInputPointer, ref rawInputSize, rawInputHeaderSize) != rawInputSize) { int errorCode = Marshal.GetLastWin32Error(); throw new ApplicationException($"GetRawInputDataデータ取得失敗。エラーコード:{errorCode}"); } rawInput = Marshal.PtrToStructure<RAWINPUT>(rawInputPointer); if (rawInput.Header.dwType != RIM_TYPEHID) { return null; } var rawInputData = new byte[rawInputSize]; Marshal.Copy(rawInputPointer, rawInputData, 0, rawInputData.Length); uint dataSize = rawInput.Hid.dwSizeHid * rawInput.Hid.dwCount; rawHidRawData = new byte[dataSize]; int rawInputOffset = (int)rawInputSize - rawHidRawData.Length; Buffer.BlockCopy(rawInputData, rawInputOffset, rawHidRawData, 0, rawHidRawData.Length); return rawHidRawData; } finally { Marshal.FreeHGlobal(rawInputPointer); } } |
RawInput API を使ったサンプルコード
デバイスの登録、データの読出し、コールバック処理を RawInputMonitor クラスとして纏めています。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 |
using System; using System.Runtime.InteropServices; using System.Windows.Forms; namespace SampleMonitor { internal class RawInputMonitor { [DllImport("user32.dll", SetLastError = true)] private static extern bool RegisterRawInputDevices( RAWINPUTDEVICE[] pRawInputDevices, uint uiNumDevices, uint cbSize); [DllImport("user32.dll")] private static extern uint GetRawInputData( IntPtr hRawInput, uint uiCommand, IntPtr pData, ref uint pcbSize, uint cbSizeHeader); private const uint RID_INPUT = 0x10000003; [StructLayout(LayoutKind.Sequential)] public struct RAWINPUTDEVICE { public ushort usUsagePage; public ushort usUsage; public uint dwFlags; public IntPtr hwndTarget; } [StructLayout(LayoutKind.Sequential)] public struct RAWINPUTHEADER { public uint dwType; public uint dwSize; public IntPtr hDevice; public IntPtr wParam; } private const uint RIM_TYPEMOUSE = 0; private const uint RIM_TYPEKEYBOARD = 1; private const uint RIM_TYPEHID = 2; [StructLayout(LayoutKind.Sequential)] public struct RAWHID { public uint dwSizeHid; public uint dwCount; public IntPtr bRawData; } [StructLayout(LayoutKind.Sequential)] public struct RAWINPUT { public RAWINPUTHEADER Header; public RAWHID Hid; } private void RegisterGamepad(IntPtr hwnd) { RAWINPUTDEVICE[] devices = new RAWINPUTDEVICE[1]; devices[0].usUsagePage = 0x01; // Generic Desktop Controls devices[0].usUsage = 0x04; // Joystick devices[0].dwFlags = 0x00000100; // RIDEV_INPUTSINKフラグを指定 devices[0].hwndTarget = hwnd; if (!RegisterRawInputDevices(devices, (uint)devices.Length, (uint)Marshal.SizeOf(typeof(RAWINPUTDEVICE)))) { int errorCode = Marshal.GetLastWin32Error(); throw new ApplicationException($"Raw Inputデバイスの登録に失敗しました。エラーコード:{errorCode}"); } } private byte[] ReadHidData(ref Message m) { uint rawInputSize = 0; uint rawInputHeaderSize = (uint)Marshal.SizeOf<RAWINPUTHEADER>(); if (GetRawInputData( m.LParam, RID_INPUT, IntPtr.Zero, ref rawInputSize, rawInputHeaderSize) != 0) { int errorCode = Marshal.GetLastWin32Error(); throw new ApplicationException($"GetRawInputDataサイズ取得失敗。エラーコード:{errorCode}"); } RAWINPUT rawInput; byte[] rawHidRawData; IntPtr rawInputPointer = IntPtr.Zero; try { rawInputPointer = Marshal.AllocHGlobal((int)rawInputSize); if (GetRawInputData( m.LParam, RID_INPUT, rawInputPointer, ref rawInputSize, rawInputHeaderSize) != rawInputSize) { int errorCode = Marshal.GetLastWin32Error(); throw new ApplicationException($"GetRawInputDataデータ取得失敗。エラーコード:{errorCode}"); } rawInput = Marshal.PtrToStructure<RAWINPUT>(rawInputPointer); if (rawInput.Header.dwType != RIM_TYPEHID) { return null; } var rawInputData = new byte[rawInputSize]; Marshal.Copy(rawInputPointer, rawInputData, 0, rawInputData.Length); uint dataSize = rawInput.Hid.dwSizeHid * rawInput.Hid.dwCount; rawHidRawData = new byte[dataSize]; int rawInputOffset = (int)rawInputSize - rawHidRawData.Length; Buffer.BlockCopy(rawInputData, rawInputOffset, rawHidRawData, 0, rawHidRawData.Length); return rawHidRawData; } finally { Marshal.FreeHGlobal(rawInputPointer); } } public RawInputMonitor(IntPtr hwnd) { RegisterGamepad(hwnd); } public byte[] OnMessage(ref Message m) { const int WM_INPUT = 0x00FF; if (m.Msg != WM_INPUT) { return null; } return ReadHidData(ref m); } } } |
ウィンドウを持つ Form1 クラスです。
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 |
using System; using System.Windows.Forms; namespace SampleMonitor { public partial class Form1 : Form { // USB-HID イベント処理 private RawInputMonitor mRawInputMonitor = null; public Form1() { InitializeComponent(); mRawInputMonitor = new RawInputMonitor(this.Handle); } protected override void WndProc(ref Message m) { if (mRawInputMonitor != null) { byte[] data = mRawInputMonitor.OnMessage(ref m); if (data != null) { // USB-HID データの処理 data = null; } } base.WndProc(ref m); } } } |
おわりに
最近のデスクトップ PC はすごい。1ms 間隔の処理を難なくこなす。
この API を使用したアプリケーション「BeatCounter」を以下に置いています。
https://github.com/SANACHAN-prog/BeatCounter
※DAOコントローラーのみに対応
※KONAMI さんの Beatmania IIDX INFINITAS 用、2P側のみ対応