Finger Style

Silverlight でのマルチタッチ サポートの詳細

Charles Petzold

サンプル コードのダウンロード

ニューヨーク市のアメリカ自然史博物館を訪れるときは、いつも霊長類のホールに立ち寄ることにしています。このホールには、さまざまな骨格や剥製が展示され、霊長類 (小さなツパイ、キツネザル、マーモセットから、チンパンジー、類人猿、人間まで、あらゆる大きさの動物) の進化の全景が展示されています。

この展示で目に付くのが、すべての霊長類に共通するきわだった特徴です。それは、親指が他の 4 本の指に対置できることを含めた手の骨の構造です。 関節と指が同じように配置されていることにより (我々の祖先や血族が木の枝をつかんだり登ったりできたのはこのためです)、人類は自身を取り巻く世界を巧みに操り、物を作ることができるのです。我々の手は、数千万年前に生息していた小さな霊長類の前脚が起源かもしれませんが、我々の手は、我々を明確に人間たらしめる重要な要因でもあります。

我々がコンピューター画面上の物体を本能的に指そうとしたり、触れようとしたりするのも不思議ではないかもしれませんね。

指とコンピューターとのつながりをより密にしたいという、人間のこのような欲求に応えて入力デバイスも進化してきました。マウスは選択やドラッグには非常に優れていますが、フリーフォームのスケッチや手書きにはまったく向いていません。タブレット スタイラスでは文字が書けますが、何かを引き伸ばしたり移動したりするには使いにくいと感じることがよくあります。ATM や博物館のキオスクでおなじみのタッチ スクリーンは、一般に、指し示したり押したりといった単純な操作に限られています。

私は、"マルチタッチ" と呼ばれるテクノロジは大きな飛躍を表すと感じています。名前からわかるように、マルチタッチはこれまでのタッチ スクリーンの概念を覆し、複数の指を検出します。これにより、画面を通じて伝達できる動きやジェスチャの種類に大きな違いがもたらされます。マルチタッチは、触れることが中心であったこれまでの入力デバイスから大きな進化を遂げましたが、同時に、本質的に異なる入力パラダイムも示唆しています。

マルチタッチが最もわかりやすいのは、テレビのニュース放送で巨大な画面に表示されている地図を専属の気象予報士や評論家が操る場面でしょう。マイクロソフトは、コーヒー テーブル サイズの Microsoft Surface コンピューターから、Zune HD のような小さなデバイスまで、マルチタッチの可能性をさまざまな方法で研究してきました。このテクノロジは、スマートフォンでもかなり標準になってきています。

Microsoft Surface は多くの指が同時に動いても反応することができます (ちなみに、Microsoft Surface には、ガラス製の画面の上に置かれた物体を見るためのカメラも内臓されています)。それに対して他のほとんどのマルチタッチ デバイスでは、反応する指の数が制限されています。多くは、2 本の指 (タッチ ポイント) にしか反応しません (ここでは、指とタッチ ポイントをほぼ同意語として使用します)。しかし、指 2 本でも相乗効果が働きます。コンピューターの画面上では、2 本の指は 1 本の指の倍以上の威力を発揮します。

タッチ ポイントが 2 か所に制限されるのは、デスクトップ PC やラップトップで最近使用できるようになってきたマルチタッチ モニターの特性によるものです。昨年 11 月に開催された Microsoft Professional Developers Conference (PDC) で出席者に配布した Acer の Aspire 1420P をカスタマイズしたラップトップ (一般に、"PDC ラップトップ" と呼ばれています) も、同様の特性を備えたモニターを装備しています。PDC ラップトップを配布したことにより、マルチタッチ対応アプリケーションを作成するまたとない機会が、何千人もの開発者にもたらされました。

今回のコラムでは、この PDC ラップトップを使用して、Silverlight 3 におけるマルチタッチ サポートを調査しました。

Silverlight のイベントとクラス

マルチタッチ サポートは、さまざまな Windows API やフレームワークで標準になってきています。このサポートは、Windows 7 とリリース間近の Windows Presentation Foundation (WPF) 4 に組み込まれています (Microsoft Surface コンピューターも WPF に基づいていますが、このコンピューターには非常に特殊な機能向けのカスタム拡張機能が含まれています)。

今回のコラムでは、Silverlight 3 でのマルチタッチ サポートを中心に紹介します。このサポートでもたらされるメリットはそれほど多くありませんが、マルチタッチの基本概念を知るには十分であり、非常に役に立つことは間違いありません。

マルチタッチ対応の Silverlight アプリケーションを Web サイトに発行するとしたら、これを利用できるのはどのようなユーザーでしょう。もちろん、ユーザーにはマルチタッチ モニターが必要です。さらに、マルチタッチをサポートする OS とブラウザーで Silverlight を実行することも必要です。現状では、Windows 7 コンピューターで実行される Internet Explorer 8 でマルチタッチ サポートが提供されます。今後は、さらに多くの OS とブラウザーでマルチタッチがサポートされることになるでしょう。

Silverlight 3 のマルチタッチ サポートは、5 つのクラス、1 つのデリゲート、1 つの列挙体、および 1 つのイベントから構成されます。Silverlight プログラムがマルチタッチ デバイスで実行されているかどうかを判断したり、マルチタッチ デバイスで実行されている場合でも、そのデバイスでサポートされるタッチ ポイント数を特定したりする方法はありません。

