ページ

2013年3月31日日曜日

JavaFX - FXMLファイルでViewを定義したときのControllerの設定(2)

前回は、FXMLファイルでViewを定義したときのControllerの設定についてみていきました。今回は、FXMLファイルを読み込んでControllerを取得するユーティリティクラスを作成してみたいと思います。

名称としては、FXControllerとして作成していきます。

まずは、FXMLファイルのfx:controller属性で、Controllerを指定した場合を考えていきます。この場合、FXMLファイルのURLを指定して、以下のようにControllerが取得できるようにします。

SimpleViewController controller = FXController.<SimpleViewController>from(location)
    .load();

ResourceBundleを指定したい場合は、以下のように指定できるようにします。

SimpleViewController controller = FXController.<SimpleViewController>from(location)
    .with(resources)
    .load();

また、ResouceBundleの代わりに、以下のようにリソースバンドルの基底名を指定できるようにします。

SimpleViewController controller = FXController.<SimpleViewController>from(location)
    .withResourceOf(resourceBaseName)
    .load();>

fx:controller属性で指定しているControllerのクラスの指定は、以下のように指定できるようにもします。

SimpleViewController controller = FXController.of(SimpleViewController.class)
    .from(location)
    .load();

次に、FXMLファイルのfx:controller属性を指定しなかった場合を考えていきます。この場合、ControllerのインスタンスとURLを指定して、以下のようにFXMLファイルを読み込めるようにします。

SimpleViewController controller = FXController.of(new SimpleViewController())
    .from(loacation)
    .load();

また、以下のように、ResouceBundle、あるいは、リソースバンドルの基底名を指定できるようにします。

SimpleViewController controller = FXController.of(new SimpleViewController())
    .from(loacation)
    .with(resources)
    .load();
SimpleViewController controller = FXController.of(new SimpleViewController())
    .from(loacation)
    .withResourceOf(resourceBaseName)
    .load();

FXMLファイル名の指定については、Controllerのクラスの定義に、FXMLLocationアノテーションで指定できるようにもします。指定する場合は、ClassのgetResourceメソッドに指定する場合と同じ名称を指定するようにします。

@FXMLLocation("SimpleView.fxml")
public class SimpleViewController {
}

この場合、以下のようにして読み込むようにします。

SimpleViewController controller = FXController.of(new SimpleViewController())
    .fromDefaultLocation()
    .load();

また、FXMLLocationアノテーションで指定していない場合は、次の命名規則で、FXMLファイルを検索するようにします。

Controllerと同じパッケージで、Controllerの名称から末尾の「Controller」を除いた名称に「.fxml」をつけたファイル

ofメソッドでControllerのインスタンスを指定したときに、そのインスタンスが、javafx.scene.Sceneクラス、あるいは、javafx.scene.Nodeクラスのサブクラスの場合は、javafx.fxml.FXMLLoaderクラスのsetRootメソッドにControllerのインスタンスを設定してから読み込むようにして、FXMLファイルをfx:root要素で定義した場合にも対応できるようにします。

public class SimpleControl extends StackPane {
    public SimpleControl() {
        initializeComponent();
    }

    protected void initializeComponent() {
        FXController.of(this).fromDefaultLocation.load();
    }
}

あと、個人的には、依存性の注入にgoogle-guiceを使いたいので、以下のように、com.google.inject.Injectorの指定もできるようにしておきます。

SimpleViewController controller = FXController.of(SimpleViewController.class)
    .using(injector)
    .from(location)
    .load();

この場合も、以下のように、アノテーション、あるいは、命名規則で、FXMLファイルを検索できるようにします。

SimpleViewController controller = FXController.of(SimpleViewController.class)
    .using(injector)
    .fromDefaultLocation()
    .load();

ResourceBundle、あるいは、リソースバンドルの基底名についても、指定できるようにしておきます。

SimpleViewController controller = FXController.of(SimpleViewController.class)
    .using(injector)
    .fromDefaultLocation()
    .with(resources)
    .load();
SimpleViewController controller = FXController.of(SimpleViewController.class)
    .using(injector)
    .fromDefaultLocation()
    .withResourceOf(resourceBaseName)
    .load();

さらに、ofメソッドに、Controllerのインスタンスを指定できるようにもしておきます。

SimpleViewController controller = FXController.of(new SimpleViewController())
    .using(injector)
    .fromDefaultLocation()
    .load();

また、com.google.inject.Injectorについては、usingメソッドで毎回指定せず、以下のようにstaticメソッドで指定し、常にそこで指定したcom.google.inject.Injectorを用いてインスタンスを取得できるようにもします。

FXController.setDefaultInjector(injector);

SimpleViewController controller = FXController.of(SimpleViewController.class)
    .fromDefaultLocation()
    .load();

以上のようなことができるユーティリティクラスを作成したいと思います。

JavaFX - FXMLファイルでViewを定義したときのControllerの設定(1)

JavaFXで、ViewをFXMLファイルで定義したときの、Controllerの設定についてみていきます。

まず、以下のようなControllerを考えます。

public class SimpleViewController {
    @FXML
    private Parent root;

    public Parent getView() {
        return root;
    }
}

とりあえず、ルートのコントロールのみを取得するだけにしています。

