読者です 読者をやめる 読者になる 読者になる

日曜プログラミング

休日趣味でやってるプログラミング関連記事をダラダラと

JavaFX イベントハンドリングおさらい(1) - イベント処理の流れについて

前ふり

以前 JavaFX イベントハンドリングに関しては fx:script 使えば Clojure でも動くよ、と言うこんな記事を書いた。
JavaFX + Clojure でのイベントハンドリング - 日曜プログラミング

また、多分邪道だが FXML の UI 部品に fx:id ではなく id プロパティを設定しておけばNode#lookup メソッドで参照できる事も分かった。
Clojure から無理やり FXML UI を参照する - 日曜プログラミング

試してはいないがこうやって取得したインスタンスで on なんちゃら系のメソッド呼んでイベントハンドラ登録すれば fx:script とはまた別の方法でイベント登録する事も可能だろう。

よっしゃこれでバリバリ書けるんじゃないか~?と思っていたが、ふと手が止まった。

  • イベントが親コントロールから子コントロールへ伝播するのは何となく分かる
  • でも子が更に分岐してたらどうなるの?
  • イベント伝播はデフォで末端の子まで行くのか?
  • そもそも親から子にイベント伝播すると言うイメージって正しいの?

とまあ一言で言うとイベント処理そのものがよく分かってない事に気づいたのでまた基本に立ち返ろうと思う。ただ日本語だとそういう疑問に答えてくれるサイトを見つけられなかったので仕方なくまた英文を読む事にする。

Oracle JavaFX 公式にイベント処理関連のチュートリアルが集約されてる。
http://docs.oracle.com/javafx/2/events/jfxpub-events.htm

トピックスタイトルをこちらにも再掲する。

  1. Processing Events
  2. Working with Convenience Methods
  3. Working with Event Filters
  4. Working with Event Handlers
  5. Working with Events from Touch-Enabled Devices
  6. Working with Touch Events

内容次第だけどトピックス見る限りでは多分ちゃんと読むのは 1,3,4 に限られると思う。2 のメソッドの使い方については別に API doc に毛が生えたようなもんだろうし、5,6 のタッチ操作関連は流行りではあるかもしれないが今のところ自分が必要性を感じてないので多分読まないと思う。

今回読むのは最初の Processing Events について。一応 datomic チュートリアルでやっていた時のように適当翻訳してトピックスタイトルは横に併記したいと思う。

また、自分のブログ記事サブタイトルは何となくトピック毎にテーマとなるものを単純訳でなくより日本語的にしっくり来るものになるべくしていくつもり。

1 イベント処理 [1 Processing Events]

このトピックでは JavaFX のイベントハンドリングについてお話します。具体的にはイベントタイプ、イベントターゲット、下りイベント伝播、上りイベント伝播、そして基礎となるイベント処理アーキテクチャについて学習します。

イベントはユーザアクションをアプリへ通知したり、アプリがイベントに対して何らかの反応を可能にする目的で使用されます。JavaFX はイベントキャプチャやターゲットへのイベント伝播、そして必要ならイベントをハンドルできるようにする仕組みを提供します。

イベント [Events]

イベントとは、マウスが動いたり、キーが押されたりなどアプリケーションへ何かしらの関心事を発生させる事を表現するものです。JavaFX においてイベントとは javafx.event.Event またはそのサブクラスのインスタンスです。DragEvent, KeyEvent, MouseEvent, ScrollEvent など様々なイベントを提供しています。Event クラスを継承する事で独自のイベントを定義することも可能です。

表 1-1 にイベントが持つ情報を記載します。

表 1-1 イベントプロパティ

プロパティ 説明
Event type イベント発生タイプ。
Source イベントディスパッチチェインに基づくイベント発生元。チェーンを通過するとイベント Source は変化する。
Target イベントディスパッチチェインの中で、アクションが発生した Node とチェインの最後の Node を示す。イベントキャプチャリングフェーズ中にイベントフィルタによりイベントが消費された場合、Target はイベントを受信しないが、Target は変化しない。

Event サブクラスにはイベント特有の情報を追加したものが提供されています。例えば、MouseEvent クラスにはどのマウスボタンが押されたのか、何回ボタンが押されたか、マウスポジションなどが追加されています。

イベントタイプ [Event Types]

イベントタイプは EventType クラスのインスタンスです。イベントタイプは 1 つのイベントクラスを分類したものです。例えば KeyEvent クラスには次のイベントが含まれます。

  • KEY_PRESSED
  • KEY_RELEASED
  • KEY_TYPED

イベントタイプは階層構造になっており、全てのイベントタイプには名前と上位のタイプを持ちます。例えば、キー押下イベントの名前は KEY_PRESSED で、その上位タイプは KeyEvent.ANY です。トップレベルの上位タイプは null です。図 1-1 階層構造の一部を示したものです。

