S-JIS[2009-03-26] 変更履歴

TransferHandler(Swing)

TransferHandlerは、Swingで外部とのデータ転送(ドラッグ&ドロップおよびコピー・カット&ペースト)を扱うクラス(JDK1.4以降)。

他からのドロップを扱うだけならAWTのDropTargetというクラスもあるが、SwingのコンポーネントはTransferHandlerをサポートしているので、TransferHandlerを使う方が良い。


概要

Swingのコンポーネント(JTreeやJTable等)は、ドラッグ&ドロップの機能を備えている。
この為に、それぞれのコンポーネント用のデフォルトのTransferHandlerサブクラスが用意されている。

自分でドラッグ&ドロップやクリップボードのコピー&ペーストの独自処理を行いたい場合は、TransferHandlerを継承したクラスを 自分で作り、コンポーネントに登録すればよい。

TransferHandlerの登録にはJComponent#setTransferHandler()を呼び出す。
getTransferHandler()で、現在登録されているTransferHandlerインスタンスを取得することも出来る。

import javax.swing.TransferHandler;
		JTree tree = new JTree();
		tree.setTransferHandler(new MyTransferHandler());

すると、そのコンポーネントに対してドラッグやらドロップやらコピーやらペーストやらといった操作が行われた際に、登録したTransferHandlerの各メソッドが呼ばれるようになる。
TransferHandlerにはいくつかのメソッドがあり、必要なものをオーバーライドする。

目的 メソッド 呼ばれるタイミング 呼ばれた時に行うべき処理
ドラッグ exportAsDrag() ドラッグを開始しようとした時。 デフォルトで下記のメソッドを呼び出す為、普通はオーバーライドする必要は無い。
getSourceActions()を呼び出し、ドラッグ可否の判定を行う。
OKであれば、マウスカーソル等の形を変更したり
createTransferable()を呼び出して転送データを取得したりする)
DragHandlerの
dragDropEnd()
ドラッグ終了時。 TransferHandler$DragHandlerのメソッドなので、オーバーライド不可。
exportDone()を呼び出す。ドロップ成功時はCOPYやMOVE、失敗時はNONE)
ドロップ canImport() ドラッグ中。
(かなり頻繁に呼ばれる)
その場所でキーが離された場合にドロップ可能かどうかを返す。
DropHandlerの
drop()
ドロップ操作を行った時。
(つまりマウスが離された時)
TransferHandler$DropHandlerのメソッドなので、オーバーライド不可。
canImport()を呼び出してドロップが受け付けられるかどうか判定を行う。
OKであれば、ドロップイベントに正常終了を登録する)
コピー
カット
exportToClipboard() コピーやカットを行う時。
(つまりCtrl+CCtrl+Xが押された時)
デフォルトで下記のメソッドを呼び出す為、普通はオーバーライドする必要は無い。
getSourceActions()を呼び出し、コピー&カット可否の判定を行う。
OKであれば、createTransferable()を呼び出し、転送用データをクリップボードへ登録する)
ペースト pasteActionの
actionPerformed()
貼り付けを行う時。
(つまりCtrl+Vが押された時)
TransferHandler#pasteActionのメソッドなので、オーバーライド不可。
(クリップボードから転送データを取り出し、importData()を呼び出す)
共通 getSourceActions() データ転送可否の判定が必要な時。 データ転送を受け付ける種類(複写・移動・リンク)を返す。
NONEを返すとドラッグ・コピー・移動(カット)不可となる。
共通
(出力)
createTransferable() ドラッグの開始時。
コピーやカットを行う時。
データ転送用オブジェクトを返す。
nullを返すと「ドラッグ不可」「クリップボードへの転送不可」となる。
exportDone() ドラッグ・コピー・カットの終了時。 処理が「移動(切り取り)」だった場合に元データを削除する。
共通
(入力)
importData() ドロップ時。
(つまりマウスが離された時)
ペースト(貼り付け)を行う時。
(つまりCtrl+Vが押された時)
転送データを読み込み、コンポーネントへの反映を行う。
正常に反映できた場合はtrue、失敗した場合はfalseを返す。

ドラッグ処理

自コンポーネントからドラッグする。(他のドロップ可能コンポーネントにデータを転送する)

