torutkのブログ

ソフトウェア・エンジニアのブログ

JavaFXのSwingNodeがJavaFXのイベントをSwingのイベントに変換しているあたり

JavaFXの画面にSwingで作ったコンポーネントを乗せることが可能です。そのときは、JavaFXのSwingNodeを置いて、その上にSwingのコンポーネントを乗せます。

(参考)OracleJavaFXドキュメント「相互運用性 7. JavaFXアプリケーションへのSwingコンテンツの埋込み」
http://docs.oracle.com/javase/jp/8/javafx/interoperability-tutorial/embed-swing.htm

ここで気になるのはマウスやキー操作のイベントです。JavaFXの画面では、各コントロールJavaFXのイベント(例:javafx.scene.input.MouseEvent)を取ることができます。一方、Swingのコンポーネントjava.awtのイベント(例:java.awt.event.MouseEvent)を取ります。

SwingNodeのJavadoc(次のURL)を見ると、イベントについて次のように言及されています。
http://docs.oracle.com/javase/jp/8/javafx/api/javafx/embed/swing/SwingNode.html

すべての入力イベントとフォーカス・イベントは、開発者に対して透過的にJComponentインスタンスに転送されます。

JavaFXの入力イベント(クラス階層)は次のようになっています。

javafx.scene.input.InputEvent
    +-- ContextMenuEvent 
    +-- DragEvent 
    +-- GestureEvent
    |       +-- RotateEvent
    |       +-- ScrollEvent
    |       +-- SwipeEvent
    |       +-- ZoomEvent
    +-- InputMethodEvent
    +-- KeyEvent
    +-- MouseEvent
    +-- TouchEvent

SwingNodeの実装(JDKに添付されるjavafx-src.zipを解凍すると中にSwingNode.javaが入っています)を調べたところ、イベントについては次のように3種類の入力イベントについて処理をしています。

        setEventHandler(MouseEvent.ANY, new SwingMouseEventHandler());
        setEventHandler(KeyEvent.ANY, new SwingKeyEventHandler());
        setEventHandler(ScrollEvent.SCROLL, new SwingScrollEventHandler());

ここを見ると、JavaFXの入力イベントのうち、一部(MouseEvent、KeyEvent、ScrollEvent)を扱っています。その他の入力イベントはSwingNode上のSwingコンポーネントには届かないと思われます。

次に、上述の対象イベントをどう処理しているのかを調べてみます。
setEventHandlerの2番目の引数に入れている各Handlerクラスは、SwingNodeの内部クラスとして実装されています。

public class SwingNode extends Node {
    : (中略)
    private class SwingMouseEventHandler implements EventHandler<MouseEvent> {
        : (中略)
    }

    private class SwingScrollEventHandler implements EventHandler<ScrollEvent> {
        : (中略)
    }

    private class SwingKeyEventHandler implements EventHandler<KeyEvent> {
        : (中略)
    }
    : (中略)
}

SwingNode.SwingKeyEventHandler

  • 空キーは何もしない
  • 上下左右カーソルキー、TABキーはイベントを握りつぶし(フォーカスをはずさないようにする)
  • JavaFXのイベントタイプをSwing(AWT)のイベントIDに変換
JavaFX EventType Swing EventID
KEY_PRESSED KEY_PRESSED
KEY_RELEASED KEY_RELEASED
KEY_TYPED KEY_TYPED
それ以外 RuntimeExceptionをスロー
  • java.awt.event.KeyEventを生成し、EventQueueにpostEvent

SwingNode.SwingMouseEventHandler

  • JavaFXのイベントタイプをSwing(AWT)のイベントIDに変換
JavaFX EventType Swing EventID
MOUSE_MOVED MOUSE_MOVED
MOUSE_PRESSED MOUSE_PRESSED
MOUSE_RELEASED MOUSE_RELEASED
MOUSE_CLICKED MOUSE_CLICKED
MOUSE_ENTERED MOUUSE_ENTERED
MOUSE_EXITED MOUSE_EXITED
MOUSE_DRAGGED MOUSE_DRAGGED
DRAG_DETECTED 対応無し(無視)
それ以外 RuntimeExceptinをスロー
  • イベントを握りつぶし(consume)
  • MouseButtonの情報を管理(MOUSE_PRESSED, MOUSE_RELEASED, MOUSE_DRAGED, MOUSE_CLICKEDの関連で)
  • 属性情報を調整してjava.awt.event.MouseEventを生成し、EventQueueにpostEvent

