MY-NOTEBOOK

Tuesday, April 4, 2006

Java Swingによるドラッグアンドドロップ(drag-and-drop)の実装

Javaで実装する場合に限らず、一般的にドラッグアンドドロップの処理は、いろいろな要素(処理やメッセージの伝達)が関わってくるのでわかりにくい部分があります。
ざっと思いつくままに挙げても、以下のような点に注意してコーディングする必要があります。

  • ドラッグ元での処理
  • ドラッグ開始時の処理
  • ドロップ先での処理
  • ドロップしたときの処理(ドラッグ完了したときのドラッグ元での処理)
  • ドラッグ中の処理(ドロップ領域に入ったとか出たとか)

など。
しかし、ドラッグアンドドロップ中に起きる処理の流れ全体を把握して、そのそれぞれでどのクラスが関わってくるのかを把握できれば、Javaでの実装はかなり簡単です。
異なるJavaVM間やネイティブなプラットフォーム上のアプリケーションとのドラッグアンドドロップに対応しているので、これをマスターするとかなりおもしろいことができそうです。

このエントリーのコードをコンパル・実行するには、 Java SE 1.4.2 SDK が必要です。

はじめに

JavaのSwingのコンポーネントに関しては、ここで書いているのとは違う方法でもっと簡単にドラッグアンドドロップを実装することができます。
その方法で、多くの場合に対処できるとしても、細部まで自分でドラッグアンドドロップの振る舞いをコントロールしたり、自前のカスタムコンポーネントにドラッグアンドドロップを実装するには、その簡単な方法ではできないことがあります。

ここでは、自分でドラッグアンドドロップを完全にコントロールする方法での実装を説明しています。

サンプルコードリスト

(サンプル1) 一番単純なドラッグとドロップ

ドラッグアンドドロップをマスターするには、
まずドラッグとドロップとを完全にわけて考える(実装する)のがコツです。
ここでは、
ドラッグに関する処理はDragSourceLabelに、
ドロップに関する処理はDropTargetPanelに、それぞれ集約するように
コーディングしています。

結果

左のパネルから右のパネルにJLabelをDragAndDropするサンプルです。
左パネルはドラッグだけに対応し、
右パネルはドロップだけに対応しています。

ドラッグ前

“orange”ラベルをドロップしたところ

コード

すべてのコードをダウンロード:dnd_1.tar.gz

  • ドラッグ関係
    • DragSourceHostPanel.java ... DragSourceLabelをのせるパネル
    • DragSourceLabel.java ... ドラッグ対象となるラベル
  • ドロップ関係
    • "DropTargetPanel.java:#DropTargetPanel ... ドロップ対象となるパネル
  • その他
    • MyLabel.java
    • TestFrame.java ... テスト用クラス

ポイントの説明

DragSourceLabel ドラッグ対象のラベル

DragSourceLabel.java

これはドラッグ対象となるクラスです。
自分自身でドラッグに対応していて、
ドラッグジェスチャーを認識したら(dragGestureRecognizedメソッド)
転送するデータを用意して、ドラッグを開始します。

DragSourceLabel.java は、 MyLabel.java を継承していますが、MyLabelクラスは、説明上見た目をわかりやすくするために、JLabelにボーダーを追加しただけのクラスです。
MyLabel = JLabelと考えて差し支えありません。

DropTargetPanel ドロップ対象のパネル

DropTargetPanel.java

new DropTarget(this,action,dropTargetListener);

にて、DropTargetに、DropTargetPanelとDropTargetListnerを登録しています。
あとは、DropTargetListenerの drop() メソッドをオーバーライドして、ここに、ドロップイベントが起きたときの処理を記述します。

ここでは、ストリングフレーバーを持った転送オブジェクトがドロップされたかどうか調べた上で、テキストを取り出し、addDroppedText() メソッドを呼んでいます。

DropTargetPanelのaddDroppedText() 内では、受け取ったテキストを使ってラベルを新規に追加し、画面を更新して変更を反映させています。

ネイティブアプリからのドロップも受け付けます
このDropTargetPanelは、DataFlavor.stringFlavorな転送オブジェクトを受け付けるため、ここで示した以外のドロップも受け付けます。
たとえば、Linux上のFirefox上の文字を選択して、DropTargetPanel にドロップしても、ドロップは成功します。

(サンプル2) ドロップした位置にラベルを表示

サンプル1 を改良し、ドロップ時の位置を反映するサンプルです。
サンプル1 では、ドロップターゲットパネル上にラベルをドロップしたら、ドロップしたラベルは、FlowLayoutにしたがって配置されました。
したがって、ドロップ位置に関係なく、末尾に追加されます。
サンプル2では、ドロップした位置にラベルを表示するように変更しています。

結果

左のパネルから右のパネルにJLabelをDragAndDropするサンプルです。
左パネルはドラッグだけに対応し、右パネルはドロップだけに対応しています。

ドラッグ前

“apple”,“orange”,“lemon”ラベルをそれぞれドロップしたところ

ソースファイル

すべてのソースをダウンロード:dnd_2.tar.gz

  • ドラッグ関係
    • DragSourceHostPanel.java ... DragSourceLabelをのせるパネル
    • DragSourceLabel.java ... ドラッグ対象となるラベル
  • ドロップ関係
  • その他
    • MyLabel.java
    • TestFrame.java ... テスト用クラス

