CustomNodeの初期化はcreateメソッドで行う件

JavaFXではCustomNodeを継承したクラスを作成することで、幾つかのノードを組み合わせたようなノードを簡単に作ることができます。CustomNodeには抽象メソッドcreate:Nodeが用意されているので、このメソッドを実装(オーバーライド)すれば良いのですが、嵌りどころがあったのでメモです。

サンプル

ドキュメントにサンプルがありますので、まずは引用。

import javafx.scene.*;
import javafx.scene.paint.*;
import javafx.scene.shape.*;

class Bars extends CustomNode {
    override function create():Node {
        return Group {
            content: for(x in [0..4]) {
                Rectangle {
                    y: indexof x * 20
                    width: 100
                    height: 10
                    fill:Color.RED
                }
            }
        };
    }
}

Group は幾つかのノードをグループ化するNodeです。これをcreateメソッドの戻り値としているだけです。
使う場合は次のようになります。

Stage {
    title: "Application title"
    width: 400
    height: 400
    scene: Scene {
        content: [
            Bars {}
        ]
    }
}

Barの幅を指定できるようにする

ここでサンプルを拡張して、Barの幅を拡張できるようにしましょう。

class Bars extends CustomNode {
    public var width: Number;
    // 以下略
}

このようにクラスにメンバ変数を定義すれば、次のように指定できるようになります。

Bars {
    width: 200
}

どこで幅を指定するか?

次に行うべきはcreateメソッドのRectangle に反映する事です。
JavaFXではコンストラクタの代わりに初期化時に実行されるinitブロックがあるので次のように書いてみます。

class Bars extends CustomNode {
    public var width: Number;
    var bars: Group;
    init {
       bars = Group {
            content: for(x in [0..4]) {
                Rectangle {
                    y: indexof x * 20
                    width: width // 動的に指定
                    height: 10
                    fill:Color.RED
                }
            }
        };
    }
    override function create():Node {
        return bars;
    }
}

これで一見はよさそうに見えるのですが、使おうとするとエラーが発生します。どうもcreateがnullを返しているようです。

問題点

実はこのcreateメソッドですが、規定クラスのCustomNode のinitから呼び出されています。initは親クラスから順番に実行されるため、実行順序としては次の順序なワケです。

  1. CustomNode のinit
  2. Bars の create
  3. Bars の init

つまり、スーパークラスでまだ初期化されていないサブクラスのメソッドが呼び出されるという、正直気持ち悪い仕様になっています(JavaFX1.2.1時点)。したがって、initブロックで初期化をしてはいけないという解り難い仕様です。フォーラムのこのトピックでも話題になっていました。
※現時点ではドキュメントに明記されていないので注意してください。

解決方法

bindを使うとスマートに記述できます。

import javafx.scene.*;
import javafx.scene.paint.*;
import javafx.scene.shape.*;

class Bars extends CustomNode {
    public var width: Number;
    override function create():Node {
        return Group {
            content: for(x in [0..4]) {
                Rectangle {
                    y: indexof x * 20
                    width: bind width
                    height: 10
                    fill:Color.RED
                }
            }
        };
    }
}