ControlとSkin

JavaFXネタです、すいません><

JavaFXのコントロールはボタンやテキストボックスなどのユーザ入出力のあるコンポーネントを提供する仕組みですが、Skinにより柔軟に見た目を変えることが出来ます。また、Behaviorはコントロールの振る舞いを定義するクラスになります。簡単に整理しましょう。

Skin

Skinはコントロールの中核となるクラスでコントロールの実態となるNodeを保持します。これはCustomNodeと同様の仕組みです。
SkinはControlとSkinの両方を参照し、保持するNodeのイベントを受け付けると、BehaviorまたはControlにハンドリングします。

Control

Controlはデベロッパが直接使うNodeで、初期化時にはコントロールの持つ属性(例:ボタンのラベル、画像のパス、クリック時の振る舞い)などを設定します。
ControlはSkinを保持しており、初期化時に設定された属性はSkinに渡され、Skinの見た目に反映されます。
また、ユーザがSkinに対してなんらかのアクションを起こした場合(例えばマウスクリック)、実行する処理を関数として設定する事になります。
SkinはControlを参照しているので、Skin内のNodeのonMouseClickなどのイベントに合わせて呼び出されるわけです。

Behavior

BehaviorはSkinを保持しており、Skinの振る舞いを定義するクラスです。ここで言う振る舞いとは、例えばマウスオーバー時のアニメーションやプログレスバーのアニメーションなどであり、クリック時の処理等ではありません。クリック時の処理等は、Controlにインスタンス毎に定義されるわけですから、Behaviorにはそのコントロールの持つ普遍的な振る舞いが定義されます。

尚、振る舞いがそれほど複雑でない場合は、Skinに記述すれば充分でしょう。

サンプル

間単にImageButtonという画像をボタンとして使うコントロールを作成してみましょう。

まずSkinですが、javafx.scene.control.Skinを継承しています。
画像2種、ホバー状態とプレス状態をメンバ変数に持っています。

また、ControlであるImageButton の参照を保持します。
this.controlのままでは使いにくいのでasでキャストしており、初期化のタイミングを解決するためにbindしています。

後はnodeに対してImageView を使い画像を表示させているだけです。
ImageView の各イベントとして、ホバー状態やプレス状態を監視しています。
bindがある為、状態が変化したときにイメージが変わる様子がわかりやすく記述できていることに注目してください。

package jp.sunflower.javafx.scene;

import javafx.scene.control.Skin;
import javafx.scene.effect.DropShadow;
import javafx.scene.image.Image;
import javafx.scene.image.ImageView;

/**
 * @author shuji
 */
/** ImageButtonの外観を定義する */
public class ImageButtonSkin extends Skin {
    def DROP_SHADOW:DropShadow = DropShadow {
    }
    public var image: Image;
    public var pressedImage: Image;
    public-read var hover:Boolean = false;
    public-read var pressd:Boolean = false;
    var button: ImageButton = bind this.control as ImageButton;
    init {
        node = ImageView {
            image: bind if (hover) then pressedImage else image
            effect: bind if (pressd) DROP_SHADOW else null;
            onMouseEntered: function(e) {
                this.hover = true;
            }
            onMouseExited: function(e) {
                this.hover = false;
            }
            onMousePressed: function(e) {
                this.pressd = true;
            }
            onMouseReleased: function(e) {
                this.pressd = false;
                this.button.fire();
            }
        }// ImageView
    }
    override function contains(localX: Number, localY: Number) : Boolean {
        return node.contains(localX, localY);
    }
    override function intersects(
                localX: Number, localY: Number,
                localWidth: Number, localHeight: Number) : Boolean {
        return node.intersects(localX, localY, localWidth, localHeight);
    }
}

もし、Skin内での状態遷移や振る舞いが複雑になる場合はBehaviorを作成します。

続けてControlとなるImageButton ですが、ButtonBaseを継承してしまいました。
あとは初期化時に必要なメンバ変数を定義し、skinをオーバーライドしているだけです。
ButtonではfireがSkin側からのイベントを受け付ける為、そこからメンバ変数(関数型)のactionを呼び出しています。

package jp.sunflower.javafx.scene;

import javafx.scene.control.ButtonBase;
import javafx.scene.image.Image;

/**
 * @author shuji
 */
public class ImageButton extends ButtonBase {
    /** The button's action, which is invoked whenever the user clicks on the button. */
    public var action: function():Void;
    public-init var image: Image;
    public-init var pressedImage: Image;
    override var skin = ImageButtonSkin {
        image: bind image
        pressedImage: bind pressedImage
    }
    init {
        if (not isInitialized(pressedImage)) pressedImage = this.image;
    }
    override function fire():Void {
        action();
    }
}

以上、簡単にSkinの仕組みでした。