Seaside Laboratory

Posts

Raw Input API の使い方

DirectInput に代わる API として登場した XInput はゲームコントローラー専用となっているため、キーボードとマウスは Raw Input で処理を行う。Raw Input の実装に関する情報は少なく、公式ドキュメントは英語のみなので、細かい仕様まで調べているとそれだけでかなりの時間を取られてしまう。

DirectInput によるキーボード入力処理を Raw Input へ置き換える際に色々と苦労したので、基本的な使い方から注意点までまとめておくことにした。

Raw Input を使う理由

DirectInput には潜在的な問題がいくつか存在する。

非推奨

DirectX 開発者向けイベント「Meltdown 2005」で発表された「DirectX SDK Roadmap」というスライドでは、

Deprecated Components

Retiring components
・DirectPlay
・DirectInput
・DirectMusic
・DirectSound
・DirectShow

DirectInput は「非推奨 API」として分類され、使い続けるのが難しくなった。

オーバーヘッド

Microsoft Docs の「Taking Advantage of High-Definition Mouse Movement」によれば、

Internally, DirectInput creates a second thread to read WM_INPUT data, and using the DirectInput APIs will add more overhead than simply reading WM_INPUT directly. DirectInput is only useful for reading data from DirectInput joysticks; however, if you only need to support the Xbox 360 controller for Windows, use XInput instead. Overall, using DirectInput offers no advantages when reading data from mouse or keyboard devices, and the use of DirectInput in these scenarios is discouraged.

内部的には、DirectInput は WM_INPUT データを読み取るために 2 つ目のスレッドを作成するため、DirectInput API を使うと単に WM_INPUT を直接読み取るよりもオーバーヘッドが大きくなります。DirectInput は DirectInput ジョイスティックからデータを読み取る場合にのみ有用ですが、Windows 用の Xbox 360 コントローラーのみをサポートする場合は、代わりに XInput を使用してください。全体として、DirectInput を使用してもマウスまたはキーボードデバイスからデータを読み取る場合には利点がなく、これらのシナリオで DirectInput を使用することはお勧めしません。

DirectInput は Raw Input のラッパーに過ぎず、オーバーヘッドがあるので推奨されていない。

複数デバイスの扱い

DirectInput デバイスを列挙する EnumDevices メソッドの説明に、

DirectInput は 1 つのマウスと 1 つのキーボードデバイスのみを列挙します。これらのデバイスは、システムマウスおよびシステムキーボードと呼ばれます。これらのデバイスは、それぞれシステム上にあるすべてのマウスとキーボードの出力を結合したものを表します。

と書かれているように、DirectInput は複数のデバイスを区別することができない。それに対し Raw Input は、

An application can distinguish the source of the input even if it is from the same type of device. For example, two mouse devices.

アプリケーションは同じタイプのデバイスであっても入力元を区別することができます。例えば 2 つのマウスデバイス。

複数のデバイスを扱うことができる。

Raw Input の概要

There are many user-input devices beside the traditional keyboard and mouse. For example, user input can come from a joystick, a touch screen, a microphone, or other devices that allow great flexibility in user input. These devices are collectively known as Human Interface Devices (HIDs). The raw input API provides a stable and robust way for applications to accept raw input from any HID, including the keyboard and mouse.

従来のキーボードとマウスのほかにも多くのユーザー入力デバイスがあります。例えばユーザー入力はジョイスティック、タッチスクリーン、マイク、またはユーザー入力の柔軟性を高めるその他のデバイスから行うことができます。これらのデバイスは、まとめてヒューマンインターフェイスデバイス (HID) と呼ばれます。Raw Input API はアプリケーションがキーボードやマウスを含む任意の HID からの生の入力データを受け取るための安定し堅牢な方法を提供します。

Raw Input という名前から「入力デバイスを低レイヤーで扱う API」という印象を強く受けてしまうが、HID を前提とした API なので "HID Input" と言った方がしっくり来るかもしれない。

Previously, the keyboard and mouse typically generated input data. The system interpreted the data coming from these devices in a way that eliminated the device-specific details of the raw information. For example, the keyboard generates the device-specific scan code but the system provides an application with the virtual key code.

