タッチパッドのスワイプを.NETで簡単に捕捉する

## 目的

タッチパッドの入力を.NETで捕捉するために、比較的ローレベルのRaw Input APIを使う方法を以前書きました。

[タッチパッドのタッチ位置をRaw Input APIで.NETから捕捉する](https://qiita.com/emoacht/items/d3fa417e59fbc6a0fb2b)

ただ、後から知ったのですが、実はスワイプだけならここまでやらなくても捕捉できます。たぶん知っている人は知っているが、広く知られているわけではないという類の知識かなと思います。

## 実装

実のところ、タッチパッドのスワイプが発生するとOSがマウスの垂直ホイールか水平ホイールのイベントとしてwindow messageで通知してくるので、それを捕捉するだけです。

といっても、垂直ホイールの方はWPFでは`UIElement.MouseWheel`イベントとして実装されているので、それを使うだけです。水平ホイールの方は[WM_MOUSEHWHEEL](https://learn.microsoft.com/en-us/windows/win32/inputdev/wm-mousehwheel) （Hに注目）を捉えればいいですが、これはwindow messageの処理に慣れていれば難しくないと思います。

一応、実装例です。

```csharp:MainWindow.xaml.cs
using System;
using System.Diagnostics;
using System.Windows;
using System.Windows.Input;
using System.Windows.Interop;

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        this.Loaded += OnLoaded;
    }

    private HwndSource? _source;

    private void OnLoaded(object sender, RoutedEventArgs e)
    {
        _source = PresentationSource.FromVisual(this) as HwndSource;
        _source?.AddHook(WndProc);
    }

    private const int WM_MOUSEHWHEEL = 0x020E;

    private static IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
    {
        switch (msg)
        {
            case WM_MOUSEHWHEEL:
                OnMouseHorizontalWheel(wParam, lParam);
                break;
        }
        return IntPtr.Zero;
    }

    private static void OnMouseHorizontalWheel(IntPtr wParam, IntPtr lParam)
    {
        int delta = unchecked((short)((long)wParam >> 16));
        if (delta is 0)
            return;

        Debug.WriteLine($"Horizontal {delta}");
    }

    protected override void OnMouseWheel(MouseWheelEventArgs e)
    {
        Debug.WriteLine($"Vertical {e.Delta}");

        base.OnMouseWheel(e);
    }

    protected override void OnClosed(EventArgs e)
    {
        _source?.RemoveHook(WndProc);
        base.OnClosed(e);
    }
}
```

さて、ここまでは簡単ですが、問題が一つ。自分が試した限り、これらのイベントが実際のマウスのホイールで発生したものか、タッチパッドのスワイプで発生したものか、判別できる情報がありません。

【これ以降は大幅に書き換え】

これの何が問題かというと、個人やマウスの設定にもよりますが、指でホイールを回すときと指でスワイプするときでは、指を動かす方向は同じでもデルタ（移動量）の計算は逆にしないと直感に合わないということがあります。

例えば、垂直方向について、自分の場合、下にスクロールしたいときはホイールの表側を指で下方向へ動かしますが、これはホイールの裏側では上方向への動きになり、デルタは負の数になります。これを画面上の動きに反映させる際に逆方向に計算すれば下へのスクロールになります。一方、同じ感覚で下方向へスワイプすれば、デルタは正の数になるので、同じ計算をすれば上へのスクロールになります。

これは水平方向についても同じで、[WM_MOUSEWHEEL](https://learn.microsoft.com/en-us/windows/win32/inputdev/wm-mousewheel)にせよWM_MOUSEHWHEELにせよ、画面上の何を動かすかにもよりますが、ホイールでの入力とスワイプでの入力ではデルタを逆方向に計算しないと直感に合わない動きになってしまいます。

このためには入力元が実際のホイールかスワイプかを判別する必要がありますが、これらのイベントに含まれる情報では判別できないのですよね。

## 解決

解決策としては、やや強引ですが、Raw Input APIを併用してイベントの直前にタッチパッドの入力があったかを調べることが考えられます。

例えば、以前作成したTouchpadHelperを使ってWM_INPUTが来るようにし、WM_INPUTが来た後、0.5秒以内[^1]に来たものはタッチパッドと判定するとすれば、以下のようになります。

```csharp:MainWindow.xaml.cs
public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
        this.Loaded += OnLoaded;
    }

    private HwndSource? _source;

    private void OnLoaded(object sender, RoutedEventArgs e)
    {
        _source = (HwndSource)PresentationSource.FromVisual(this);
        _source.AddHook(WndProc);
        TouchpadHelper.RegisterInput(_source.Handle);
    }

    private const int WM_INPUT = 0x00FF;
    private const int WM_MOUSEHWHEEL = 0x020E;

    private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wParam, IntPtr lParam, ref bool handled)
    {
        switch (msg)
        {
            case WM_INPUT:
                _touchpadTime = DateTime.Now;
                break;

            case WM_MOUSEHWHEEL:
                OnMouseHorizontalWheel(wParam, lParam);
                break;
        }
        return IntPtr.Zero;
    }

    private DateTime _touchpadTime;
    private readonly TimeSpan _touchpadInterval = TimeSpan.FromMilliseconds(500);

    private bool IsTouchpad() => DateTime.Now - _touchpadTime <= _touchpadInterval;

    private void OnMouseHorizontalWheel(IntPtr wParam, IntPtr lParam)
    {
        int delta = unchecked((short)((long)wParam >> 16));
        if (delta is 0)
            return;

        Debug.WriteLine($"Horizontal Delta:{delta}, Touchpad:{IsTouchpad()}");
    }

    protected override void OnMouseWheel(MouseWheelEventArgs e)
    {
        Debug.WriteLine($"Vertical Delta:{e.Delta}, Touchpad:{IsTouchpad()}");

        base.OnMouseWheel(e);
    }

    protected override void OnClosed(EventArgs e)
    {
        _source?.RemoveHook(WndProc);
        TouchpadHelper.UnregisterInput();
        base.OnClosed(e);
    }
}
```

これで対応は可能になるはずです。

## まとめ

以上のように、スワイプを捕捉すること自体は簡単ですが、実用的に違和感が出ないようにするには手間がかかるということで、中途半端な結論でした。

[^1]: 0.5秒は長いと感じるかもしれませんが、スワイプの速さに応じてOSがイナーシャの動きを作って通知してくるためです。自分で試したところでは、WM_INPUTが最後に来てからWM_MOUSEHWHEELが止まるまで最長で約0.4秒の間がありました。