例えばJTreeのデフォルト処理では、ノード(枝要素・葉要素)をドラッグして、ワードパッドやらMS-Wordやらといった外部アプリケーションにドロップすることが出来る。

ドラッグする為には、ドラッグを行いたいコンポーネント(JTree等)に対してTransferHandlerを登録する。
その他に、例えばJTreeではsetDragEnabled(true)を呼び出して“ドラッグ可能”にしておく必要がある。

ユーザーがドラッグ開始に該当する操作を行うと、まずTransferHandler#exportAsDrag()が呼び出される。
このメソッドは、getSourceActions()を呼び出してドラッグ開始できるかどうか判定し、OKであればマウスカーソルをドラッグ用に変更したりといった準備作業を行う。


独自のドラッグ処理を行いたい場合、まずgetSourceActions()をオーバーライドする。
このメソッドは、ユーザーがドラッグ動作を行った際に、まず最初に呼ばれる。
そして、そのコンポーネントでどんな種類のドラッグが可能かどうかを示す値を返す。

class DragTransferHandler extebds TransferHandler {

	@Override
	public int getSourceActions(JComponent c) {
		// return NONE;
		// return MOVE;
		// return COPY;
		return COPY_OR_MOVE;
		// return COPY_OR_MOVE | LINK;
	}

このメソッドでは、COPY・MOVE・LINKのいずれか、又はそれらのビット論理和の値を返す。
NONEを返した場合はドラッグ動作は行われない。
(オーバーライドしない場合のデフォルトは、プロパティー指定の場合はCOPY、それ以外はNONE)

COPYやMOVE単独だと、コピーのみ、あるいは移動のみしか行えない。
コピーの場合はマウスカーソル(のアイコン)に「+」マークが付く。

COPY_OR_MOVEは、コピーと移動のどちらも行えることを意味する。
ユーザーがCtrlキーを押している場合はコピー、そうでない場合は移動という扱いになる。
COPY_OR_MOVEの実際の値は、COPYとMOVEのビット論理和(つまり「COPY | MOVE」)である。


次に、createTransferable()をオーバーライドする。
このメソッドは、ドラッグして他コンポーネントへ受け渡す為の転送データを作成する。

import java.awt.datatransfer.StringSelection;
import java.awt.datatransfer.Transferable;
	@Override
	protected Transferable createTransferable(JComponent c) {
		JTree tree = (JTree) c;
		TreePath path = tree.getSelectionPath();
		if (path != null) {
			// ツリーのノードのパス文字列をデータとして転送する例
			return new StringSelection(path.toString()); //Transferableを実装しているクラス
		}
		return null;
	}

引数cには、このDragTransferHandlerが登録されているコンポーネントのインスタンスが入ってくる。
通常は1つのTransferHandlerを複数のコンポーネントには登録しないだろうから、クラス名を決め打ちしてキャストしてしまっていいだろう。

戻り値はTransferableインターフェースのインスタンス。上の例ではStringSelection(文字列を転送する)を返している。
nullを返した場合はドラッグ動作は行われない。
ドロップ先に独自形式のデータを転送したい場合は、Transferableを実装したクラスを自作し、当メソッドでそのインスタンスを返すようにする。


ドロップ処理

他コンポーネントや他アプリケーションからドラッグ開始されたデータをドロップで受け取る。

AWTにDropTargetというクラスがあるので、そちらを使えばドロップ処理を行うことが出来る。
TransferHandlerのドロップ処理も、実際にはDropTargetを継承したクラス(TransferHandler$SwingDropTarget)がTransferHandlerのメソッドを呼び出すようになっている。
なので、DropTargetの独自クラスを作ってデフォルトのDropTarget(TransferHandler$SwingDropTarget)の代わりにコンポーネントに登録すると、TransferHandlerのドロップ処理は呼ばれなくなる。


ドロップ処理の実装では、まずcanImport()をオーバーライドする。

canImport()には、引数が「JComponentとDataFlavor配列である」ものと「TransferSupportである」ものがある。
後者はJDK1.6で追加されたメソッドで、デフォルトでは前者のメソッドを呼び出すようになっている。

概念イメージ:
	public boolean canImport(TransferSupport support) {
		return canImport((JComponent)support.getComponent(), support.getDataFlavors());
	}

情報量は後者のメソッドの引数TransferSupportの方が多いので、JDK1.6以降なら後者をオーバーライドするのがいいだろう。

このメソッドでは、trueを返すとドロップ許可、falseを返すとドロップ不可となる。
ドロップ不可の場合、マウスカーソル(アイコン)が禁止マークになる。

JTreeで、ドロップ先が葉ノードのときだけドロップ許可する例:

import javax.swing.TransferHandler;
class DropTransferHandler extebds TransferHandler {

