JavaFX ListView の要素を Drag&Dropで移動させてみた

f:id:hideoku:20130530000639p:plain:w400

Drag&Dropで参考となるサイト

JavaFX の Drag&Drop の基本を理解する上で参考になるのは次のサイトです。
英語ですが、登場人物がひとつずつ紹介されていて、サンプルコードも添付されています。

http://docs.oracle.com/javafx/2/drag_drop/jfxpub-drag_drop.htm

作ったサンプルの概要

ListViewで作ったリストの1項目をドラッグして、もうひとつのListViewにドロップすると、ドロップした側のリスト末尾にドラッグした値が追加されるサンプルを作りました。

・ListDrag.fxml(GUI部分)
・ListDragController.groovy(MVCのControllerあたり)
・ListDragApp.groovy(アプリ起動)
の3つのファイルで構成しています。
すべて同じパッケージ内にあって、sample パッケージに配置しています。

まずはソースコードをすべて書き出します。
パッケージ宣言は省略していますが、基本的に javafx パッケージ、もしくは javafx.scene パッケージになります。

ListDrag.fxml

<AnchorPane id="AnchorPane" maxHeight="-Infinity" maxWidth="-Infinity" minHeight="-Infinity" minWidth="-Infinity" 
    prefHeight="400.0" prefWidth="600.0" xmlns:fx="http://javafx.com/fxml" fx:controller="sample.ListDragController">
  <children>
    <ListView fx:id="listViewSrc" layoutX="42.0" layoutY="40.0" prefHeight="200.0" prefWidth="200.0" />
    <ListView fx:id="listViewDest" layoutX="331.0" layoutY="40.0" prefHeight="200.0" prefWidth="200.0" />
  </children>
</AnchorPane>

ListDragController.groovy

class ListDragController implements Initializable {

    @FXML ListView listViewSrc
    @FXML ListView<String> listViewDest

    javafx.collections.ObservableList<String> listRecordsSrc = FXCollections.observableArrayList()
    javafx.collections.ObservableList<String> listRecordsDest = FXCollections.observableArrayList()

    @Override
    void initialize(URL url, ResourceBundle resourceBundle) {

        (1..10).each {
            listRecordsSrc.add("item" + it)
        }
        listViewSrc.setItems(listRecordsSrc)

        (1..5).each {
            listRecordsDest.add("アイテム" + it)
        }
        listViewDest.setItems(listRecordsDest)

        listViewSrc.setOnDragDetected(new EventHandler<MouseEvent>() {
            @Override
            void handle(MouseEvent event) {
                String selectedValue = listViewSrc.getSelectionModel().getSelectedItem()
                println "drag detected : " + selectedValue

                Dragboard dragboard = listViewSrc.startDragAndDrop(TransferMode.COPY)
   
                ClipboardContent content = new ClipboardContent()
                content.putString(selectedValue)
                dragboard.setContent(content)

                event.consume()
            }
        })

        listViewDest.setOnDragOver(new EventHandler<DragEvent>() {
            @Override
            void handle(DragEvent event) {
                if (event.getGestureSource() == listViewSrc) {
                    event.acceptTransferModes(TransferMode.COPY)
                }
                event.consume()
            }
        })

        listViewDest.setOnDragDropped(new EventHandler<DragEvent>() {
            @Override
            void handle(DragEvent event) {
                Dragboard dragboard = event.getDragboard()
                if (dragboard.hasString()) {
                    listViewDest.getItems().add(dragboard.getString())
                }
                event.setDropCompleted(dragboard.hasString())

                event.consume()
            }
        })
    }
}

ListDragApp.groovy

class ListDragApp extends Application {

    @Override
    void start(Stage stage) {
        Parent root = FXMLLoader.load(getClass().getResource("ListDrag.fxml"))
        stage.setTitle("リストDrag&Dropサンプル")

        def width = 600
        def height = 300
        stage.setScene(new Scene(root, width, height))
        stage.show()
    }

