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

JavaFX1.2がリリースされてからかなりたちますが、いかがお過ごしですか?
NetBeansの6.7ではプラグインが対応していなかったり、そもそも流行らないだろうという噂ですが、JavaOneで発表されたオーサリングツールが公開されれば、また違う展開もあるかもしれません。

さて、GUI面白いよGUIの自分なので久しぶりに1.2を動かしつつ色々と試してみました。今回はカスタムコンポーネントを作成し、いわゆるパタパタアニメーションを行う汎用コンポーネントを作成してみます。
JavaFX Scriptの文法を説明しながらまとめました。

はてなには貼り付けられないので、こちらからゆっくりお楽しみください。JavaWebStart版はこちらアプレット版はこちらです(jarはすべて共通)。
尚、アイコンはこちらからお借りしました。

目標

簡単に言えば、幾つかの画像を保持し、定期的に画像を切り替えるクラスです。指定する属性としては画像(複数)とアニメーションの期間(サイクル)の2つ、後は自動的に切り替わるような動きを実装します。

CustomNode

カスタムノードを作る場合には、javafx.scene.CustomNodeのサブクラスを作成します。

class ComponentNode extends CustomNode {
    // ...
    override function create():Node {
        return view;
    }
}

クラス宣言、継承などは特にJavaの文法と変わりません。また、CustomNode には抽象メソッドcreate()があるので、これをオーバライドします。overrideはアノテーションではなく修飾子(予約語)です。
また、functionキーワードを指定し、戻り値型は後ろに指定します。尚、一般的なfunctionでは、戻り値型を省略することも可能です。

カスタムノードではこの create()で実際にレンダリングするコンポーネントを返すように実装します。SwingのJTableでカスタムセルレンダラとか作ったことがあればなんとなく筋道は見えるでしょう。

フィールドの定義

class ComponentNode extends CustomNode {
    var images: Image[] = [];
    var span: Duration = 1s;
}

画像はImageクラスで指定し、シーケンス(配列というよりはList)で指定します。デフォルトは空のシーケンスとしました。同じようにspanを設定します。

JavaFX Scriptでは型は省略できますが、指定すればコンパイル時に型チェックを行ってくれます。省略してOKな箇所は省略し、宣言などは省略しない方が良いでしょう。

インスタンスの作成

var component = ComponentNode{
    images: [
        Image { url: "{__DIR__}images/y_mari.gif"},
        Image { url: "{__DIR__}images/y_mari2.gif"},
    ]
    span: 1.0s
};

JavaFX Scriptでは宣言的にインスタンスを定義していくため、new演算子インスタンスを作りません。
このように クラス名 {} というフォーマットで記述します。

一見しては無名クラスの宣言?と思いますが、内部ではインスタンスが生成されています。
また、各プロパティを「属性名:値」で記述していきます。
ここではimagesにImageインスタンスのシーケンスを、spanに期間(Duration)型の1sを指定しています。

JavaFXScriptでは引数のあるコンストラクタは作成できません。
初期パラメータはすべて属性の指定となります。

シーケンス

シーケンスはLL系の言語と同様に[]とカンマで区切った形で宣言します。
ここではImageクラスの2つのインスタンスを持つシーケンスが宣言され、imagesに設定されています。

尚、urlに指定している文字列の{__DIR__}は実行時のクラスファイルのある場所に置き換わります。
したがって、このクラスのあるパッケージのサブパッケージ(images)に画像を配置します。
尚、画像のURLはhttpではじまるインターネット上のアドレスでも当然OKです。

Duration

JavaFXでは基本型の1つに単位付の時間があります。1s、10msなど時間を簡潔に記述できます。

Timelineとinit

Timeline は名前の通りに時間軸を扱うクラスで、アニメーションの基本になります。

var component = ComponentNode{
    var currentImage: Image;
    var timeline: Timeline = Timeline {
        repeatCount: 999
    }
    init {
        var time_unit = span / images.size();
        timeline.keyFrames = for (image in images) {
            KeyFrame {
                time: time_unit * indexof image
                values: [ currentImage => image]
            };
        }
        insert KeyFrame {time: span} into timeline.keyFrames;
    }
}

