本日は、Visual Studio® User Group より、複数のキーボードを別処理するには というスレッドを話題にしようと思います。飽くまで簡単に。
同じタイプの複数の入力デバイスからの入力を受けたい、と言うのは、例えばゲームパッドでは良くある話でしょう。ですが、これがキーボードやマウスとなるとなかなか要望も稀で解決策も難しい物があります。ついでに、使いこなすのも大変そうです。
基本的なところでは、Windows はそれらを扱いません。今使っているノートパソコンにはタッチパッドが付いていて、かつ USB でマウスも接続していますが、この両者はごくナチュラルに同居しており、どちらでもカーソルやボタンを操作できます。プログラム的にも、この両者からの入力は分け隔て無く .NET で言えば MouseDown とか MouseMove とかが発生します。そして、どちらからの入力なのかというのは考慮されず、透過的に扱うことを強制されます。
.NET のイベントや、その下層レベルのメッセージである WM_LBUTTONDOWN とか WM_MOUSEMOVE などで扱えないのならばどうするか。その解決になるかも知れない一つが、Raw Input に関連する API 群です。その端っこをちょっとだけ踏んでみます。
言葉通り、生の入力を直接扱う API です。受け取り方にはポーリングで判断する方法と Windows メッセージでイベントドリブンを行う方法があり、後者は WM_INPUT メッセージで受信することができます。
しかしこれ、情報が(特に日本語の)ほとんど無いのでなかなか大変。取りあえず形にしてみましたが、まともに扱おうとするとかなりの手間が予想されました。そもそも、デバイスを識別するハンドルは手に入りましたが、これをどう使うのかさっぱり分かりませんし。
なお、今回のコードは Windows XP 以降限定です。
// C# のコード。 using System; using System.Runtime.InteropServices; using System.Windows.Forms; public class RawInputForm : Form { [STAThread] public static void Main() { Application.Run(new RawInputForm()); } private TextBox textBox = new TextBox(); public RawInputForm() { this.textBox.Dock = DockStyle.Fill; this.textBox.Multiline = true; this.textBox.ScrollBars = ScrollBars.Vertical; this.Controls.Add(this.textBox); int size = Marshal.SizeOf(typeof(RawInputDevice)); RawInputDevice[] devices = new RawInputDevice[1]; // UsagePage=1,Usage=2 でマウスデバイスを指す devices[0].UsagePage = 1; devices[0].Usage = 2; //WM_INPUT を受け取るウィンドウ devices[0].Target = this.Handle; //WM_INPUT を有効にするデバイス群、devices の数、 // RawInputDevice の構造体サイズ RegisterRawInputDevices(devices, 1, size); } private void ProcessInputKey(ref Message m) { const int RidInput = 0x10000003; int headerSize = Marshal.SizeOf(typeof(RawInputHeader)); int size = Marshal.SizeOf(typeof(RawInput)); RawInput input; GetRawInputData(m.LParam, RidInput, out input, ref size, headerSize); RawMouse mouse = input.Mouse; this.textBox.AppendText( //デバイスの番号と直前からの移動量を表示 string.Format("{0}({1},{2})\r\n", input.Header.Device, mouse.LastX, mouse.LastY)); } protected override void WndProc(ref Message m) { const int WmInput = 0xFF; if (m.Msg == WmInput) this.ProcessInputKey(ref m); base.WndProc(ref m); } [DllImport("user32.dll")] private static extern int RegisterRawInputDevices( RawInputDevice[] devices, int number, int size); [DllImport("user32.dll")] private static extern int GetRawInputData( IntPtr rawInput, int command, out RawInput data, ref int size, int headerSize); private struct RawInputDevice { public short UsagePage; public short Usage; public int Flags; public IntPtr Target; } private struct RawInputHeader { public int Type; public int Size; public IntPtr Device; public IntPtr WParam; } private struct RawInput { public RawInputHeader Header; public RawMouse Mouse; } private struct RawMouse { public short Flags; public short ButtonFlags; public short ButtonData; public int RawButtons; public int LastX; public int LastY; public int Extra; } } ' VB.NET のコード。 Imports System Imports System.Runtime.InteropServices Imports System.Windows.Forms Public Class RawInputForm Inherits Form Public Shared Sub Main() Application.Run(New RawInputForm()) End Sub Private m_textBox As New TextBox() Public Sub New() Me.m_textBox.Dock = DockStyle.Fill Me.m_textBox.Multiline = True Me.m_textBox.ScrollBars = ScrollBars.Vertical Me.Controls.Add(Me.m_textBox) Dim size As Integer _ = Marshal.SizeOf(GetType(RawInputDevice)) Dim devices(0) As RawInputDevice ' UsagePage=1,Usage=2 でマウスデバイスを指す devices(0).UsagePage = 1 devices(0).Usage = 2 'WM_INPUT を受け取るウィンドウ devices(0).Target = Me.Handle 'WM_INPUT を有効にするデバイス群、devices の数、 ' RawInputDevice の構造体サイズ RegisterRawInputDevices(devices, 1, size) End Sub Private Sub ProcessInputKey(ByRef m As Message) Const RidInput As Integer = &H10000003 Dim headerSize As Integer _ = Marshal.SizeOf(GetType(RawInputHeader)) Dim size As Integer _ = Marshal.SizeOf(GetType(RawInput)) Dim input As RawInput GetRawInputData(m.LParam, RidInput, _ input, size, headerSize) Dim mouse As RawMouse = input.Mouse 'デバイスの番号と直前からの移動量を表示 Me.m_textBox.AppendText( _ String.Format("{0}({1},{2})" & Environment.NewLine, _ input.Header.Device, _ mouse.LastX, mouse.LastY)) End Sub Protected Overrides Sub WndProc(ByRef m As Message) Const WmInput As Integer = &HFF If m.Msg = WmInput Then Me.ProcessInputKey(m) End If MyBase.WndProc(m) End Sub 'WndProc Private Declare Function RegisterRawInputDevices _ Lib "user32.dll" ( _ ByVal devices As RawInputDevice(), _ ByVal number As Integer, ByVal size As Integer) As Integer Private Declare Function GetRawInputData Lib "user32.dll" ( _ ByVal rawInput As IntPtr, ByVal command As Integer, _ ByRef data As RawInput, ByRef size As Integer, _ ByVal headerSize As Integer) As Integer Private Structure RawInputDevice Public UsagePage As Short Public Usage As Short Public Flags As Integer Public Target As IntPtr End Structure Private Structure RawInputHeader Public Type As Integer Public Size As Integer Public Device As IntPtr Public WParam As IntPtr End Structure Private Structure RawInput Public Header As RawInputHeader Public Mouse As RawMouse End Structure Private Structure RawMouse Public Flags As Short Public ButtonFlags As Short Public ButtonData As Short Public RawButtons As Integer Public LastX As Integer Public LastY As Integer Public Extra As Integer End Structure End Class
WMI のデバイス系のクラスに「ハンドル」が入っている場合があるんですが、あれと同じだったりするとまだ救いがありますね。
Computer System Hardware Classes の方のクラスなのでそりゃそうだ……です。
まあ、起動時にユーザに確認させればいいと思いますが……いきなりハンドルが再作成されたりしないだろうな。
デバイスID 辺りと相互変換できれば便利ですけどね。
念のために SetupDiEnumDeviceInfo で漁ってみたけど出てくる値は案の定全然違いますし。
……と改めて Raw Input API を眺めてたら GetRawInputDeviceInfo を発見。RIDI_DEVICENAME を使えばデバイスインスタンス ID が手に入りました。GUID 付きで、こっちはデバイスを指す GUID みたいですね。
HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Control\DeviceClasses
にその辺の情報が登録されている模様。
この GUID を使えば SetupDi API でもごにょごにょできるという情報も。
こちらの記事が大変参考になりました。ありがとうございました。