はじめに
Androidからのタッチ信号を受け取って、Windowsネイティブのタッチにアサインする機会があったので、その時にハマった事などをまとめていきます。
Androidから信号を受け取る
今回は以前の記事で書いた方法でAndroidからタッチを取得し、UDPでWindowsで待機しているアプリに送りました。
送信方法はUDPでもTCPでも、なんでも良いと思います。とりあえず今回はPCまで何らかの方法で信号が届いている前提で話を進めます。
タッチをシミュレートするには
Win32のAPIに、InitializeTouchInjectionとInjectTouchInputという関数があります。
この関数を使う事で、Windowsのタッチをシミュレートすることが出来るのですが、いかんせんCなコードなので、C#で使おうと思うと扱いは結構面倒です。
ラッパーを使う
色々探しているうちに、NuGetで便利なラッパーライブラリを発見しました。
使い方はこのStackOverflowが参考になります。
CのライブラリをC#で使おうとすると、DLLから直接ネイティブな関数を呼ぶだけじゃなく、自分で必要な構造体を定義しないといけなくて中々面倒なのですが、このライブラリなら全部やってくれているので楽です。
Win32 APIを自分で呼ぶ
NuGetが使えるなら↑の方法で良いと思いますが、自分で定義する方法もあります。
InitializeTouchInjectionとInjectTouchInputのページで記載されている構造体を定義してdllから関数を呼び出すのですが、構造体が別の構造体を含んでいたりして、結構面倒です…
僕は自分で構造体を一つ一つ作っていったのですが、それだけではうまく動かず、最終的にGitHubで公開されていたこちらのコードを参考にして、コードを修正しました。
最終的に、関数と構造体の呼び出し部分は以下のようになりました。
[DllImport("user32.dll")]
private static extern bool InitializeTouchInjection(int maxCount = 256, TouchFeedbacKMode dwMode = TouchFeedbacKMode.NONE);
[DllImport("user32.dll")]
private static extern bool InjectTouchInput(int count, [MarshalAs(UnmanagedType.LPArray), In] PointerTouchInfo[] info);
private enum TouchFeedbacKMode {
DEFAULT = 0x1,
INDIREC = 0x2,
NONE = 0x3,
}
[StructLayout(LayoutKind.Sequential)]
public struct PointerTouchInfo {
public PointerInfo pointerInfo;
public TouchFlags touchFlags;
public TouchMask touchMask;
public Rect rcContact;
public Rect rcContactRaw;
public uint orientation;
public uint pressure;
}
[StructLayout(LayoutKind.Sequential)]
public struct Rect {
public int left;
public int right;
public int top;
public int bottom;
}
[StructLayout(LayoutKind.Sequential)]
public struct PointerInfo {
public PointerInputType pointerType;
public uint pointerId;
public uint frameId;
public PointerFlags pointerFlags;
public IntPtr sourceDevice;
public IntPtr hwndTarget;
public Point ptPixelLocation;
public Point ptHimetricLocation;
public Point ptPixelLocationRaw;
public Point ptHimetricLocationRaw;
public uint dwTime;
public uint historyCount;
public uint InputData;
public uint dwKeyStates;
public ulong PerformanceCount;
public int ButtonChangeType;
}
public enum PointerInputType {
POINTER = 0x00000001,
TOUCH = 0x00000002,
PEN = 0x00000003,
MOUSE = 0x00000004,
TOUCHPAD = 0x00000005
}
[StructLayout(LayoutKind.Sequential)]
public struct Point {
public int x;
public int y;
}
public enum PointerFlags {
NONE = 0x00000000,
NEW = 0x00000001,
INRANGE = 0x00000002,
INCONTACT = 0x00000004,
FIRSTBUTTON = 0x00000010,
SECONDBUTTON = 0x00000020,
THIRDBUTTON = 0x00000040,
FOURTHBUTTON = 0x00000080,
FIFTHBUTTON = 0x00000100,
PRIMARY = 0x00002000,
CONFIDENCE = 0x000004000,
CANCELED = 0x000008000,
DOWN = 0x00010000,
UPDATE = 0x00020000,
UP = 0x00040000,
WHEEL = 0x00080000,
HWHEEL = 0x00100000,
CAPTURECHANGED = 0x00200000,
HASTRANSFORM = 0x00400000
}
public enum TouchFlags {
NONE = 0x00000000
}
public enum TouchMask {
NONE = 0x00000000,
CONTACTAREA = 0x00000001,
ORIENTATION = 0x00000002,
PRESSURE = 0x00000004
}
ハマったポイント
実際にハマった個所と、先人の知恵ではまらずに済んだ個所がありますが、全部挙げていきます。
Up時には直前のイベントと同じ座標が入っていないといけない
つまり、直前のイベントがdownなら、downした時と同じ座標が、moveなら、moveした時と同じ座標が入っていないとエラーになるようです。
ここは先人の知恵により回避できました。
動いていない指も、存在するならアップデートする
InjectTouchInput(int count, PointerTouchInfo[] info);
とあるように、アップデート時には複数のタッチを同時にアップデートする必要があるのですが、例えば指Aと指Bがあった時に、
A(down) -> A(move) -> A(move) / B(down) -> ※B(move) -> A(up) / B(move) -> B(up)
という挙動だったとします。
※の個所で、Aはまだ存在しているけど動いていない、という状況が発生していますが、***InjectTouchInputを実行する時はAもアップデートする必要があります。***ここは割とハマりポイントなんじゃないかなと思います。
ちなみに、down状態のままアップデートすると何度もdownが飛ぶので、downではなくmoveとしてアップデートする必要があります。
終わりに
実際にはアプリを作る上でハマった個所はもっとあるのですが、TouchInjection関連ではこんなところかなと思います。
僕は諸事情があって自分で実装しましたが、先程も紹介したこちらのコードや、ラッパーライブラリを使えば比較的簡単に実現できると思います。
先人の知恵をまとめただけになってしまいましたが、同じ事でハマっている方の助けになれば幸いです。