	@Override
	public boolean canImport(TransferSupport support) {
		// JTree tree = (JTree) support.getComponent();
		javax.swing.JTree.DropLocation loc = (javax.swing.JTree.DropLocation) support.getDropLocation();
		TreePath path = loc.getPath();
		TreeNode node = (TreeNode) path.getLastPathComponent();
		return node.isLeaf();
	}

TransferSupportはTransferHandlerの内部クラスなので、TransferHandlerを継承したクラスからは特にimportしなくてもクラス名だけで記述できる。
逆に、DropLocationという名前のクラスもTransferHandlerの内部クラスに有るので、JTreeの内部クラスであるDropLocationを使いたい場合、「import javax.swing.JTree.DropLocation」を書いても単純なクラス名「DropLocation」だけでは指定できない。FQCNあるいは「JTree.DropLocation」で指定する必要がある。
(ちなみに、JTree$DropLocationは、TransferHandler$DropLocationのサブクラス)

転送データが文字列の場合だけドロップ許可する例:

	@Override
	public boolean canImport(TransferSupport support) {
		DataFlavor[] dfs = support.getDataFlavors();
		for (DataFlavor df : dfs) {
			if (DataFlavor.stringFlavor.equals(df)) {
				return true;
			}
		}
		return false;
	}

転送データがファイル一覧だったら移動だけ許可する例:

			if (DataFlavor.javaFileListFlavor.equals(df)) {
				return support.getDropAction() == MOVE;
			}

getSourceActions()ではCOPYやMOVEのビット論理和を返しているが、canImport()時点では現在の状態がどれか1つに決まっているので、「==COPY」で比較してよい。


次に、importData()をオーバーライドする。
これもcanImport()と同じく二種類のメソッドがあるので、JDK1.6で導入された方をオーバーライドする。

概念イメージ:
	public boolean importData(TransferSupport support) {
		return importData((JComponent)support.getComponent(), support.getTransferable());
	}

importData()は、ドロップ処理だけでなく、クリップボードのペースト処理の時も呼ばれる。
したがって、ドロップ処理かどうかを判断して処理を行う。