このControllerをFXMLファイルで定義したViewに設定するには、FXMLファイルにfx:controller属性で指定します。

<StackPane xmlns:fx="http://javafx.com/fxml"
           fx:controller="SimpleViewController"
           fx:id="root">
    <Label text="Hello, world!"/>
</StackPane>

あとは、javafx.fxml.FXMLLoaderでFXMLファイルをロードした後、getControllerメソッドからfx:controller属性で指定したControllerクラスのインスタンスを取得します。

FXMLLoader loader = new FXMLLoader(simpleViewLocation);
loader.load();
SimpleViewController controller = (SimpleViewController)loader.getController();

上記では、FXMLファイルにControllerクラスを直接指定していますので、ViewがControllerに依存している形になっています。そこで、ViewがControllerに依存しないように、FXMLファイルに直接指定せずにControllerを設定してみたいと思います。

まず、FXMLファイルには次のようにfx:controller属性を指定しないようにします。

<StackPane xmlns:fx="http://javafx.com/fxml"
           fx:id="root">
    <Label text="Hello, world!"/>
</StackPane>

あとは、javafx.fxml.FXMLLoaderでFXMLファイルをロードする前に、setControllerメソッドでControllerクラスのインスタンスを設定します。

SimpleViewController controller = new SimpleViewController();
FXMLLoader loader = new FXMLLoader(simpleViewLocation);
loader.setController(controller);
loader.load();

この場合、FXMLファイルに直接fx:controller属性を指定しませんので、ViewがControllerに依存せずに、Controllerを設定できます。また、Controllerのインスタンスを指定しますので、他の依存するオブジェクトをControllerに注入したい場合にも利用できます。

次に、ControllerクラスをFXMLファイルで定義しているルート要素のコントロールとして定義する場合についてみていきます。

まず、FXMLファイルでViewを定義します。このとき、ルート要素はfx:rootとして定義します。

<fx:root xmlns:fx="http://javafx.com/fxml" type="StackPane">
    <Label text="Hello, world!"/>
</fx:root>

type属性に、ルートとなるコントロールの型を指定します。

次に、type属性で指定した型を継承して、Controllerクラスを定義します。

public class SimpleControl extends StackPane {
    private static final URL VIEW_LOCATION = SimpleControl.class.getResource("SimpleControl.fxml");
    
    public SimpleControl() {
        initializeComponent();
    }

