LoginSignup
8

More than 1 year has passed since last update.

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

Last updated at Posted at 2021-06-09

目的

Windows上でタッチパッドのタッチ位置を捕捉する機能は、.NETでは標準で提供されていませんが、これをWin32のRaw Input APIで.NETから捕捉できるようにします。内容的には、TouchpadGestures AdvancedのC++のコードに基本的にならって、C#のP/Invokeで実行するようにしたものです。
surface-typecover.jpg

背景

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

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

ノートPCのタッチパッドでタブを切り替えるソフトウェアを作りました ~TouchpadGestures Advanced~

その途中で@mfakaneさんのRawInput.Sharpを知り、既にライブラリ化されていたわけですが、いずれにせよこの二つを非常に参考にさせていただきました。

コード

タッチパッドからの入力を捕捉するには、WM_INPUTメッセージが送られてくるように登録が必要ですが、ここは難しくないので省きます。問題はWM_INPUTメッセージを受けて、このデータからタッチ位置などの情報をどう取得するかですが、基本的にTouchpadGestures AdvancedのHidManagerの手順にならっています。

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

  1. WM_INPUTのlParamからRAWINPUT構造体を取得
  2. RAWINPUT構造体中のRAWINPUTHEADER構造体から、どの種類のデータがあるかを取得
  3. これを使って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で実際に実行してみると、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です。

テスト

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

  • Windows 10 21H1
  • Surface Pro 4(+タイプカバーのタッチパッド)
  • .NET 5.0のWPF

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

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


  1. Synapticsのタッチパッド用のものは以前から存在しますが、こちらは汎用的なRaw Input APIを使うので、高精度タッチパッド(Precision Touchpad)であれば動くはず、というのも利点です。 

  2. RawInput.Sharpではunsafeにしてポインターでスマートに処理しています。 

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
8