マルチタッチに反応する Silverlight アプリケーションでは、次のように静的な Touch.FrameReported イベントにハンドラーをアタッチしなければなりません。

Touch.FrameReported += OnTouchFrameReported;

コンピューターにマルチタッチ モニターが装備されていない場合に、このイベント ハンドラーをアタッチしても支障はありません。FrameReported イベントは、静的 Touch クラスの唯一のパブリック メンバーです。アタッチするハンドラーは次のようになります。

void OnTouchFrameReported(
  object sender, TouchFrameEventArgs args) {
  ...
}

アプリケーションには、複数の Touch.FrameReported イベント ハンドラーをインストールできます。これらのすべてのイベント ハンドラーは、アプリケーション内の任意の場所で発生するすべてのタッチ イベントを報告します。

TouchFrameEventArgs には、私がこれまで使用する機会がなかった TimeStamp という 1 つのパブリック プロパティと、次の 3 つの基本的なパブリック メソッドがあります。

  • TouchPoint GetPrimaryTouchPoint(UIElement relativeTo)
  • TouchPointCollection GetTouchPoints(UIElement relativeTo)
  • void SuspendMousePromotionUntilTouchUp()

GetPrimaryTouchPoint メソッドや GetTouchPoints メソッドの引数は、TouchPoint オブジェクトの位置情報を報告するためだけに使用します。この引数に null を使用すると、位置情報は、Silverlight アプリケーション全体の左上隅からの相対位置になります。

マルチタッチでは、複数の指で画面に触れることができ、画面に触れているそれぞれの指がタッチ ポイントになります (タッチ ポイントには最大数があり、一般的な現状は 2 が最大です)。プライマリ タッチ ポイントとは、他の指が画面に触れておらず、マウスのボタンがクリックされていないときに、画面に触れている指のことです。

画面に 1 本の指で触れれば、それがプライマリ タッチ ポイントです。1 本目の指が画面に触れたままの状態で、2 本目の指が画面に触れたとすると、この 2 本目の指はプライマリ タッチ ポイントでないことは明らかです。しかしここで、2 本目の指が画面に触れたままの状態で、1 本目の指を離してからもう一度画面に触れるとします。この場合、2 本目の指にプライマリ タッチ ポイントが移るかといえば、そうはなりません。プライマリ タッチ ポイントになるのは、他の指が画面に触れていないときのみです。

プライマリ タッチ ポイントは、マウスに昇格されるタッチ ポイントにマップされます。実際のマルチタッチ アプリケーションでは、プライマリ タッチ ポイントに依存しないよう注意してください。というのも、通常、ユーザーは、最初に画面に触れる指を特に重視しないからです。

イベントは、指が実際に画面に触れている場合にのみ発生します。指が画面のすぐ近くにあっても、画面に触れていなければ検出されません。

既定では、プライマリ タッチ ポイントに関わる操作はさまざまなマウス イベントに昇格します。その結果、特殊なコーディングを行うことなく、既存のアプリケーションでタッチ操作に反応できるようになります。指で画面に触れると MouseLeftButtonDown イベントになり、画面に触れたままの状態で指を動かすと MouseMove イベントになります。指を離すと MouseLeftButtonUp イベントになります。

マウスのメッセージを含む MouseEventArgs オブジェクトには、StylusDevice というプロパティがあります。このプロパティによって、マウス イベントを、スタイラス イベントやタッチ イベントと区別することができます。PDC ラップトップでテストした結果、マウス操作によってイベントが発生すると DeviceType プロパティが TabletDeviceType.Mouse になります。ただし、指で触れてもスタイラスを使用しても、このプロパティは TabletDeviceType.Touch になります。

プライマリ タッチ ポイントのみがマウス イベントに昇格しますが、TouchFrameEventArgs の 3 つ目のメソッドでは、(その名が示すように) その昇格を抑制することができます。これについてはすぐに説明します。

Touch.FrameReported イベントには、1 つまたは複数のタッチ ポイントに基づいて発生する特定のイベントがあります。この特定のイベントに対して GetTouchPoints メソッドを呼び出すと、そのイベントに関連付けられるすべてのタッチ ポイントを含む TouchPointCollection が返されます。GetPrimaryTouchPoint から返される TouchPoint は、常にプライマリ タッチ ポイントです。特定のイベントにプライマリ タッチ ポイントが関連付けられていなければ、GetPrimaryTouchPoint は null を返します。

GetPrimaryTouchPoint から返される TouchPoint が null 以外でも、この TouchPoint は、GetTouchPoints から返される TouchPoint オブジェクトのうちの 1 つと同じオブジェクトにはなりません。ただし、メソッドに渡された引数が同じであれば、すべてのプロパティが同じになります。

TouchPoint クラスは、次の 4 つの取得専用プロパティを定義します。これらはすべて、依存関係プロパティによってサポートされます。

  • TouchAction 型の Action プロパティ: Down、Move、および Up をメンバーに持つ列挙値です。
  • Point 型の Position プロパティ: GetPrimaryTouchPoint メソッドまたは GetTouchPoints メソッドに引数として渡される要素との相対位置です (引数が null であればアプリケーションの左上隅からの相対位置になります)。
  • Size 型の Size プロパティ: PDC ラップトップではサイズ情報を利用できなかったため、このプロパティはまったく使用しませんでした。
  • TouchDevice 型の TouchDevice プロパティ

