ページ

2013年4月28日日曜日

JavaFX - Dragging gestures (5)

今回は、Platform-supported drag-and-drop gestureについてみていきたいと思います。

Platform-supported drag-and-drop gestureは、アプリケーション間でデータを転送したいときに利用します。データの転送には、クリップボードを利用しますので、JavaFXのアプリケーションでないアプリケーションに対してデータを転送することもできます。

Platform-supported drag-and-drop gestureを開始するためには、DRAG_DETECTEDイベントで、startDragAndDropメソッドを呼び出します。このとき、Dragboardクラスのインスタンスが返されますので、ドラッグアンドドロップで転送したいデータを、ClipboardContentを利用して設定します。例えば、TextAreaの選択されているテキストの内容を転送したい場合は、DRAG_DETECTEDイベントで以下のようにします。

@Override
public void handle(MouseEvent event) {
    TextArea source = (TextArea)event.getSource();
    if (source.getSelectedText().isEmpty()) { return; }

    Dragboard board = source.startDragAndDrop(TransferMode.COPY_OR_MOVE);
    ClipboardContent content = new ClipboardContent();
    content.putString(source.getSelectedText());
    board.setContent(content);
    event.consume();
}

startDragAndDropメソッドを呼び出して、Dragboardに何も設定しない場合は、Platform-supported drag-and-drop gestureは開始されません。

Platform-supported drag-and-drop gestureが開始されると、DragEventが発生しますので、このイベントにハンドラを追加して、処理を行います。

DragEventのイベントタイプには、以下のものがあります。

  • DRAG_DONE
  • DRAG_DROPPED
  • DRAG_ENTERED
  • DRAG_ENTERED_TARGET
  • DRAG_EXITED
  • DRAG_EXITED_TARGET
  • DRAG_OVER

基本的には、ドラッグ元でDRAG_DONEイベントにハンドラを追加し、ドラッグ先で残りのイベントにハンドラを追加します。DRAG_ENTERED_TARGETとDRAG_EXITED_TARGETについては、MouseEventやMouseDragEventのときと同様に、キャプチャリングフェーズあるいはバブリングフェーズでハンドラを追加したいときに利用します。

ドラッグ先では、DRAG_OVERイベントが発生したときに、Dragboardに設定されているデータを確認し、ドロップ対象とするかどうかの判定を、acceptTransferModesメソッドを呼び出すことによって行います。このメソッドの引数には、どのTransferModeでドロップするかを指定します。例えば、文字列のデータを移動させるためのドロップ対象とする場合は、DRAG_OVERイベントで以下のようにします。

@Override
public void handle(DragEvent event) {
    Dragboard board = event.getDragboard();
    if (board.hasString()) {
        event.acceptTransferModes(TransferMode.MOVE);
    }
    event.consume();
}

ドロップ対象とした場合、ドラッグ先のノード上でマウスをリリースすると、DRAG_DROPPEDイベントが発生しますので、このイベントにハンドラを追加して、ドロップしたときの処理を行います。

@Override
public void handle(DragEvent event) {
    Dragboard board = event.getDragboard();
    if (board.hasString()) {
        // ドロップ時の処理を実装
        event.setDropCompleted(true);
    }
    event.consume();
}

ドロップの処理が正常に行われた場合は、DragEventのsetDropCompletedメソッドの引数にtrueを指定して呼び出しておきます。

ドロップが行われると、正常に終了したかどうかに関わらず、ドラッグ元にDRAG_DONEイベントが発生します。ドロップが正常に行われたかどうかは、DragEventのgetTransferModeメソッドで、TransferModeが取得できたかどうかで判断します。ドロップ対象ノード以外にドロップしたか、ESCでキャンセルされたか、setDropCompletedメソッドでfalseが設定された場合は、取得したTransferModeがNULLとなります。それ以外の場合は、ドロップされたときのTransferModeが取得されますので、取得したTransferModeの値に対する処理を行います。基本的には、TransferMode.MOVEを取得したときに、ドラッグしたデータを削除することになります。例えば、TextAreaの選択されているテキストの内容を転送していた場合に、TransferMode.MOVEでドロップした場合は、DRAG_DONEイベントで以下のようにします。

@Override
public void handle(DragEvent event) {
    if (event.getTransferMode() == TransferMode.MOVE) {
        TextArea source = (TextArea)event.getSource();
        source.deleteText(source.getSelection());
    }
    event.consume();
}