これまではキーボードとマウスが入力データを生成するのが一般的でした。システムは、これらのデバイスから送られてくるデータからデバイス固有の情報を除去する形で処理をしていました。例えばキーボードはデバイス固有のスキャンコードを生成しますが、システムはアプリケーションに仮想キーコードを提供します。

デバイスを抽象化することで多様なデバイスを簡単に扱うことができるようになったが、その反面、デバイス固有の情報にアクセスする手段はなくなってしまった。例えば WM_MOUSEMOVE 経由で取得したマウス座標はピクセル精度のため、高解像度のマウスを接続してもそれ以上の解像度を得ることができない。

Raw Input は、多様なデバイスへの対応とデバイス固有の情報へのアクセスを両立させるために用意された API と言える。

デバイス種別

HID のデバイス種別は Usage Page と Usage ID からなる Usage という値で管理され、どのような割り当てになっているかは USB-IF が定義した「HID Usage Tables」で確認することができる。

Usage Page はデバイスのグループを表す。

Page ID Page Name
01 Generic Desktop Page
02 Simulation Controls Page
03 VR Controls Page
04 Sport Controls Page
05 Game Controls Page
06 Generic Device Controls Page
07 Keyboard/Keypad Page
08 LED Page
09 Button Page

Usage ID は Usage Page 毎に定義され、主に使われる「Generic Desktop Page」であれば以下のようになっている。

Usage ID Usage Name
01 Pointer
02 Mouse
03 Reserved
04 Joystick
05 Gamepad
06 Keyboard
07 Keypad
08 Multi-axis Controller
09 Tablet PC System Controls

キーボードとマウスであれば以下のような割り当てになる。

デバイス Usage Page Usage ID
キーボード 01 06
マウス 01 02

キーボードに関連する Usage

Usage にはキーボードと関連しそうな "Key" を含む項目がいくつかあり、通常のキーボードと何が違うのか分からなかったので、HID Usage Tables に書かれている定義を確認した。

Keyboard/Keypad Page

This section is the Usage Page for key codes to be used in implementing a USB keyboard.

このセクションは USB キーボードの実装に使用されるキーコードの Usage Page です。

キーコードを表すための Usage Page で単体のデバイスというわけではなさそう。

Keypad

Any keyboard configuration that does not meet the minimum requirements of the Boot Keyboard. Keypad often refers to a supplementary calculator-style keyboard.

ブートキーボードの最小要件を満たしていないキーボード構成。キーパッドは補助的な電卓形式のキーボードを指すことが多い。

ブートキーボード (BIOS がサポートするブートデバイス) 以外のキーボードとのこと。

デバイスの列挙

Raw Input はデバイスの接続状況に関係なく登録することができるので、必ずしもデバイスの列挙が必要というわけではない。

Note that an application can register a device that is not currently attached to the system. When this device is attached, the Windows Manager will automatically send the raw input to the application.

アプリケーションは現在システムに接続されていないデバイスを登録できることに注意してください。登録したデバイスが接続されると、ウィンドウマネージャーは自動的に入力をアプリケーションに送信します。

ただ、Raw Input で取得した入力データに含まれるデバイス識別情報はデバイスハンドルしかなく、そのままではプログラム上で複数のデバイスを管理するのが難しいため、GetRawInputDeviceList でデバイスを列挙してデバイスハンドルと番号 (列挙順) の対応表を作成する。

UINT GetRawInputDeviceList(
  PRAWINPUTDEVICELIST pRawInputDeviceList,    // RAWINPUTDEVICELIST 構造体の配列
  PUINT               puiNumDevices,          // デバイス数
  UINT                cbSize                  // RAWINPUTDEVICELIST 構造体のサイズ
);

呼び出すと接続されている全デバイスの情報が pRawInputDeviceList に格納される。

typedef struct tagRAWINPUTDEVICELIST {
  HANDLE hDevice; // デバイスハンドル
  DWORD  dwType;  // デバイス種別
} RAWINPUTDEVICELIST, *PRAWINPUTDEVICELIST;

dwType にはデバイス種別が入っているので対象のデバイスかどうかはこれで判断する。