GetPrimaryTouchPoint が null 以外のオブジェクトを返し、Action プロパティが TouchAction.Down になっている場合のみ、イベント ハンドラーから SuspendMousePromotionUntilTouchUp メソッドを呼び出すことができます。

TouchDevice オブジェクトには、依存関係プロパティによってサポートされる、2 つの取得専用プロパティがあります。

  • UIElement 型の DirectlyOver プロパティ: 指が触れている位置の最上位要素です。
  • int 型の Id プロパティ

DirectlyOver プロパティは、GetPrimaryTouchPoint または GetTouchPoints に渡される要素の子要素になるとは限りません。このプロパティは、(Silverlight プラグイン オブジェクトのサイズによる定義に従って) 指が Silverlight アプリケーションの内部に触れていても、ヒットテストに反応するコントロールに囲まれた領域内になければ、null になる可能性があります (たとえば、背景ブラシが null の状態のパネルはヒットテストに反応しません)。

ID プロパティは、複数の指を区別するのに不可欠です。特定の指に関連付けられる一連の特定のイベントは、指が画面に触れたときに必ず Action プロパティが Down になることから始まり、次に Move イベントが発生して、最後に Up イベントが発生します。これらのすべてのイベントは、同じ ID に関連付けられます (ただし、プライマリ タッチ ポイントだからといって ID の値が 0 や 1 になるとは想定しないでください)。

ほとんどの複雑なマルチタッチ コードでは、TouchDevice の ID プロパティがディクショナリ キーである Dictionary コレクションを使用します。これが、複数のイベント間で特定の指に関する情報を格納する方法です。

イベントを調べる

新しい入力デバイスについて調べるときは、画面上にイベントのログを表示するちょっとしたアプリケーションを作成して、イベントがどのように発生するかを把握すると役に立ちます。このコラムに付属するダウンロード可能なコードの中に、MultiTouchEvents というプロジェクトがあります。このプロジェクトは、横並びで表示される 2 つの TextBox コントロールから構成され、2 本の指向けのマルチタッチ イベントを表示します。マルチタッチ モニターがあれば、このプログラムを charlespetzold.com/silverlight/MultiTouchEvents で実行できます。

XAML ファイルは、txtbox1 と txtbox2 という 2 つの TextBox コントロールが含まれている、2 列のグリッドだけで構成されています。図 1 にコードを示します。

図 1 MultiTouchEvents のコード

using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;

namespace MultiTouchEvents {
  public partial class MainPage : UserControl {
    Dictionary<int, TextBox> touchDict = 
      new Dictionary<int, TextBox>();

    public MainPage() {
      InitializeComponent();
      Touch.FrameReported += OnTouchFrameReported;
    }

    void OnTouchFrameReported(
      object sender, TouchFrameEventArgs args) {

      TouchPoint primaryTouchPoint = 
        args.GetPrimaryTouchPoint(null);

      // Inhibit mouse promotion
      if (primaryTouchPoint != null && 
        primaryTouchPoint.Action == TouchAction.Down)
        args.SuspendMousePromotionUntilTouchUp();

      TouchPointCollection touchPoints = 
        args.GetTouchPoints(null);

      foreach (TouchPoint touchPoint in touchPoints) {
        TextBox txtbox = null;
        int id = touchPoint.TouchDevice.Id;
        // Limit touch points to 2
        if (touchDict.Count == 2 && 
          !touchDict.ContainsKey(id)) continue;

        switch (touchPoint.Action) {
          case TouchAction.Down:
            txtbox = touchDict.ContainsValue(txtbox1) ? 
              txtbox2 : txtbox1;
            touchDict.Add(id, txtbox);
            break;

          case TouchAction.Move:
            txtbox = touchDict[id];
            break;
 
          case TouchAction.Up:
            txtbox = touchDict[id];
            touchDict.Remove(id);
            break;
        }

        txtbox.Text += String.Format("{0} {1} {2}\r\n", 
          touchPoint.TouchDevice.Id, touchPoint.Action, 
          touchPoint.Position);
        txtbox.Select(txtbox.Text.Length, 0);
      }
    }
  }
}

クラスの先頭にディクショナリの定義があるのがわかります。このディクショナリは、どちらの TextBox が 2 つのタッチ ポイントの ID に関連付けられているかを追跡します。

OnTouchFrameReported ハンドラーでは、まず、マウスへの昇格をすべて禁止しています。このためだけに、GetPrimaryTouchPoint を呼び出しています。そして、実際のプログラムでも、ほぼすべての場合に、この理由でこのメソッドを呼び出します。

foreach ループでは、GetTouchPoints から返された TouchPointCollection の TouchPoint メンバーを列挙します。このプログラムには 2 つの TextBox コントロールしかなく、2 つのタッチ ポイントだけを処理するため、ディクショナリに既に 2 つのタッチポイントが含まれていて、そのディクショナリに ID が含まれていないタッチ ポイントは無視されます (マルチタッチ対応の Silverlight プログラムでは複数の指を扱うことを考えるだけでなく、想定よりも多くの指を検出してもクラッシュしないようにします)。ID は、Down イベントでディクショナリに追加され、Up イベントでディクショナリから削除されます。

