JavaFXでカスタムノードを作る

JavaFXで提供されているコンポーネントは、JavaFX 1.2の段階ではまったく足りていません。
ある程度のコンポーネントに関しては今後のバージョンアップで作られていったり、サードパーティ製のライブラリで補間できるかと思いますが、それでもない場合は自分で作ることになるでしょう。

一方で自作コンポーネントの作成に関して、JavaFXは非常に作りやすくなっています。
幾つかのポイントを押さえる必要はありますが、基本的には既存コンポーネントの組み合わせで大抵のコンポーネントは作成可能です。
今回は自作コンポーネントの作り方を解説します。

CustomNode

自作コンポーネント(ノード)を作るためには、基本的にはCustomNodeを継承したクラスを作成します。
CustomNodeは抽象クラスになっており、createメソッドをオーバーライドする必要があります。

public class MyNode extends CustomNode {
    override function create(): Node {
        // implements here
        null;
    }
}

後はこのcreateメソッドの返値としてオリジナルのノードを返せば完了です。
典型的なパターンとしては複数のシェイプやコントロールをGrop化して1つのノードとして扱うパターンでしょう。
次の例はラベルとテキスト入力を複合させたカスタムノードです。

public class LabelAndInput extends CustomNode {
    public var label: String;
    override function create(): Node {
        Flow {
            content: [
                Label { text: bind label }
                InputText {}
            ]
        }
    }
}

さて、どうすればいいかは非常に簡単だったわけですが、複雑なコンポーネントを作成する場合、押さえておきたいポイントがあります。

CustomNodeもNodeであること

第1にCustomNodeもNodeであり、createで返すのもNodeであることです。
つまり、CustomNodeはcreateで返したNodeをラップするNodeであると言うことです。
したがって、Nodeの持つ機能はすべて持ちます。
デフォルトで「何か」を仕込みたい場合は、実装クラスにオーバーライド可能です。

public class MyNode extends CustomNode {
    // 不透明度を0.5にする
    override var opacity = 0.5;
    override function create(): Node {}
}

また、利用者側はonMouseClickイベントやエフェクトなどをインスタンス生成時に指定することもできるわけです。

MyNode {
    opacity: 1.0
    onMouseClick: function(e) {}
}

ここでポイントですが、CustomNodeのプロパティをオーバーライドする場合、それがデフォルトの振る舞いであるならば正しいですが、カスタムノードの機能としてはいけません。
なぜならば、インスタンス生成時に上書きされてしまうからです。
特にマウスイベントなどは上書きされてしまうと、期待する振る舞いを内部で行えなくなる可能性が高いので注意します。

では、カスタムノードの機能として実装するにはどうすれば良いでしょうか?
答えは簡単で、createで返すノードに対して設定すれば良いのです。

public class LabelAndInput extends CustomNode {
    public var label: String;
    override function create(): Node {
        Flow {
            content: [
                Label { text: bind label }
                InputText {}
            ]
            onMouseClick: function(e) {}
        }
    }
}

この場合のonMouseClickイベントはFlowに設定されているため、カスタムノードと衝突するコトはないでしょう。

また、イベントなどを内部のノードに渡したい場合は、関数型変数を定義すれば簡単に実現できます。

public class MyButton extends CustomNode {
    public-init var action: function(e): Void;
    override function create(): Node {
        Button {
            action: action
        }
    }
}

これでかなり複雑なコンポーネントでも簡単にCustomNodeで作成できることが解ったかと思います。
大雑把に言えば、ある程度まとまったシーングラフの一部をリファクタリングのように抽出し、CustomNodeして再利用するのが非常に簡単なのです。

createのはまりどころ

第2のポイントはcreateの実行タイミングです。これは以前にも書いたと思いますが、重要なので再度書きます。
まず、JavaFXインスタンス生成の手順は以下のようになります。

  1. パラメータの初期化
  2. 外部から指定されたパラメータの設定
  3. スーパークラスのinit
  4. サブクラスのinit
  5. インスタンス生成完了
  6. スーパークラスのpostinit
  7. サブクラスのpostinit

コンストラクタがないため、それに相当するのがinitブロックですが、初期化の順序はJavaと大きく変わりません。
ここで注意したいのは、createメソッドはCustomNodeのinitブロックから呼び出される点です。
したがって、サブクラスのinitの前にオーバーライドされたcreateが呼び出されます*1

つまり、次のコードは典型的なハマりパターンです。

public class MyNode extends CustomNode {
    // カスタムノード
    var node: Node;
    init {
        node = // 初期化 
    }
    override function create(): Node {
        node;
    }
}

createが呼ばれる段階ではnodeはnullなので何も表示されません。

インスタンスの参照を保持したい場合でも、Nodeの初期化に関してはcreateで行う必要があります。
尚、JavaFXの代入式は代入した結果を返す性質があるため、次のように記述できます。

public class MyNode extends CustomNode {
    // カスタムノード
    var node: Node;
    override function create(): Node {
        node = Group {}// 初期化
    }
}

以上、カスタムノードの作り方と2つのポイントでした。

*1:この設計自体は問題があるとも言えますが、仕方ないのかも・・・