意味
RIM_TYPEMOUSE マウス
RIM_TYPEKEYBOARD キーボード
RIM_TYPEHID マウスとキーボード以外の HID

以下は、サンプルプログラムを元にしたデバイスを列挙するコード。

// デバイス数を取得
UINT nNumDevices;
if ( GetRawInputDeviceList( NULL, &nNumDevices, sizeof( RAWINPUTDEVICELIST ) ) != 0 )
{
    // エラー
}

// デバイス情報を取得
auto plists = new RAWINPUTDEVICELIST[nNumDevices];
if ( GetRawInputDeviceList( plists, &nNumDevices, sizeof( RAWINPUTDEVICELIST ) ) == nNumDevices )
{
    // エラー
}

// 列挙されたデバイス情報を参照
for ( int i = 0; i < nNumDevices; i++ )
{
    auto plist = &plists[i];
}

delete[] plist;

デバイスを列挙する際の、

  1. データを格納する引数に NULL を指定して要素数を取得する。
  2. 要素数を元にデータ領域を確保する。
  3. データ領域と要素数を指定してデータを取得する。

という関数を 2 回呼ぶイディオムは入力データの取得でも使われている。

デバイスの登録

使用する入力デバイスを RegisterRawInputDevices で登録する。

BOOL RegisterRawInputDevices(
  PCRAWINPUTDEVICE pRawInputDevices,  // RAWINPUTDEVICE 構造体の配列
  UINT             uiNumDevices,      // 配列の要素数
  UINT             cbSize             // RAWINPUTDEVICE 構造体のサイズ
);

引数の RAWINPUTDEVICE 構造体に入力デバイスの情報をセットする。

typedef struct tagRAWINPUTDEVICE {
  USHORT usUsagePage; // Usage Page
  USHORT usUsage;     // Usage ID
  DWORD  dwFlags;     // モードフラグ
  HWND   hwndTarget;  // ウィンドウハンドル
} RAWINPUTDEVICE, *PRAWINPUTDEVICE, *LPRAWINPUTDEVICE;

usUsagePage と usUsage には入力デバイスの Usage を指定する。定数等は用意されていないので、マジックナンバーを避けたい場合は自分で定義するしかない。

dwFlags は細かい設定を行うためのフラグで、デフォルト値は 0 となっている。主なフラグは以下の通り。

意味
RIDEV_REMOVE デバイスを削除する。
RIDEV_PAGEONLY usUsagePage のみを指定する。usUsage は 0 でなければならない。
RIDEV_NOLEGACY WM_MOUSEMOVE のようなレガシーメッセージを生成しない。
RIDEV_INPUTSINK ウィンドウが前面になくても入力を受け取る。hwndTarget を設定しなければならない。
RIDEV_DEVNOTIFY デバイスの接続と取り外し時に WM_INPUT_DEVICE_CHANGE を生成する。

以下は、サンプルプログラムを元にしたキーボード登録を行うコード。

RAWINPUTDEVICE rids[1];

// HID_~ は自分で定義したもの
rid[0].usUsagePage = HID_USAGE_PAGE_GENERIC;
rid[0].usUsage     = HID_USAGE_GENERIC_KEYBOARD;
rid[0].dwFlags     = 0;
rid[0].hwndTarget  = NULL;

if ( RegisterRawInputDevices( rids, 1, sizeof( RAWINPUTDEVICE ) ) == FALSE )
{
    // 登録失敗
}

入力デバイスの数だけ登録が必要になりそうな関数名だが、デバイスが属する Usage を登録するものなので、同系統のデバイスであれば複数台あったとしても登録は一度だけで済む。

入力データの取得

デバイスを登録すると WM_INPUT メッセージが送られて来るようになるので、その際の lParam を HRAWINPUT にキャストして GetRawInputData に渡すと入力データを取得することができる。

UINT GetRawInputData(
  HRAWINPUT hRawInput,      // RAWINPUT 構造体へのハンドル
  UINT      uiCommand,      // コマンド
  LPVOID    pData,          // RAWINPUT 構造体からくるデータ
  PUINT     pcbSize,        // pData のサイズ
  UINT      cbSizeHeader    // RAWINPUTHEADER 構造体のサイズ
);