ClipboardContentに設定できるデータは、DataFormatで定義されている以下のものがあります。

  • FILES
  • HTML
  • IMAGE
  • PLAIN_TEXT
  • RTF
  • URL

上記以外にも、DataFormatクラスのインスタンスを作成することによって、独自のデータを設定することもできます。ただし、設定するデータはシリアル化できるものである必要があります。

今回は、Platform-supported drag-and-drop gestureについて、どのようにして行うかをみていきました。次回は、これを利用したものを何か作ってみたいと思います。

2013年4月14日日曜日

JavaFX - Dragging gestures (4)

前回は、Full press-drag-release gesture を利用するにあたって、MouseDragEventについてみていきました。今回は、Full press-drag-release gestureを利用して、以下のようなものを作成してみたいと思います。

Paneが2つあり、1つのPaneに配置されているノードをドラッグして、もう1つのPaneに移動するというようなものです。

ノードをドラッグし、マウスをリリースしたときに、もう1つのPane上にある場合は、そのPaneにノードを移動し、それ以外の場合は、もとのPaneに戻っていきます。また、ノードをドラッグ中に、移動先のPane上にきたときは、そのPaneの外枠を赤線で強調表示します。

まずは、以下のようにViewを定義します。

<Scene xmlns:fx="http://javafx.com/fxml"
       fx:id="scene"
       width="640" height="480">
    <stylesheets>
        <URL value="@FullPressDragReleaseGestureDemoStyle.css"/>
    </stylesheets>

    <StackPane>
        <GridPane>
            <StackPane GridPane.columnIndex="0"
                       GridPane.hgrow="ALWAYS" GridPane.vgrow="ALWAYS">
                <StackPane fx:id="leftSidePane" styleClass="pane"
                           prefWidth="200" prefHeight="200"
                           maxWidth="-Infinity" maxHeight="-Infinity">
                    <Circle fx:id="draggableCircle"
                            radius="30"/>
                </StackPane>
            </StackPane>
            
            <StackPane GridPane.columnIndex="1"
                       GridPane.hgrow="ALWAYS" GridPane.vgrow="ALWAYS">
                <StackPane fx:id="rightSidePane" styleClass="pane"
                           prefWidth="200" prefHeight="200"
                           maxWidth="-Infinity" maxHeight="-Infinity"/>
            </StackPane>
        </GridPane>
        
        <Pane fx:id="dragPane" mouseTransparent="true"/>
    </StackPane>
</Scene>

ノードをドラッグするときに、シーンの最前面で移動したいので、ルートをStackPaneで定義し、ノードをドラッグするためのPaneを最前面になるように設定しています。

次に、ドラッグするノードとそのノードを配置するPaneにイベントハンドラを設定します。

ドラッグするノードについては、以下のようにイベントハンドラを設定します。

private Pane sourcePane;
private double sourceLayoutX;
private double sourceLayoutY;

private DragContext context;

draggableCircle.setOnMousePressed(new EventHandler<MouseEvent>() {
    @Override
    public void handle(MouseEvent event) {
        Circle target = (Circle)event.getSource();
        sourcePane = (Pane)target.getParent();
        sourcePane.getChildren().remove(target);
        dragPane.getChildren().add(target);

        Point2D p = dragPane.sceneToLocal(event.getSceneX(), event.getSceneY());
        target.setLayoutX(p.getX() - (event.getX() - target.getCenterX()));
        target.setLayoutY(p.getY() - (event.getY() - target.getCenterY()));
        sourceLayoutX = target.getLayoutX();
        sourceLayoutY = target.getLayoutY();

        context = DragContext.of(target)
            .atMouseSceneLocation(event.getSceneX(), event.getSceneY());
    }
});

draggableCircle.setOnDragDetected(new EventHandler<MouseEvent>() {
    @Override
    public void handle(MouseEvent event) {
        ((Node)event.getSource()).startFullDrag();
    }
});

draggableCircle.setOnMouseDragged(new EventHandler<MouseEvent>() {
    @Override
    public void handle(MouseEvent event) {
        if (context == null) { return; }
        context.dragNodeForMouseSceneLocation(event.getSceneX(), event.getSceneY());
    }
});

draggableCircle.setOnMouseReleased(new EventHandler<MouseEvent>() {
    @Override
    public void handle(MouseEvent event) {
        context = null;
        if (sourcePane == null) { return; }
        animate((Node)event.getSource(), sourcePane, sourcePane);
    }
});