図 1-1 イベントタイプ階層
http://docs.oracle.com/javafx/2/events/img/event_type_hierarchy.gif

図 1-1 イベントタイプ階層についての補足(未翻訳)

イベントタイプ階層のトップは Event.ROOT でこれは Event.ANY とも同じです。サブタイプで ANY と名付けられているものはその更に下にあるいずれのイベントタイプでもある事を意味しています。例えばキーイベントで共通の反応を返すようにしたい場合はイベントフィルタかイベントハンドラのイベントタイプに KeyEvent.ANY を指定します。キーが離された時のみ何かしらの処理を行いたい時は KeyEvent.KEY_RELEASED を使用します。

イベントターゲット [Event Targets]

イベントターゲットは EventTarget インターフェース実装クラスのインスタンスであればどれでもターゲットとなります。buildEventDispatchChain の実装はイベントがターゲットに必ず届くイベントディスパッチチェインを生成します。

Window, Scene と Node クラスには EventTarget インターフェースが実装されており、それらのサブクラスにもインターフェースは継承されています。この事から殆どの UI 部品には既にディスパッチチェインは定義済となるため、イベントディスパッチチェインの生成を気にすることなくイベント処理に注力する事が可能です。

Window, Scene, Node のサブクラスとしてあなた自身のカスタム UI コントロールを作成した場合は、イベントターゲットとしても継承されています。そうでない場合は、EventTarget インターフェースを実装しなければなりません。例えば、MenuBar は継承したが MenuItem は継承していないならば、MenuItem の方はイベントを受け取るのを可能にするために EventTarget インターフェースを実装する必要が出てきます。*1

イベント伝達プロセス [Event Delivery Process]

イベント伝達プロセスは大まかには次のステップを踏みます。

  1. ターゲット選択
  2. イベント伝達ルート構築
  3. 下りイベント伝達
  4. 上りイベント伝達
ターゲット選択 [Target Selection]

アクション発生時、システムは以下のルールでターゲットを決定します。

  • キーイベントの場合、ターゲットは focus を持つノードとなる
  • マウスイベントの場合、ターゲットはカーソルがあるノードとなる。複合マウスイベントの場合、タッチポイントはカーソル位置が考慮される。
  • タッチスクリーンによる継続的なジェスチャイベントの場合、ターゲットはジェスチャ開始位置から全てのタッチされた位置の中心にあるノードとなる。トラックパッドなどタッチスクリーン以外の間接的なジェスチャイベントの場合のターゲットはカーソル位置にあるノードとなる。
  • タッチスクリーンのスワイプイベントの場合、ターゲットは全ての指が通過した位置の中心のノードとなる。間接的スワイプの場合は前述同様カーソル位置にあるノードとなる。
  • タッチイベントの場合デフォルトは最初にタッチされた位置のノードとなる。イベントフィルタ・イベントハンドラで ungrab(), grab(), grab(node) を使用して異なるターゲットとなるようにする事も可能。

カーソルやタッチで 1 つ以上のノードがターゲットとなった場合は、一番上にあるノードがターゲットになります。例えばユーザが図 1-2 にある△をクリックやタッチした場合、横の○やその後ろにある長方形でなくまさに△がターゲットとなります。

図 1-2 イベントターゲット説明用サンプル画面
http://docs.oracle.com/javafx/2/events/img/node_image.png
図 1-2 について補足(未翻訳)

マウスボタンが押されターゲットが選択された時、ボタンが離されるまで後続のイベントも同じターゲットへ伝達されます。*2ジェスチャーイベントも同様に、ジェスチャ操作が完了するまでジェスチャーの開始時点で決定したターゲットへイベントが伝達され続けます。デフォルトのタッチイベントは ungrab(), grab(), grab(node) メソッドが呼ばれるまでタッチした所の最初の node がターゲットとなります。

イベント伝達ルート構築(Route Construction)

初期イベントルートは、選択されたイベントターゲット node の buildEventDispatchChain() メソッド実装により生成されたディスパッチチェインにより決定されます。

例えばユーザが図 1-2 にある△をクリックした場合、初期ルートは図 1-3 のグレーで示したルートになります。これはシーングラフのノードがイベントターゲットとして選択された時、Node クラスの buildEventDispatchChain() メソッドデフォルト実装により、stage から選択された node までを初期イベントルートとして設定するためです。

図 1-3 イベントディスパッチチェイン
http://docs.oracle.com/javafx/2/events/img/dispatch_chain.png
図 1-3 イベントディスパッチチェインについて補足(未翻訳)

この初期ルートはイベント処理時にイベントハンドラやイベントフィルタで修正できます。また、どこかのイベントのイベントハンドラやイベントフィルタでイベントを消費すれば、その後のイベントルートはなくなります。

下りイベント伝達フェーズ [Event Capturing Phase]