デバイスによって異なるデータサイズに対応するためか、pData の型は LPVOID になっている。実体は RAWINPUT 構造体へのポインタなのでキャストしてから使用する。

typedef struct tagRAWINPUT {
  RAWINPUTHEADER header;
  union {
    RAWMOUSE    mouse;
    RAWKEYBOARD keyboard;
    RAWHID      hid;
  } data;
} RAWINPUT, *PRAWINPUT, *LPRAWINPUT;

RAWINPUTHEADER 構造体には各デバイス共通の情報が入っている。

typedef struct tagRAWINPUTHEADER {
  DWORD  dwType;  // デバイス種別
  DWORD  dwSize;  // データのサイズ
  HANDLE hDevice; // デバイスへのハンドル
  WPARAM wParam;  // WM_INPUT 時の wParam パラメーター
} RAWINPUTHEADER, *PRAWINPUTHEADER, *LPRAWINPUTHEADER;

入力データは RAWINPUT.data 共用体の中にあり、マウスとキーボードは専用の構造体が用意されている。今回はキーボードの入力データが必要なので RAWKEYBOARD 構造体を参照する。

typedef struct tagRAWKEYBOARD {
  USHORT MakeCode;            // スキャンコード
  USHORT Flags;               // スキャンコード情報フラグ
  USHORT Reserved;            // 予約フィールド
  USHORT VKey;                // 仮想キーコード
  UINT   Message;             // ウィンドウメッセージ
  ULONG  ExtraInformation;    // デバイス固有の追加情報
} RAWKEYBOARD, *PRAWKEYBOARD, *LPRAWKEYBOARD;

以下は、サンプルプログラムを元にした入力を取得するコード。

auto hRawInput = reinterpret_cast< HRAWINPUT >(lParam);

// バッファサイズを取得
UINT uSize;
if ( GetRawInputData( hRawInput, RID_INPUT, NULL, &uSize, sizeof( RAWINPUTHEADER ) ) != 0 )
{
    // エラー
}

// 入力データを取得
auto lpbyData = new BYTE[uSize];
if ( GetRawInputData( hRawInput, RID_INPUT, lpbyData, &uSize, sizeof( RAWINPUTHEADER ) ) != uSize )
{
    // エラー
}

// 入力データを参照
auto lprawinput = reinterpret_cast< RAWINPUT* >(lpbyData);

delete[] lpbyData;

キーの入力状態判定

押されたキーの種別は VKey、入力状態は Flags で判別することができる。Flags の定数名はスキャンコードの用語がそのまま使われているので、馴染みがないと少々分かりづらい。

意味
RI_KEY_MAKE キーが押された。
RI_KEY_BREAK キーが離された。
RI_KEY_E0 E0 プレフィックスあり。
RI_KEY_E1 E1 プレフィックスあり。

Wikipedia では、

キーが押されたときに発生するスキャンコードをメイク (make) コード、離されたときに発生するものをブレイク (break) コードという。

と解説されている。

キーは「押されている」状態と「押されていない」状態の 2 値なので、RI_KEY_MAKE と RI_KEY_BREAK のどちらを使うかは好みの問題だと思っていたが、RI_KEY_MAKE は値が 0 でビット表現がなく Flags に記録されない。代わりに値が非 0 の RI_KEY_BREAK を使って判定を行う。

// 離されていなければ押している
auto bHold = (rawkeyboard.Flags & RI_KEY_BREAK) == 0;

複数キーの同時押し判定

複数のキーを同時に押した場合、押した瞬間と離した瞬間は全てのキー情報が送られて来るが、押し続けている間は一部のキー情報しか送られて来ない。瞬間的なキー情報だけでは同時押しを判定することができないので、VKey がキー (またはオフセット) になる bool 配列を用意し、各キーの入力状態を保存しておく。仮想キーコードの範囲は、

The code must be a value in the range 1 to 254.

コードは 1 ~ 254 の範囲の値である必要があります。

となっているので、切り良く 256 要素確保することにした。

// クラスのメンバ変数などに定義
std::array< bool, 256 > bHolds;

// コンストラクタあたりで初期化する
bHolds.fill( false );