ポイントの説明

DropTargetPanel内で、ドロップした位置を把握して反映するのがポイントです。

DropTargetPanel.java

DropTargetListenerのdrop()メソッド内で、

Point droppedLocation=e.getLocation();

にて、ドロップ位置を把握します。
あとは、この位置情報に基づいて、ラベルを配置するだけです。

ラベルのサイズに注意
今回のDropTargetPanelは、こちらで指定した位置にラベルを描画するため、レイアウトマネージャを使用していません。(nullをセットしている)
この場合、JLabelなどをそのまま配置しても、JLabelのサイズが幅・高さとも0に設定されるため、一切描画されません。
明示的に setBounds() でサイズと位置を指定する必要があります。

(サンプル3) ドロップしたらドラッグ元から削除(DnDによるラベルの移動)

サンプル1 を改良し、ドロップが成功したら、ドラッグしたラベルをドラッグソースから削除します。
(つまり、移動したように見せかけます。)

サンプル1 では、ドラッグ元は、ドラッグジェスチャーを検出して、ドラッグをスタートさせるだけでそれ以上のことは何もしていませんでした。
サンプル3では、これを改良し、DragSourceListenerを使って、ドロップが成功したら、ドラッグ元パネルからドラッグしたラベルを削除する機能を追加します。

結果

左のパネルから右のパネルにJLabelをDragAndDropするサンプルです。
左パネルはドラッグだけに対応し、右パネルはドロップだけに対応しています。

ドラッグ前

“orange”ラベルをドロップしたところ

コード

すべてのソースをダウンロード:dnd_3.tar.gz

  • ドラッグソース関係
    • "DragSourceHostPanel.java:#DragSourceHostPanel_3 ... DragSourceLabelをのせるパネル
    • "DragSourceLabel.java:#DragSourceLabel_3 ... ドラッグ対象となるラベル
    • DragSourceLabelListener.java ... DragSourceLabelへのリスナ(ドロップ完了を観察)
  • ドロップターゲット関係
    • DropTargetPanel.java ... ドロップ対象となるパネル
  • その他
    • MyLabel.java
    • TestFrame.java ... テスト用クラス

ポイントの説明

ポイントは、ドラッグ時にDragSourceListenerを追加して、ドロップ完了を把握できるようにしておくことです。

今回のサンプルでは、DragSourceLabel内でDragSourceListenerを実装しているため、このままでは、DragSourceLabel 外からドロップ完了を把握できません。
しかし、やりたいことは、ドロップが完了したら、ドロップ対象だったラベルをドラッグソースパネル(DragSourceHostPanel) 上から削除することなので、DragSourceHostPanel内でドロップ完了を把握する必要があります。
そこで、DragSourceLabelListener を使って、DragSourceHostPanelから、DragSouceLabelを観察して、ドロップ完了を検出します。

DragSourceLabel.java

ドラッグを認識して、ドラッグをスタートさせるときに、
DragGuestureEventのstartDrag()メソッドをコールしますが、
このとき、オプションとして三番目の引数で、DragSourceListenerを指定できます。

DragSourceLabe.java

DragSourceLabelListener.java

ドラッグアンドドロップが完了した時点で、
ドラッグ対象になったラベルを通知します。

DragSourceLabelListener.java

DragSourceHostPanel.java

DragSourceLabelを観察して、削除通知を受け取ったら、
該当ラベルを削除します。

DragSourceHostPanel.java

(サンプル4)移動かコピーか、状況センシティブなドラッグアンドドロップの実装

サンプル3 を改良し、Ctrlキーを押しながらドラッグしたときは、コピーモードで、そうでない場合は、移動モードでドラッグアンドドロップが行われるようにします。

コード

すべてのソースをダウンロード:dnd_4.tar.gz

ポイントの説明

ポイント1 モード判定

ドラッグ開始時点でコピーか移動かを判別します。
判別方法はCtrlキーが押されているかどうか、ですが以下のコードで判定できます。

static private boolean isCtrlDown(InputEvent e){
	int mod=e.getModifiersEx();
	if( (mod & InputEvent.CTRL_DOWN_MASK) != 0){
		return true;
	}
	return false;
}
ポイント2 カーソル表現によるユーザへの通知

コピーモードでドラッグしているのか、移動モードでドラッグしているのか、ユーザに通知する必要があります。
それはドラッグ中のカーソルで表現します。

これは、DragGestureEventのstartDrag()メソッドの第一引数にカーソルを指定します。
このカーソルは、DragSourceオブジェクトのクラス変数として定義済なので、それを利用します。

  • 移動→DragSource.DefaultMoveDrop
  • コピー→DragSource.DefaultCopyDrop
ポイント3 モードによってラベルを削除するかどうか決める

ドロップ完了時点で、モードによってドラッグ対象ラベルを
ドラッグ元パネル(DragSourceHostPanel)上から削除するかどうかを
決めます。

これは、DragSourceLabel内で、
移動モードの場合だけ、
DragSourceLabelListenerのrequestRemove()メソッドを
呼び出すことで解決します。

DragSourceLabel.java

© 2006-2012 Tomoaki Oshima