MOUSE_PRESSEDイベントで、対象のノードを現在配置されているPaneから、ドラッグするためのPaneに移動し、そのPane上で、現在配置されいる位置と同じ位置になるように、ノードの位置を設定しています。

DRAG_DETECTEDイベントで、startFullDragメソッドを呼び出して、Full press-drag-release gestureを開始します。

ノードのドラッグに利用しているDragContextについては、こちらを参照してください。

MOUSE_RELEASEDイベントで、ドラッグ終了時の処理を行っています。ドラッグ終了時に、もう1つのPane上にない場合は、元のPaneにノードを戻します。ドラッグ終了時には、MOUSE_DRAG_RELEASED -> MOUSE_RELEASED -> MOUSE_DRAG_EXITEDの順にイベントが発生しますので、もう1つのPane上にノードを移動する場合は、MOUSE_DRAG_RELEASEDイベントで、MOUSE_PRESSEDで設定したsourcePaneをnullにするようにします。そうすると、MOUSE_RELEASEDイベントで、sourcePaneがnullの場合は、もう1つのPane上に移動したことになりますので、何も処理を行いません。逆に、sourcePaneがnullでない場合は、元のPaneに移動する必要があります。元のPaneへの移動は、animateメソッドでアニメーションで移動するようにしています。animateメソッドは、以下のようになります。

private void animate(final Node node, final Pane sourcePane, final Pane destinationPane) {
    double toLayoutX = sourceLayoutX + getPaneHGap(sourcePane, destinationPane);
    double toLayoutY = sourceLayoutY + getPaneVGap(sourcePane, destinationPane);

    ParallelTransitionBuilder.create()
        .children(
            TimelineBuilder.create()
                .keyFrames(
                    new KeyFrame(
                        Duration.millis(400),
                        new KeyValue(node.layoutXProperty(), toLayoutX)
                    )
                )
                .build(),
            TimelineBuilder.create()
                .keyFrames(
                    new KeyFrame(
                        Duration.millis(400),
                        new KeyValue(node.layoutYProperty(), toLayoutY)
                    )
                )
                .build()
        )
        .onFinished(new EventHandler<ActionEvent>() {
            @Override
            public void handle(ActionEvent event) {
                dragPane.getChildren().remove(node);
                destinationPane.getChildren().add(node);
            }
        })
        .build()
        .play();
}

private double getPaneHGap(Pane sourcePane, Pane destinationPane) {
    return destinationPane.localToScene(destinationPane.getBoundsInParent()).getMinX() -
        sourcePane.localToScene(sourcePane.getBoundsInParent()).getMinX();
}

private double getPaneVGap(Pane sourcePane, Pane destinationPane) {
    return destinationPane.localToScene(destinationPane.getBoundsInParent()).getMinY() -
        sourcePane.localToScene(sourcePane.getBoundsInParent()).getMinY();
}

アニメーションでノードを移動させ、アニメーションの終了時に、ドラッグのためのPaneから、移動先のPaneにノードを移動させています。

移動先のノードの位置については、2つのPaneの大きさを同じにしていますので、移動元の位置に対して、2つのPaneの位置のギャップを追加するようにしています。

最後に、ノードを配置するPaneのイベントハンドラについて、以下のように設定します。

EventHandler<MouseDragEvent> mouseDragReleaseHandler = new EventHandler<MouseDragEvent>() {
    @Override
    public void handle(MouseDragEvent event) {
        Pane targetPane = (Pane)event.getSource();
        if (sourcePane.equals(targetPane)) { return; }
        animate((Node)event.getGestureSource(), sourcePane, targetPane);
        sourcePane = null;
    }
};

EventHandler<MouseDragEvent> mouseDragEnteredHandler = new EventHandler<MouseDragEvent>() {
    @Override
    public void handle(MouseDragEvent event) {
        Pane targetPane = (Pane)event.getSource();
        if (sourcePane.equals(targetPane)) { return; }
        targetPane.getStyleClass().add("target-pane");
    }
};

EventHandler<MouseDragEvent> mouseDragExitedHandler = new EventHandler<MouseDragEvent>() {
    @Override
    public void handle(MouseDragEvent event) {
        Pane targetPane = (Pane)event.getSource();
        if (sourcePane != null && sourcePane.equals(targetPane)) { return; }
        targetPane.getStyleClass().remove("target-pane");
    }
};

