Qt Quick Pointer Handler の紹介

この記事は The Qt BlogSay hello to Qt Quick Pointer Handlers を翻訳したものです。
執筆: Shawn Rutledge, 2017年11月23日

Qt Quick でのマルチタッチのサポートは実際のユースケースに対して不完全ではないかいうことをここ数年考えていました。2本指でスケールやローテーションやドラッグを行うための PinchArea や、複数のタッチを感知し JavaScript の簡単な状態遷移でジェスチャーを認識するような用途で利用可能な MultiPointTouchArea をこれまで提供してきました。これ以外の場合では、1)マウスのイベントが先に発生する。2)Qt は一つのマウス(core pointer)のみを想定している。3)QMouseEvent と QTouchEvent(など)に適切な共通の基底クラスが存在していないため別々にイベントが配信される。4)ショートカットが先にハンドリングされるため、タッチのイベントをマウスイベントとして扱いイベントの配信を一元化するのは難しい。これらの理由により、複数の MouseArea や Flickable を同時に使用できないようになっていました。例えば、二つのボタンを同時に押したり、二つのスライダーを同時にスライドさせたりは MouseArea ではできませんでした。

最初は MouseArea や Flickable でタッチイベントのハンドリングをすることでこの問題を解決しようと思いました。しかし、このためのコードは非常に複雑で、QMouseEvent の処理と QTouchEvent の処理をまったく同じように動かすためにはこの二つのコードパスにたいしてまったく同じコードを大量に書くハメになりました。数ヶ月取り組んだ結果、一応動く形にはなりました…が、既存のすべてのテストを通すのが非常に難しく、動作の振る舞いが変わる恐れがあるとの忠告を仲間からは受けました。MouseArea はその名のとおりマウスイベントのハンドリングを目的としているため、タッチイベントのハンドリングをしてしまうと不適切な感じになってしまいます。そして、アプリケーションやコントロールのセットは意図と反して2つのボタンやタブやラジオボタンを同時に押すことが可能になってしまいます。(ということで、オプトイン用のブール型のプロパティを追加して試してみたりはしましたが、それをすべての MouseArea に設定するのはツラいでしょう。)MouseArea と Flickable は協調して動く必要もあり、すべてを正しく動作させるように注意深く変更をする必要がありました。これ自体は可能ではありましたが、いまいち自信がなかったため Qt 5.5 での提供は控えました。

というわけで、試行錯誤を繰り替えした結果、別の方法でこの問題を解決することに落ち着きました。

まず、バイナリの互換性の壊すような QEvent の階層構造の変更は(現段階では)できないため、イベントを私たちの都合のいいように見せるようなラッパークラスを導入し、QML 用にプロパティを一通り用意し、QQuickWindow や QQuickItem にほぼ統一した形でイベントを配信できるようにしました。

次に、それらのラッパーイベントを動的に生成したり破棄したりするのは無駄だと気づいたため、シグナルの発生に伴う “event” オブジェクトの扱いなど Qt の他の場所でも行ったような最適化として(MouseArea の positionChanged で渡されるオブジェクトは Qt 5.8 では常に同じインスタンスになっている)インスタンスのプールを作成するようにしました。これにより、一つのイベントを他のイベントでラップする処理でのパフォーマンスの低下も回避できました。

その次はある提案から生まれたアイディアで、マウスやタッチのポイント処理を、Key のアタッチトプロパティのように記述できないかというものでした。Mouse.onclicked: { … }PointingDevice.onTapped: { … } といった形です。しかし、アタッチトプロパティは1つのアイテムに対して1つしかアタッチできないことに気づきました。MouseArea の問題は、それ自身がたくさんのことをしすぎで、個々の機能を個々の MouseAttached に分割するというのは理にかないません。私たちが求めているものは、個々のボタンコントロールが必要としているように、どのデバイスに由来するかを気にせずとも click や tap のハンドリングができることです。マウスやタッチの動きから生成されるジェスチャーではまさにこれを実現してます。ということは、デバイスごとにではなくてジェスチャーごとのアタッチトプロパティの方がいいかもしれません。