// 入力状態をキー別に保存する
bHolds[rawkeyboard.VKey] = (rawkeyboard.Flags & RI_KEY_BREAK) == 0;

キーボードの A と B を同時に押したときの入力状態の変化は以下のようになる。

順番 操作 VKey Flags A の値 B の値
1 false false
2 A と B を押す A 非 BREAK true false
3 押し続ける B 非 BREAK true true
4 押し続ける B 非 BREAK true true
5 A と B を離す A BREAK false true
6 B BREAK false false
7 false false

不定期に発生する入力データをゲーム側で一括取得できるようにするには、どこかにキー情報を溜め込んでおかなければならないので、特別な対応をせずともこの問題を解決できてしまうケースがほとんどではないだろうか。

同じキーの区別

キーボードの左右に配置されている同一のキー (Ctrl など) は、仮想キーコードが同じ値になっているため、VKey だけではどちらのキーが押されたのか判別することができない。このようなキーのスキャンコードにはプレフィックスを付加する決まりになっているので、Flags に格納されたプレフィックス情報を使って判別を行う。

auto usVKey = rawkeyboard.VKey;

// E0 プレフィックスの有無
auto bE0 = (rawkeyboard.Flags & RI_KEY_E0) != 0;

// 左右に振り分け
switch ( usVKey )
{
case VK_CONTROL:
    usVKey = bE0 ? VK_RCONTROL : VK_LCONTROL;
    break;
case VK_MENU:
    usVKey = bE0 ? VK_RMENU : VK_LMENU;
    break;
}

Shift キーはプレフィックスが付加されないので判別することができない。この理由については Stack Overflow の投稿が参考になる。

There's ancient history behind this. The keyboard controller was redesigned for the IBM AT, again for the Enhanced keyboard. It started sending out 0xe0 and 0xe1 prefixes for keys that were added to the keyboard layout. Like the right Ctrl and Alt keys. But keyboards always had two shift keys. The original IBM PC didn't consider them special keys, they simply have a different scan code. Which was maintained in later updates. Accordingly, you don't get the RI_KEY_E0 or E1 flags for them. You have to distinguish them by the RAWKEYBOARD.MakeCode value. The left shift key has makecode 0x2a, the right key is 0x36.

背景には古い歴史があります。キーボードコントローラーは IBM AT 用に、また拡張キーボード用に再設計されました。キーボードに追加されたキーのために 0xe0 および 0xe1 プレフィックスの送信を開始しました。右の Ctrl キーと Alt キーのように。しかし、キーボードには常にふたつの Shift キーがありました。元の IBM PC は、それらを特別なキーとは見なしておらず、単に異なるスキャンコードを持っているだけです。これは後のアップデートで維持されました。従って RI_KEY_E0 や E1 フラグは取得できません。RAWKEYBOARD.MakeCode 値で区別する必要があります。左のシフトキーの makecode は 0x2a、右のキーは 0x36 です。

自前でスキャンコードを仮想キーコードに変換するのは大変なので、MapVirtualKey という変換関数を使ってみたが、

スキャンコードを VK_SHIFT、VK_CONTROL、VK_MENU などの仮想キーコード定数へ変換できます。また、その逆の変換も行えます。これらの変換では、左右の Shift、Ctrl、Alt の各キーを区別しません。

左右のキーを区別することはできなかった。

デバイスの削除

WM_INPUT メッセージはデバイスを削除するまで送られ続けるので、デバイスが不要になり次第削除を行うのが望ましい。削除は登録とほぼ同じで、RAWINPUTDEVICE.dwFlags に RIDEV_REMOVE を入れて RegisterRawInputDevices を呼び出すだけ。

RAWINPUTDEVICE rids[1];

rid.usUsagePage = HID_USAGE_PAGE_GENERIC;
rid.usUsage     = HID_USAGE_GENERIC_KEYBOARD;
rid.dwFlags     = RIDEV_REMOVE;
rid.hwndTarget  = NULL;

if ( RegisterRawInputDevices( rids, 1, sizeof( RAWINPUTDEVICE ) ) == FALSE )
{
    // 削除失敗
}