    protected void initializeComponent() {
        FXMLLoader loader = new FXMLLoader(VIEW_LOCATION);
        loader.setController(this);
        loader.setRoot(this);
        try {
            loader.load();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }
}

javafx.fxml.FXMLLoaderでFXMLファイルをロードする前に、setControllerメソッドとsetRootメソッドにControllerクラスのインスタンスを設定します。

上記で定義したControllerをシーンに設定してみます。

Scene scene = SceneBuilder.create()
    .root(new SimpleControl())
    .build();

上記のように設定した場合は、.NETでいうコードビハインドのような感じになります。

FXMLファイルでViewを定義した場合、Controllerの設定には、基本的に上記の2番目か3番目の方法で行うことになると思います。3番目の方法の場合は、Controllerというよりは、カスタムコントロールという位置づけになり、他のFXMLファイルにそのまま要素として指定することができますので、.NETで開発したことがある人には、馴染みやすいかもしれません。

FXMLファイルでViewを定義した場合は、javafx.fxml.FXMLLoaderクラスを使ってFXMLファイルを読み込むのですが、同じような記述を毎回行うことになりますので、次回は、FXMLファイルを読み込んでControllerを取得するユーティリティクラスを作成してみたいと思います。

2013年3月23日土曜日

JavaFX - カスタムペインの作成(3)

JavaFXで円状にノードを配置するカスタムペインを作成してみたいと思います。

作成するものですが、前回同様、以下でFerrisWheelPanelとして紹介されているものを作成してみます。

円状にノードを配置する場合に、配置するノードの向きを回転方向にして配置するか、同じ向き(通常の座標軸の方向)にして配置するかを指定して配置するペインになります。「Ferris Wheel」は観覧車という意味があり、回転するときに同じ状態を保つということを表すみたいです。

ノードを回転方向の向きに配置したものは、以下のようになります。

ノードを同じ向きに配置(FerrisWheelLayout)したものは、以下のようになります。

名称としては、FerrisWheelPaneとして作成していきます。

public class FerrisWheelPane extends Pane {
}

まずは、配置するノードの幅と高さを表すitemWidthプロパティとitemHeightプロパティ、ノードの配置向きを同じ方向にするかどうかを表すuseFerrisWheelLayoutプロパティを定義します。

public DoubleProperty itemWidthProperty() { return itemWidth; }
private final DoubleProperty itemWidth = new SimpleDoubleProperty(this, "itemWidth");
public final double getItemWidth() { return itemWidth.get(); }
public final void setItemWidth(double itemWidth) { this.itemWidth.set(itemWidth); }

public DoubleProperty itemHeightProperty() { return itemHeight; }
private final DoubleProperty itemHeight = new SimpleDoubleProperty(this, "itemHeight");
public final double getItemHeight() { return itemHeight.get(); }
public final void setItemHeight(double itemHeight) { this.itemHeight.set(itemHeight); }

public BooleanProperty useFerrisWheelLayoutProperty() { return useFerrisWheelLayout; }
private final BooleanProperty useFerrisWheelLayout = new SimpleBooleanProperty(this, "useFerrisWheelLayout");
public final boolean getUseFerrisWheelLayout() { return useFerrisWheelLayout.get(); }
public final void setUseFerrisWheelLayout(boolean useFerrisWheelLayout) { this.useFerrisWheelLayout.set(useFerrisWheelLayout); }

useFerrisWheelLayoutがtrueの場合はノードを同じ方向に、falseの場合はノードを回転方向に配置するようにします。

次に、layoutChidlrenをオーバーライドして、各ノードを配置します。

@Override
protected void layoutChildren() {
    double topInset = snapSpace(getInsets().getTop());
    double rightInset = snapSpace(getInsets().getRight());
    double bottomInset = snapSpace(getInsets().getBottom());
    double leftInset = snapSpace(getInsets().getLeft());

    double actualWidth = getWidth() - leftInset - rightInset;
    double actualHeight = getHeight() - topInset - bottomInset;
    double actualItemWidth = getItemWidth();
    double actualItemHeight = getItemHeight();

    double radiusX = (actualWidth - actualItemWidth) * 0.5;
    double radiusY = (actualHeight - (getUseFerrisWheelLayout() ? actualItemHeight : actualItemWidth)) * 0.5;

    List<Node> managedChildren = getManagedChildren();

    double deltaAngle = 2 * Math.PI / managedChildren.size();
    Point2D center = new Point2D(getWidth() * 0.5, getHeight() * 0.5);

    for (int index = 0; index < managedChildren.size(); ++index) {
        Node node = managedChildren.get(index);

        double angle = index * deltaAngle;
        double left = center.getX() + radiusX * Math.cos(angle) - actualItemWidth * 0.5;
        double top = center.getY() + radiusY * Math.sin(angle) - actualItemHeight * 0.5;

        if (getUseFerrisWheelLayout()) {
            node.setRotate(0);
        } else {
            node.setRotate(angle * 180 / Math.PI);
        }

        layoutInArea(
            node, left, top, actualItemWidth, actualItemHeight,
            actualItemHeight, HPos.CENTER, VPos.CENTER
        );
    }
}

配置方法については、まず、ノードを配置する楕円の半径を求めます。Y方向の半径については、useFerrisWheelLayoutがtrueの場合は、ペインの向きとノードの向きが等しいので、計算はそれぞれの高さの値を用いて行い、falseの場合は、ノードの向きが回転方向なので、どの位置でもノードの中心からペインの中心方向の長さは幅の値となりますので、計算はペインの高さとノードの幅の値を用いて行います。

次に、配置するノードの位置を求めます。ノードを配置する楕円の半径が求まっていますので、配置するノードの数で角度を分割して、ノードの中心の楕円上での位置を求め、ノードの左上の位置を計算します。

最後に、ノードの方向を決定します。ノードの方向の指定は、ノードを回転することで指定します。ノードを回転する場合、ノードの中心を中心として回転するようにしたいと思いますので、setRotateメソッドを使用してノードを回転したいと思います。setRotateでノードを回転する場合の中心は、ノードの中心となっています。回転の中心が異なる場合は、transformsプロパティにpivotを設定したRotateを追加します。

useFerrisWheelLayoutがtrueの場合は、ノードの方向はペインの方向と等しくしますので、rotateプロパティに0を設定し、falseの場合は、ノードの方向は回転方向としますので、rotateプロパティに回転角を設定します。

以上で、配置については完了になります。

最小の幅と高さについてですが、useFerrisWheelLayoutがfalseの場合は、配置するノードの、ペインの中心側にある頂点が一致するときの幅と高さで、計算すればよさそうですが、useFerrisWheelLayoutがtrueの場合に、どのようなときに最小の幅と高さにすればよいのかがわかりませんでしたので、今回はデフォルトのままにしておきます。

FXMLで、作成したFerrisWheelPaneを利用してみます。

<StackPane xmlns:fx="http://javafx.com/fxml">
    <FerrisWheelPane itemWidth="96" itemHeight="48"
                     useFerrisWheelLayout="true">
        <Button text="Button 1" prefWidth="Infinity" prefHeight="Infinity"/>
        <Button text="Button 2" prefWidth="Infinity" prefHeight="Infinity"/>
        <Button text="Button 3" prefWidth="Infinity" prefHeight="Infinity"/>
        <Button text="Button 4" prefWidth="Infinity" prefHeight="Infinity"/>
        <Button text="Button 5" prefWidth="Infinity" prefHeight="Infinity"/>
        <Button text="Button 6" prefWidth="Infinity" prefHeight="Infinity"/>
        <Button text="Button 7" prefWidth="Infinity" prefHeight="Infinity"/>
        <Button text="Button 8" prefWidth="Infinity" prefHeight="Infinity"/>
    </FerrisWheelPane>
</StackPane>

Javaのコード上で利用するために、Builderも作成しておきます。

public class FerrisWheelPaneBuilder<B extends FerrisWheelPaneBuilder<B>> extends PaneBuilder<B> {
    private static final int ITEM_WIDTH = 0;
    private static final int ITEM_HEIGHT = 1;
    private static final int USE_FERRIS_WHEEL_LAYOUT = 2;

