How to implement Drag and Drop on JavaFX
JavaFX 8で,GUIの使い勝手を大幅に向上させる,ドラッグ・ドロップを勉強する。Oracleの以下の公式ガイドを参照した。内容は同一で,自分が理解しやすいように整理し直した。
概念
ドラッグ・ドロップに登場する概念を整理しておく。
オブジェクトとデータ型
ドラッグ・ドロップには2個のオブジェクトが登場する。
- ジェスチャーソース
- ドラッグ元
- ジェスチャーターゲット
- ドロップ先
これらのドラッグ・ドロップの対象となるのは,以下の2個のオブジェクトとなる。
- ノード
- シーン
ドラッグ・ドロップは以下の一連の動作を表す。
- ドロップ元でマウスボタンをクリック
- マウスをドラッグ
- ドロップ先にマウスを移動
- ボタンを離す
ドラッグ・ドロップはjavafx.scne.input.DragEventクラスが基本クラスとなる。
転送モード
ドラッグ元とドロップ先の間で実行される転送のタイプは,転送モードによって以下の3種類に決まる。
- COPY
- MOVE
- LINK
JavaFXだと,ドラッグ元がなんであっても特に気にしなくても大丈夫な模様。ファイルでもドロップを受け付けられた。
ドラッグ・ドロップの実装手順
大きく以下の6手順でドラッグ・ドロップを実装する。
ステップ | 手順概要 | イベント | コールメソッド | 備考 |
---|---|---|---|---|
1 | ドラッグ元とドロップ先オブジェクトを作成 | |||
2 | ドラッグ元オブジェクトを設定 | DRAG_DETECTED (setOnDragDetected) | startDragAndDrop | 転送モードを指定し,クリップボードに内容を格納する。 |
3 | ドロップ先オブジェクトを設定 | DRAG_OVER (setOnDragOver) | acceptTransferModes | ドラッグ・ドロップで受け付ける転送モードを指定する。 |
4 | ドラッグ・ドロップの視覚効果を設定 | DRAG_ENTERED (setOnDragEntered) DRAG_EXIT (setOnDragExite) | ||
5 | ドロップ後処理を設定 | DRAG_DROPPED (setOnDragDropped) | setDropCompleted | クリップボードの内容を受け取る。 |
6 | ドロップ完了処理を設定 | DRAG_DONE (setOnDragDone) |
この内必須なのは2, 3, 5の手順である。その他はおまけであるが,可能であれば実装したほうがよいだろう。
ドラッグ元とドロップ先オブジェクトを作成
ドラッグ・ドロップのドラッグ元とドロップ先のオブジェクトを作る。
// まずはオブジェクトを作成
final private Text source = new Text(50, 100, "DRAG ME");
final private Text target = new Text(50, 100, "DROP HERE");
WindowsのエクスプローラーやLinuxのNautilus,MacのFinderのようにファイルマネージャーからドラッグ・ドロップを行う場合は,ドラッグ元またはドロップ先のどちらかのオブジェクトは省略可能。
ドラッグ元オブジェクトを設定
ドラッグ元オブジェクトのDRAG_DETECTEDにイベントハンドラーを割り当てる。(ドラッグ開始の設定)
// ハンドラーを設定
// ジェスチャーソースのDRAG_DETECTEDイベントを処理する
source.setOnDragDetected(event -> dragDrop(event));
ドラッグドロップをstartDragAndDrop()で開始して,コンテンツをダッシュボードに格納する。
public void dragDrop(MouseEvent event){
// 転送モードを指定して,startDragAndDropメソッドでドラッグ・ドロップを開始
// COPY, MOVE, LINK, ANY, COPY_OR_MOVE, NONE
Dragboard db = source.startDragAndDrop(TransferMode.ANY);
// クリップボードを作成して格納
ClipboardContent content = new ClipboardContent();
content.putString(source.getText());
db.setContent(content);
event.consume();
}
ドロップ先オブジェクトを設定
DRAG_OVERイベント・ハンドラーを処理する。
// ジェスチャーターゲットのDRAG_OVERイベントを処理
target.setOnDragOver(event -> dragOver(event));
ダッシュボードとドラッグ中のオブジェクトが妥当であるかをチェックしてから,acceptTransferModesを実行する。
public void dragOver(DragEvent event){
// 現在のダッシュボードのデータ型から受け入れるか判定
// 最初のevent.getGesutureSourceでオブジェクトの判定
// getDragboard()でダッシュボードに格納されているデータにアクセス
if (event.getGestureSource() != target &&
event.getDragboard().hasString()){
// ドロップの受け入れ関数を実行
// 受け入れる転送モードを指定する
event.acceptTransferModes(TransferMode.MOVE);
}
event.consume();
}
ドラッグ・ドロップの視覚効果を設定
ドラッグジェスチャーがジェスチャーターゲット候補の境界に入ると,DRAG_ENTEREDイベントが発生する。また,境界から出るとDRAG_EXITEDイベントが送信される。これらのイベントハンドラーを利用して,ドラッグドロップの視覚効果を設定する。
// ドラッグドロップの視覚効果を設定
target.setOnDragEntered(event -> dragEntered(event));
target.setOnDragExited(event -> dragExited(event));
public void dragEntered(DragEvent event){
// ドラッグ対象範囲内に入った視覚効果を設定
if (event.getGestureSource() != target &&
event.getDragboard().hasString())
{
target.setFill(Color.GREEN);
}
event.consume();
}
public void dragExited(DragEvent event){
// ドラッグ対象範囲外に入った視覚効果を設定
target.setFill(Color.BLACK);
event.consume();
}
ドロップ後処理を設定
ドロップ先でDRAG_OVERイベントが受け入れられた後,マウスボタンが離されると,DRAG_DROPPEDイベントが発生する。イ ベント発生時にsetDropCompleted(Boolean)メソッドをコールしてドラッグ・ドロップジェスチャーを完了させる。これを呼ばないとジェスチャーは失敗とみなされる。
// ドロップ後の処理を設定
target.setOnDragDropped(event -> dragDropped(event));
public void dragDropped(DragEvent event){
// ドロップ時の処理を設定
Dragboard db = event.getDragboard();
final boolean HAS_DB_STRING = db.hasString();
if (HAS_DB_STRING){
target.setText(db.getString());
}
event.setDropCompleted(HAS_DB_STRING);
event.consume();
}
ドロップ完了処理を設定
ドラッグ・ドロップが完了したら,ドロップ元にDRAG_DONEイベントが送信される。このイベントハンドラーで,getTransferMode()により転送 モードを取得できる。転送モードがNULLの場合,転送が失敗したとみなされる。この転送モードは,受け入れ先の転送モードが使われるようだ。
// ドラッグドロップ完了時のドロップ元の設定
source.setOnDragDone(event -> dragDone(event));
public void dragDone(DragEvent event){
// ドロップ後のドロップ元の処理を設定
if (event.getTransferMode() == TransferMode.MOVE){
source.setText("");
}
event.consume();
}
サンプルアプリ
以下のリポジトリーに今回のサンプルコードと画像を格納している。
動作イメージ
サンプルアプリの動作イメージを以下に示す。
DRAG MEと表示された部分をドラッグして,DROP HEREに移動させると,DROP HEREにドラッグ元の内容が表示され,ドラッグ元は空文字に変更する。DROP HEREには外部からファイルをドロップすることもでき,その場合はそのファイルの絶対パスが表示される。
01. 起動直後 | 04. ドロップ後 |
02. ドラッグ開始 | 05. ファイルドラッグ |
03. ドロップオーバー時の視覚効果 | 06. ファイルドロップ後 |
参考ソース全体
/// \file DragDrop.java
package dragdrop;
import javafx.application.Application;
import javafx.scene.Group;
import javafx.scene.Scene;
import javafx.scene.input.ClipboardContent;
import javafx.scene.input.DragEvent;
import javafx.scene.input.Dragboard;
import javafx.scene.input.MouseEvent;
import javafx.scene.input.TransferMode;
import javafx.scene.paint.Color;
import javafx.scene.text.Text;
import javafx.stage.Stage;
public class DragDrop extends Application {
// まずはオブジェクトを作成
final private Text source = new Text(50, 100, "DRAG ME");
final private Text target = new Text(250, 100, "DROP HERE");
@Override
public void start(Stage stage) throws Exception {
// ジェスチャーソースのDRAG_DETECTEDイベントを処理する
source.setOnDragDetected(event -> dragDrop(event));
// ジェスチャーターゲットのDRAG_OVERイベントを処理
target.setOnDragOver(event -> dragOver(event));
// ドラッグドロップの視覚効果を設定
target.setOnDragEntered(event -> dragEntered(event));
target.setOnDragExited(event -> dragExited(event));
// ドロップ後の処理を設定
target.setOnDragDropped(event -> dragDropped(event));
// ドラッグドロップ完了時のドロップ元の設定
source.setOnDragDone(event -> dragDone(event));
// その他レイアウトなどの設定
stage.setTitle("Hello Drag and Drop");
Group root = new Group();
Scene scene = new Scene(root, 400, 200);
scene.setFill(Color.LIGHTGREEN);
source.setScaleX(2.0);
source.setScaleY(2.0);
target.setScaleX(2.0);
target.setScaleY(2.0);
root.getChildren().addAll(source, target);
stage.setScene(scene);
stage.show();
}
public void dragDrop(MouseEvent event){
// 転送モードを指定して,startDragAndDropメソッドでドラッグ・ドロップを開始
// COPY, MOVE, LINK, ANY, COPY_OR_MOVE, NONE
Dragboard db = source.startDragAndDrop(TransferMode.ANY);
// クリップボードを作成して格納
ClipboardContent content = new ClipboardContent();
content.putString(source.getText());
db.setContent(content);
event.consume();
}
public void dragOver(DragEvent event){
// 現在のダッシュボードのデータ型から受け入れるか判定
// 最初のevent.getGesutureSourceでオブジェクトの判定
// getDragboard()でダッシュボードに格納されているデータにアクセス
if (event.getGestureSource() != target &&
event.getDragboard().hasString()){
// ドロップの受け入れ関数を実行
// 受け入れる転送モードを指定する
event.acceptTransferModes(TransferMode.MOVE);
}
event.consume();
}
public void dragEntered(DragEvent event){
// ドラッグ対象範囲内に入った視覚効果を設定
if (event.getGestureSource() != target &&
event.getDragboard().hasString())
{
target.setFill(Color.GREEN);
}
event.consume();
}
public void dragExited(DragEvent event){
// ドラッグ対象範囲外に入った視覚効果を設定
target.setFill(Color.BLACK);
event.consume();
}
public void dragDropped(DragEvent event){
// ドロップ時の処理を設定
Dragboard db = event.getDragboard();
final boolean HAS_DB_STRING = db.hasString();
if (HAS_DB_STRING){
target.setText(db.getString());
}
event.setDropCompleted(HAS_DB_STRING);
event.consume();
}
public void dragDone(DragEvent event){
// ドロップ後のドロップ元の処理を設定
if (event.getTransferMode() == TransferMode.MOVE){
source.setText("DROP FINISH");
}
event.consume();
}
}