2021/02/11

マルチタッチによるクリックの判別

WPF上のタッチ操作でシングルタッチによるものか、マルチタッチによる(指を複数使う)ものかは、Manipulation系のイベントであれば直接判別できるようになっていますが、移動を伴わないTouch系のイベントのときは直接分かるものがないので、どうするか考えてみた話です。

そんなに難しいことでもなく、Touch系のイベントで来るTouchEventArgsのTouchDeviceがイベントを起こした個々のデバイス(指)を示すので、このIdを記録して、これが一連のイベントが終わるまでに一つしか来ていなければシングルタッチ、複数来ていればマルチタッチと判別できます。

以下のButtonの例では、PreviewTouchDownイベントごとにTouchDeviceのIdをHashSetに記録していき、Clickイベントのときに複数来ているか判別した後で、HashSetを初期化しています。なお、タッチ操作の終わりに常にClickイベントが来るわけではないので、PreviewTouchUpイベントを引っ掛けた上で、これはClickイベントより先に来るので、1秒の猶予をおいてから初期化するようにしています。
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading.Tasks;
using System.Windows.Controls;
using System.Windows.Input;
public class MultiTouchButton : Button
{
public bool IsMultiTouch => (_touchDeviceIds.Count > 1);
private readonly HashSet<int> _touchDeviceIds = new HashSet<int>();
protected override void OnPreviewTouchDown(TouchEventArgs e)
{
base.OnPreviewTouchDown(e);
_touchDeviceIds.Add(e.TouchDevice.Id);
}
protected override void OnPreviewTouchUp(TouchEventArgs e)
{
base.OnPreviewTouchUp(e);
Task.Run(async () =>
{
await Task.Delay(TimeSpan.FromSeconds(1));
_touchDeviceIds.Clear();
});
}
protected override void OnClick()
{
base.OnClick();
Trace.WriteLine($"{(IsMultiTouch ? "Multi" : "Single")}");
_touchDeviceIds.Clear();
}
}
なお、一つのTouchDeviceがPreviewTouchDownで何回も来ることはないと割り切れば、単なるカウンターでも十分な気はします。

より実用的に、ClickイベントからBehaviorのCallMethodActionを実行するようにしている場合、そのIsEnabled依存関係プロパティとbindingを張ってもいいですが、そのために妙にコードが増えるのもうまくないので、CallMethodActionの分も含めてBehaviorにまとめたのが以下。
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
using Microsoft.Xaml.Behaviors;
public class MultiTouchBehavior : Behavior<ButtonBase>
{
public object TargetObject
{
get { return (object)GetValue(TargetObjectProperty); }
set { SetValue(TargetObjectProperty, value); }
}
public static readonly DependencyProperty TargetObjectProperty =
DependencyProperty.Register(
"TargetObject",
typeof(object),
typeof(MultiTouchBehavior),
new PropertyMetadata(
null,
(d, e) => ((MultiTouchBehavior)d).SetMethods()));
public string SingleTouchClickMethodName { get; set; }
public string MultiTouchClickMethodName { get; set; }
private MethodInfo _singleTouchClickMethod;
private MethodInfo _multiTouchClickMethod;
private void SetMethods()
{
if (TargetObject is null)
return;
var targetType = TargetObject.GetType();
if (!string.IsNullOrEmpty(SingleTouchClickMethodName))
_singleTouchClickMethod = targetType.GetMethod(SingleTouchClickMethodName, Type.EmptyTypes);
if (!string.IsNullOrEmpty(MultiTouchClickMethodName))
_multiTouchClickMethod = targetType.GetMethod(MultiTouchClickMethodName, Type.EmptyTypes);
}
protected override void OnAttached()
{
base.OnAttached();
this.AssociatedObject.PreviewTouchDown += OnPreviewTouchDown;
this.AssociatedObject.PreviewTouchUp += OnPreviewTouchUp;
this.AssociatedObject.Click += OnClick;
}
protected override void OnDetaching()
{
base.OnDetaching();
this.AssociatedObject.PreviewTouchDown -= OnPreviewTouchDown;
this.AssociatedObject.PreviewTouchUp -= OnPreviewTouchUp;
this.AssociatedObject.Click -= OnClick;
}
private readonly HashSet<int> _touchDeviceIds = new HashSet<int>();
private void OnPreviewTouchDown(object sender, TouchEventArgs e)
{
_touchDeviceIds.Add(e.TouchDevice.Id);
}
private void OnPreviewTouchUp(object sender, TouchEventArgs e)
{
Task.Run(async () =>
{
await Task.Delay(TimeSpan.FromSeconds(1));
_touchDeviceIds.Clear();
});
}
private void OnClick(object sender, RoutedEventArgs e)
{
var isMultiTouch = (_touchDeviceIds.Count > 1);
_touchDeviceIds.Clear();
if (!isMultiTouch)
{
_singleTouchClickMethod?.Invoke(TargetObject, null);
}
else
{
_multiTouchClickMethod?.Invoke(TargetObject, null);
}
}
}
サンプル全体は以下。 以上でシングルタッチかマルチタッチかに応じて実行するメソッドを切り替えられるようになりましたが、実際に試してみると、2つの指を合わせて、それぞれ認識されるまでタッチしてから離す(完全に同時にタッチする必要はない)操作は慣れが必要で、タッチデバイスにもよるでしょうが、この操作の実用性自体が少し微妙なことに気づきました。