    static void main(String[] args) {
        launch(ListDragApp.class, args)
    }
}

※ Groovy で書いているので、Java としてコピペするとそのまま動かない所はあります。

ドラッグ元リストの挙動を実装する

Drag&Drop を行う上でキモとなっているのは、Controller クラスです。

setOnDragDetected() といった、setOnDragXxxx() というメソッドに EventHandler をセットしていくことで Drag&Drop の挙動を実装していきます。
今回は、3つの setOnDragXxxx() を使いました。

まず、ドラッグ元のリスト(listViewSrc)で setOnDragDetected() を実装しています。

listViewSrc.setOnDragDetected(new EventHandler<MouseEvent>() {
    @Override
    void handle(MouseEvent event) {
        String selectedValue = listViewSrc.getSelectionModel().getSelectedItem()
        println "drag detected : " + selectedValue

        Dragboard dragboard = listViewSrc.startDragAndDrop(TransferMode.COPY)
   
        ClipboardContent content = new ClipboardContent()
        content.putString(selectedValue)
        dragboard.setContent(content)

        event.consume()
    }
})

これはリストの上でドラッグが開始されたときに行う処理です。
リストの選択行の値を取得して、Drag&Dropでつかんでいる内容として Dragboard にセットしています。
今 Drag&Drop しているのはこういう情報だというのを Dragboard や ClipboardContent を使って実装します。
ClipboardContent はその内容として文字列だけでなくファイルなどもセットできるので、ファイルの Drag&Drop も簡単に実現できそうです。

Dragboard に ClipboardContent をセットすると、Drag&Drop したときのマウスアイコンがファイルイメージに変わるようになります。

ちなみに、この Drag&Drop している内容は JavaFX だけに閉じているわけではありません。
たとえば、今回のように JavaFX 上で Drag 選択した文字列をそのままテキストエディタなどに Drop すると、その文字列がペーストされます。
試してはないですが、逆の Drag&Drop もおそらくできて JavaFX 上の要素にペーストできます(そのはず)。

こんなの簡単にできるなんて、なんかすごいなと。

ドロップ先リストの挙動を実装する

次に、ドロップ先のリスト(listViewDest)で setOnDragOver() と setOnDragDropped() を実装しています。

setOnDragOver() で Drag されてマウスオーバーしているのを検知した時の処理、setOnDragDropped() で Drop されたときの処理を実装します。

listViewDest.setOnDragOver(new EventHandler<DragEvent>() {
    @Override
    void handle(DragEvent event) {
        if (event.getGestureSource() == listViewSrc) {
            event.acceptTransferModes(TransferMode.COPY)
        }
        event.consume()
    }
})

listViewDest.setOnDragDropped(new EventHandler<DragEvent>() {
    @Override
    void handle(DragEvent event) {
        Dragboard dragboard = event.getDragboard()
        if (dragboard.hasString()) {
            listViewDest.getItems().add(dragboard.getString())
        }
        event.setDropCompleted(dragboard.hasString())

        event.consume()
    }
})

DragEvent#acceptTransferModes() メソッドで、Dragされてきた内容を受け取る方法を設定するみたいです。
メソッド引数に TransferMode.COPY や TransferMode.MOVE といった挙動を指定します。
COPY も MOVE も実行結果は見た目変わらないのですが…(詳細は違うのでしょうね)

Dragboard で保持されている文字列をドロップ先のリストの要素として末尾に追加しています。

まとめ

setOnDragXxxx() メソッドを適当にいじってみると、結構簡単に Drag&Drop が実現できました。
細やかな挙動を作りこんでいくとなると、悩むポイントが多くなりそうです。

今回はドロップ先のリストの末尾に追加するサンプルを作りましたが、
リストの任意の行に挿入することもできたので近々まとめたいと思います。

◯2013/05/31 追記:行挿入パターンもまとめました。
Drag&Dropで JavaFX ListView の任意の行に挿入させてみた - Java開発のんびり日記