leftSidePane.setOnMouseDragReleased(mouseDragReleaseHandler);
leftSidePane.setOnMouseDragEntered(mouseDragEnteredHandler);
leftSidePane.setOnMouseDragExited(mouseDragExitedHandler);

rightSidePane.setOnMouseDragReleased(mouseDragReleaseHandler);
rightSidePane.setOnMouseDragEntered(mouseDragEnteredHandler);
rightSidePane.setOnMouseDragExited(mouseDragExitedHandler);

MOUSE_DRAG_RELEASEDイベントで、移動元のPaneでない場合は、ノードをイベント発生もとのPaneに移動し、ノードがもう1つのPaneに移動したことを表すために、sourcePaneをnullに設定しています。ノードの移動には、先ほど定義したanimateメソッドを利用しています。

もう1つのPane上に、ドラッグ中のノードが入ってきたときに、外枠を赤色で強調表示するために、MOUSE_DRAG_ENTEREDイベントとMOUSE_DRAG_EXITEDイベントで処理を行っています。外枠を強調表示するには、スタイルクラスを変更することで行っています。スタイルは以下のように定義しています。

.root {
    -fx-base: black;
    -fx-background-color: linear-gradient(to bottom, derive(-fx-base, 60%), derive(-fx-base, 40%));
    -fx-target-border-color: transparent;
}

.pane {
    -fx-background-color:
        -fx-pane-color,
        linear-gradient(to bottom, derive(-fx-pane-color, 60%), derive(-fx-pane-color, 40%));
    -fx-background-insets: 0, 1;
    -fx-background-radius: 10, 9;
    -fx-border-color: -fx-target-border-color;
    -fx-border-width: 5;
    -fx-border-radius: 10;
}

.target-pane {
    -fx-target-border-color: red;
}

#leftSidePane {
    -fx-pane-color: purple;
}

#rightSidePane {
    -fx-pane-color: green;
}

ベースとなるスタイルに、外枠の定義をしておき、MOUSE_DRAG_ENTEREDイベントが発生したときに設定するスタイルで、外枠の色を設定しています。

以上で完了になります。

ノードをドラッグしたときに、他のノードに対して、マウスイベントを発生させたい場合は、DRAG_DETECTEDイベントで、startFullDragメソッドを呼び出して、Full press-drag-release gestureを開始し、MouseDragEventの対象となるイベントに対して、ハンドラを追加すればよいことになります。その場合は、MOUSE_PRESSEDイベントでmouseTransparentプロパティをtrueに設定するようにします。次回は、Platform-supported drag-and-drop gestureについてみていきたいと思います。

JavaFX - Dragging gestures (3)

今回は、Full press-drag-release gestureについてみていきたいと思います。

Simple press-drag-release gestureでは、ドラッグ中は、ドラッグしているノードにだけ、マウスイベントが発生し、ドラッグしているノード以外のノードでは、マウスイベントが発生しませんでした。ドラッグ中に、ドラッグしているノード以外で、マウスイベントが発生するようにするためには、Full press-drag-release gestureを利用する必要があります。

Full press-drag-release gestureを開始するためには、DRAG_DETECTEDイベントで、startFullDragメソッドを呼び出します。startFullDragメソッドを呼び出すと、ドラッグ中、マウスカーソルの下にあるノードに対して、MouseDragEventが発生しますので、このイベントにハンドラを追加して、ドラッグ中の処理を行います。

MouseDragEventのイベントタイプには、以下のものがあります。

  • MOUSE_DRAG_ENTERED
  • MOUSE_DRAG_ENTERED_TARGET
  • MOUSE_DRAG_EXITED
  • MOUSE_DRAG_EXITED_TARGET
  • MOUSE_DRAG_OVER
  • MOUSE_DRAG_RELEASED

MOUSE_DRAG_ENTERED_TARGETとMOUSE_DRAG_EXITED_TARGETについては、MouseEventのMOUSE_ENTERED_TARGETとMOUSE_EXITED_TARGETと同様で、対象となるノードの親ノードで、キャプチャリングフェーズあるいはバブリングフェーズで、対象となるノードのMOUSE_DRAG_ENTEREDやMOUSE_DRAG_EXITEDイベントに対して、ハンドラを追加したいときに利用します。

