UI 最前線
WPF におけるマルチタッチ操作イベント
Charles Petzold
わずかここ数年で、マルチタッチは、斬新な SF 映画の小道具から UI の主流へと進化しました。新型のスマートフォンやタブレット PC ではマルチタッチ画面が標準になりつつあり、キオスクなどの公共施設にあるコンピューターや、Microsoft Surface によって最初に開発されたテーブル型コンピューターでも目にするようになってきました。
唯一、きわめて不確かなのは、従来のデスクトップ コンピューターにおけるマルチタッチの需要です。おそらく、マルチタッチを導入するにあたっての一番の障害は、垂直な画面上で長時間にわたって指を動かすことで腕が疲れる「ゴリラ腕」として知られる現象でしょう。個人的には、マルチタッチの力で、デスクトップ画面のデザインの見直しが実際に行われることを願っています。デスクトップ コンピューターの画面が、製図台の形状に似た、おそらく製図台とほぼ同じ大きさの画面になっているところを思い描くことができます。
しかし、それは (多分) かなり先の話でしょう。現状としては、開発者は新しい API を習得しておくことです。Windows 7 のマルチタッチ サポートは、低レベル インターフェイスと高レベル インターフェイスの両方を備え、Microsoft .NET Framework のさまざまな領域に及んでいます。
マルチタッチ サポートを整理する
画面上で複数の指を使用することで可能となる表現の複雑さを考えると、マルチタッチの "正しい" プログラミング インターフェイスは、おそらく、まだだれも知らないと思われます。正しいプログラミング インターフェイスが定着するまで、ある程度時間がかかるでしょう。それまでの間、いくつか選択肢があります。
Windows Presentation Foundation (WPF) 4.0 では、Windows 7 で実行するプログラムで 2 つのマルチタッチ インターフェイスを使用できます。マルチタッチの特殊な使用法として、プログラマは、TouchDown、TouchMove、TouchUp、TouchEnter、TouchLeave という名前の UIElement で定義されるいくつかのルーティング イベントと、down、move、および up の各プレビュー イベントから成る、低レベル インターフェイスを調べることになるでしょう。わかりきったことですが、これらはマウス イベントをモデルとしています (画面上で複数の指を追跡するために整数の ID プロパティが必要であることは除きます)。Microsoft Surface は WPF 3.5 に組み込まれていますが、より拡張性の高い、低レベルな Contact インターフェイスをサポートし、タッチ入力の種類と形状を区別します。
今月のコラムのテーマは、WPF 4.0 における高レベルのマルチタッチ サポートです。これは、Manipulation という単語で始まる名前を持つイベントのコレクションで構成されます。これらの Manipulation イベントは、次に示す、きわめて重要な複数のマルチタッチ ジョブを実行します。
- 2 本の指による操作を、1 つの操作に統合する
- 1 本または 2 本の指の動きを変換に置き換える
- 指が画面から離れるときに慣性を実装する
Manipulation イベントのサブセットが Silverlight 4 のドキュメントに一覧されていますが、あまり当てになりません。このイベントは Silverlight 自体ではまだサポートされていませんが、Windows Phone 7 用に作成された Silverlight アプリケーションではサポートされています。図 1 に、Manipulation イベントを示します。
図 1 Windows Presentation Foundation 4.0 における Manipulation イベント
イベント | Windows Phone 7 によるサポート |
ManipulationStarting | × |
ManipulationStarted | ○ |
ManipulationDelta | ○ |
ManipulationInertiaStarted | × |
ManipulationBoundaryFeedback | × |
ManipulationCompleted | ○ |
Web ベースの Silverlight 4 アプリケーションでは、Touch.FrameReported イベントが引き続き使用されます。このイベントについては、MSDN Magazine の 2010 年 3 月号の記事「Finger Style: Silverlight でのマルチタッチ サポートの詳細」を参照してください。
Manipulation イベント自体に加えて、WPF の UIElement クラスは、Manipulation イベントに対応する、OnManipulationStarting などのオーバーライド可能なメソッドもサポートしれます。Silverlight for Windows Phone 7 では、これらのオーバーライド可能なメソッドは Control クラスで定義されます。
マルチタッチの例
おそらく、典型的なマルチタッチ アプリケーションはフォト ビューアーでしょう。フォト ビューアーでは、画面上で写真を移動したり、2 本の指で写真を拡大、縮小したり、写真を回転したりできます。こうした操作は、パン、ズーム、回転とも呼ばれ、標準グラフィックス変換の移動、拡大縮小、および回転に相当します。
わかりきったことですが、写真を表示するプログラムでは、写真のコレクションを管理し、新しい写真の追加と写真の削除を可能にし、小さなグラフィック フレームで写真を常に美しく表示する必要があります。しかし、ここではそれらをすべて無視して、マルチタッチ操作のみに集中的に取り組みます。私は、Manipulation イベントを使用することで、マルチタッチ操作がどれほど簡単になるかを知り、驚きました。きっと、皆さんも私と同じように驚かれることでしょう。
今月のコラムに掲載するすべてのソース コードは、WpfManipulationSamples という、1 つのダウンロード可能なソリューションにしました。最初のプロジェクトは SimpleManipulationDemo です。図 2 に、このプロジェクトの MainWindow.xaml ファイルを示します。
図 2 SimpleManipulationDemo の XAML ファイル
<Window x:Class="SimpleManipulationDemo.MainWindow"
xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
Title="Simple Manipulation Demo">
<Window.Resources>
<Style TargetType="Image">
<Setter Property="Stretch" Value="None" />
<Setter Property="HorizontalAlignment" Value="Left" />
<Setter Property="VerticalAlignment" Value="Top" />
</Style>
</Window.Resources>
<Grid>
<Image Source="Images/112-1283_IMG.JPG"
IsManipulationEnabled="True"
RenderTransform="0.5 0 0 0.5 100 100" />
<Image Source="Images/139-3926_IMG.JPG"
IsManipulationEnabled="True"
RenderTransform="0.5 0 0 0.5 200 200" />
<Image Source="Images/IMG_0972.JPG"
IsManipulationEnabled="True"
RenderTransform="0.5 0 0 0.5 300 300" />
<Image Source="Images/IMG_4675.JPG"
IsManipulationEnabled="True"
RenderTransform="0.5 0 0 0.5 400 400" />
</Grid>
</Window>
まず、4 つの Image 要素すべての以下の設定に注目してください。
IsManipulationEnabled="True"
既定では、このプロパティは false です。マルチタッチ入力を受け取り、Manipulation イベントを生成する予定のすべての要素では、このプロパティを true に設定しなければなりません。
Manipulation イベントは、WPF ルーティング イベントです。つまり、イベントがビジュアル ツリーをバブルアップします。このプログラムでは、Grid と MainWindow のどちらも IsManipulationEnabled プロパティを true に設定していません。それでも、Grid 要素と MainWindow 要素に Manipulation イベントのハンドラーをアタッチするか、MainWindow クラスで OnManipulation メソッドをオーバーライドすることができます。
また、次のように、各 Image 要素の RenderTransform 値に 6 個の数値からなる文字列を設定しているのがわかります。
RenderTransform="0.5 0 0 0.5 100 100"
これは、RenderTransform プロパティを、初期化済みの MatrixTransform オブジェクトに設定するためのショートカットです。この場合は、MatrixTransform に設定する Matrix オブジェクトを初期化し、0.5 の倍率 (写真を実際のサイズの半分にする) で、右下方向への 100 単位の移動を実行します。ウィンドウの分離コード ファイルでは、この MatrixTransform にアクセスして、値を変更します。
図 3 に、完全な MainWindow.xaml.cs ファイルを示します。このファイルでは、OnManipulationStarting と OnManipulationDelta の 2 つのメソッドのみをオーバーライドします。これらのメソッドは、Image 要素で行われた操作を処理します。
図 3 SimpleManipulationDemo の分離コード ファイル
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
namespace SimpleManipulationDemo {
public partial class MainWindow : Window {
public MainWindow() {
InitializeComponent();
}
protected override void OnManipulationStarting(
ManipulationStartingEventArgs args) {
args.ManipulationContainer = this;
// Adjust Z-order
FrameworkElement element =
args.Source as FrameworkElement;
Panel pnl = element.Parent as Panel;
for (int i = 0; i < pnl.Children.Count; i++)
Panel.SetZIndex(pnl.Children[i],
pnl.Children[i] ==
element ? pnl.Children.Count : i);
args.Handled = true;
base.OnManipulationStarting(args);
}
protected override void OnManipulationDelta(
ManipulationDeltaEventArgs args) {
UIElement element = args.Source as UIElement;
MatrixTransform xform =
element.RenderTransform as MatrixTransform;
Matrix matrix = xform.Matrix;
ManipulationDelta delta = args.DeltaManipulation;
Point center = args.ManipulationOrigin;
matrix.ScaleAt(
delta.Scale.X, delta.Scale.Y, center.X, center.Y);
matrix.RotateAt(
delta.Rotation, center.X, center.Y);
matrix.Translate(
delta.Translation.X, delta.Translation.Y);
xform.Matrix = matrix;
args.Handled = true;
base.OnManipulationDelta(args);
}
}
}
Manipulation (操作) の基礎
Manipulation (操作) とは、1 本または複数の指が特定の要素に触れることと定義します。完全な操作は、ManipulationStarting イベントから始まり (直後に ManipulationStarted イベントが続き)、ManipulationCompleted イベントで終了します。この間、多くの ManipulationDelta イベントが発生する可能性があります。
各 Manipulation イベントには、EventArgs を後ろに付けて名付けられたクラス (ManipulationStartingEventArgs や ManipulationDeltaEventArgs など) にカプセル化された、それぞれ独自のイベント引数が付属します。これらのクラスは、おなじみの InputEventArgs から派生します。つまり、RoutedEventArgs から派生します。このクラスには、Source プロパティと OriginalSource プロパティがあり、イベントの発生元を示します。
SimpleManipulationDemo プログラムでは、Source プロパティと OriginalSource プロパティはどちらも、Manipulation イベントを生成する Image 要素に設定されます。こうした Manipulation イベントでは、IsManipulationEnabled プロパティが true に設定されている要素のみ、Source プロパティおよび OriginalSource プロパティが設定されます。
また、Manipulation イベントに関連付けられるイベント引数の各クラスには、ManipulationContainer というプロパティがあります。これは、マルチタッチ操作が行われる要素です。Manipulation イベント内のすべての座標は、このコンテナーとの相対座標になります。
既定では、ManipulationContainer プロパティは、Source プロパティおよび OriginalSource プロパティと同じ要素 (操作される要素) に設定されますが、おそらくこれは望ましくない動作です。一般に、操作コンテナーと操作対象の要素が同じになることは望ましくありません。それは、動的に移動、拡大縮小、および回転する要素と、タッチに関する情報を報告する要素が同じになると、要素との対話が扱いにくくなるためです。代わりに、操作コンテナーを、操作対象の親要素にするか、おそらくビジュアル ツリーのさらに上部にある要素にするのが望ましいと考えられます。
ほとんどの Manipulation イベントの ManipulationContainer プロパティは取得専用のプロパティです。ただし、要素が受け取る最初の Manipulation イベントは例外です。ManipulationStarting は、ManipulationContainer を適切な要素に変更するチャンスです。SimpleManipulationDemo プロジェクトでは、この変更を次に示す 1 行のコードで行っています。
args.ManipulationContainer = this;
その後発生するすべてのイベントの ManipulationContainer は、Image 要素ではなく MainWindow 要素になり、すべての座標がメイン ウィンドウからの相対座標になります。Image 要素を含む Grid もメイン ウィンドウに沿って調整されるので、これはうまく機能します。
OnManipulationStarting メソッドの残りの部分では、Grid のすべての Image 要素の添付プロパティ Panel.ZIndex を再設定することにより、タッチされた Image 要素を前面に表示します。これは、ZIndex を扱う単純な方法ですが、画面上では急激な変化が起こるため、最善の方法とは言えません。
ManipulationDelta と DeltaManipulation
SimpleManpulationDemo で処理しているイベントはあと 1 つだけで、それは ManipulationDelta イベントです。ManipulationDeltaEventArgs クラスは、ManipulationDelta 型の 2 つのプロパティを定義します (そう、イベントとクラスには同じ名前が付いています)。定義されているプロパティは、DeltaManipulation と CumulativeManipulation です。その名のとおり、DeltaManipulation は最後の ManipulationDelta イベント以降に行われた操作を反映し、CumulativeManipulation は ManipulationStarting イベントから始まったすべての操作を表します。
ManipulationDelta には、次の 4 つのプロパティがあります。
- Vector 型の Translation
- Vector 型の Scale
- Vector 型の Expansion
- double 型の Rotation
Vector 構造体は、double 型の X および Y という 2 つのプロパティを定義します。Silverlight for Windows Phone 7 での Manipulation サポートに関する大きな違いの 1 つが、Expansion プロパティと Rotation プロパティが存在しないことです。
Translation プロパティは、水平方向と垂直方向の移動 (パン) を示します。ある要素の上に置いた 1 本の指は、移動における変化を生成することができますが、移動を他の操作の一部にすることもできます。
Scale プロパティと Expansion プロパティは、いずれもサイズの変化 (ズーム) を示し、常に 2 本の指を必要とします。Scale プロパティは変化を乗算で、Expansion プロパティは変化を加算で表します。倍率変換を設定するには Scale プロパティの値を使用し、要素の Width プロパティと Height プロパティをデバイスに依存しない単位で増加または減少するには Expansion プロパティの値を使用します。
WPF 4.0 では、Scale ベクトルの X 値と Y 値は、必ず同じになります。Manipulation イベントでは、要素を異なる方向に倍率変換する (つまり、水平方向と垂直方向に異なる倍率を設定する) ために十分な情報が提供されません。
既定では、Rotation でも 2 本の指を必要としますが、指 1 本でも回転を可能にする方法があります。これについては後で説明します。特定の ManipulationDelta イベントでは、これらの 4 つすべてのプロパティが設定されることがあります。つまり、2 本の指で要素を拡大しながら、同時に回転して別の場所に移動することが可能です。
倍率変換と回転は、常に、特定の点を中心にして相対的に行われます。この中心点は、ManipulationDeltaEventArgs の Point 型の ManipulationOrigin というプロパティでも提供されます。この原点は、ManipulationStarting イベントで設定する ManipulationContainer の相対座標です。
ManipulationDelta イベント内で開発者が行うことは、デルタ値に応じて操作対象オブジェクトの RenderTransform プロパティを、倍率変換、回転、移動の順に変更することです (実際には、水平方向と垂直方向の倍率は同じなので、倍率変換と回転の順序を入れ替えても、同じ結果になります)。
図 3 の OnManipulationDelta メソッドは、標準のアプローチを示しています。Matrix オブジェクトは、操作対象の Image 要素に設定される MatrixTransform から取得します。Matrix オブジェクトは、ScaleAt と RotateAt (どちらも ManipulationOrigin に相対)、および Translate を呼び出すことによって変更します。Matrix オブジェクトは、クラスではなく構造体なので、最後に MatrixTransform の古い値を新しい値に置き換える必要があります。
このコードは、少し変更することができます。ご覧のように、現在は次のステートメントを使用して、中心点を基準に倍率を変換しています。
matrix.ScaleAt(delta.Scale.X, delta.Scale.Y, center.X, center.Y);
これは、次のように、中心点から負の方向に移動し、倍率を変換してから、元の位置に戻すことと同じです。
matrix.Translate(-center.X, -center.Y);
matrix.Scale(delta.Scale.X, delta.Scale.Y);
matrix.Translate(center.X, center.Y);
同様に、RotateAt メソッドは次のように置き換えることができます。
matrix.Translate(-center.X, -center.Y);
matrix.Rotate(delta.Rotation);
matrix.Translate(center.X, center.Y);
2 つの Translate の呼び出しは、どちらも同じことを行っているため、これらを組み合わせると次のようになります。
matrix.Translate(-center.X, -center.Y);
matrix.Scale(delta.Scale.X, delta.Scale.Y);
matrix.Rotate(delta.Rotation);
matrix.Translate(center.X, center.Y);
やや効率があがったのではないでしょうか。
図 4 に、実行中の SimpleManipulationDemo プログラムを示します。
図 4 SimpleManipulationDemo プログラム
コンテナーを有効にする基準
SimpleManpulationDemo プログラムの興味深い機能の 1 つは、2 つの Image 要素を同時に操作できることです。つまり、ハードウェアのサポートや十分な数の指があれば、さらに複雑な操作を実行できることです。各 Image 要素は、ManipulationStarting イベントと、一連の ManipulationDelta イベントをそれぞれ独自に生成します。コードでは、イベント引数の Source プロパティに応じて、複数の Image 要素を効果的に区別します。
このため、フィールドに状態情報を設定しないことが重要です。フィールドに状態情報を設定すると、暗黙のうちに、一度に 1 つの要素しか操作できないことが示されます。
各 Image 要素それぞれの IsManipulationEnabled プロパティの値を true に設定しているため、複数の要素の同時操作が可能になります。各 Image 要素が、一連の Manipulation イベントをそれぞれ一意に生成できます。
こうした Manipulation イベントに初めて取り組む際、MainWindow クラスのみ、またはコンテナーとして使用する要素のみ、IsManpulationEnabled を true に設定して調査された方もいるかもしれません。これは可能ですが、実際にはやや面倒で、それほど強力というわけでもありません。唯一実質的なメリットは、ManipulationStarting イベントで ManipulationContainer プロパティを設定する必要がない点です。しかし、後で、ManipulatedStarted イベントの ManipulationOrigin プロパティを使用し、子要素のヒット テストを行って、操作されている要素を特定する必要があるときに、やっかいな問題が発生します。
将来 ManipulationDelta イベントで使用するため、操作されている要素をフィールドとして格納する必要があります。この場合は、コンテナー内で一度に 1 つの要素しか操作できなくなるため、状態情報をフィールドに格納しても問題ありません。
操作モード
既に説明したように、ManipulationStarting イベント中に設定するきわめて重要なプロパティの 1 つが、ManipulationContainer です。その他のいくつかのプロパティは、特定の操作をカスタマイズするのに役立ちます。
Mode プロパティを ManipulationModes 列挙体のメンバーで初期化することによって、実行できる操作の種類を制限できます。たとえば、水平方向にスクロールする操作だけを使用している場合は、イベントを水平方向への移動のみに制限することができます。ManipulationModesDemo プログラムでは、オプションを一覧する RadioButton 要素のリストを表示することで、モードを動的に設定できるようにしています (図 5 参照)。
図 5 ManipulationModeDemo の表示
もちろん、RadioButton は、画面にタッチしたときに直接反応する、WPF 4.0 の多数のコントロールのうちの 1 つです。
1 本指による回転
既定では、オブジェクトを回転するには 2 本の指が必要です。ただし、実際の写真が机の上にあれば、写真の隅に指を置いて円を描くように回転できます。回転は、だいたい、オブジェクトの中心を基準に行われます。
これは、ManipulationStartingEventArgs の Pivot プロパティを設定することにより、Manipulation イベントを使用して実行できます。既定では、Pivot プロパティは null です。1 本指による回転は、ManipulationPivot オブジェクトにプロパティを設定することで有効になります。ManipulationPivot の重要なプロパティは Center です。これが操作対象要素の中心として計算されると考えてかまいません。
Point center = new Point(element.ActualWidth / 2,
element.ActualHeight / 2);
ただし、この中心点は操作コンテナーとの相対座標でなければなりません。ここで示したプログラムでは、イベントを処理する要素でこのことについて説明してきました。この中心点を、操作対象要素からコンテナーに移動するのは簡単です。
center = element.TranslatePoint(center, this);
他にもちょっとした情報を設定する必要があります。指定しているのが中心点だけであれば、指を要素の中心に置いたときに問題が発生します。ちょっと指を動かすだけで、要素がすごい勢いで回転します。このため、ManipulationPivot には Radius プロパティも存在します。指が Center 点から Radius 単位内にある場合は、回転を行いません。ManipulationPivotDemo プログラムでは、この Radius を 1/2 インチ (13 mm) に設定しています。
args.Pivot = new ManipulationPivot(center, 48);
これで、1 本の指で回転と移動が実行できるようになります。
高度な内容
ここでは、WPF 4.0 の Manipulation イベントの使用に関する基礎を紹介しました。もちろん、これらの技法にはいくつかのバリエーションがあります。このバリエーションと、操作の慣性の機能については、今後のコラムで説明するつもりです。
また、Surface Toolkit for Windows Touch (英語) も確認することをお勧めします。このツールキットは、アプリケーション用にタッチが最適化されるコントロールを提供します。具体的に言うと、ScatterView コントロールでは、写真の操作などの基本的な操作で、Manipulation イベントを直接使用する必要がなくなります。すばらしい効果と動作がもたらされる、ご使用のアプリケーションが他のタッチ アプリケーションと同じように動作するようになります。
Charles Petzold は MSDN Magazine の記事を長期にわたって担当している寄稿編集者です。現在は、『Programming Windows Phone 7』(英語) を執筆中です。この書籍は、無償のダウンロード可能な電子書籍として、2010 年秋に公開されます。現時点では、彼の Web サイト (charlespetzold.com、英語) から、プレビュー版を入手できます。
この記事のレビューに協力してくれた技術スタッフの Doug Kramer、Robert Levy、Anson Tsao に心より感謝いたします。