テキストが多くなりすぎると TextBox コントロールの動作が緩慢になるため、その場合はすべてのテキストを選択して削除し、プログラムが再度スムーズに実行されるようにしてください。

マルチタッチ入力がアプリケーション レベルでキャプチャされることが、このプログラムからわかります。たとえば、アプリケーション画面に指で触れてから、その指をアプリケーションの外部に移動すると、アプリケーションは Move イベントを受け取り続け、指を離したときにようやく Up イベントを受け取ります。実際に、アプリケーションがいくつかのマルチタッチ入力を受け取ると、他のアプリケーションへのマルチタッチ入力が抑制され、マウスのカーソルが表示されなくなります。

このように、マルチタッチ入力のキャプチャをアプリケーション中心で行うことで、MultiTouchEvents アプリケーションでの入力の確実性を高めることができます。たとえば、Move イベントや Down イベントでは、プログラムは単に ID がディクショナ内にあると想定します。実際のアプリケーションでは、何かおかしなことが起こる場合に備えて安全策を講じるかもしれませんが、常に、Down イベントは処理することになるでしょう。

2 本の指による操作

標準的マルチタッチ シナリオの 1 つに、指を使って写真を移動したり、サイズを変更したり、回転したりできるフォト ギャラリーがあります。マルチタッチ シナリオに関連する原理に少し慣れておきたいという理由から、同じようなシナリオを単純にして試してみることにしました。ここで作成したプログラムには、操作できるアイテムとして "TOUCH" というテキスト文字列が 1 つしかありません。この TwoFingerManipulation プログラムは、次の Web サイト (charlespetzold.com/silverlight/TwoFingerManipulation) で実行できます。

マルチタッチ アプリケーションのコードを記述するときは、おそらく、マルチタッチ対応コントロールのマウスへの昇格を常に禁止することになります。しかし、マルチタッチ モニターがなくてもプログラムを利用できるように、特定のマウス処理も追加しておきます。

マウスまたは 1 本の指だけを使って操作すると、TwoFingerManipulation プログラム内で文字列を動かすことはできますが、文字列の位置を変えることしかできません (これは、変換というグラフィック操作です)。マルチタッチ画面上で 2 本の指を使えば、オブジェクトを拡大縮小したり回転したりできます。

この拡大縮小と回転に必要なアルゴリズムについて考えるためにメモ帳とペンを手に取りましたが、すぐに、このアルゴリズム特有のソリューションはないことが明らかになりました。

1 本の指が ptRef というポイントに固定されたままだとします (ここでは、すべてのポイントが、操作対象のオブジェクトの下にある表示画面に対する相対位置になります)。次に、もう 1 つの指がポイント ptOld から ptNew に移動します。この場合は、図 2 に示すように、これらの 3 つのポイントをそれぞれ使用して、オブジェクトの横方向と縦方向の倍率を計算できます。


図 2 倍率に変換される 2 本の指の動き

たとえば、横方向の倍率は、ptRef.X から ptOld.X までの距離と ptRef.X から ptNew.X までの距離の増加率です。つまり、次のように計算します。

scaleX = (ptNew.X – ptRef.X) / (ptOld.X – ptRef.X)

縦方向の倍率も同じように計算します。図 2 の例であれば、横方向の倍率は 2、縦方向の倍率は 1/2 になります。

確かに、この方法は簡単です。しかし、2 本の指を使用してオブジェクトを回転する方が、プログラムがより自然に機能するように思われます。これを図 3 に示します。


図 3 回転と拡大縮小に変換される 2 本の指の動き

まず、2 つのベクトル (ptRef から ptOld までのベクトルと、ptRef から ptNew までのベクトル) の角度を計算します (Math.Atan2 メソッドがこの作業に適しています)。ptOld の位置を ptRef に相対に、この角度の違い分だけ回転します。次に、回転後の ptOld、および ptRef と ptNew から倍率を計算します。回転の要素が取り除かれるため、先ほどよりも倍率ははるかに小さくなります。