また、startFullDragメソッドを呼び出すだけでは、ドラッグしているノードにMouseDragEventが発生し、他のノードには発生しません。ドラッグしているノード以外のノードにMouseDragEventを発生させるためには、ドラッグしているノードに対して、mouseTransparentプロパティをtrueにして、マウスイベントを透過させる必要があります。基本的には、MOUSE_PRESSEDイベントで、mouseTransparentプロパティをtrueにし、MOUSE_RELEASEDイベントでfalseにして元に戻せばよいようです。このように設定することによって、ドラッグ中は、ドラッグしているノードにはMouseDragEventは発生せず、ドラッグしているノード以外のノードに対してMouseDragEventが発生します。

実際に、イベントがどのように発生するかを、以下にのようなデモでみてみます。

左のテキストエリアには、ドラッグしているノード(青色のCircle)で発生したMouseEventとMouseDragEventを表示し、右のテキストエリアには、ドラッグしているノード以外のノード(緑色のPane)で発生したMouseDragEventを表示しています。

Simpre press-drag-release gestureでは、ドラッグ対象のノードに対して、MouseEventが以下のように発生しています。

MOUSE_PRESSED
MOUSE_DRAGGED
MOUSE_DRAGGED
...
MOUSE_DRAGGED
DRAG_DETECTED
MOUSE_DRAGGED
...
MOUSE_DRAGGED
MOUSE_RELEASED

Full press-drag-release gestureでは、mouseTransparentをfalseのままの場合は、ドラッグ対象のノードに対して、MouseDragEventが以下のように発生しています。

MOUSE_PRESSED
MOUSE_DRAGGED
MOUSE_DRAGGED
...
MOUSE_DRAGGED
DRAG_DETECTED
MOUSE_DRAG_ENTERED
MOUSE_DRAGGED
MOUSE_DRAG_OVER
MOUSE_DRAGGED
MOUSE_DRAG_OVER
...
MOUSE_DRAGGED
MOUSE_DRAG_OVER
MOUSE_DRAG_RELEASED
MOUSE_RELEASED
MOUSE_DRAG_EXITED

ドラッグが終了したときのイベント発生順は、MOUSE_DRAG_RELEASED -> MOUSE_RELEASED -> MOUSE_DRAG_EXITEDの順になっているようですので、ドラッグ終了時の処理を行う場合は、この順に着目して行うとよさそうです。

Full press-drag-release gestureでmouseTranparentをtrueにした場合は、ドラッグ対象のノードに対しては、MouseDragEventは発生せず、ドラッグ対象のノード以外のノードに対して、MouseDragEventが以下のように発生しています。

MOUSE_DRAG_ENTERED
MOUSE_DRAG_OVER
MOUSE_DRAG_OVER
...
MOUSE_DRAG_OVER
MOUSE_DRAG_RELEASED
MOUSE_DRAG_EXITED

MOUSE_DRAG_RELEASEDイベントは、対象となるノード上でマウスをリリースしたときのみ発生しています。

以上で簡単ですが、MouseDragEventがどのように発生するかがわかりましたので、次回はFull press-drag-release gestureを利用したものを作成していきたいと思います。

2013年4月7日日曜日

JavaFX - Dragging gestures (2)

前回は、Simple press-drag-release gestureを使って、ドラッグでノードを移動する動作についてみていきました。今回は、この動作を行うユーティリティクラスを作成してみたいと思います。また、前回は、マウスのカーソルがシーンの外に移動した場合に、ノードがシーンの外に出てしまっていましたので、今回はその部分についても改善してみたいと思います。

まずは、ドラッグに関する情報を保持するDragContextクラスを作成します。このクラスでは、ドラッグ対象のノードとMOUSE_PRESSEDイベント発生時の位置を保持し、MOUSE_DRAGGEDイベント時のマウスのシーン上での位置を指定して、ノードの位置を変更するようにします。また、マウスのカーソルが移動できる領域を表すノードを指定して、その領域内でのみドラッグできるようにもします。

DragContextクラスのインスタンスの生成は、MOUSE_PRESSEDイベント発生時に、以下のようにして生成できるようにします。

DragContext context = DragContext.of(node)
    .atMouseSceneLocation(sceneX, sceneY);

また、マウスのカーソルが移動できる領域を表すノードを指定する場合は、以下のようにして生成できるようにします。

DragContext context = DragContext.of(node)
    .boundTo(boundingNode)
    .atMouseSceneLocation(sceneX, sceneY);

MOUSE_DRAGGEDイベント発生時には、以下のようにして、ノードを移動します。

