Drag and Drop on JavaFX Part 6 (FileDroppable)

前々回の 『 Drag and Drop on JavaFX Part 5 (FileDroppable) 』の続き...
今回は 前々回 紹介した FileDroppable.fx のちょっとした解説 + α です。

FileDroppable.fx のポイントは 以下の 5 つです。
  1. ファイルをドロップできるのは Component にだけ...
  2. Node から取得できる Component は SwingScenePanel だけ... (たぶん...)
  3. SwingScenePanel は コアAPI では取得できない...
  4. Component に 設定できる DropTarget は 一つだけ...
  5. DropTarget に 設定できる DropTargetListener は一つだけ...

これらのポイントさえ把握しておけば...
前々回のサンプルに ブラウザ等から画像をドロップする機能を追加するのもとっても簡単です。

以下のサンプルは...

  • ローカルディスク上の画像ファイル
  • ブラウザ、iPhoto (他のアプリも可?) のイメージ (今回追加
をドラッグして出力できます。

※ ローカルファイルにアクセスするので、セキュリティの警告がでます。

では、以下 簡単な解説です。
1. ファイルをドロップできるのは Component にだけ...

残念ながら、JavaFX 自体には ファイルの Drag and Drop の機能は含まれていませんが...
以下の Java で提供されているクラスを 使えば、JavaFX でも簡単に ファイルの Drag and Drop を実現できる のです。
Drag and Drop の詳細については こちら を 参照してみてください。

  1. java.awt.Component
  2. java.awt.dnd.DropTarget
  3. java.awt.dnd.DropTargetListener

ところが ここで一つ問題が...
ファイルは Component にしかドロップできないのです。
JavaFX の Node は Component ではないので、 そのままではファイルをドロップすることが できません。
javafx.ext.swing パッケージのクラスであれば、Component の サブクラスの JComponent を取得するための API があるので良いのですが...
やはり、どの Node でも ファイルをドロップしたくなるのが、 人情というものでしょう。

なんとか Node から Component を取得できないだろうか?

2. Node から取得できる Component は SwingScenePanel だけ...

色々調べていたら、どんな Node にも関連づけられている Component が一つだけありました。
それは SwingScenePanel というクラスです。

SwingScenePanel は 以下のような階層を持つクラスです。

java.awt.Component
    +- java.awt.Container
        +- javax.swing.JComponent
            +- com.sun.scenario.scenegraph.JSGPanel
                +- com.sun.javafx.tk.swing.SwingScene$SwingScenePanel

SwingScenePanel は JComponent のサブクラスでもあるので ファイルのドロップ以外の機能も 利用できそうですね。

では、SwingScenePanel はどのように取得すれば良いのでしょうか?

3. SwingScenePanel は コアAPI では取得できない...

上記のように SwingScenePanel は コアAPI の クラスではありません。
もちろん、Node から取得するための API も コアAPI では提供されていないのでが...
以下のようにリフレクションで 内部? API を利用すれば、 SwingScenePanel を取得できるのです。

function getPanel(node: Node) {

    var context = FXLocal.getContext();

    // SGNode
    var _nodeClass = context.findClass("javafx.scene.Node");
    var _getPGNode = _nodeClass.getFunction("impl_getPGNode");
    var _sgNode = _getPGNode.invoke(context.mirrorOf(node)) as FXLocal.ObjectValue;

    // SwingScenePanel
    var _sgNodeClass = context.findClass("com.sun.scenario.scenegraph.SGNode");
    var _getPanel = _sgNodeClass.getFunction("getPanel");
    return (_getPanel.invoke(_sgNode) as FXLocal.ObjectValue).asObject();
}

上記のようにリフレクションを使わないと、 コンパイルエラーになってしまうのです。
もちろん、コンパイルさえ通れば問題なく、 実行できるのですが...
どうやら、内部 API のクラスは 実行時にしか 参照できないようです。

Component のサブクラスの SwingScenePanel が取得できたら、
後は JavaAPI を利用 して簡単に実装できるはずなのですが...
また、ここでも問題が...

4. Component に 設定できる DropTarget は 一つだけ...

DropTarget は一つの Component に一つしか 設定できないのです。
実は Node から取得できる SwingScenePanel は 名称からも推測できる通り Scene に関 するクラスのようで...
同じ Scene に属する Node から 取得できる SwingScenePanel は全て同一の インスタンスなのです。
つまり、SwingScenePanel に設定した DropTarget 一つで 該当する全ての Node の処理を実装しなくてはならないという訳です。

5. DropTarget に 設定できる DropTargetListener は一つだけ...

DropTarget には addDropTargetListener(...) で DropTargetListener の実装を 追加することができるので...
Node 一つに対し DropTargetListener を一つずつ追加していこうと思ったのですが...
なんと addDropTargetListener(...) なのに 複数の DropTargetListener を追加すると...
TooManyListenersException がスローされるのです。なんで...

Javadoc によると そういう仕様ということ なので、仕方なく自分で実装することに...
興味のある方は、『 Drag and Drop on JavaFX Part 5 (FileDroppable) 』 のソースを参照してみてください。