⌨️

C#でRawInput

に公開

RawInputReceiver

C#のRawInput APIのラッパークラス、RawInputReceiver を公開しました。

🧭 RawInputとは?

RawInput は、Windowsが提供する低レベルの入力APIで、複数の入力デバイス(キーボード・マウスなど)を個別に識別して扱えるのが特徴です。
通常のイベント処理では分からない「どのデバイスからの入力か」を判定できるため、入力元を区別して受信するといった事が可能です。

ただし、RawInputはWin32メッセージループやウィンドウハンドルが前提の設計になっていて、ウィンドウの動作原理を理解していないと扱いが少し難しいのが難点です。

🎯 セールスポイント

他のRawInputラッパーとは違うポイントとしては、

  • 極限までシンプルな使い方
    インスタンスを作成し、イベントハンドラを登録するだけで即受信開始できます。

  • イベントが別スレッド動作
    入力イベントは専用スレッドで処理されるため、UIスレッドをブロックしません。

  • ウィンドウハンドル不要
    自前で WM_INPUT 受信ウィンドウとスレッドを生成するため、コンソールアプリでも使えます。

  • デバイス詳細を自動取得
    プロダクト名や製造元などの情報を自動取得&内部キャッシュ。

  • 不完全な仮想キーコードを内部変換
    VK_CONTROLVK_SHIFT のような左右区別のないコードを、VK_LCONTROLVK_RSHIFT に変換します。

  • 無効なキー入力を無視
    KEYBOARD_OVERRUN_MAKE_CODE (0xFF) を受信した場合、イベントハンドラに送信しません。

  • 依存関係ゼロ
    他ライブラリに依存せず、WinAPIもセルフ定義しています。当初、WinAPI定義は CsWin32 を使う予定でしたが、下記の理由でやめました。

    • SetupApi など未カバーのAPIがある
    • DllImport 定義が好みじゃない(ポインタ多用)
    • enum値にまとめていない定数がそこそこある
  • 絶対座標デバイス対応(未確認)
    タッチパッドなどの絶対座標をスクリーン座標に変換する処理も搭載(現物未所持のため未検証)。

⚠️ 注意事項

  • .NET 9 以上が必須
    このライブラリでは、イベントハンドラに ref struct を使用しています。そのため、.NET 9 以上が必須です。.NET 8 以下ではコンパイルできません。
    EventArgs 関連の ref struct を通常の struct に変更すれば、.NET 8 以下でもコンパイル可能です。
  • キーボード・マウスのみ対応(ゲームパッドなどのHIDデバイスは未対応)
    RawInputではゲームパッドのようなHIDデバイスも受信可能ですが、メーカーやデバイスごとにデータ仕様が異なるため、汎用的な処理が困難です。
    そのため、本ライブラリでは対応していません。ゲームパッドの入力を扱いたい場合は、XInputなどの専用APIを使用することをおすすめします。

🧪 使用例

これだけで動作します。

using RadianTools.Hardware.Input.Windows;

internal class Program
{
    static void Main(string[] args)
    {
        using var receiver = new RawInputReceiver();

        receiver.MouseReceived += (e) => Console.WriteLine(e.ToString());
        receiver.KeyboardReceived += (e) => Console.WriteLine(e.ToString());

        Console.WriteLine("Push ESC key to exit.");

        while (Console.ReadKey(true).Key != ConsoleKey.Escape)
        {
        }
    }
}

📤 実行結果

Push ESC key to exit.
Handle = 0x0000000000010044, MouseOp = Move, PushState = False, MoveAbsolute = False, X = -2, Y = 1, WheelDelta = 0, MappingVirtualDesktop = False, ProductName = 2.4G INPUT DEVICE, Manufacturer = MOSART Semi.
Handle = 0x0000000000010044, MouseOp = Move, PushState = False, MoveAbsolute = False, X = -1, Y = 1, WheelDelta = 0, MappingVirtualDesktop = False, ProductName = 2.4G INPUT DEVICE, Manufacturer = MOSART Semi.
Handle = 0x0000000000010044, MouseOp = Move, PushState = False, MoveAbsolute = False, X = 0, Y = 0, WheelDelta = 0, MappingVirtualDesktop = False, ProductName = 2.4G INPUT DEVICE, Manufacturer = MOSART Semi.
Handle = 0x00000000002223D9, VKey = VK_F, PushState = True, ProductName = REALFORCE 108JP, Manufacturer = Topre
Handle = 0x00000000002223D9, VKey = VK_F, PushState = False, ProductName = REALFORCE 108JP, Manufacturer = Topre
Handle = 0x00000000002223D9, VKey = VK_H, PushState = True, ProductName = REALFORCE 108JP, Manufacturer = Topre
Handle = 0x00000000002223D9, VKey = VK_H, PushState = False, ProductName = REALFORCE 108JP, Manufacturer = Topre
Handle = 0x0000000000010044, MouseOp = VWheel, PushState = False, MoveAbsolute = False, X = 0, Y = 0, WheelDelta = -120, MappingVirtualDesktop = False, ProductName = 2.4G INPUT DEVICE, Manufacturer = MOSART Semi.
Handle = 0x0000000000010044, MouseOp = VWheel, PushState = False, MoveAbsolute = False, X = 0, Y = 0, WheelDelta = 120, MappingVirtualDesktop = False, ProductName = 2.4G INPUT DEVICE, Manufacturer = MOSART Semi.
Handle = 0x00000000002223D9, VKey = VK_ESCAPE, PushState = True, ProductName = REALFORCE 108JP, Manufacturer = Topre

🧵 苦労した点

ウィンドウを作って WM_INPUT を受信するまではスムーズだったけど、RawInputの「ウィンドウありき」な設計が気に入らず、独自ウィンドウ+スレッドに隠蔽しようとしたら P/Invoke だらけになって大変でした。
また、デバイス情報の取得も、あまり情報が無いのでCopilotに聞きつつ、実際に動作確認しながら試行錯誤しました。本当にこれでいいのか?という点もありますが、とりあえず形にはなったと思います。

WinFormsのウィンドウプロシージャを使えば楽だったかもしれないけど、RawInputのためだけにWinForms依存にするのは避けたかったんですよね。
メッセージポンプとウィンドウプロシージャを自前で組んだの、C++以来かも。

Discussion