context.dragNodeForMouseSceneLocation(sceneX, sceneY);

このとき、マウスのカーソルが移動できる領域を表すノードを指定している場合は、マウスのカーソルがそのノードの外にある場合は、ノードの位置を移動しないようにします。また、マウスのカーソルが移動できる領域を表すノードを指定しなかった場合は、マウスのカーソルがシーンの外にある場合は、ノードの位置を移動しないようにします。

public final class DragContext {
    private final Node dragNode;
    private final Node boundingNode;
    private final double dragAnchorX;
    private final double dragAnchorY;

    private DragContext(Builder builder) {
        this.dragNode = builder.dragNode;
        this.boundingNode = builder.boundingNode;
        this.dragAnchorX = builder.dragAnchorX;
        this.dragAnchorY = builder.dragAnchorY;
    }

    public static Builder of(Node node) {
        return new Builder(Objects.requireNonNull(node));
    }

    public void dragNodeForMouseSceneLocation(double sceneX, double sceneY) {
        if (!canDragNodeForMouseSceneLocation(sceneX, sceneY)) { return; }

        dragNode.setLayoutX(sceneX - dragAnchorX);
        dragNode.setLayoutY(sceneY - dragAnchorY);
    }

    private boolean canDragNodeForMouseSceneLocation(double sceneX, double sceneY) {
        if (boundingNode != null) {
            return boundingNode.getLayoutBounds().contains(
                boundingNode.sceneToLocal(sceneX, sceneY)
            );
        }

        Scene scene = dragNode.getScene();
        Bounds sceneBounds = new BoundingBox(0, 0, scene.getWidth(), scene.getHeight());
        return sceneBounds.contains(sceneX, sceneY);
    }

    public static final class Builder {
        private final Node dragNode;
        private Node boundingNode;
        private double dragAnchorX;
        private double dragAnchorY;

        private Builder(Node node) {
            dragNode = node;
        }

        public Builder boundTo(Node node) {
            boundingNode = node;
            return this;
        }

        public DragContext atMouseSceneLocation(double sceneX, double sceneY) {
            dragAnchorX = sceneX - dragNode.getLayoutX();
            dragAnchorY = sceneY - dragNode.getLayoutY();
            return new DragContext(this);
        }
    }
}

次に、ノードのマウスイベントにハンドラを追加し、ドラッグ時にノードの移動を行う処理を行うDraggableクラスを作成します。

Draggableクラスのインスタンスの生成は、ドラッグ対象のノードを指定して、以下のように生成できるようにします。

Draggable draggable = Draggable.forNode(node)
    .enable();

また、マウスのカーソルが移動できる領域を表すノードを指定する場合は、以下のようにして生成できるようにします。

Draggable draggable = Draggable.forNode(node)
    .boundTo(bindingNode)
    .enable();

マウスのカーソルが移動できる領域を表すノードが、ドラッグ対象のノードの親の場合は、以下のようにして生成できるようにします。

Draggable draggable = Draggable.forNode(node)
    .boundToParent()
    .enable();

また、FXMLで、以下のように指定できるように、DragNodeプロパティとBoundingNodeプロパティを定義しておきます。

<Draggable dragNode="$dragNode" boundingNode="$boundingNode"/>

さらに、マウスのカーソルが移動できる領域を表すノードを指定しない(マウスのカーソルが移動できる領域をシーン内とする)場合は、以下のようにスタティックプロパティで指定できるようにもしておきます。

<Node Draggable.enable="true"/>

あとは、DragNodeプロパティにノードを指定したときに、MOUSE_PRESSED, MOUSE_DRAGGED, MOUSE_RELEASEDイベントに、DragContextを使ってノードを移動するハンドラを追加します。

上記のようにして利用できるDraggableクラスを使って、以下のような、ノードの位置をドラッグで移動するものを作成してみます。

左の赤のCircleでは、マウスのカーソルが移動できる領域をCircleの親のノードに指定し、右の青のCircleでは、マウスのカーソルが移動できる領域を指定していません。

まずは、FXMLで以下のようにViewを定義します。