    private final BitSet valueApplied = new BitSet(3);
    private double itemWidth;
    private double itemHeight;
    private boolean useFerrisWheelLayout;

    protected FerrisWheelPaneBuilder() {
    }

    public static FerrisWheelPaneBuilder<?> create() {
        return new FerrisWheelPaneBuilder<>();
    }

    public void applyTo(FerrisWheelPane ferrisWheelPane) {
        super.applyTo(Objects.requireNonNull(ferrisWheelPane));

        if (valueApplied.get(ITEM_WIDTH)) { ferrisWheelPane.setItemWidth(itemWidth); }
        if (valueApplied.get(ITEM_HEIGHT)) { ferrisWheelPane.setItemHeight(itemHeight); }
        if (valueApplied.get(USE_FERRIS_WHEEL_LAYOUT)) { ferrisWheelPane.setUseFerrisWheelLayout(useFerrisWheelLayout); }
    }

    @SuppressWarnings("unchecked")
    public B itemWidth(double itemWidth) {
        this.itemWidth = itemWidth;
        valueApplied.set(ITEM_WIDTH);
        return (B)this;
    }

    @SuppressWarnings("unchecked")
    public B itemHeight(double itemHeight) {
        this.itemHeight = itemHeight;
        valueApplied.set(ITEM_HEIGHT);
        return (B)this;
    }

    @SuppressWarnings("unchecked")
    public B useFerrisWheelLayout(boolean useFerrisWheelLayout) {
        this.useFerrisWheelLayout = useFerrisWheelLayout;
        valueApplied.set(USE_FERRIS_WHEEL_LAYOUT);
        return (B)this;
    }

