SIN@SAPPOROWORKSの覚書

C#を中心に、夜な夜な試行錯誤したコードの記録です。

マウスでタッチ操作

stackoverflowの3日の記事でJavaによる「hello workd」なプログラムについて書かれてました。
Why does this code print “hello world”?

[java]
System.out.println(
randomString(-229985452) + " " + randomString(-147909649));

public static String randomString(int i){
Random ran = new Random(i);
StringBuilder sb = new StringBuilder();
for (int n = 0; ; n++){
int k = ran.nextInt(27);
if (k == 0)
break;
sb.append*1;
}
return sb.toString();
}
[/java]

ちょっと???なコードですが、実行してみると確かにhello worldが表示されてます。

記事を読んでみると、すぐに分かりますが、初期化に使用するSeed値で疑似乱数が規則的に現れることを利用して、hello\0とworld\0のパターンになるSeed値を指定しているだけでした。


8  + 96 = 104 --> h
5  + 96 = 101 --> e
12 + 96 = 108 --> l
12 + 96 = 108 --> l
15 + 96 = 111 --> o

23 + 96 = 119 --> w
15 + 96 = 111 --> o
18 + 96 = 114 --> r
12 + 96 = 108 --> l
4  + 96 = 100 --> d

Javaと.NETでは、当然Randomの実装が違いますので、同じコードを実行してみても当然ながら「hello world」にはなりません。

ってことで、.NETのRandomでも同じパターンが無いか検索してみました。