<Scene xmlns:fx="http://javafx.com/fxml"
       fx:id="scene"
       width="640" height="480">
    <stylesheets>
        <URL value="@DraggableDemoStyle.css"/>
    </stylesheets>

    <HBox>
        <Pane fx:id="boundingPane" styleClass="pane">
            <Circle fx:id="boundedDraggableCircle"
                    centerX="300" centerY="200" radius="30"/>
            <fx:define>
                <Draggable dragNode="$boundedDraggableCircle" boundingNode="$boundingPane"/>
            </fx:define>
        </Pane>

        <Pane HBox.hgrow="ALWAYS" styleClass="pane">
            <Circle fx:id="unboundedDraggableCircle"
                    Draggable.enable="true"
                    centerX="100" centerY="50" radius="30"/>
        </Pane>
    </HBox>
</Scene>

最初のCircleでは、DraggableクラスのインスタンスにDragNodeとBoundingNodeを指定し、次のCircleでは、スタティックプロパティで指定しています。

次に、コントローラクラスを定義します。

public class DraggableDemoSceneController {
    @FXML
    private Scene scene;

    public void performOn(Stage stage) {
        stage.setScene(scene);
        stage.setTitle("Draggable Demo");
        stage.sizeToScene();
        stage.centerOnScreen();
        stage.show();
    }
}

Draggableクラスで、ドラッグ時の処理を行っていますので、コントローラクラスでは特に何も処理を行いません。

あとは、Applicationクラスを作成しておきます。

public class DraggableDemo extends Application {
    @Override
    public void start(Stage primaryStage) {
        FXController.of(new DraggableDemoSceneController())
            .fromDefaultLocation()
            .load()
            .performOn(primaryStage);
    }

    public static void main(String... args) {
        launch(args);
    }
}

最後に、以下のように、複数のノードを作成して、それらをドラッグで移動するものを作成してみます。

右クリックで、Circleをクリックした位置を中心に作成し、それをドラッグで移動できるようにしています。

public class SimplePressDragReleaseGestureDemo extends Application {
    private final Random random = new Random();

    @Override
    public void start(Stage stage) throws Exception {
        stage.setScene(makeScene());
        stage.setTitle("Simple press-drag-release gesture Demo");
        stage.sizeToScene();
        stage.centerOnScreen();
        stage.show();
    }

    private Scene makeScene() {
        return SceneBuilder.create()
            .stylesheets(getClass().getResource("SimplePressDragReleaseGestureDemoStyle.css").toExternalForm())
            .width(640).height(480)
            .root(
                PaneBuilder.create()
                    .onMouseClicked(new EventHandler() {
                        @Override
                        public void handle(MouseEvent mouseEvent) {
                            if (mouseEvent.getButton() != MouseButton.SECONDARY) { return; }

                            Pane pane = (Pane)mouseEvent.getSource();
                            Color color = Color.color(random.nextDouble(), random.nextDouble(), random.nextDouble());
                            Circle circle = CircleBuilder.create()
                                .centerX(mouseEvent.getX()).centerY(mouseEvent.getY())
                                .radius(30)
                                .stroke(color)
                                .fill(
                                    RadialGradientBuilder.create()
                                        .centerX(0.3).centerY(0.3)
                                        .stops(
                                            StopBuilder.create()
                                                .color(color.brighter())
                                                .offset(0)
                                                .build(),
                                            StopBuilder.create()
                                                .color(color.darker())
                                                .offset(1)
                                                .build()
                                        )
                                        .build()
                                )
                                .scaleX(0).scaleY(0)
                                .build();
                            Draggable.setEnable(circle, true);
                            pane.getChildren().add(circle);
                            ScaleTransitionBuilder.create()
                                .node(circle)
                                .toX(1).toY(1)
                                .duration(Duration.millis(300))
                                .build()
                                .play();
                        }
                    })
                    .build()
            )
            .build();
    }

    public static void main(String... args) {
        launch(args);
    }
}

Simple press-drag-release gestureを使って、ドラッグでノードを移動する動作を行うユーティリティクラスを作成してみました。ノードにこの動作を指定する方法として、Draggableクラスのインスタンスにノードを指定する場合と、スタティックプロパティで指定する場合の両方で指定できるようにしてみました。ノードに対して、何か振る舞いを追加したい場合は、これらの方法を利用することができますので、他の振る舞いを追加したい場合にも使ってみたいと思います。次回は、Full press-drag-release gestureについてみていきたいと思います。

JavaFX - Dragging gestures (1)

JavaFXのドラッグの動作についてみていきたいと思います。

JavaFXのドラッグの動作には、以下の3つのタイプがあります。

  • Simple press-drag-release gesture
  • Full press-drag-release gesture
  • Platform-supported drag-and-drop gesture

