Quantcast
Channel: C#タグが付けられた新着記事 - Qiita
Viewing all articles
Browse latest Browse all 9701

タッチパッドのタッチ位置をRaw Input APIで.NETから捕捉する

$
0
0
目的 Windows上でタッチパッドのタッチ位置を捕捉する機能は、.NETでは標準で提供されていませんが、これをWin32のRaw Input APIで.NETから捕捉できるようにします。内容的には、TouchpadGestures AdvancedのC++のコードに基本的にならって、C#のP/Invokeで実行するようにしたものです。 背景 タッチパッド(タッチパネルにあらず)からの入力は、.NETではマウスの動きに変換された後に扱うようになっていて、タッチパッドの入力を直接扱えるようにはなっていません。それで実用的に困ることはないですし、タッチ操作用のデバイスとしてはタッチスクリーンの方が優れているので、タッチパッドにこだわる必要もないです。 ただ、入力デバイスの選択肢としてあって困ることはなく、その一方で先行例が見つからず手が出なかったのですが1、@kamektxさんのTouchpadGestures Advancedを見かけてC++での方法は分かったので、C#でも書けるかなと。 ノートPCのタッチパッドでタブを切り替えるソフトウェアを作りました ~TouchpadGestures Advanced~ その途中で@mfakaneさんのRawInput.Sharpを知り、既にライブラリ化されていたわけですが、いずれにせよこの二つを非常に参考にさせていただきました。 コード タッチパッドからの入力を捕捉するには、WM_INPUTメッセージが送られてくるように登録が必要ですが、ここは難しくないので省きます。問題はWM_INPUTメッセージを受けて、このデータからタッチ位置などの情報をどう取得するかですが、基本的にTouchpadGestures AdvancedのHidManagerの手順にならっています。 大まかな流れは、以下のようになります。 WM_INPUTのlParamからRAWINPUT構造体を取得 RAWINPUT構造体中のRAWINPUTHEADER構造体から、どの種類のデータがあるかを取得 これを使ってRAWINPUT構造体中のRAWHID構造体から、実際のデータの値を取得 始めに、タッチ位置などのコンタクトの値を格納するためのTouchpadContact構造体を作成。ContactIdはタッチ中に各コンタクトに継続的に振られる番号で、これでどのコンタクトかを判別します。XとYはタッチパッドの左上角を原点とした座標。 TouchpadContact.cs public struct TouchpadContact { public int ContactId { get; } public int X { get; } public int Y { get; } public TouchpadContact(int contactId, int x, int y) => (this.ContactId, this.X, this.Y) = (contactId, x, y); } 次に、TouchpadContactを生成するためのTouchpadContactCreatorクラスを作成。これはTouchpadContact中の値は一遍に取得できず、順番に一時保存してから生成する必要があるためです。 TouchpadContactCreator.cs internal class TouchpadContactCreator { public int? ContactId { get; set; } public int? X { get; set; } public int? Y { get; set; } public bool TryCreate(out TouchpadContact contact) { if (ContactId.HasValue && X.HasValue && Y.HasValue) { contact = new TouchpadContact(ContactId.Value, X.Value, Y.Value); return true; } contact = default; return false; } public void Clear() { ContactId = null; X = null; Y = null; } } 最後に、lParamからTouchpadContactを配列で取得するParseInputメソッド。 TouchpadHelper.cs internal static class TouchpadHelper { public static TouchpadContact[] ParseInput(IntPtr lParam) { // Get RAWINPUT. uint rawInputSize = 0; uint rawInputHeaderSize = (uint)Marshal.SizeOf<RAWINPUTHEADER>(); if (GetRawInputData( lParam, RID_INPUT, IntPtr.Zero, ref rawInputSize, rawInputHeaderSize) != 0) { return null; } RAWINPUT rawInput; byte[] rawHidRawData; IntPtr rawInputPointer = IntPtr.Zero; try { rawInputPointer = Marshal.AllocHGlobal((int)rawInputSize); if (GetRawInputData( lParam, RID_INPUT, rawInputPointer, ref rawInputSize, rawInputHeaderSize) != rawInputSize) { return null; } rawInput = Marshal.PtrToStructure<RAWINPUT>(rawInputPointer); var rawInputData = new byte[rawInputSize]; Marshal.Copy(rawInputPointer, rawInputData, 0, rawInputData.Length); rawHidRawData = new byte[rawInput.Hid.dwSizeHid * rawInput.Hid.dwCount]; int rawInputOffset = (int)rawInputSize - rawHidRawData.Length; Buffer.BlockCopy(rawInputData, rawInputOffset, rawHidRawData, 0, rawHidRawData.Length); } finally { Marshal.FreeHGlobal(rawInputPointer); } // Parse RAWINPUT. IntPtr rawHidRawDataPointer = Marshal.AllocHGlobal(rawHidRawData.Length); Marshal.Copy(rawHidRawData, 0, rawHidRawDataPointer, rawHidRawData.Length); IntPtr preparsedDataPointer = IntPtr.Zero; try { uint preparsedDataSize = 0; if (GetRawInputDeviceInfo( rawInput.Header.hDevice, RIDI_PREPARSEDDATA, IntPtr.Zero, ref preparsedDataSize) != 0) { return null; } preparsedDataPointer = Marshal.AllocHGlobal((int)preparsedDataSize); if (GetRawInputDeviceInfo( rawInput.Header.hDevice, RIDI_PREPARSEDDATA, preparsedDataPointer, ref preparsedDataSize) != preparsedDataSize) { return null; } if (HidP_GetCaps( preparsedDataPointer, out HIDP_CAPS caps) != HIDP_STATUS_SUCCESS) { return null; } ushort valueCapsLength = caps.NumberInputValueCaps; var valueCaps = new HIDP_VALUE_CAPS[valueCapsLength]; if (HidP_GetValueCaps( HIDP_REPORT_TYPE.HidP_Input, valueCaps, ref valueCapsLength, preparsedDataPointer) != HIDP_STATUS_SUCCESS) { return null; } uint scanTime = 0; uint contactCount = 0; TouchpadContactCreator creator = new(); List<TouchpadContact> contacts = new(); foreach (var valueCap in valueCaps.OrderBy(x => x.LinkCollection)) { if (HidP_GetUsageValue( HIDP_REPORT_TYPE.HidP_Input, valueCap.UsagePage, valueCap.LinkCollection, valueCap.Usage, out uint value, preparsedDataPointer, rawHidRawDataPointer, (uint)rawHidRawData.Length) != HIDP_STATUS_SUCCESS) { continue; } // Usage Page and ID in Windows Precision Touchpad input reports // https://docs.microsoft.com/en-us/windows-hardware/design/component-guidelines/windows-precision-touchpad-required-hid-top-level-collections#windows-precision-touchpad-input-reports switch (valueCap.LinkCollection) { case 0: switch (valueCap.UsagePage, valueCap.Usage) { case (0x0D, 0x56): // Scan Time scanTime = value; break; case (0x0D, 0x54): // Contact Count contactCount = value; break; } break; default: switch (valueCap.UsagePage, valueCap.Usage) { case (0x0D, 0x51): // Contact ID creator.ContactId = (int)value; break; case (0x01, 0x30): // X creator.X = (int)value; break; case (0x01, 0x31): // Y creator.Y = (int)value; break; } break; } if (creator.TryCreate(out TouchpadContact contact)) { contacts.Add(contact); if (contacts.Count >= contactCount) break; creator.Clear(); } } return contacts.ToArray(); } finally { Marshal.FreeHGlobal(rawHidRawDataPointer); Marshal.FreeHGlobal(preparsedDataPointer); } } [DllImport("User32.dll", SetLastError = true)] private static extern uint GetRawInputData( IntPtr hRawInput, // lParam in WM_INPUT uint uiCommand, // RID_HEADER IntPtr pData, ref uint pcbSize, uint cbSizeHeader); private const uint RID_INPUT = 0x10000003; [StructLayout(LayoutKind.Sequential)] private struct RAWINPUT { public RAWINPUTHEADER Header; public RAWHID Hid; } [StructLayout(LayoutKind.Sequential)] private struct RAWINPUTHEADER { public uint dwType; // RIM_TYPEMOUSE or RIM_TYPEKEYBOARD or RIM_TYPEHID public uint dwSize; public IntPtr hDevice; public IntPtr wParam; // wParam in WM_INPUT } private const uint RIM_TYPEMOUSE = 0; private const uint RIM_TYPEKEYBOARD = 1; private const uint RIM_TYPEHID = 2; [StructLayout(LayoutKind.Sequential)] private struct RAWHID { public uint dwSizeHid; public uint dwCount; public IntPtr bRawData; // This is not for use. } [DllImport("User32.dll", SetLastError = true)] private static extern uint GetRawInputDeviceInfo( IntPtr hDevice, // hDevice by RAWINPUTHEADER uint uiCommand, // RIDI_PREPARSEDDATA IntPtr pData, ref uint pcbSize); private const uint RIDI_PREPARSEDDATA = 0x20000005; [DllImport("Hid.dll", SetLastError = true)] private static extern uint HidP_GetCaps( IntPtr PreparsedData, out HIDP_CAPS Capabilities); private const uint HIDP_STATUS_SUCCESS = 0x00110000; [StructLayout(LayoutKind.Sequential)] private struct HIDP_CAPS { public ushort Usage; public ushort UsagePage; public ushort InputReportByteLength; public ushort OutputReportByteLength; public ushort FeatureReportByteLength; [MarshalAs(UnmanagedType.ByValArray, SizeConst = 17)] public ushort[] Reserved; public ushort NumberLinkCollectionNodes; public ushort NumberInputButtonCaps; public ushort NumberInputValueCaps; public ushort NumberInputDataIndices; public ushort NumberOutputButtonCaps; public ushort NumberOutputValueCaps; public ushort NumberOutputDataIndices; public ushort NumberFeatureButtonCaps; public ushort NumberFeatureValueCaps; public ushort NumberFeatureDataIndices; } [DllImport("Hid.dll", CharSet = CharSet.Auto)] private static extern uint HidP_GetValueCaps( HIDP_REPORT_TYPE ReportType, [Out] HIDP_VALUE_CAPS[] ValueCaps, ref ushort ValueCapsLength, IntPtr PreparsedData); private enum HIDP_REPORT_TYPE { HidP_Input, HidP_Output, HidP_Feature } [StructLayout(LayoutKind.Sequential)] private struct HIDP_VALUE_CAPS { public ushort UsagePage; public byte ReportID; [MarshalAs(UnmanagedType.U1)] public bool IsAlias; public ushort BitField; public ushort LinkCollection; public ushort LinkUsage; public ushort LinkUsagePage; [MarshalAs(UnmanagedType.U1)] public bool IsRange; [MarshalAs(UnmanagedType.U1)] public bool IsStringRange; [MarshalAs(UnmanagedType.U1)] public bool IsDesignatorRange; [MarshalAs(UnmanagedType.U1)] public bool IsAbsolute; [MarshalAs(UnmanagedType.U1)] public bool HasNull; public byte Reserved; public ushort BitSize; public ushort ReportCount; [MarshalAs(UnmanagedType.ByValArray, SizeConst = 5)] public ushort[] Reserved2; public uint UnitsExp; public uint Units; public int LogicalMin; public int LogicalMax; public int PhysicalMin; public int PhysicalMax; // Range public ushort UsageMin; public ushort UsageMax; public ushort StringMin; public ushort StringMax; public ushort DesignatorMin; public ushort DesignatorMax; public ushort DataIndexMin; public ushort DataIndexMax; // NotRange public ushort Usage => UsageMin; // ushort Reserved1; public ushort StringIndex => StringMin; // ushort Reserved2; public ushort DesignatorIndex => DesignatorMin; // ushort Reserved3; public ushort DataIndex => DataIndexMin; // ushort Reserved4; } [DllImport("Hid.dll", CharSet = CharSet.Auto)] private static extern uint HidP_GetUsageValue( HIDP_REPORT_TYPE ReportType, ushort UsagePage, ushort LinkCollection, ushort Usage, out uint UsageValue, IntPtr PreparsedData, IntPtr Report, uint ReportLength); } 基本的には粛々とP/Invokeを書けばいいのですが、少し難しいのはRAWHID構造体で、Win32の定義は以下のようになっています。 RAWHID typedef struct tagRAWHID { DWORD dwSizeHid; DWORD dwCount; BYTE bRawData[1]; } RAWHID この1番目のdwSizeHidは各HID inputのデータの長さ、2番目のdwCountはHID inputの数で、3番目のbRawDataが実際のデータですが、byte配列でありながら長さは1になっています。RemarksによればdwSizeHidとdwCountの積がbRawDataの長さになるということですが、実際に実行してみると、Surface Pro 4の例では30となり、計算が合いません。 ではどういうことかというと、これはWin32では時々ある、構造体の後ろにbyte配列が続いている形式で、bRawDataはこの配列の先頭byteを指しています。つまり、Surface Pro 4の例ではRAWINPUT構造体の長さは62だったので、RAWINPUTHEADERの長さが24(16 + 8)で、dwSizeHidとdwCountの長さがそれぞれ4なので、差し引き62 - 24 - 4 - 4 = 30がbRawDataの本当の長さということになります。これは、dwSizeHidが30でdwCountが1だったので、この積とも符合します。ちなみに、30でコンタクト5つ分のデータがありました。 もしこの長さが30で固定であれば、[MarshalAs(UnmanagedType.ByValArray, SizeConst = 30)]を付ければいいわけですが、固定ではないのでしょうから、これはなし。 したがって、RAWINPUT構造体を取得するときに、先にこのサイズを取得した後、IntPtrにメモリを確保し、このIntPtrにGetRawInputDataのデータを格納し、このIntPtrからRAWINPUT構造体に変換してbRawData以外の値を取得し、さらに同じIntPtrから直接byte配列を取得して、この後半のbRawDataに当たる部分を取り出すということをやっています。2 上記のコードのレポジトリはRawInput.Touchpadです。 テスト テスト環境は以下のとおりです。 Windows 10 21H1 Surface Pro 4(+タイプカバーのタッチパッド) .NET 5.0のWPF タッチパッドに指を同時に5本当てたところ。5つのコンタクトで各指のタッチ位置が示されています。 タッチ中は非常に短い間隔でWM_INPUTメッセージが送られてきますが、指が浮くとその指のコンタクトは途切れてしまうので、動きを自然にトレースするには同じIDのコンタクトを追う必要があります。ちなみに、Surface Pro 4の例ではタッチパッドの左上角が原点の0,0で、右下角が1956,997だったので、物理的なサイズ(101 × 53mm)に対比して結構細かいです。 以上のとおり、もし必要が出てくれば使えるのではないかと思います。 Synapticsのタッチパッド用のものは以前から存在しますが、こちらは汎用的なRaw Input APIを使うので、高精度タッチパッド(Precision Touchpad)であれば動くはず、というのも利点です。 ↩ RawInput.Sharpではunsafeにしてポインターでスマートに処理しています。 ↩

Viewing all articles
Browse latest Browse all 9701

Trending Articles