QML は宣言型の言語なので、signal ハンドラで JavaScript の if/else 文を書くよりも、振る舞いを宣言できる方がよいでしょう。もしイベントをハンドリングするオブジェクトがどのデバイスからのジェスチャーかを気にせずに設計されていたとしたら、アプリケーション側での対応が必要となった場合に問題になります。タッチスクリーンのタップとマウスの右クリックで処理を分けたい場合や、オブジェクトのドラッグ中にコントロールキーが押されているような場合にこれが該当します。こういった複数のハンドラを組み合わせられるようにするために、プロパティを設定することでその宣言ができるようにしました。これにより、必要な数だけハンドラを持っても大丈夫になりました。ここのハンドラは軽量につくられているため、多くのインスタンスの所持を懸念する必要はありません。実装は C++ で行われていて、シンプルで理解しやすいようになっています。個々のハンドラは1つか多くても2つ3つくらいの関係性の深い処理を正しく実行するようにしましょう。

とりあえずこれらのアイディアによりアタッチトプロパティの導入からは離れることができました。代わりに Qt Quick に導入したのがいくつかのポインターハンドラーです。

ポインターハンドラーはすべての Item の中で使用することができるエレメントで、Item の代わりにポインティングデバイスからのイベントをハンドリングする役割を果たします。必要な数だけ宣言をすることが可能ですが、一般的には1つのシナリオに対して1つのハンドラを利用することを想定しています。すべての一般的なケースは宣言的に記述可能で、タッチイベントにのみ反応したり、いくつかのマウスのボタンにのみ反応したり、アイテムの範囲内で指定した指の数のタッチにのみ反応したり、特定のモディファイアキーが押されていた場合にのみ反応したり、といったことが可能になっています。

複数のボールのドラッグ

一般的なジェスチャーは専用のハンドラーが用意されています。

Rectangle {
width: 50; height: 50; color: "green"
DragHandler { }
}

例えば上記のように記述することで、Javascript のコードを一切書かずとも、マウスやタッチでシーンの中を ドラッグ可能 な Rectangle を生成できます。ハンドラーを parent に何かしらの方法で明示的に関連付ける必要もありません。このハンドラには target プロパティがあり、初期値は parent になっています。(target を別の Item に設定することで、イベントの取得と生成を別々にすることが可能になります。)

もちろん、この DragHandler を含む緑の矩形を2つ用意した場合には、別々の指で同時にドラッグをすることが可能になります。

すべてのポインターハンドラーは QObject ですが、QQuickItem ではありません。多くの変数を抱えているわけでもないため、個々のインスタンスの大きさは普通の QObject とそれほど変わりがありません。

すべてのシングルポイントのハンドラーは point プロパティを持ち、タッチポイントやマウスの位置などの情報が提供されます。pressureellipseDiameters といったプロパティを用意し、(デバイスが提供していないことの方が多いですが)圧力センサーや接触面積などの情報も扱えるようになっています。また、設定が保証されている velocity をいうプロパティを用意し、直近のいくつかの移動を元に速度の平均を計算し(平坦化し)たものを提供しています。この velocity プロパティによって、フリック動作開始の閾値の調整など、速度に応じたジェスチャーも実現可能になっています。(ドラッグとフリックを区別するのはただしいのかな?)また、リリース時にに何かしらの速度があった場合には、ドラッグの終了時に速度方向に少しアニメーションを付与することも可能です。このような一工夫により、アプリがあたかも生きているように見えるでしょう。現時点では MomentumAnimation をアニメーションのサポートに追加をしてはいませんが、QML のみで作られたプロトタイプが tests/manual/pointer/content/MomentumAnimation.qml に存在しています。

two red balls being dragged simultaneously with DragHandler

タップのダンス

TapHandlerはすべてのプレスとリリースのジェスチャーをハンドリングします。シングル短タップやクリック、ダブルタップ、それ以上の数のタップ、設定時間を越えた長押し、様々な時間サイクルのホールドをそれぞれの方法で扱うことが可能です。(スクリーンをタッチした際には何も音がしないけれど、マウスのボタンをタップする事は可能です。このため「クリック」よりも「タップ」の方がよりこのジェスチャーにはふさわしい名前だということにしました。)また、(円を拡大したり、プログレスバーなどの方法を用いて)タップの時間を可視化することも可能です。

TapHandler detecting a triple-tap and then a long press

ピンチ操作について

