目的
マウスやキーの入力を記録したい。
背景
昔はこういう目的には SetWindowsHookEx() を使うのが勧められていたように思う。この API は非常に強力で、既存のプロセスに自作の dll を潜り込ませてウィンドウメッセージ hook 用のコールバックを呼ばせることができ、これで全てのウィンドウに送られてくる WM_MOUSEMOVE を監視、といったことができる。
しかしこの API、強力すぎて悪用されまくったそうで、Vista 以降使用が厳しく制限されている。管理者権限、UI Access、デジタル証明など、ありとあらゆる権限を付与しないと機能しなくなっており、もう事実上使えなくなったと言っていい。
(既存プロセスに自作 dll を潜り込ませたい場合、CreateRemoteThread() が今でも使える。未だにこの API が野放しなのは驚きだが、既存の色んな便利ツールがこれに依存してるから、という理由が強いんだと思う)
GetKeyboardState() を 1 ms 毎に呼んで状態を監視、みたいな雑な実装でも実用上問題ないくらいには目的を達成できたが、もうちょいスマートにやりたい。
というわけで、Raw Input API を使う。Raw Input API は名前通りローレベルな入力 API で、あらゆる入力デバイスからのデータを取れる。今回はとりあえずマウスとキーボードだけ面倒を見る。
実装
以下のような手順になる。
- 入力監視用の不可視のウィンドウを作る
- 入力を監視したいデバイスを登録する
- ウィンドウプロシージャ内で WM_INPUT を処理する
GUI 用のウィンドウを作る場合などはそちらに入力監視を兼任させることもできるが、汎用性を考えると専用ウィンドウを作るほうがいいと思う。ウィンドウの作成はまあいつも通り。
WNDCLASS wc{};
wc.lpfnWndProc = &InputProc;
wc.hInstance = ::GetModuleHandle(nullptr);
wc.lpszClassName = TEXT("InputClass");
::RegisterClass(&wc);
HWND hwnd = ::CreateWindow(wc.lpszClassName, nullptr, 0, 0, 0, 0, 0, HWND_MESSAGE, nullptr, wc.hInstance, nullptr);
デバイスの登録。入力を受け取りたいデバイスを指定する。今回はマウスとキーボードを指定。
RAWINPUTDEVICE rid[2]{};
// mouse
rid[0].usUsagePage = 0x01;
rid[0].usUsage = 0x02;
rid[0].dwFlags = RIDEV_INPUTSINK;
rid[0].hwndTarget = hwnd;
// keyboard
rid[1].usUsagePage = 0x01;
rid[1].usUsage = 0x06;
rid[1].dwFlags = RIDEV_INPUTSINK;
rid[1].hwndTarget = hwnd;
::RegisterRawInputDevices(rid, std::size(rid), sizeof(RAWINPUTDEVICE));
usUsagePage と usUsage は上のコードでマウスとキーボードらしい (よくわかってない)。
RIDEV_INPUTSINK フラグがあるとウィンドウがアクティブではない時でも入力が来るようになる。今回の目的には必須。
また、RIDEV_NOLEGACY フラグがあると、そのプロセス内では指定デバイスからは WM_MOUSEMOVE のような従来のメッセージが発生しなくなる。GUI 用のウィンドウを他に作った場合、マウス&キーボードメッセージが一切来なくなって操作不能に陥る。このフラグを使っているサンプルをコピペしてハマったので注意喚起しておきたい。
ともあれ、これで WM_INPUT メッセージが来るようになる。
WM_INPUT の処理。
LRESULT CALLBACK InputProc(HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam)
{
if (Msg == WM_INPUT) {
auto hRawInput = (HRAWINPUT)lParam;
UINT dwSize = 0;
std::vector<char>buf;
::GetRawInputData(hRawInput, RID_INPUT, nullptr, &dwSize, sizeof(RAWINPUTHEADER));
buf.resize(dwSize);
::GetRawInputData(hRawInput, RID_INPUT, buf.data(), &dwSize, sizeof(RAWINPUTHEADER));
auto& input = *(RAWINPUT*)buf.data();
if (input.header.dwType == RIM_TYPEMOUSE) {
if (input.data.mouse.usButtonFlags & RI_MOUSE_LEFT_BUTTON_DOWN) {
// 左クリック
}
}
else if (input.header.dwType == RIM_TYPEKEYBOARD) {
if (input.data.keyboard.Flags & RI_KEY_BREAK) {
if (input.data.keyboard.VKey == VK_ESCAPE) {
// Esc キーが押された
}
}
}
}
return ::DefWindowProc(hWnd, Msg, wParam, lParam);
}
こんな感じで入力を取れる。
注意点として、マウスの座標は相対位置 (移動量) になっており、スクリーン座標ではない。(RAWMOUSE にはマウス座標が相対位置か絶対位置かのフラグがあるが、少なくとも私の環境では常に相対位置だった) Raw Input API の中からはスクリーン座標を得る手段を見つけられず、GetCursorInfo() に頼った。
また、ウィンドウメッセージ方式ではない入力データ受け取り方法も用意されているが (GetRawInputBuffer())、入力のタイミングを知る方法が無いっぽい?ので今回の目的には適さず、触れていない。
結果
いい感じに入力の記録ができた。結果はこちらのツールに活かされている。
https://github.com/i-saint/MouseReplayer