    @Override
    public FerrisWheelPane build() {
        FerrisWheelPane ferrisWheelPane = new FerrisWheelPane();
        applyTo(ferrisWheelPane);
        return ferrisWheelPane;
    }
}

作成したBuilderを使って、Javaのコード上でFerrisWheelPaneを作成してみます。

FerrisWheelPane pane = FerrisWheelPaneBuilder.create()
    .itemWidth(96).itemHeight(48)
    .useFerrisWheelLayout(true)
    .children(
        ButtonBuilder.create()
            .text("Button 1")
            .prefWidth(Double.MAX_VALUE).prefHeight(Double.MAX_VALUE)
            .build(),
        ButtonBuilder.create()
            .text("Button 2")
            .prefWidth(Double.MAX_VALUE).prefHeight(Double.MAX_VALUE)
            .build(),
        ButtonBuilder.create()
            .text("Button 3")
            .prefWidth(Double.MAX_VALUE).prefHeight(Double.MAX_VALUE)
            .build(),
        ButtonBuilder.create()
            .text("Button 4")
            .prefWidth(Double.MAX_VALUE).prefHeight(Double.MAX_VALUE)
            .build(),
        ButtonBuilder.create()
            .text("Button 5")
            .prefWidth(Double.MAX_VALUE).prefHeight(Double.MAX_VALUE)
            .build(),
        ButtonBuilder.create()
            .text("Button 6")
            .prefWidth(Double.MAX_VALUE).prefHeight(Double.MAX_VALUE)
            .build(),
        ButtonBuilder.create()
            .text("Button 7")
            .prefWidth(Double.MAX_VALUE).prefHeight(Double.MAX_VALUE)
            .build(),
        ButtonBuilder.create()
            .text("Button 8")
            .prefWidth(Double.MAX_VALUE).prefHeight(Double.MAX_VALUE)
            .build()
    )
    .build();

JavaFXで円状にノードを配置するカスタムペインをしてみました。ノードの配置する位置が求まれば、layoutInAreaメソッドを利用して配置するだけとなります。

2013年3月17日日曜日

JavaFX - カスタムペインの作成(2)

JavaFXでスタティックプロパティを利用するカスタムペインを作成してみたいと思います。

作成するものですが、前回同様、以下でWeightedPanelとして紹介されているものを作成してみます。

以下のようなものになります。

weightプロパティで指定した値の割合で分割された領域に配置されます。デフォルトでは、横方向に分割して配置しますが、以下のように縦方向に分割して配置することもできます。

名称としては、WeightedPaneとして作成していきます。

public class WeightedPane extends Pane {
}

まずは、分割方向を表すorientationプロパティを定義します。

public ObjectProperty<Orientation> orientationProperty() { return orientation; }
private final ObjectProperty<Orientation> orientation = new SimpleObjectProperty<>(this, "orientation", Orientation.HORIZONTAL);
public final Orientation getOrientation() { return orientation.get(); }
public final void setOrientation(Orientation orientation) { this.orientation.set(orientation); }

ObjectPropertyとしてOrientation列挙体で定義し、デフォルトの値としてOrientation.HORIZONTALを設定しています。

次に、分割する割合を表すweightプロパティをスタティックプロパティとして定義します。

private static final String WEIGHT_KEY = "weighted-pane-weight";
private static final int DEFAULT_WEIGHT = 1;

public static double getWeight(Node node) {
    Objects.requireNonNull(node);
    if (!node.hasProperties()) { return DEFAULT_WEIGHT; }

    Object weight = node.getProperties().get(WEIGHT_KEY);
    return weight == null ? DEFAULT_WEIGHT : (double)weight;
}

public static void setWeight(Node node, double weight) {
    Objects.requireNonNull(node);
    if (weight <= 0) { throw new IllegalArgumentException(); }

    node.getProperties().put(WEIGHT_KEY, weight);

    if (node.getParent() != null) {
        node.getParent().requestLayout();
    }
}

定義方法としては、プロパティ名を用いて、javafx.scene.Nodeを第1引数としたスタティックなGetterとSetterを定義します。

public static T getPropertyName(Node node);
public static void setPropertyName(Node node, T value);

Getterでは、指定されたノードから、プロパティに設定されているweightの値を取得して返します。weightプロパティが設定されていない場合は、デフォルト値として1を返すようにしています。

Setterでは、指定されたノードのプロパティにweightの値を設定します。weightの値が0以下の場合は、例外を投げるようにします。また、プロパティの値を設定したときに、そのノードの親が存在する場合は、親ノードに対してレイアウトの要求をして、設定された値が反映されるようにしています。

次に、layoutChildrenをオーバーライドして、各ノードを配置します。配置方法としては、orientationプロパティで指定されている方向に、配置対象のノードを、取得したweightプロパティの割合で大きさを決定して、layoutInAreaメソッドを利用して配置します。

@Override
protected void layoutChildren() {
    double topInset = snapSpace(getInsets().getTop());
    double rightInset = snapSpace(getInsets().getRight());
    double bottomInset = snapSpace(getInsets().getBottom());
    double leftInset = snapSpace(getInsets().getLeft());

    double actualWidth = getWidth() - leftInset - rightInset;
    double actualHeight = getHeight() - topInset - bottomInset;
    double left = leftInset;
    double top = topInset;

    List<Node> managedChildren = getManagedChildren();
    double[] normalWeights = normalizeWeights();
    for (int index = 0; index < managedChildren.size(); ++index) {
        switch (getOrientation()) {
            case HORIZONTAL:
                double itemWidth = actualWidth * normalWeights[index];
                layoutInArea(
                    managedChildren.get(index), left, top, itemWidth, actualHeight,
                    actualHeight, HPos.CENTER, VPos.CENTER
                );
                left += itemWidth;
                break;
            case VERTICAL:
                double itemHeight = actualHeight * normalWeights[index];
                layoutInArea(
                    managedChildren.get(index), left, top, actualWidth, itemHeight,
                    itemHeight, HPos.CENTER, VPos.CENTER
                );
                top += itemHeight;
                break;
        }
    }
}

protected double[] normalizeWeights() {
    double totalWeights = getTotalWeights();
    List<Node> managedChildren = getManagedChildren();
    double[] normalWeights = new double[managedChildren.size()];
    for (int index = 0; index < normalWeights.length; ++index) {
        normalWeights[index] = getWeight(managedChildren.get(index)) / totalWeights;
    }
    return normalWeights;
}

protected double getTotalWeights() {
    double totalWeights = 0;
    for (Node node: getManagedChildren()) {
        totalWeights += getWeight(node);
    }
    return totalWeights;
}

以上で、配置については完了になります。あとは、最小の幅と高さ、適切な幅と高さについて設定することにします。

最小の幅については、配置対象のノードの最小の幅のうち、最大のものを対象とし、orientationプロパティがHORIZONTALの場合は、その幅に対して、設定されているweightプロパティの値を考慮して、全体の幅を計算します。