	@Override
	public boolean importData(TransferSupport support) {
		if (support.isDrop()) {
			// ドロップ処理
			int action = support.getDropAction();
			if (action == COPY || action == MOVE) {
				Transferable t = support.getTransferable();
				〜
			}
			return true;
		}
		return false;
	}

TransferSupport#getDropAction()がCOPYになるのは、getSourceActions()が“COPYのみを返している場合”と、“COPYとその他を含んだ値を返していて、かつCtrlキーが押されていた場合”になる。

転送されたデータを取得する処理そのものは、クリップボードから値を取得するロジックと似た感じ。

//文字列データを取得する例
				Transferable t = support.getTransferable();
				try {
					String str = (String) t.getTransferData(DataFlavor.stringFlavor);
					〜
				} catch (UnsupportedFlavorException e) {
					return false;
				} catch (IOException e) {
					return false;
				}

なお、getTransferable()で得られるTransferableは、ドラッグの開始時にcreateTransferable()で返されるTransferableと同じ。とは限らない。
自分のコンポーネントからドラッグしてドロップする場合はたぶん同一インスタンスだが、クリップボード経由や他アプリケーションからの場合は“Transferableプロキシー”の場合がある。
したがって、自分でTransferableの実装クラスを作ったとしても、instanceofで判断したりキャストしたりしてはいけない。
あくまでもTransferable#isDataFlavorSupported()で判断すべき。


最後に、exportDone()をオーバーライドする。
このメソッドは処理が終わったときに呼ばれる。デフォルトでは無処理。
操作が出来なかった場合もaction=NONEでこのメソッドが呼ばれる。
action=MOVEだった場合に、コンポーネントからデータを削除するのが主な役割だと思われる。
したがって、MOVEをサポートしない(getSourceActions()がMOVEを含まない)場合は当メソッドをオーバーライドする必要は無い。

引数 内容
第1引数 JComponent source コンポーネント
第2引数 Transferable data 転送データ。処理されなかった場合はnullも有り得る。
第3引数 int action どういう処理が終わったかを示す。COPY・MOVE・LINK・NONE

ドロップ操作が正常に終わった場合、コピー扱いだった場合はCOPY、移動扱いだった場合はMOVE。失敗だった場合はNONE。
(移動扱い(MOVE)の場合は、移動なので、元データを削除する必要がある)

Ctrl+Cによるコピー(複写)処理が正常に行われた場合はCOPY、Ctrl+Xによるカット(切り取り)処理が正常に行われた場合はMOVE。失敗だった場合はNONE。
(カット(MOVE)の場合は、切り取りなので、元データを削除する必要がある)
COPYやMOVEだった場合は、このメソッドが呼ばれる前にクリップボードへのデータ転送は終わっている。

したがって、MOVEの場合は、コンポーネントから該当データを削除する(当メソッドでは削除だけ行えばよい)。
第2引数が転送データ(クリップボードに登録されたデータ、すなわちcreateTransferable()で返されたデータ)。


コピー&カット処理

Ctrl+CCtrl+Xを押すと、コピー&カット処理(クリップボードへのデータ登録)が行える。

コピー&カットをサポートするコンポーネントでは、Ctrl+CやCtrl+Xが押されるとTransferHandler#exportToClipboard()を呼び出すようになっている。
このメソッドはgetSourceActions()を呼び出してコピーやカットが出来るかどうか判定し、OKであればcreateTransferable()を呼び出してデータ転送用オブジェクトを取得し、それをクリップボードに登録する。
その後、exportDone()を呼び出す。

つまり、プログラマーはcreateTransferable()でデータ転送オブジェクトを返す部分だけ作ればよく、後のクリップボード関連はデフォルト処理がやってくれる。

getSourceActions()createTransferable()exportDone()はドラッグ&ドロップ処理でも呼ばれるメソッドなので、そちらを参照。
(つまり、ドラッグ複写+ドロップはコピー&ペースト、ドラッグ移動+ドロップはカット&ペーストに該当する)


ペースト処理

Ctrl+Vを押すと、ペースト処理(クリップボードからデータを取得し、コンポーネントへ反映させる)が行える。

貼り付けをサポートするコンポーネントでは、Ctrl+Vが押されると、クリップボードからデータを取得してTransferHandler#importData()に渡すようになっている。
importData()自体はドロップ処理でも呼ばれるメソッドである。(ドロップと異なり、事前にcanImport()は呼ばれない)
ただし、貼り付け処理の場合は、ドロップ専用のsupport.getDropAction()getDropLocation()といったメソッドは使用できない。(呼び出すとIllegalStateException("Not a drop")が発生する)

	@Override
	public boolean importData(TransferSupport support) {
		if (!support.isDrop()) {
			// ペースト(貼り付け)処理
			〜
			return true;
		}
		return false;
	}

ドロップやコピー/カットでは、処理が終わると(成功でも失敗でも)最後にexportDone()が呼ばれる。
しかしペーストの場合は、(成功でも失敗でも)exportDone()は呼ばれない。
…まぁ、ペーストはデータ取り込みであって、出力(export)じゃないからなー。


プロパティー指定のコンストラクター

TransferHandlerのコンストラクターには、プロパティー名を引数にとるものがある。

Swingのコンポーネントは内部データをプロパティーという形で持っている。
例えばnew TransferHandler("text")という指定だった場合、上述の各メソッドのデフォルト処理は、コンポーネントのgetText()・setText()メソッドを呼び出してデータの取得・設定を行うようになっている。
JEditorPaneJTextPane等は編集している文字列をgetText()・setText()で取得・設定できるが、まさにそれと同じメソッドが呼ばれることになる。

データ転送の処理がそのプロパティーの値を取得したり設定したりするだけである場合、このコンストラクターが便利なのだろう。


Swing目次へ戻る / Java目次へ戻る
メールの送信先:ひしだま