ページ

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のようにスタティックプロパティを使用するようなものを作成したいと思います。