これらの動作は、MOUSE_PRESSEDイベントで開始し、MOUSE_RELEASEDイベントで終了することになり、Simple press-drag-release gestureがデフォルトの動作になります。

まずは、Simple press-drag-release gestureからみていきたいと思います。

この動作については、ノードの大きさを変更したり、ノードの位置をドラッグして移動したりするときのように、対象となるノードのみの操作で、他のノードと関連しないような操作を行いたいときに利用します。

MOUSE_PRESSEDイベントが発生したノードに対して、MOUSE_RELEASEDイベントが発生するまで、そのノードに対してマウスイベントが発生しますので、基本的には、MOUSE_PRESSEDイベントで、ドラッグしたいノードに対して初期処理を行い、MOUSE_DRAGGEDイベントで、ドラッグしたときの処理を行い、MOUSE_RELEASEDイベントで、終了処理を行うことになります。

以下のような、1つのノードの位置をドラッグで移動するものを作成してみたいと思います。

シーン上に1つのCircleがあり、それを操作する単純なものになります。

まずは、FXMLで以下のようにViewを定義します。

<Scene xmlns:fx="http://javafx.com/fxml"
       fx:id="scene"
       width="640" height="480">
    <stylesheets>
        <URL value="@NodeDragDemoStyle.css"/>
    </stylesheets>

    <Pane>
        <Circle fx:id="draggableCircle"
                centerX="100" centerY="100" radius="30"/>
    </Pane>
</Scene>

次に、コントローラクラスを定義し、Circleのドラッグに対する動作を実装します。

public class NodeDragDemoSceneController {
    @FXML
    private Scene scene;
    @FXML
    private Circle draggableCircle;

    private double dragAnchorX;
    private double dragAnchorY;

    public void performOn(Stage stage) {
        stage.setScene(scene);
        stage.setTitle("Node Drag Demo");
        stage.sizeToScene();
        stage.centerOnScreen();
        stage.show();
    }

    @FXML
    protected void initialize() {
        draggableCircle.setOnMousePressed(new EventHandler<MouseEvent>() {
            @Override
            public void handle(MouseEvent mouseEvent) {
                Node node = (Node)mouseEvent.getSource();
                dragAnchorX = mouseEvent.getSceneX() - node.getLayoutX();
                dragAnchorY = mouseEvent.getSceneY() - node.getLayoutY();
            }
        });

        draggableCircle.setOnMouseDragged(new EventHandler<MouseEvent>() {
            @Override
            public void handle(MouseEvent mouseEvent) {
                Node node = (Node)mouseEvent.getSource();
                node.setLayoutX(mouseEvent.getSceneX() - dragAnchorX);
                node.setLayoutY(mouseEvent.getSceneY() - dragAnchorY);
            }
        });
    }
}

ノードの位置を変更するには、layoutXとlayoutYを変更することによって行っています。ドラッグしたときのマウスの位置の変化分をlayoutXとlayoutYに反映するようにしています。

MOUSE_PRESSEDイベントで、マウスのシーンに対する位置からノードのlayoutX, layoutYを引いた値を保持しておき、MOUSE_DRAGGEDイベントで、ドラッグしたときのマウスのシーンに対する位置から保持していた値を引くことによって、ドラッグ開始時のlayoutX, layoutYに、ドラッグしたときのマウスの移動量を追加するようにしています。今回は、終了処理として特に何も行いませんので、MOUSE_RELEASEDイベントは設定しませんでした。

あとは、Applicationクラスを作成しておきます。

public class NodeDragDemo extends Application {
    @Override
    public void start(Stage primaryStage) throws Exception {
        FXController.of(new NodeDragDemoSceneController())
            .fromDefaultLocation()
            .load()
            .performOn(primaryStage);
    }

    public static void main(String... args) {
        launch(args);
    }
}

FXControllerについては、こちらの記事を参照してください。

ドラッグの動作として、Simple press-drag-release gestureについてみていきました。ドラッグの動作としては基本的に、MOUSE_PRESSEDイベントで初期処理、MOUSE_DRAGGEDイベントでドラッグ中の処理、MOUSE_RELEASEDイベントで終了処理を実装すればよさそうです。次回は、このドラッグでノードを移動する動作を、コントローラクラスに直接記述するのではなく、再利用可能となるようなユーティリティクラスとして作成することを考えてみます。