class Program {
    private static void Main(){
        for (var i = Int32.MinValue; i < Int32.MaxValue; i++){
            SearchHello(i);
            SearchWorld(i);
        }
    }
    static void SearchHello(int i){
        //8,5,12,12,15,0を検索する
        Random ran = new Random(i);
        int n = ran.Next(27);
        if (n - 3 != ran.Next(27)) return;
        if (n + 4 != ran.Next(27)) return;
        if (n + 4 != ran.Next(27)) return;
        if (n + 7 != ran.Next(27)) return;
        if (n - 8 == ran.Next(27)){
            Console.WriteLine(string.Format(
                "find [Hello] {0} {1} {2} {3} {4}   {5}"
                , n , n-3,n+4,n+4,n-3,i));
            }
        }
    static void SearchWorld(int i) {
        //23,15,18,12,4,0を検索する
        var ran = new Random(i);
        var n = ran.Next(27);
        if (n - 8 != ran.Next(27)) return;
        if (n - 5 != ran.Next(27)) return;
        if (n - 11 != ran.Next(27)) return;
        if (n - 19 != ran.Next(27)) return;
        if (n - 23 == ran
<a href="http://clr-h.jp/">CLR/H</a>の勉強会「第77回カソウ化デイ」で、マイクロソフトのエバンジェリストである<a href="http://www.masatohirai.com/GeniusSite/">ジニアス平井 氏</a>から「InjectTouchInut API を利用して非タッチPCでもマルチタッチしちゃうんだから」というセッションがありました。APIを使用してタッチ操作のメッセージをWindowsに送る事で疑似的にタッチ操作ができるというものでした。

最近、私もWindows8を主に使うようになってきたのですが、残念ながらタッチPCではありあません。タッチできないPCでWindows8を使用していると、ショートカットなどを駆使しても、マウス操作だけでは、ちょっと物足りなく感じています。
そして、先のセッションで見た、ジニアス氏のマウスによるスライド操作が、とっても羨ましくなるわけです。

そのようなソフトは無いのかなと検索してみたのですが、ちょっと見つけきれなかったので、作成して見ることにしました。


<h2>1 仕様及び使用方法</h1>

マウス操作も、それはそれで便利なので、非常に控えめに、使いたい時だけタッチできるようにと、ALTキーを押している間だけドラッグ操作がタッチ操作になるようにしてみました。

詳しくは、下記のページをご覧ください。
<a href=http://altouch.codeplex.com/>
<img src="http://www.sapporoworks.ne.jp/files_WP/2013/02/006-300x254.png" alt="" title="CodePlex" width="300" height="254" class="alignnone size-medium wp-image-918" />
http://altouch.codeplex.com/</a>

<h2>2 InjectTouchInput</h1>
<a href=http://msdn.microsoft.com/ja-jp/library/windows/desktop/hh802881(v=vs.85).aspx>InjectTouchInput</a>は、タッチ入力をシュミレートするAPIでWindows8及びWindows Server 2012のデスクトップアプリのみで使用可能になっています。このAPIは、<a href="http://msdn.microsoft.com/ja-jp/library/windows/desktop/hh802880(v=vs.85).aspx">InitializeTouchInjection</a>で初期化した後に使用できます。

<a href="http://www.sapporoworks.ne.jp/files_WP/2013/02/008.png"><img src="http://www.sapporoworks.ne.jp/files_WP/2013/02/008-300x83.png" alt="" title="008" width="450" class="alignnone size-medium wp-image-951" /></a>

<a href="http://www.sapporoworks.ne.jp/files_WP/2013/02/009.png"><img src="http://www.sapporoworks.ne.jp/files_WP/2013/02/009-300x84.png" alt="" title="009" width="450" class="alignnone size-medium wp-image-952" /></a>

2つのAPIは、User32.dllに含まれており、C#から利用する場合は、DllImportでextern宣言します。

<br clear="left">
>|cs|
[DllImport("User32.dll")]
private static extern bool InitializeTouchInjection(
    uint maxCount,
    int dwMode
);

[DllImport("User32.dll")]
private static extern bool InjectTouchInput(
    uint count,
    [MarshalAs(UnmanagedType.LPArray),
    [In] POINTER_TOUCH_INFO[] contacts
);

また、必要な、パラメータはWinuser.hに含まれているので、これをC#で利用できるように翻訳します。


public enum POINTER_INPUT_TYPE {
  PT_POINTER = 0x00000001,
   PT_TOUCH = 0x00000002,
   PT_PEN = 0x00000003,
   PT_MOUSE = 0x00000004
};

[StructLayout(LayoutKind.Sequential)]
public struct POINTER_TOUCH_INFO {
  public POINTER_INFO pointerInfo;
   public TouchFlags touchFlags;
   public TouchMask touchMask;
   public RECT rcContact;
   public RECT rcContactRaw;
   public uint orientation;
   public uint pressure;
}

上記のPOINTER_INPUT_TYPEを見て想像できるように、このAPIでは、マウス・ペン・ポインタなどもシュミレートできるようです。

3 タッチのシュミレート

タッチ入力(スライドやスワイプ)をシュミレートする場合の手順は、次のようになります。

(1)ダウン
タッチ開始の「XY座標」及びフラグに「POINTER_FLAG.DOWN」を指定して実行

(2)スライド
移動した軌跡に沿って複数回「XY座標」及びフラグに「POINTER_FLAG.UPDATE」を指定して実行

(3)アップ
フラグに「POINTER_FLAG.UPDATE」を指定して実行

それぞれ、POINTER_TOUCH_INFO構造体に情報を格納してInjectTouchInputを呼び出します。
次のコードは、タッチ開始の例です。

public void Down(int x, int y) {
  InitializeTouchInjection(10, TOUCH_FEEDBACK_NONE);//初期化
  //contactsは、情報を格納する構造体配列 指1本分の情報なので先頭配列のみを使用している
  contacts[0].pointerInfo.pointerType = POINTER_INPUT_TYPE.PT_TOUCH; //タッチ動作
  contacts[0].touchFlags = TouchFlags.NONE;
  contacts[0].orientation = 90; 
  contacts[0].pressure = 32000;
   contacts[0].pointerInfo.pointerFlags = 
        POINTER_FLAG.DOWN | POINTER_FLAG.INRANGE | POINTER_FLAG.INCONTACT; //DOWNをシュミレート
   contacts[0].touchMask = TouchMask.CONTACTAREA | TouchMask.ORIENTATION | TouchMask.PRESSURE;
   contacts[0].pointerInfo.ptPixelLocation.x = x; //座標指定
   contacts[0].pointerInfo.ptPixelLocation.y = y;
   contacts[0].pointerInfo.pointerId = 0;
   const int marge = 2; 
   contacts[0].rcContact.left = x - marge;
   contacts[0].rcContact.right = x + marge;
   contacts[0].rcContact.top = y - marge;
   contacts[0].rcContact.bottom = y - marge;
   InjectTouchInput(1, contacts);
}

詳しくは、[Touch.csの Down() Drag() Up()を参照してください。

ピッチやズームをシュミレートする場合も、基本的には同じですが、POINTER_TOUCH_INFO構造体を複数(指2本の場合は2つ)使用します。
最初に指2本分の「XY座標」を指定し、指の移動に合わせて、それぞれの方向に座標を移動させるだけです。


public void Zoom(bool pinch, int x, int y) {
  //初期化
  InitializeTouchInjection(10, TOUCH_FEEDBACK_NONE);
  //ダウン
  InitContact(0, x - 150, y);
  InitContact(1, x + 150, y);
  InjectTouchInput(2, contacts);
  //スライド
  contacts[0].pointerInfo.pointerFlags = POINTER_FLAG.UPDATE | 
    POINTER_FLAG.INRANGE | POINTER_FLAG.INCONTACT;
  contacts[1].pointerInfo.pointerFlags = POINTER_FLAG.UPDATE | 
    POINTER_FLAG.INRANGE | POINTER_FLAG.INCONTACT;
  for (int i = 0; i < 150; i++) {
    if (pinch) {
      contacts[0].pointerInfo.ptPixelLocation.x += 1;
      contacts[1].pointerInfo.ptPixelLocation.x -= 1;
    } else {
      contacts[0].pointerInfo.ptPixelLocation.x -= 1;
      contacts[1].pointerInfo.ptPixelLocation.x += 1;
    }
    InjectTouchInput(2, contacts);
    Thread.Sleep(2);
  }
  //アップ
  contacts[0].pointerInfo.pointerFlags = POINTER_FLAG.UP;
  contacts[1].pointerInfo.pointerFlags = POINTER_FLAG.UP;
  InjectTouchInput(2, contacts);
}

InitContact(配列番号、X座標、Y座標)は、上記のDown()の中でのcontacts[]の初期化部分を抽出したファンクションです。詳しくはTouch.csをご参照ください。

4 マウス+キーボード操作のフック

自プログラム以外へのメッセージを処理する場合、通常、DLLを使用したグローバルフックを記述しなければなりませんが、マウスとキーボードの入力だけは、SetWindowsHookEx()でグローバルフックが可能であり、この場合、外部DLLが必要ありませんので、すべてをC#のみで書くことができます。


[DllImport("user32.dll")]
static extern IntPtr SetWindowsHookEx(
    int idHook, HookProcDelegate lpfn, IntPtr hMod, int dwThreadId);

[DllImport("user32.dll")]
static extern IntPtr CallNextHookEx(
    IntPtr hHook, int nCode, IntPtr wParam, IntPtr lParam);
[DllImport("user32.dll")]
static extern bool UnhookWindowsHookEx(IntPtr hHook);

SetWindowsHookExに関しては、各所に多数の情報が公開されておりますので、ここでの紹介は控えますが、VisualStudioでの開発で一点だけ注意する事項として、インスタンスハンドルの取得があります。

SetWindowHookEx()では、引数にインスタンスハンドルが必要になりますが、Marshal.GetHINSTANCE()などで現在のモジュールからインスタンスハンドルを取得しようとすると、実行中のプロセス名から検索されるため、本来の名前と同一でない時、正常に動作できません。
VisualStudioは、デバッグモードで実行する際、デフォルトでホスティングプロセスとして「プログラム名.vshost.exe」という名前で実行されるので、結果的にデバッグ時のみフックに失敗するという現象が発生します。意識していないと、悩むことになるかも知れません。

プロジェクトのプロパティで「VisualStudioホスティングプロセスを有効にする」のチェックを外せば、デバッグ時もvshost~は使用されなくなります。

5 ショートカットの送信

ドラッグをタッチに置き換えている中で初めて気が付いたのですが、単純にドラッグを全部タッチに変換しただけでは、左右上下端からのスワイプが反応しませんでした。

Windows8シュミレータを注意深く観察ていると、マウスからタッチにモードを変更した瞬間に、横スクロールのバーが消えるなど、どうやらWindowsのモードが変わっているように見えます。

本プログラムでは、便利なマウス入力も同時に生かしたいと言うコンセプトなので、ドラッグが始まった位置によって動作を切り替えることで、スワイプ動作をシュミレートし、チャームメニューやアプリバーの表示に対応することにしました。

具体的には、画面の端近くでドラッグが始まった時、スライド動作はせず、スワイプ検出モードに入り、その後のドラッグの軌跡によりスワイプが成立した場合に必要な処理を実行しています。
なお、デスクトップアプリからチャームメニューやアプリバーを軽易に呼び出すAPIが存在しなかったので、ショートカットを使用して実現しています。

Windows8のモダンUIに関連するショートカットは、殆どがWindowsキーとの組み合わせになっているのですが、System.Windows.Forms.SendKeysでは、Widowsキーの組み合わせを送ることができません。
仕方がないので、今回は、InputSimulator.dllを使用させて頂きました。このライブラリは、内部で、Win32APIのSendInputを使用しているようです。

6 ダウンロード

本プログラムのコードは下記にあります。興味を持っていただけた方は、ぜひご利用ください。

CodePrex http://altouch.codeplex.com/

GitHub https://github.com/furuya02/AlTouch
Console.WriteLine(String.Format(
"find [World] {0} {1} {2} {3} {4} {5}"
, n, n - 3, n + 4, n + 4, n - 3, i));
}
}
}
|

*1:char)('`' + k