SwingNode.SwingScrollEventHandler

  • 修飾キーの変換(Altキー、Ctrlキー、Metaキー、Shiftキー)
  • Shiftキーが押されておらず、かつdeltaYが0以外ならjava.awt.event.MouseWheelEventを生成
  • Shiftキーが押されており、かつdeltaYが0以外ならjava.awt.event.MouseWheelEventを生成
  • deltaXが0以外ならjava.awt.event.MouseWheelEventを生成

ScrollEventがくると、deltaXおよびdeltaYの値が0以外ならMouseWheelEventを発生させています。
もし、deltaXとdeltaYがともに0以外でShiftキーが押されていない状態では、2つのMouseWheelEventが発行されるように見えます。SwingNode.javaの該当コードを次に抜粋します。

        @Override
        public void handle(ScrollEvent event) {
            JLightweightFrame frame = lwFrame;
            if (frame == null) {
                return;
            }

            int swingModifiers = SwingEvents.fxScrollModsToMouseWheelMods(event);
            final boolean isShift = (swingModifiers & InputEvent.SHIFT_DOWN_MASK) != 0;

            // Vertical scroll.
            if (!isShift && event.getDeltaY() != 0.0) {
                sendMouseWheelEvent(frame, event.getX(), event.getY(),
                        swingModifiers, event.getDeltaY() / event.getMultiplierY());
            }
            // Horizontal scroll or shirt+vertical scroll.
            final double delta = isShift && event.getDeltaY() != 0.0
                                  ? event.getDeltaY() / event.getMultiplierY()
                                  : event.getDeltaX() / event.getMultiplierX();
            if (delta != 0.0) {
                swingModifiers |= InputEvent.SHIFT_DOWN_MASK;
                sendMouseWheelEvent(frame, event.getX(), event.getY(),
                        swingModifiers, delta);
            }
        }

タッチパネルを持つPCでのイベント

やっかいなのは、タッチパネル操作をおこなったときのMouseWheelEventの発生状況です。
Swingはもともとタッチパネルには対応していないのですが、このSwingNodeの実装では、タッチパネル上でジェスチャー(ピンチ、スワイプ、スクロール)を行った際に発生するScrollEventをことごとく拾ってMouseWheelEventに変換してSwingComponentに処理させています。

この結果、SwingNode上に貼ったSwingコンポーネントでMouseWheelEventを扱っていると、凄い事態になってしまいます。

SwingNode.SwingScrollEventHandlerの実装


この問題は、SwingNode特有の話ではなく、JavaFXコントロールにおいてMouseWheelを扱うものに共通の話となります。

この対応については、以下のWikiに記載しています。

JavaFXとアナログ時計 - ソフトウェアエンジニアリング - Torutk

しかし、上記Wikiページの対応も不完全です。

ScrollEventのJavadocに次の記載があります。

If scrolling inertia is active on the given platform, some SCROLL events with isInertia() returning true can come after SCROLL_FINISHED.

SCROLL_FINISHEDイベントが発生した後も、isInertia()がtrueを返すSCROLLイベントがタッチパネル操作で発生する可能性があります。
なので、isInertia()がtrueであればマウスホイール操作ではないと判断させるコードも追加する必要があります。

追記

@Yucchi_jp さんのブログで、マウスホイール操作によるSCROLLイベントか、タッチ操作によるSCROLLイベントかを判定する別な方法が記載されていました。
http://yucchi.jp/blog/?p=1750

if (sc.getTouchCount() == 0 && !sc.isInertia()) {

ScrollEventクラスのJavadocを見ると、getTouchCountメソッドに次の説明があります。

Gets number of touch points that caused this event. For non-touch source devices as mouse wheel and for inertia events after gesture finish it returns zero.

「マウスホイールなどの非タッチデバイスおよびジェスチャー終了後の慣性イベントについては、0を返却します。」とのことです。

SCROLL_STARTEDとSCROLL_FINISHEDで状態遷移を管理する必要がなく(状態遷移でずれる心配をしないで済む)、SCROLLイベントだけで判定できるので、この方法で判別するのがよさそうです。