ページ

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を定義することで利用可能となりました。スタティックプロパティを利用すると、各ノードにプロパティを付け加えることができますので、ペイン以外でもいろいろと利用価値がありそうです。次回は、もう少しノードの配置が複雑になるものを作成したいと思います。