hwndTarget の設定には注意が必要で、情報が多い分には問題ないだろうと思ってウィンドウハンドルを設定すると、関数の呼び出しに失敗してしまう。

If a RAWINPUTDEVICE structure has the RIDEV_REMOVE flag set and the hwndTarget parameter is not set to NULL, then parameter validation will fail.

RAWINPUTDEVICE 構造体に RIDEV_REMOVE フラグが設定されていて hwndTarget パラメーターが NULL に設定されていない場合、パラメーターの検証は失敗します。

ウィンドウプロシージャを経由しない入力データ取得方法

自作のゲームライブラリを Raw Input に対応させる場合、ライブラリ内部にあるオブジェクトは外部からアクセスする手段を持たないので、グローバル変数にでもしない限りウィンドウプロシージャで取得した入力データをライブラリ側に反映することができない。これを解決するにはウィンドウプロシージャに頼らない入力取得方法を考える必要がある。

ポーリング

GetRawInputBuffer はポーリング方式なので、ウィンドウプロシージャを経由せずに直接入力データを取得することができる。ただ、この関数は実行環境によって構造体のパディングサイズが異なるというバグがあるので扱いには注意が必要。

To get the correct size of the raw input buffer, do not use *pcbSize, use *pcbSize * 8 instead. To ensure GetRawInputBuffer behaves properly on WOW64, you must align the RAWINPUT structure by 8 bytes.

Raw Input バッファの正しいサイズを取得するには、*pcbSize を使用せず、代わりに *pcbSize * 8 を使用してください。WOW64 上で GetRawInputBuffer が正しく動作するようにするには、RAWINPUT 構造体を 8 バイト境界に揃える必要があります。

また、Stack Overflow の投稿によると、

I guess with GetRawInputBuffer() you can only read HID data. That means only the hid structure in the data part of the RAWINPUT structure is filled with input data. I was able to read input from my keyboard using the bRawData member of RAWHID but i think thats useless because that values vary from keyboard to keyboard. So I switched back to GetRawInputData....

GetRawInputBuffer() では、HID データしか読み取れないと思います。つまり、RAWINPUT 構造体の data 部分の hid 構造体のみが入力データで埋められます。RAWHID の bRawData メンバーを使ってキーボードから入力を読み取ることはできましたが、その値はキーボードごとに異なるため役に立たないと思います。そのため、GetRawInputData に切り替えました。

RAWINPUT.data.hid にしかデータが書き込まれないので使い物にならないとのこと。

サブクラス化

既定のウィンドウプロシージャを別のウィンドウプロシージャにすり替えることでメッセージを横取りすることができる。このすり替えはサブクラス化と呼ばれ、以前は SetWindowLongPtr を使ったウィンドウプロシージャ書き変えのことを指していたが、Windows XP 以降であれば SetWindowSubclass という専用の関数が用意されている。

BOOL SetWindowSubclass(
  HWND         hWnd,          // ウィンドウハンドル
  SUBCLASSPROC pfnSubclass,   // ウィンドウプロシージャ
  UINT_PTR     uIdSubclass,   // サブクラス ID
  DWORD_PTR    dwRefData      // ユーザーデータ
);

試しにサブクラス化してみたら関数の呼び出しで失敗していた。マニュアルには、

Warning You cannot use the subclassing helper functions to subclass a window across threads.

警告 サブクラス化ヘルパー関数を使用して、スレッドをまたいでウィンドウをサブクラス化することはできません。

と書かれていて、マルチスレッドには対応していない模様。

メッセージフック

メッセージフックを使うとウィンドウプロシージャへ送られる前のメッセージを監視することができる。横取りだけでなく書き変えも行えるのでサブクラス化よりも強力な機能となっている。SetWindowsHookEx でフックプロシージャ (メッセージを受け取る関数) を設定すると、それ以降のメッセージはフックプロシージャへ送られるようになる。

HHOOK SetWindowsHookEx(
  int       idHook,       // フックタイプ
  HOOKPROC  lpfn,         // フックプロシージャ
  HINSTANCE hmod,         // フックプロシージャを含む DLL へのハンドル
  DWORD     dwThreadId    // スレッド ID
);

引数にスレッド ID があることから分かるように、マルチスレッドでも使用可能。