 @Override
protected double computeMinWidth(double height) {
    double rightInsets = snapSpace(getInsets().getRight());
    double leftInsets = snapSpace(getInsets().getLeft());
    double maxNodeMinWidth = 0;
    double maxNodeMinWidthWeight = DEFAULT_WEIGHT;
    for (Node node : getManagedChildren()) {
        double nodeMinWidth = node.minWidth(height);
        if (maxNodeMinWidth < nodeMinWidth) {
            maxNodeMinWidth = nodeMinWidth;
            maxNodeMinWidthWeight = getWeight(node);
        }
    }

    double minWidth = maxNodeMinWidth;
    if (getOrientation() == Orientation.HORIZONTAL) {
        minWidth = maxNodeMinWidth * getTotalWeights() / maxNodeMinWidthWeight;
    }

    return leftInsets + snapSize(minWidth) + rightInsets;
}

最小の高さについても、同様にして計算します。

@Override
protected double computeMinHeight(double width) {
    double topInsets = snapSpace(getInsets().getTop());
    double bottomInsets = snapSpace(getInsets().getBottom());
    double maxNodeMinHeight = 0;
    double maxNodeMinHeightWeight = DEFAULT_WEIGHT;
    for (Node node : getManagedChildren()) {
        double nodeMinHeight = node.minHeight(width);
        if (maxNodeMinHeight < nodeMinHeight) {
            maxNodeMinHeight = nodeMinHeight;
            maxNodeMinHeightWeight = getWeight(node);
        }
    }

    double minHeight = maxNodeMinHeight;
    if (getOrientation() == Orientation.VERTICAL) {
        minHeight = maxNodeMinHeight * getTotalWeights() / maxNodeMinHeightWeight;
    }

    return bottomInsets + snapSize(minHeight) + topInsets;
}

適切な幅については、orientationプロパティがHORIZONTAL以外の場合は、デフォルトの計算を行い、HORIZONTALの場合は、配置対象のノードの適切な幅の和として、全体の幅を計算します。

@Override
protected double computePrefWidth(double height) {
    if (getOrientation() != Orientation.HORIZONTAL) {
        return super.computePrefWidth(height);
    }

    double rightInsets = snapSpace(getInsets().getRight());
    double leftInsets = snapSpace(getInsets().getLeft());
    double width = rightInsets + leftInsets;
    for (Node node : getManagedChildren()) {
        width += snapSize(
            Math.min(
                Math.max(node.minWidth(height), node.prefWidth(height)),
                Math.max(node.minWidth(height), node.maxWidth(height))
            )
        );
    }
    return width;
}

各配置対象のノードの幅については、最小・最大・適切な幅の値の大小関係によって、次のようにして値を決定しています。

最小の幅と最大の幅の値の大きさがこの順の場合は、適切な幅の値が最小・最大の値の間のときはその値を、範囲外にあるときはその境界の値を採用し、逆順の場合は、最小の幅の値を採用するようにしています。

min < pref < max -> pref
pref < min < max -> min
min < max < pref -> max

max < pref < min -> min
pref < max < min -> min
max < min < pref -> min

適切な高さについても、同様にして計算します。

@Override
protected double computePrefHeight(double width) {
    if (getOrientation() != Orientation.VERTICAL) {
        return super.computePrefHeight(width);
    }

    double topInset = snapSpace(getInsets().getTop());
    double bottomInset = snapSpace(getInsets().getBottom());
    double height = topInset + bottomInset;
    for (Node node : getManagedChildren()) {
        height += snapSize(
            Math.min(
                Math.max(node.minHeight(width), node.prefHeight(width)),
                Math.max(node.minHeight(width), node.maxHeight(width))
            )
        );
    }
    return height;
}

以上で、完了になります。

FXMLで、作成したWeightedPaneを利用してみます。

<StackPane xmlns:fx="http://javafx.com/fxml">
    <WeightedPane>
        <Button WeightedPane.weight="6"
                text="weight=6"
                prefWidth="Infinity" prefHeight="Infinity"/>
        <Button WeightedPane.weight="4"
                text="weight=4"
                prefWidth="Infinity" prefHeight="Infinity"/>
    </WeightedPane>
</StackPane>

分割方向を縦方向にして利用してみます。

<StackPane xmlns:fx="http://javafx.com/fxml">
    <WeightedPane orientation="VERTICAL">
        <Button WeightedPane.weight="3"
                text="weight=3"
                prefWidth="Infinity" prefHeight="Infinity"/>
        <Button WeightedPane.weight="2"
                text="weight=2"
                prefWidth="Infinity" prefHeight="Infinity"/>
        <Button WeightedPane.weight="4"
                text="weight=4"
                prefWidth="Infinity" prefHeight="Infinity"/>
    </WeightedPane>
</StackPane>

Javaのコード上で利用するために、Builderも作成しておきます。

public class WeightedPaneBuilder<B extends WeightedPaneBuilder<B>> extends PaneBuilder<B> {
    private boolean orientationApplied;
    private Orientation orientation;

    protected WeightedPaneBuilder() {
    }

    public static WeightedPaneBuilder<?> create() {
        return new WeightedPaneBuilder<>();
    }

    public void applyTo(WeightedPane weightedPane) {
        super.applyTo(Objects.requireNonNull(weightedPane));

        if (orientationApplied) { weightedPane.setOrientation(orientation); }
    }

    @SuppressWarnings("unchecked")
    public B orientation(Orientation orientation) {
        this.orientation = orientation;
        orientationApplied = true;
        return (B)this;
    }

