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

## 目的

Windows上でタッチパッドのタッチ位置を捕捉する機能は、.NETでは標準で提供されていませんが、これをWin32の[Raw Input](https://docs.microsoft.com/en-us/windows/win32/inputdev/raw-input) APIで.NETから捕捉できるようにします。内容的には、[TouchpadGestures Advanced](https://github.com/kamektx/TouchpadGestures_Advanced)のC++のコードに基本的にならって、C#のP/Invokeで実行するようにしたものです。
<img width="500" alt="surface-typecover.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/60028/84d618ee-d52b-52ba-15d5-26c8b2d11492.jpeg"/>

## 背景

タッチパッド（タッチパネルにあらず）からの入力は、.NETではマウスの動きに変換された後に扱うようになっていて、タッチパッドの入力を直接扱えるようにはなっていません。それで実用的に困ることはないですし、タッチ操作用のデバイスとしてはタッチスクリーンの方が優れているので、タッチパッドにこだわる必要もないです。

ただ、入力デバイスの選択肢としてあって困ることはなく、その一方で先行例が見つからず手が出なかったのですが[^1]、@kamektxさんのTouchpadGestures Advancedを見かけてC++での方法は分かったので、C#でも書けるかなと。

[ノートPCのタッチパッドでタブを切り替えるソフトウェアを作りました ～TouchpadGestures Advanced～](https://qiita.com/kamektx/items/97b9452db47e85e9b967)

その途中で@mfakaneさんの[RawInput.Sharp](https://github.com/mfakane/rawinput-sharp)を知り、既にライブラリ化されていたわけですが、いずれにせよこの二つを非常に参考にさせていただきました。

## コード

タッチパッドからの入力を捕捉するには、[WM_INPUT](https://docs.microsoft.com/en-us/windows/win32/inputdev/wm-input)メッセージが送られてくるように登録が必要ですが、ここは難しくないので省きます。問題はWM_INPUTメッセージを受けて、このデータからタッチ位置などの情報をどう取得するかですが、基本的にTouchpadGestures Advancedの[HidManager](https://github.com/kamektx/TouchpadGestures_Advanced/blob/64254e70196e00d1b72832a0016bd676853e742f/TGA_Win32/HidManager.cpp#L8)の手順にならっています。

大まかな流れは、以下のようになります。

1. WM_INPUTのlParamからRAWINPUT構造体を取得
2. RAWINPUT構造体中のRAWINPUTHEADER構造体から、どの種類のデータがあるかを取得
3. これを使ってRAWINPUT構造体中のRAWHID構造体から、実際のデータの値を取得

始めに、タッチ位置などのコンタクトの値を格納するためのTouchpadContact構造体を作成。ContactIdはタッチ中に各コンタクトに継続的に振られる番号で、これでどのコンタクトかを判別します。XとYはタッチパッドの左上角を原点とした座標。

```csharp: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中の値は一遍に取得できず、順番に一時保存してから生成する必要があるためです。

```csharp: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メソッド。

```csharp: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](https://docs.microsoft.com/en-us/windows/win32/api/winuser/ns-winuser-rawhid)構造体で、Win32の定義は以下のようになっています。

```cpp: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で実際に実行してみると、dwSizeHidが30、dwCountが1だったので、この積は30となり、計算が合いません。

ではどういうことかというと、これはWin32では時々ある、構造体の後ろにbyte配列が続いている形式で、bRawDataはこの配列の先頭byteを指しています。つまり、Surface Pro 4の例ではRAWINPUT構造体の長さは62だったので、RAWINPUTHEADERの長さが24（16 + 8）で、dwSizeHidとdwCountの長さがそれぞれ4なので、差し引き62 - 24 - 4 - 4 = 30がbRawDataの本当の長さということになります。これは、dwSizeHidとdwCountの積とも符合します。ちなみに、30でコンタクト5つ分のデータがありました。

もしこの長さが30で固定であれば、`[MarshalAs(UnmanagedType.ByValArray, SizeConst = 30)]`を付ければいいわけですが、固定ではないのでしょうから、これはなし。

したがって、RAWINPUT構造体を取得するときに、先にこのサイズを取得した後、IntPtrにメモリを確保し、このIntPtrにGetRawInputDataのデータを格納し、このIntPtrからRAWINPUT構造体に変換してbRawData以外の値を取得し、さらに同じIntPtrから直接byte配列を取得して、この後半のbRawDataに当たる部分を取り出すということをやっています。[^2]

上記のコードのレポジトリは[RawInput.Touchpad](https://github.com/emoacht/RawInput.Touchpad)です。

## テスト

テスト環境は以下のとおりです。

- Windows 10 21H1
- Surface Pro 4（＋タイプカバーのタッチパッド）
- .NET 5.0のWPF

タッチパッドに指を同時に5本当てたところ。5つのコンタクトで各指のタッチ位置が示されています。
![RawInput.Touchpad.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/60028/822f6608-0cab-de31-bcbc-67f2281ea45b.png)
タッチ中は非常に短い間隔でWM_INPUTメッセージが送られてきますが、指が浮くとその指のコンタクトは途切れてしまうので、動きを自然にトレースするには同じIDのコンタクトを追う必要があります。ちなみに、Surface Pro 4の例ではタッチパッドの左上角が原点の0,0で、右下角が1956,997だったので、物理的なサイズ(101 × 53mm)に対比して結構細かいです。

以上のとおり、もし必要が出てくれば使えるのではないかと思います。

[^1]: Synapticsのタッチパッド用のものは以前から存在しますが、こちらは汎用的なRaw Input APIを使うので、高精度タッチパッド（Precision Touchpad）であれば動くはず、というのも利点です。
[^2]: RawInput.Sharpではunsafeにしてポインターでスマートに処理しています。