PinchHandler も用意しました。Item の中に置くことで、その Item に対してピンチを用いたスケール、ローテーション、ドラッグが可能になります。どんな Item のどの部分であってもピンチでのズームが可能になりました(これは PinchArea からの改善です)。また、3本以上での操作にも対応可能で、PinchHandler { minimumToucPoints: 3 } と記述することで3本以上でのピンチ操作になります。この場合、変換の基準位置は3本の指の中心になり、スケーリングは3本指間の距離の増減の平均を用いて計算されます。Ubuntu のあるバージョンで3本指でのウィンドウ管理が実現されていて、これは通常のアプリケーションは2本指でのジェスチャーを使う可能性はあるけれど3本以上はほとんどないことから、これをデスクトップ上のウィンドウの操作に使うという判断でした。最近では QML で Wayland のコンポジタを記述することもできますので、その際には思い出してください。

zooming into a map with PinchHandler

ポイントの取得

最後は PointHandlerです。他のハンドラとは異なり、target のアイテムを生成はしません。単に point プロパティを提供するために存在します。これは MultiPointTouchArea の個々の TouchPoint に良く似ていて、同じように使うことが可能です。シーンの中でタッチの指やマウスカーソルが動いた際の情報を提供します。 MultiPointTouchArea とは異なり、タッチポイントやマウスの排他的なグラブは行わないため、これを用いた情報の取得によって他のアイテムの他のハンドラーの処理の邪魔をしたりはしません。このページのアニメーションの可視化の処理のところでこのハンドラが用いられています。

まだ完成していないの?

ここで Qt 5.10 ではこれらの新機能が Tech Preview である理由を説明します。1つ目はまだ不完全だということです。マウスのホバーやマウスのホイール、タブレット向けのスタイラス(スタイラスはまだマウスとして扱っています)の対応がまだ行われていません。また、すべてのハンドラには速度に由来する動きが実装されていません。また、他にもいくつかのハンドラーの追加を予定しています。C++ 向けの API も提供しているため、自分でハンドラーを記述する事も可能です。ハンドラーと Flickable はある意味似ているのですが、Flickable はやや複雑で独自の対応が必要になるため、後日リファクタリングをしようと思っています。FakeFlickable というマニュアルテストがあり、そこでは普通の2個の Item と DragHandler とアニメーションを用いて Flickable を再実装するということが行われています。

FakeFlickable: a componentized Flickable

「ポインターハンドラー」という名前も悩みの一つです。これだけを聞くと良さそうですが、既存の用語との衝突で混乱を招きかねません。たとえば、「ポインター」というとメモリの位置を表すための変数のようですし(ここでのポインターは別の意味ですね)、「ハンドラー」は JavaScript で処理を書くためのコールバック関数で良く使われている用語です。TapHandler { onActiveChanged: … } のようなコードを書いた場合、このハンドラーはハンドラーを持っている??のでしょうか。もちろん「コールバック」という言葉を使うことは可能ですが、今さら感がありますし、既存の QML の慣習をこれから変えるのも難しいことです。

他の理由としては、QtLocation においてケーススタディとして提起したい複雑なユースケースが存在します。それは読める範囲のコード量でマップのナビゲーション(とマップ上のコンテンツの)操作を記述できるかどうかということです。

ジェスチャー認識の方法と、将来的なポインターハンドラーのアプローチとの関係については別の記事で続きを書くことになると思います。今回「パッシブグラブ」というコンセプトについては説明できませんでした。また内部的にポインターハンドラーを使って作成したコンポーネントと、それを使う側で必要に応じて振る舞いを上書きできるような仕組みについても説明が必要でしょう。

方法に不満があったり不可能なことをしたかった方を中心に色々試してもらえるとありがたいです。C++ でのイベントフォワードや QObject のイベントフィルターと格闘することなく(複数人によるような)高度なタッチによる UI を作ろうとしていた方も同様です。まだまだ大きな変更ができる状態ではあるので、当面はみなさんからのフィードバックや珍しいユースケースをいただきながら、どうあるべきかを探っていこうと思っています。

現時点でのサンプルはほぼすべてが tests/manual/pointer/ 以下にあります。リリースパッケージにはこれらのマニュアルのテストコードは含まれていないため、試したい場合は Qt のソースパッケージをダウンロードするか git リポジトリ から取得する必要があります。今後のリリースでテストのいくつかを正式なサンプルにしたいと思っています。

まだ Tech Preview なので、実装は 5.10 をベースに改善していく予定です。興味のある方は新機能やバグの修正が行われる 5.10 のブランチ をウォッチしてみてくださいね。

このトピックの ウェブセミナー も開催します。是非ご覧ください


Blog Topics:

Comments