いくつかあるフックタイプの中から今回の用途に使えそうなものをピックアップした。

フックタイプ 意味
WH_CALLWNDPROC SendMessage で送られるメッセージを監視。
WH_GETMESSAGE PostMessage で送られるメッセージを監視。
WH_KEYBOARD キーボードメッセージ (WM_KEYUP または WM_KEYDOWN) を監視。

WM_INPUT はウィンドウプロシージャへ直接送られるメッセージではなく、従来のキーボードメッセージ (レガシーメッセージ) でもないので、WH_GETMESSAGE を使用する。

フックタイプ毎にフックプロシージャの形式は違い、WH_GETMESSAGE の場合は GetMsgProc 形式のコールバック関数を定義する。

LRESULT CALLBACK GetMsgProc(
  _In_ int    code,   // フックコード
  _In_ WPARAM wParam, // メッセージが削除されたか (PM_~ 定数)
  _In_ LPARAM lParam  // MSG 構造体
);

メッセージの種別は lParam から取り出した MSG 構造体で判断することができるので、MSG.message が WM_INPUT の時だけ処理を行えば、ウィンドウプロシージャを経由することなく入力データを読み取ることができる。

偽キーボードを除外する

HID として登録されているキーボードデバイスの中には、ホットキー (Fn キー) の制御といった特殊用途のキーボード (偽キーボード) が含まれることがあるので、GetRawInputDeviceList で最初に列挙されたキーボードを使うといった処理を書いてしまうと、運悪く先頭に偽キーボードが来たときに動作しなくなってしまう。

この偽キーボード問題についてはマイクロソフトの中の人である Raymond Chen 氏のブログ記事「Filtering out fake keyboards from the GetRawInputDeviceList function」で触れられている。

You may notice that on some systems, the function reports a lot of phantom keyboards. The phantom keyboards are devices that report themselves to HID as keyboard devices, even though they aren't keyboards in a traditional sense. They might be a separate numeric keypad, or buttons on the chassis for volume or brightness control that report themselves as keyboard keys.

一部のシステムでは、この関数が多くの偽キーボードを報告していることに気付くかもしれません。偽キーボードとは、従来の意味でのキーボードではないにもかかわらず HID にキーボードデバイスとして自己申告するデバイスのことです。それらは独立したテンキーやキーボード上にある音量や明るさを調整するためのボタンである可能性があります。

記事内には偽キーボードを判別するための関数があり、

bool SmellsLikeARealKeyboard(HANDLE hDevice)
{
    RID_DEVICE_INFO info;
    UINT size = sizeof(info);
    UINT actualSize = GetRawInputDeviceInfoW(
        hDevice, RIDI_DEVICEINFO, &info, &size);
    if (actualSize == (UINT)-1 || actualSize < sizeof(info))
    {
        // Weird failure.
        return false;
    }
    assert(info.dwType == RIM_TYPEKEYBOARD);
    return info.keyboard.dwNumberOfKeysTotal >= 30;
}

GetRawInputDeviceInfo で取得したキーの総数を元に判定を行っている。

I'm going to say that a keyboard device smells like a real keyboard if it has at least 30 keys. This will rule out most devices that pretend to be a keyboard just so they can provide a handful of hardware buttons.

キーボードデバイスに少なくとも 30 個のキーがあれば本物のキーボードということにしています。これにより、キーボードのふりをした物理ボタンをいくつか搭載しただけのデバイスのほとんどが除外されます。

ノート PC に外付けキーボードを接続した自分の開発環境では以下のようなデバイス情報になっていて、

dwType dwNumberOfKeysTotal 実際のデバイス デバイス説明
0x07 (Japanese Keyboard) 101 本体キーボード 日本語 PS/2 キーボード
0x51 (Unknown type or HID keyboard) 8 ホットキー制御用? HID キーボード デバイス
0x51 (Unknown type or HID keyboard) 264 外付けキーボード HID キーボード デバイス

dwType では区別がつかず、dwNumberOfKeysTotal で判断するしかないことが分かる。ただ、キー総数が 91 個の外付けキーボードの値が 264 になっていたりと、これを見ておけば OK と言い切れないのが悩ましいところ。