    public WeightedPane build() {
        WeightedPane weightedPane = new WeightedPane();
        applyTo(weightedPane);
        return weightedPane;
    }
}

作成したBuilderを使って、Javaのコード上でWeightedPaneを作成してみます。

Button button1;
Button button2;
Button button3;

WeightedPane pane = WeightedPaneBuilder.create()
    .orientation(Orientation.VERTICAL)
    .children(
        button1 = ButtonBuilder.create()
            .text("weight=3")
            .prefWidth(Double.MAX_VALUE).prefHeight(Double.MAX_VALUE)
            .build(),
        button2 = ButtonBuilder.create()
            .text("weight=2")
            .prefWidth(Double.MAX_VALUE).prefHeight(Double.MAX_VALUE)
            .build(),
        button3 = ButtonBuilder.create()
            .text("weight=4")
            .prefWidth(Double.MAX_VALUE).prefHeight(Double.MAX_VALUE)
            .build()
    )
    .build();

WeightedPane.setWeight(button1, 3);
WeightedPane.setWeight(button2, 2);
WeightedPane.setWeight(button3, 4);

JavaFXでスタティックプロパティを利用したカスタムペインを作成してみました。スタティックプロパティは、javafx.scene.Nodeを第1引数としたGetterとSetterを定義することで利用可能となりました。スタティックプロパティを利用すると、各ノードにプロパティを付け加えることができますので、ペイン以外でもいろいろと利用価値がありそうです。次回は、もう少しノードの配置が複雑になるものを作成したいと思います。

2013年3月9日土曜日

JavaFX - カスタムペインの作成(1)

JavaFXでカスタムペインを作成してみたいと思います。

カスタムペインを作成するには、javafx.scene.layout.Paneクラスを継承し、

layoutChildren()
をオーバーライドして、各ノードを配置します。

また、ペインの最小・最大・適切な幅と高さについて、以下のメソッドをオーバーライドして計算します。

computeMinWidth(height)
computeMinHeight(width)
computeMaxWidth(height)
computeMaxHeight(width)
computePrefWidth(height)
computePrefHeight(width)
最小のデフォルト値は、設定されているpaddingの値となり、最大のデフォルト値は、Double.MAX_VALUEとなります。 適切のデフォルト値は、各ノードの適切な位置と大きさで格納することができる値となります。

基本的には、上記のメソッドをオーバーライドすればできそうなので、実際に作成してみます。

作成するものですが、自分ではあまりよい例が思い浮かびませんので、以下の本のChapter4で紹介されているものでやってみたいと思います。

まずは、VanishingPointPanelとして紹介されているものを作成してみます。

以下のようなものになります。

透視図のように、上段にあるノードがある一定の割合で小さくなって配置されます。

名称としては、VanishingPointPaneとして作成していきます。

public class VanishingPointPane extends Pane {
}

まずは、どの程度の割合で小さくしていくかを表すzFactorプロパティと配置する領域の高さを表すitemHeightプロパティを定義します。

public DoubleProperty zFactorProperty() { return zFactor; }
private final DoubleProperty zFactor = new SimpleDoubleProperty(this, "zFactor", 1);
public final double getZFactor() { return zFactor.get(); }
public final void setZFactor(double zFactor) { this.zFactor.set(zFactor); }

public DoubleProperty itemHeightProperty() { return itemHeight; }
private final DoubleProperty itemHeight = new SimpleDoubleProperty(this, "itemHeight", 1);
public final double getItemHeight() {return itemHeight.get(); }
public final void setItemHeight(double itemHeight) { this.itemHeight.set(itemHeight); }

どちらもDoublePropertyとし、デフォルトの値として1を設定しています。

次に、layoutChildrenをオーバーライドして、各ノードを配置します。 配置方法としては、末尾のノードを、最下部の位置に、幅がPaneの幅で、高さがitemHeightの高さの領域内に配置し、先頭のノードに向かって、配置する領域がzFactorの割合で小さくなるようにして、横幅の中央に配置していきます。

@Override
protected void layoutChildren() {
    double rightInset = snapSpace(getInsets().getRight());
    double bottomInset = snapSpace(getInsets().getBottom());
    double leftInset = snapSpace(getInsets().getLeft());

    double top = getHeight() - bottomInset;
    double actualWidth = getWidth() - leftInset - rightInset;

    List<Node> managedChildren = getManagedChildren();
    for (int index = managedChildren.size() - 1; index >= 0; --index) {
        Node node = managedChildren.get(index);
        double factor = Math.pow(getZFactor(), managedChildren.size() - 1 - index);
        double itemWidth = actualWidth * factor;
        double itemHeight = getItemHeight() * factor;

        double left = (actualWidth - itemWidth) * 0.5 + leftInset;
        top -= itemHeight;

        layoutInArea(node, left, top, itemWidth, itemHeight, itemHeight, HPos.CENTER, VPos.CENTER);
    }
}

配置するノードの領域ですが、paddingを除いた部分に配置しますので、その部分を除いておきます。そのとき、snapToPixelプロパティがtrueの場合も考慮に入れて、

snapSpace(value)
を使用して値を取得します。

配置対象のノードとしては、managedプロパティがtrueのものを対象としますので、

getManagedChildren()
で子ノードを取得します。

実際の子ノードの配置には、javafx.scene.layout.Regionクラスに定義されている以下のいずれかのメソッドを利用して配置します。

layoutInArea(
    child, areaX, areaY, areaWidth, areaHeight,
    areaBaselineOffset, halignment, valignment
)
layoutInArea(
    child, areaX, areaY, areaWidth, areaHeight,
    areaBaselineOffset, margin, halignment, valignment
)
layoutInArea(
    child, areaX, areaY, areaWidth, areaHeight,
    areaBaselineOffset, margin, fillWidth, fillHeight,
    halignment, valignment
)
positionInArea(
    child, areaX, areaY, areaWidth, areaHeight,
    areaBaselineOffset, halignment, valignment
)
positionInArea(
    child, areaX, areaY, areaWidth, areaHeight,
    areaBaselineOffset, margin, halignment, valignment
)
layoutInAreaやpositionInAreaでは、snapPosition、snapSizeを使用して位置や大きさを設定しますので、計算した値をそのまま引数に設定しています。

以上で、配置については完了になります。あとは、最小の高さと幅について設定することにします。

最小の高さですが、これはitemHeightの高さが配置するノードの数だけあるとして求めます。

@Override
protected double computeMinHeight(double width) {
    double topInset = snapSpace(getInsets().getTop());
    double bottomInset = snapSpace(getInsets().getBottom());
    double totalHeight = snapSize(getItemHeight() * getManagedChildren().size());
    return topInset + totalHeight + bottomInset;
}

最小の幅ですが、これは配置する各ノードの最小の幅のうち最大のものが配置できるように求めます。

@Override
protected double computeMinWidth(double height) {
    double maxNodeMinWidth = super.computeMinWidth(height);
    int nodeMinWidthIndex = -1;
    List<Node> managedChildren = getManagedChildren();
    for (int index = 0; index < managedChildren.size(); ++index) {
        double nodeMinWidth = managedChildren.get(index).minWidth(height);
        if (nodeMinWidth > maxNodeMinWidth) {
            maxNodeMinWidth = nodeMinWidth;
            nodeMinWidthIndex = index;
        }
    }

    double rightInset = snapSpace(getInsets().getRight());
    double leftInset = snapSpace(getInsets().getLeft());
    double bottomItemMinWidth = snapSize(maxNodeMinWidth / Math.pow(getZFactor(), managedChildren.size() - 1 - nodeMinWidthIndex));
    return rightInset + bottomItemMinWidth + leftInset;
}

最後に、適切な高さとして、最小の高さを設定することにします。

@Override
protected double computePrefHeight(double width) {
    return computeMinHeight(width);
}

以上で、完了になります。

FXMLで、作成したVanishingPointPaneを利用してみます。

<StackPane xmlns:fx="http://javafx.com/fxml">
    <VanishingPointPane zFactor="0.8" itemHeight="50">
        <Button text="Button 1" prefWidth="Infinity" prefHeight="Infinity" minWidth="100"/>
        <Button text="Button 2" prefWidth="Infinity" prefHeight="Infinity"/>
        <Button text="Button 3" prefWidth="Infinity" prefHeight="Infinity"/>
        <Button text="Button 4" prefWidth="Infinity" prefHeight="Infinity"/>
        <Button text="Button 5" prefWidth="Infinity" prefHeight="Infinity"/>
    </VanishingPointPane>
</StackPane>

Javaのコード上で利用するために、Builderも作成しておきます。

public class VanishingPointPaneBuilder<B extends VanishingPointPaneBuilder<B>> extends PaneBuilder<B> {
    private static final int Z_FACTOR = 0;
    private static final int ITEM_HEIGHT = 1;

