ページ

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メソッドを利用して配置するだけとなります。