下りイベント伝達フェーズでは、イベントは前述で構築されたルートで root node からターゲットとなった node までディスパッチされていきます。図 1-3 で言えば、イベントは Stage node から Triangle node までイベントが続く事になります。

そのルートの途中でイベントフィルタが登録されていた場合はイベントフィルタが呼ばれます。フィルタ処理が完了すると、イベントは引き続きチェインの残りの node へディスパッチされます。イベントフィルタでイベント消費する処理がなければ、最終的にターゲットとなった node はイベントを受け取りイベント処理を行います。

上りイベント伝達フェーズ [Event Bubbling Phase]

イベントターゲットに届き全てのイベントフィルタが処理されると、イベントはターゲットから root node まで戻っていきます。図 1-3 で言うと上りイベント伝達フェーズでは Triangle node から始まり Stage node までさかのぼっていきます。

このフェーズで該当するイベントのイベントハンドラが登録されていた場合、そのハンドラが呼ばれます。ハンドラ処理が完了すると、イベントはチェーンに従って上位の node へ通過していきます。イベント通過中にイベント消費処理がなければ最終的には root node がイベントを受け取り処理が完了した事になります。

イベントハンドリング [Event Handling]

イベントハンドリングは前述したようにイベントフィルタとイベントハンドラによって行い、実装的には EventHandler を実装する事になります。
言い換えると、イベント発生時に何かするようなアプリを作成したい場合イベントフィルタまたはイベントハンドラを登録します。

イベントハンドラとイベントフィルタの主な違いは実行されるタイミングです。

イベントフィルタ [Event Filters]

前述した通りイベントフィルタは下りイベント伝達フェーズで実行されます。

親ノードにイベントフィルタを実装する事で複数の子ノードに共通のイベント処理を実行することも、一部の子ノードではイベントを消費する事でイベントを受け取る事を防ぐ事もできます。Filters that are registered for the type of event that occurred are executed as the event passes through the node that registered the filter.*3

node には 2 つ以上のフィルタを登録することができます。フィルタの実行順はイベントタイプの階層によって決定されます。例えば MouseEvent.MOUSE_PRESSED イベントフィルタは InputEvent.ANY イベントより前に呼ばれます。同一階層で 2 つフィルタが登録されている場合の実行順は決まっていません。
*4イベントタイプとしては下位の層から実行されます。

イベントハンドラ [Event Handlers]

イベントハンドラは上りイベント伝達フェーズで実行されます。子 node でイベントを消費しなければ、親 node 向けのイベントハンドラを子 node のイベントハンドラ処理後に複数子 node へ向けて実行する事も可能です。

node には 2 つ以上のハンドラを登録する事ができます。ハンドラの実行順に関しての動作はイベントフィルタの時と同様です。但し Working with Convenience Methods に記載されているメソッドで登録したハンドラは例外的に最後に実行されます。

イベントの消費 [Consuming of an Event]

イベントは任意のタイミングのイベントフィルタ・ハンドラにおいて comsume() メソッドを呼ぶ事で消費することが可能です。このメソッドはイベント処理が完了した事を通知し、イベントディスパッチチェインによるイベントルートも停止します。

イベントフィルタでのイベント消費処理は以降の子ノードのイベント伝達を抑制します。逆にイベントハンドラでのイベント消費処理は上位親ノードへのイベント伝達を抑制します。但し、ノードに複数のフィルタやハンドラが登録されている場合は実行されます。

例えば図 1-3 で、仮に Pane node に KeyEvent.KEY_PRESSED と InputEvent.ANY にイベントフィルタが登録されているとします。さらに KeyEvent.KEY_PRESSED イベントフィルタに comsume() が入っていた場合、Triangle node はイベントは受け取りません。

注意として、JavaFX UI コントロールのデフォルトハンドラは大部分がインプットイベントを消費します。

訳してみて

Event Capturing Phase と Event Bubbling Phase は下りイベント伝達、上りイベント伝達と超意訳をしてるが、イベントハンドラとフィルタとセットで考えるとこの言い方が近いのかなあと。何にせよ個人的にはそれなりにしっくり来たので訳してみて良かったと思う。

対比的な表にするとこんな感じかな。

フェーズ イベント伝達方向 ハンドリング
Capturing root から target Event Filter
Bubbling target から root Event Handler

*1:訳注: 標準にも MenuItem あるけどそれは EventTarget 実装されているので多分流れとしてカスタム UI として作った場合の話だと思う。

*2:階層の下の事を言ってる?単に時間軸の話?

*3:ここ何が言いたいのか良く分からないので原文まま。

*4:と言う事は node が持つイベントタイプにつき 1 つのイベントを持てる、と言った方が日本語としては分かり易いか?さすがに同一イベントタイプに複数のフィルタは持てない&持つ意味もないだろうし。ソースで複数登録したとしても多分処理として最後に登録したのだけ有効になるんじゃないかと予想。