    private final BitSet valueApplied = new BitSet(2);
    private double zFactor;
    private double itemHeight;

    protected VanishingPointPaneBuilder() {
    }

    public static VanishingPointPaneBuilder<?> create() {
        return new VanishingPointPaneBuilder<>();
    }

    public void applyTo(VanishingPointPane vanishingPointPane) {
        super.applyTo(Objects.requireNonNull(vanishingPointPane));

        if (valueApplied.get(Z_FACTOR)) { vanishingPointPane.setZFactor(zFactor); }
        if (valueApplied.get(ITEM_HEIGHT)) { vanishingPointPane.setItemHeight(itemHeight); }
    }

    @SuppressWarnings("unchecked")
    public B zFactor(double zFactor) {
        this.zFactor = zFactor;
        valueApplied.set(Z_FACTOR);
        return (B)this;
    }

    @SuppressWarnings("unchecked")
    public B itemHeight(double itemHeight) {
        this.itemHeight = itemHeight;
        valueApplied.set(ITEM_HEIGHT);
        return (B)this;
    }

    public VanishingPointPane build() {
        VanishingPointPane vanishingPointPane = new VanishingPointPane();
        applyTo(vanishingPointPane);
        return vanishingPointPane;
    }
}

作成したBuilderを使って、Javaのコード上でVanishingPointPaneを作成してみます。

VanishingPointPane pane = VanishingPointPaneBuilder.create()
    .padding(new Insets(10))
    .zFactor(0.8).itemHeight(50)
    .children(
        ButtonBuilder.create()
            .text("Button1")
            .prefWidth(Double.MAX_VALUE).prefHeight(Double.MAX_VALUE)
            .build(),
        ButtonBuilder.create()
            .text("Button2")
            .prefWidth(Double.MAX_VALUE).prefHeight(Double.MAX_VALUE)
            .build(),
        ButtonBuilder.create()
            .text("Button3")
            .prefWidth(Double.MAX_VALUE).prefHeight(Double.MAX_VALUE)
            .build(),
        ButtonBuilder.create()
            .text("Button4")
            .prefWidth(Double.MAX_VALUE).prefHeight(Double.MAX_VALUE)
            .build(),
        ButtonBuilder.create()
            .text("Button5")
            .prefWidth(Double.MAX_VALUE).prefHeight(Double.MAX_VALUE)
            .build()
    )
    .build();

JavaFXでカスタムペインを作成してみました。基本的には、layoutChildrenをオーバーライドして適切な位置・大きさでノードを配置するだけでいけそうです。次回は、GridPaneのようにスタティックプロパティを使用するようなものを作成したいと思います。