実際のアルゴリズム (C# ファイルの ComputeMoveMatrix メソッドに実装) は、かなり簡単であることがわかりました。ただし、プログラムでは、Silverlight の変換クラスの不足を補うために、一連の変換サポート コードも必要でした (Silverlight には、WPF にあるようなパブリックな Value プロパティや行列乗算がありません)。

実際のプログラムでは、2 本の指を同時に動かしてもかまいません。2 本の指で行われる操作は最初に想像していたよりも単純でした。それぞれ移動する指は、もう一方の指を参照ポイントとして使用して、独立して処理されます。計算の複雑さは増しますが、結果はより自然に見えます。これについては、簡単に説明できます。現実の世界では、指を使って物体を回転させるのはごくありふれたことですが、拡大縮小することはきわめてまれです。

回転は現実の世界でごく一般的なことなので、1 本の指やマウスのみを使用してオブジェクトを操作するときには、回転を実装する方が理にかなっているのかもしれません。これは、別の AltFingerManipulation プログラムで例を示しています (charlespetzold.com/silverlight/AltFingerManipulation)。2 本の指を使うと、TwoFingerManipulation と同じように機能します。1 本の指を使うと、オブジェクトの中心から相対に回転が計算されます。そのため、中心から離れるほど変換の動きが大きくなります。

多くのイベントをラップする

通常、私は、独自のコードでクラスをラップするよりも、フレームワークに含まれる、マイクロソフトが熟慮のうえ用意したクラスを使用する方が好きです。しかし、私には、もっと洗練されたイベント インターフェイスからメリットを得ることができるマルチタッチ アプリケーションが頭にありました。

まず、よりモジュール化されたシステムを望んでいました。また、独自のタッチ入力を処理するカスタム コントロールと、単純にタッチ入力をマウス入力に変換する既存の Silverlight コントロールを混在させることも考えていました。さらに、キャプチャも実装したいと思っていました。Silverlight アプリケーション自体でもマルチタッチ デバイスをキャプチャしますが、特定のタッチ ポイントを単独でキャプチャする個別のコントロールが望みでした。

Enter イベントと Leave イベントも必要です。ある意味、これらのイベントはキャプチャのパラダイムとは対照的です。この違いを理解するため、画面上にピアノの鍵盤があるとします。鍵盤のそれぞれの鍵は、PianoKey コントロールのインスタンスです。一見すると、これらの鍵はマウスでトリガーされるボタンのように思えます。つまり、マウスの Down イベントでピアノの鍵の音を鳴らし、マウスの Up イベントで音を止めます。

しかし、ピアノの鍵に必要なのはそれだけではありません。鍵盤上で指を高音から低音に移動させ、グリッサンド効果を得る機能も必要です。この場合、各鍵でわざわざ Down イベントと Up イベントを使用するのではなく、実際には、Enter イベントと Leave イベントのみを考慮します。

WPF 4 と Microsoft Surface では、既にタッチ イベントがルーティングされます。将来的には、Silverlight にも含まれる予定です。しかし、現時点では、TouchManager というクラスを使用してこのニーズを満たしています。このクラスは、TouchDialDemos ソリューションの Petzold.MultiTouch ライブラリ プロジェクトに実装されています。TouchManager の大部分は、静的メソッド、フィールド、および Touch.FrameReported イベント用の静的ハンドラーで構成されていて、アプリケーション全体のタッチ イベントを管理することができます。

TouchManager への登録を希望するクラスは、次のようにインスタンスを作成します。

TouchManager touchManager = new TouchManager(element);

コンストラクター引数は UIElement 型にし、通常は、オブジェクトを作成している要素にします。

TouchManager touchManager = new TouchManager(this);

クラスを TouchManager に登録することで、そのクラスでは (TouchDevice の DirectlyOver プロパティが TouchManager コンストラクターに渡される要素の子要素である) すべてのマルチタッチ イベントが処理の対象となり、これらのマルチタッチ イベントをマウス イベントに昇格すべきでないことが示されます。要素の登録を解除する方法はありません。

TouchManager の新しいインスタンスを作成したら、クラスは、TouchDown、TouchMove、TouchUp、TouchEnter、TouchLeave、および LostTouchCapture というイベントのハンドラーを組み込むことができます。

touchManager.TouchEnter += OnTouchEnter;

すべてのハンドラーは、EventHandler<TouchEventArgs> デリゲートに従って定義されます。

void OnTouchEnter(
  object sender, TouchEventArgs args) {
  ...
}

TouchEventArgs では、次の 4 つのプロパティを定義します。

  • UIElement 型の Source プロパティ: TouchManager コンストラクターに渡された要素の元の要素です。
  • Point 型の Position プロパティ: Source プロパティとの相対位置です。
  • UIElement 型の DirectlyOver プロパティ: TouchDevice オブジェクトから単純にコピーされます。
  • int 型の Id プロパティ: TouchDevice オブジェクトからコピーされます。

クラスは、TouchDown イベントの処理中のみ、このイベントに関連付けられているタッチ ポイントの ID を指定して Capture メソッドを呼び出すことができます。

touchManager.Capture(id);

その ID のそれ以降のすべてのタッチ入力は、TouchUp イベントが発生するまで、または ReleaseTouchCapture が明示的に呼び出されるまで、この TouchManager インスタンスに関連付けられている要素に渡されます。いずれかの状態になったら、TouchManager が LostTouchCapture イベントを発生します。

通常、イベントは、TouchEnter、TouchDown、TouchMove、TouchUp、TouchLeave、LostTouchCapture の順に発生します (該当する場合)。もちろん、TouchDown イベントと TouchUp イベントの間に複数の TouchMove イベントが発生することもあります。タッチ ポイントがキャプチャされていないときは、タッチ ポイントが登録済みの要素から離れ、別の要素に入るときに、TouchLeave、TouchEnter、TouchMove の順に複数のイベントが発生することがあります。

TouchDial コントロール

ユーザー入力のパラダイムを変えるときは、多くの場合、コントロールの適切なデザインやその他の入力メカニズムについての従来からの想定に疑問を持つことが必要です。たとえば、スクロール バーやスライダーは、しっかりとした概念が確立された GUI コントロールです。これらのコントロールは、大きなドキュメントや画像を移動するためだけでなく、メディア プレーヤーの小さなボリューム コントロールとしても使用されます。

タッチ操作に反応する画面上にボリューム コントロールを作成しようと考えたときに、これまでのアプローチは本当に正しかったのだろうかと疑問に感じました。現実の世界では、ときにはスライダーがボリューム コントロールとして使用されることもありますが、通常、プロが使用するミキシング パネルやグラフィック イコライザーに限られています。実際のボリューム コントロールのほとんどはダイヤル式です。タッチ対応のボリューム コントロールにはダイヤルの方が適しているのでしょうか。

明確な解答があるようなそぶりはやめて、このソリューションの作成方法を説明しましょう。

TouchDial コントロールは、TouchDialDemos ソリューションの Petzold.MultiTouch ライブラリに含まれています (詳細については、コード ダウンロードを参照してください)。TouchDial は RangeBase から派生しているため、Minimum、Maximum、および Value の各プロパティ (Value を Minimum と Maximum の範囲内に収まるようにするロジックも含みます) と、ValueChanged イベントを使用できます。ただし、TouchDial では、Minimum、Maximum、および Value の各プロパティはすべて度単位の角度です。

TouchDial はマウスとタッチの両方に反応し、TouchManager クラスを使用してタッチ ポイントをキャプチャします。マウス入力でもタッチ入力でも、TouchDial は、マウスまたは指の新しい位置と前の位置を中心点から相対位置として、この 2 つの位置を基に Move イベント中に Value プロパティを変更します。この処理は図 3 に非常によく似ていますが、この場合は拡大縮小を行いません。Move イベントでは、Math.Atan2 メソッドを使用してデカルト座標を角度に変換してから、2 つの角度の差を Value に加算します。

TouchDial には既定のテンプレートを含めていないため、表示上の外観には既定値がありません。TouchDial を使用するときにはテンプレートを指定することになりますが、いくつか要素を指定するだけの簡単な操作です。言うまでもありませんが、このテンプレートで指定する対象は、おそらく Value プロパティの変化に応じて回転します。便宜上、TouchDial は取得専用の RotateTransform プロパティを提供します。この場合、Angle プロパティは RangeBase の Value プロパティに相当し、コントロールの中心は CenterX プロパティと CenterY プロパティに反映されます。

図 4 に、リソースとして定義されるスタイルとテンプレートを参照する、2 つの TouchDial コントロールを含む XAML を示します。

図 4 SimpleTouchDialTemplate プロジェクトの XAML ファイル

<UserControl x:Class="SimpleTouchDialTemplate.MainPage"
  xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation" 
  xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:multitouch="clr-namespace:Petzold.MultiTouch;assembly=Petzold.MultiTouch">
  <UserControl.Resources>
    <Style x:Key="touchDialStyle" 
      TargetType="multitouch:TouchDial">
      <Setter Property="Maximum" Value="180" />
      <Setter Property="Minimum" Value="-180" />
      <Setter Property="Width" Value="200" />
      <Setter Property="Height" Value="200" />
      <Setter Property="HorizontalAlignment" Value="Center" />
      <Setter Property="VerticalAlignment" Value="Center" />
      <Setter Property="Template">
        <Setter.Value>
          <ControlTemplate TargetType="multitouch:TouchDial">
            <Grid>
              <Ellipse Fill="{TemplateBinding Background}" />
              <Grid RenderTransform="{TemplateBinding RotateTransform}">
                <Rectangle Width="20" Margin="10"
                  Fill="{TemplateBinding Foreground}" />
              </Grid>
            </Grid>
          </ControlTemplate>
        </Setter.Value>
      </Setter>
    </Style>
  </UserControl.Resources>
    
  <Grid x:Name="LayoutRoot">
    <Grid.ColumnDefinitions>
      <ColumnDefinition Width="*" />
      <ColumnDefinition Width="*" />
    </Grid.ColumnDefinitions>
        
    <multitouch:TouchDial Grid.Column="0"
      Background="Blue" Foreground="Pink"
      Style="{StaticResource touchDialStyle}" />
        
    <multitouch:TouchDial Grid.Column="1"
      Background="Red" Foreground="Aqua"
      Style="{StaticResource touchDialStyle}" />
  </Grid>
</UserControl>

Style によって、Maximum プロパティが 180 に、Minimum プロパティが -180 に設定されることがわかります。これにより、バーを左と右に 180°回転できます (奇妙なことに、スタイル定義でこれらの 2 つのプロパティの順序を入れ替えると機能しませんでした)。このダイヤルは、Ellipse 内に Rectangle 要素で作られた 1 つのバーを配置することで構成されています。バーは、単一セルのグリッド内にあります。このグリッドには、TouchDial で計算され、RotateTransform プロパティにバインドされる RenderTransform 属性があります。

図 5 に、実行中の SimpleTouchDialTemplate プログラムを示します。


図 5 SimpleTouchDialTemplate プログラム

このプログラム (およびこのコラムでこれから説明する 2 つのプログラム) は、charlespetzold.com/silverlight/TouchDialDemos で試すことができます。

円の中にあるバーを回転する操作は、マウスではややぎこちなくなり、指だとはるかに自然に感じられます。円の中の任意の場所でマウス左ボタンをクリックしたとき、または指で画面に触れたときにバーが回転することがあります。マウスと指の両方がキャプチャされるため、バーを回転している間にマウスと指を切り替えることができます。

マウスまたは指がバーに直接触れたときだけバーを回転させるには、Ellipse の IsHitTestVisible プロパティを False に設定します。

最初のバージョンの TouchDial には、RotateTransform プロパティを含めませんでした。Angle プロパティを コントロールの Value プロパティへの TemplateBinding のターゲットにする RotateTransform を、テンプレートに明示的に含めることができることは十分理解していました。しかし、Silverlight 3 では、FrameworkElement から派生していないクラスのプロパティではバインディングが機能しないため、RotateTransform の Angle プロパティをバインディング ターゲットにすることができません (これは、Silverlight 4 で解決されます)。

回転は、常に、中心点を基準にします。このちょっとした事実が、TouchDial コントロールを複雑にします。TouchDial では、2 とおりの方法で中心点を使用します。1 つは角度を計算するためです (図 3 参照)。もう 1 つは RotateTransform の CenterX プロパティと CenterY プロパティを設定するためです。既定では、TouchDial によって、2 つの中心点が ActualWidth プロパティと ActualHeight プロパティの中央にあるとして計算されます。これはコントロールの中心ではありますが、この方法が適さない場合も非常にたくさんあります。

たとえば、図 4 のテンプレートで、Rectangle の RenderTransform プロパティを、TouchDial の RotateTransform プロパティにバインドするとします。TouchDial では RotateTransform の CenterX プロパティと CenterY プロパティを 100 に設定していますが、Rectangle の自身に相対な中心点は実際にはポイント (10, 90) になるため、これでは正しく機能しません。TouchDial がコントロールのサイズから計算するこれらの既定値をオーバーライドできるように、このコントロールでは RenderCenterX プロパティと RenderCenterY プロパティを定義しています。SimpleTouchDialTemplate プロパティでは、Style のこれらのプロパティを次のように設定できます。

<Setter Property="RenderCenterX" Value="10" />
<Setter Property="RenderCenterY" Value="90" />

または、これらのプロパティを 0 に設定し、中心が自身に相対であることを示すために、Rectangle 要素の RenderTransformOrigin を設定することもできます。

RenderTransformOrigin="0.5 0.5"

マウスや指の動きを決める基準となるポイントがコントロールの中心ではなくても、TouchDial を使用できます。この場合は、InputCenterX プロパティと InputCenterY プロパティを設定して、既定値をオーバーライドします。

図 6 に、OffCenterTouchDial プロジェクトの XAML ファイルを示します。

図 6 OffCenterTouchDial の XAML ファイル

<UserControl x:Class="OffCenterTouchDial.MainPage"
  xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation" 
  xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:multitouch="clr-namespace:Petzold.MultiTouch;assembly=Petzold.MultiTouch">
  <Grid x:Name="LayoutRoot">
    <multitouch:TouchDial Width="300" Height="200" 
      HorizontalAlignment="Center" VerticalAlignment="Center"
      Minimum="-20" Maximum="20"
      InputCenterX="35" InputCenterY="100"
      RenderCenterX="15" RenderCenterY="15">
      <multitouch:TouchDial.Template>
        <ControlTemplate TargetType="multitouch:TouchDial">
          <Grid Background="Pink">
            <Rectangle Height="30" Width="260"
              RadiusX="15" RadiusY="15" Fill="Lime"
              RenderTransform="{TemplateBinding RotateTransform}" />
            <Ellipse Width="10" Height="10"
              Fill="Black" HorizontalAlignment="Left"
              Margin="30" />
          </Grid>
        </ControlTemplate>
      </multitouch:TouchDial.Template>
    </multitouch:TouchDial>
  </Grid>
</UserControl>

このファイルには、1 つの TouchDial コントロールが含まれています。このコントロールのプロパティはコントロール自体で設定され、Template プロパティが、Rectangle と Ellipse を含む単一セルのグリッドの Control テンプレートに設定されます。Ellipse は、Rectangle の回転の中心の目安となる小さな円です。Rectangle は、上下に 20 度旋回できます (図 7 参照)。


図 7 OffCenterTouchDial プログラム

InputCenterX プロパティと InputCenterY プロパティは、ピンク色のグリッド内の Ellipse 要素の中心位置を示すため、常にコントロール全体からの相対になります。RenderCenterX プロパティと RenderCenterY プロパティは、常に、コントロールの中で RotateTransform プロパティが適用される部分からの相対になります。

ボリューム コントロールとピッチパイプ (調子笛)

前述の 2 つの例では、TouchDial に表示上の外観を指定する方法を示しました。最初の例では、マークアップで Template プロパティを明示的に設定しました。次の例では、複数のコントロールでテンプレートを共有する必要がある場合に、リソースとして定義されている ControlTemplate を参照することによって設定しました。

TouchDial から新しいクラスを派生し、テンプレートを設定するためだけに XAML ファイルを使用することもできます。この例は、Petzold.MultiTouch ライブラリに含まれている RidgedTouchDial で示しています。RidgedTouchDial は、サイズと表示上の外観が明確に定義されていることを除けば、TouchDial と同じです (これについてはすぐにわかります)。

UserControl から派生したクラス内で、TouchDial (または RidgedTouchDial のような派生クラス) を使用することも可能です。このアプローチのメリットは、Minimum、Maximum、Value といった RangeBase で定義されるすべてのプロパティを非表示にして、これらを新しいプロパティに置き換えられることです。

VolumeControl がこの例です。VolumeControl は表示上の外観を RidgedTouchDial から派生し、Volume という新しいプロパティを定義します。Volume プロパティは依存関係プロパティでサポートされ、このプロパティに変更を加えると VolumeChanged イベントが発生します。

VolumeControl の XAML ファイルでは、単純に RidgedTouchDial コントロールを参照し、Minimum、Maximum、Value など、いくつかのプロパティを設定します。

<src:RidgedTouchDial 
  Name="touchDial"
  Background="{Binding Background}"
  Maximum="150"
  Minimum="-150"
  Value="-150"
  ValueChanged="OnTouchDialValueChanged" />

このため、TouchDial は最小位置から最大位置まで、300 度回転できます。図 8 に、VolumeControl.xaml.cs を示します。このコントロールは、ダイヤルの 300 度の範囲を、デシベルという対数スケールの 0 ~ 96 に変換します。

図 8 VolumeControl の C# ファイル

using System;
using System.Windows;
using System.Windows.Controls;

namespace Petzold.MultiTouch {
  public partial class VolumeControl : UserControl {
    public static readonly DependencyProperty VolumeProperty =
      DependencyProperty.Register("Volume",
      typeof(double),
      typeof(VolumeControl),
      new PropertyMetadata(0.0, OnVolumeChanged));

    public event DependencyPropertyChangedEventHandler VolumeChanged;

    public VolumeControl() {
      DataContext = this;
      InitializeComponent();
    }

    public double Volume {
      set { SetValue(VolumeProperty, value); }
      get { return (double)GetValue(VolumeProperty); }
    }

    void OnTouchDialValueChanged(object sender, 
      RoutedPropertyChangedEventArgs<double> args) {

      Volume = 96 * (args.NewValue + 150) / 300;
    }

    static void OnVolumeChanged(DependencyObject obj, 
      DependencyPropertyChangedEventArgs args) {

      (obj as VolumeControl).OnVolumeChanged(args);
    }

    protected virtual void OnVolumeChanged(
      DependencyPropertyChangedEventArgs args) {

      touchDial.Value = 300 * Volume / 96 - 150;

      if (VolumeChanged != null)
        VolumeChanged(this, args);
    }
  }
}

なぜ 96 なのでしょう。デシベル スケールが 10 進数に基づいていても、シグナルの振幅が 10 の倍数因子ずつ増加するときに、ラウドネスは直線的に 20 デシベルずつ増加します。10 の 3 乗がほぼ 2 の 10 乗に相当するとの同じです。つまり、振幅が 2 倍になると、ラウドネスは 6 デシベル増加します。したがって、16 ビット値を使用して振幅を表す場合 (これは CD や PC の音の場合に当てはまります)、16 ビット × 6 デシベル/ビットの範囲 (つまり 96 デシベル) になります。

PitchPipeControl クラスも UserControl から派生し、Frequency という新しいプロパティを定義します。XAML ファイルには、TouchDial コントロールと、オクターブの 12 音を表す一連の TextBlocks があります。また、PitchPipeControl は、まだ説明していない TouchDial の別のプロパティを使用します。角度の SnapIncrement を 0 以外の値に設定すると、ダイヤルの動きがスムーズにならなくなりますが、増加する値が大きく変化します。PitchPipeControl はオクターブの 12 音に設定できるため、SnapIncrement を 30 度に設定します。

図 9 に、VolumeControl と PitchPipeControl を組み合わせた PitchPipe プログラムを示します。PitchPipe は、charlespetzold.com/silverlight/TouchDialDemos で実行できます。


図 9 PitchPipe プログラム

おまけのプログラム

このコラムの前半で例を説明する際に PianoKey というコントロールについて触れました。PianoKey は実在するコントロールで、Piano プログラムのいくつかのコントロールの 1 つです (このプログラムは、charlespetzold.com/silverlight/Piano で実行できます)。Piano プログラムは、ブラウザーの最大サイズで表示されるようになっています (F11 キーを押して、Internet Explorer を全画面表示モードにしてさらに大きく表示できます)。図 10 に非常に小さくした画像を示します。鍵盤は、高音域と低音域に分かれ、一部は重なった状態です。赤い点は、中央ハ (ピアノの中央にあるドの音) を示しています。


図 10 Piano プログラム

Piano プログラムではタッチを 3 とおりの方法で使用するため、このプログラム向けに TouchManager を作成しました。青色の VolumeControl については既に説明しました。これは、TouchDown イベントでタッチ ポイントをキャプチャして、TouchUp イベントでキャプチャを解放します。鍵盤となる PianoKey コントロールでも TouchManager が使用されていますが、これらのコントロールは TouchEnter イベントと TouchLeave イベントをリッスンするだけです。もちろん、複数の鍵の上で指を滑らせて、グリッサンド効果を得ることもできます。ダンパー ペダルとして機能する茶色の四角形は、通常の Silverlight の ToggleButton コントロールです。これらは厳密に言うとタッチ対応ではありませんが、代わりに、タッチ ポイントがマウス イベントに変換されます。

Piano プログラムでは、マルチタッチの 3 とおりの使用方法が示されています。今後、さらに多くの使用方法が考案されるでしょう。

 

Charles Petzold は MSDN Magazine の記事を長期にわたって担当している寄稿編集者です。最新の著書には『The Annotated Turing: A Guided Tour Through Alan Turing's Historic Paper on Computability and the Turing Machine』(Wiley、2008 年) があります。Petzold のブログは、彼の Web サイト (charlespetzold.com、英語) で公開されています。

この記事のレビューに協力してくれた技術スタッフの Robert Levy と Anson Tsao に心より感謝いたします。