init ブロックはクラスのインスタンスが生成される最後のタイミングで呼び出されるので、ここでコンストラクタ風な事を記述します。ここでは初期化時に与えられたスパンとイメージ画像から分割単位を計算(1s/2 = 500ms)し、timelineのkeyFramesに設定を行っています。

postiit

postinitブロックはインスタンスの生成後に実行されるブロックです。ここではTimelineの再生を行っています。

    postinit {
        timeline.play();
    }
for式

keyFramesにはKeyFrame型のシーケンスを与えなくてはなりません。ここではそのシーケンスを生成するためにfor式を使用しています。このfor式はRubyのcollectのようなもので、内部で作ったオブジェクトの配列を返す動きになります。

単純なパターンでは次のようにして1から100までの2乗値のシーケンスを作成できます。

var array:Number[] = for (x in [1:100]) {x * x};
indexof

indexof imageでは、シーケンスのループにおけるインデックスが取得できます。

KeyFrame

KeyFrameはタイムラインの中での特定のタイミングです。何時(time)タイムラインがどんな状態かのスナップショットと言えます。
ここでは次のようなKeyFrameが作成されるでしょう。

[
  KeyFrame {
    time: 0ms
    values: [ currentImage => images[0]]
  },
  KeyFrame {
    time: 500ms
    values: [ currentImage => images[1]]
  },

期間とイメージの枚数が変われば変化します。

valuesはそのKeyFrameでの値です。ここにはX座標などの値を入れることが出来ます。
今回は点と点で切り替わるので指定していませんが、0秒から10秒までの線形的な変化なども指定可能です(たとえば時間とともに平行移動したり)。

insert into

JavaFX Scriptではシーケンスにinsert文やdelete文が使用できます。よくわかりませんが使ってみました。

ImageView

画像を表示するノードはImageViewです。このimage属性に画像を指定します。

    var view: ImageView = ImageView {
        image: bind currentImage
    };

ここにImageのインスタンスをひもづければいいのですが、動的に変化するため変数としています。

ここでSwingなどを使っている場合は、currentImageの参照が変わった時、その旨をコンポーネントに通知して再描画しなくてはなりません。JavaFX Scriptではbindを使えば簡単に変数の変化を監視してくれます。SwingでもBeansBindingというJSRがありますが、それのJavaFX版です。

これで定期的にcurrentImageが代わり、ImageViewのimageも連動して変化するようになりました。

ソース全体

ソースコードの全体です。

/*
 * Main.fx
 *
 * Created on 2009/07/30, 22:39:11
 */

package jp.sunflower.fx.yukkuri;


import javafx.scene.CustomNode;

import javafx.scene.Node;
import javafx.scene.image.ImageView;

import javafx.scene.image.Image;

import javafx.scene.Scene;
import javafx.stage.Stage;
import javafx.animation.Timeline;

import javafx.animation.KeyFrame;

/**
 * @author shuji
 */
class ComponentNode extends CustomNode {
    var images: Image[] = [];
    var span: Duration = 1s;
    var currentImage: Image;
    var view: ImageView = ImageView {
        image: bind currentImage
    };
    var timeline: Timeline = Timeline {
        repeatCount: 999
    }
    init { // 画像の枚数とループ感覚からKeyFrameを生成
        var time_unit = span / images.size();
        timeline.keyFrames = for (image in images) { // 戻り値はKeyFrame[]
            print(time_unit * indexof image);
            print(image);
            KeyFrame {
                time: time_unit * indexof image
                values: [ currentImage => image]
            };
        }
        // 最終KeyFrameを追加
        insert KeyFrame {time: span} into timeline.keyFrames;
    }
    postinit { // インスタンス生成後に実行される。timelineを再生
        timeline.play();
    }
    override function create():Node { // オーバーライド
        return view
    }
}
// カスタムコンポーネントの作成
var component = ComponentNode{
    images: [ // 画像を2枚
        Image { url: "{__DIR__}images/y_mari.gif"},
        Image { url: "{__DIR__}images/y_mari2.gif"},
    ]
    span: 1.0s // 1秒でループ
};

Stage {
    title: "ゆっくりしていってね!"
    width: 250
    height: 80
    scene: Scene {
        content: [
                component
        ]
    }
}