JTreeノードのD&D(JAVA6)
ここではJTreeにおけるD&D操作について解説したい。
ただし、JAVA6を使用する。
これはJAVA6においてJTreeが次のような修正をされたためだ。
・ノードをドラッグしたときに、ドラッグ元の選択状態はそのままでドラッグ先がハイライト表示される。
・ノード間へのドロップが可能になった。これはドラッグ時のドラッグ先表示でも表示される。
D&D操作はこの2点を除きJAVA5と同じだ。
D&Dの操作を実装するにはいくつかの方法がある。
swingではD&Dの複雑な処理を簡単に行えるための仕組みが用意されているのでこれを使うのがいいだろう。
またJAVA6で追加された機能はこの方式でなければ使うことができない。
従ってここではこの方式のみを説明する。
●概念の説明
まず最初に覚えておかなければならないことがある。
swingの転送方式ではD&D操作はデータ転送をするためのもの
という考え方を元にした設計がされているということだ。
そのため、D&Dの表示だけを行うというのは出来ない。必ずデータ転送が関わってくる。
従ってデータ転送についても知っておかなければならない。
●処理概要
今回のJTreeではドラッグの許可属性を設定するだけで、
実はドラッグの表示が行われる。
しかし、どこにドラッグしても全て受け入れ拒否のアイコンが表示されてしまう。
この表示を変えるには適切なデータ転送処理をしなければならない。
ドラッグを開始するとそのノードに対して、「転送データの提供要求」が行われる。
このとき何も提供されないと、
転送するものが存在しない
↓
転送は必要ない
↓
ドラッグ表示は行われない
となる。
つまり、ドラッグをさせたいなら何らかの転送データを提供しなければならない。
逆にドラッグをさせたくないなら何も提供しなければいい(nullを返す)。
ただし、デフォルトの動作ではドラッグしたノードのテキストが転送される。
ドラッグの表示をするだけならともかく、ノードの移動などをするには使えない。
同じ文字列で別のノードのとき、それらのノードの区別がつかないからだ。
そのため、「転送データの提供」も定義する必要がある。
システムは、ドラッグ元から転送データの提供を受けたら、
ドラッグ先に対し「転送データの受け入れ判定」を行う。
この結果に応じて受け入れ拒否などのアイコンを表示する。
JTreeのデフォルト状態では転送データの提供は行われているが、全てのデータを受け入れ不可としているため、
先に説明したような動作となった。
●転送データ
転送データはJAVAのオブジェクトをポンと渡して終了と言うわけではない。
データ転送の仕組みは、ネイティブアプリケーションとの間でも行えるように設計されている。
そのため、いろいろと複雑だ。
例えば動画ファイルをドラッグする場合、ドラッグ開始と共に動画ファイルを全て読み込むのは効率がよくないし、
動画のように巨大なものをメモリに格納するのは現実的ではない。
ならば、動画ファイルの場所のパス名の文字列を転送するとしよう。
すると、パス名文字列そのものが転送された場合と区別が付かなくなる。
そのため、データと共にデータのタイプも必要になってくる。
この考え方からできたものが、Transferableオブジェクトだ。
ここでは次の3つのメソッドが定義されている。
Object getTransferData(DataFlavor flavor) 転送されるデータを表すオブジェクトを返します。
DataFlavor[] getTransferDataFlavors() データを提供することができるフレーバを示す DataFlavor オブジェクトの配列を返します。
boolean isDataFlavorSupported(DataFlavor flavor) 指定されたデータフレーバが、このオブジェクトに対してサポートされているかどうかを返します。
DataFlavorというのはデータのタイプを表すコンスタント・オブジェクトで、基本的にはMIMEタイプを使用する。
Transferableを実装済みで汎用的に使えるクラスは定義されてないので、自分で定義しなければならない。
今回はTreeNode配列を転送する専用クラスを作る。
/**
*/
class TreePathTransferable
implements Transferable
{
TreePath[] paths;
DataFlavor[] dflv;
/**
*/
public TreePathTransferable(TreePath[] paths){
this.paths = paths;
dflv = new DataFlavor[]{new DataFlavor(TreePath[].class, "array of TreePath")};
}
/**
*/
public Object getTransferData(DataFlavor flavor){
return paths;
}
/**
*/
public DataFlavor[] getTransferDataFlavors(){
return dflv;
}
/**
*/
public boolean isDataFlavorSupported(DataFlavor flavor){
DataFlavor[] flv = getTransferDataFlavors();
for(int i = 0 ; i < flv.length ; i++){
if(flv[i].equals(flavor)){
return true;
}
}
return false;
}
}
●転送処理制御
さて核心部分である。
「転送データの提供要求」等の処理は、javax.swing.TransferHandlerで行う。
これを適切に書き換えなければならない。
●転送データの提供要求
protected Transferable createTransferable(JComponent c) を上書きする。
protected Transferable createTransferable(JComponent c){
if(!(c instanceof JTree)){
return null;
}
JTree jtree = (JTree)c;
TreePath[] paths = jtree.getSelectionPaths();
return new TreePathTransferable(paths);
}
やっていることは簡単で、選択されているノードからTransferableを作成するだけだ。
実はこれだけではダメで、getSourceActionsメソッドもオーバーライドしてやる必要がある。
public int getSourceActions(JComponent c) {
return MOVE;
}
●転送データの受け入れ判定
canImportメソッドが呼び出される。
受け入れ可能なら、trueを返せばよい。
D&Dの表示だけならこれでできるはずだ。
しかし、実際にD&Dによって移動処理をしようとすると、いくつかテクニックが必要となってくる。
例えば、ドロップ先としてドラッグしたノード指定するのは、拒否するようにしたい。
この時の判定として、
if(dstNode == srcNode){
return false;
}
としたのでは失敗する。
それは、転送されてきたオブジェクトは、転送用として送り出したオブジェクトのコピーだからだ。
しかも、DEEPコピーだ。
そのため、オブジェクト比較では必ずfalseとなってしまう。
ではどうやって、転送元と転送先が同じだと判断したらいいか。
そのままでは、打つ手なしだ。
ノード自体に仕組みを持たせるしかない。
つまり、各ノードにノードIDを持たせ、これを比較することで判断する。
ノードIDというのは各ノードで違う値を持たなければならない。
どうやって割り振るか?
ここはObjectクラスのhashCode()メソッドを使おう。
さらに、hashCode()メソッドはノードIDを返すように変更し、equalsメソッドもノードIDの
比較で判断するようにする。
class MyTreeNode
extends DefaultMutableTreeNode
{
int nodeid;
public MyTreeNode(){
super();
nodeid = super.hashCode();
}
@override
public int hashCode(){
return nodeid;
}
@override
public boolean equals(Object obj){
if(!(onj instanceof MyTreeNode)){
return false;
}
MyTreeNode node = (MyTreeNode)obj;
if(node.nodeid == nodeid){
return true;
}
return false;
}
public int getNodeid(){
return nodeid;
}
}
ノードには全てこのクラスを使用する。
受け入れ判定時に、
if(srcNode.equals(dstNode)){
return false;
}
とすればよい。
また、転送されてきたオブジェクトは元のDEEPコピーだ。
つまり、元のツリーとコピーのツリーとが存在することになる。
そのため、転送先が転送元の下位ノードであった場合拒否をするという処理をする際も注意しなければならない。
if(srcnode.isAcsnded(dstNode)){
return false;
}
これでは判定できない。
srcNodeとdstNodeは別のツリーに所属しているからだ。
従って、ソースツリーの中のdstNodeに相当するノードを取得しなければならない。
/** root1からのツリーの中から、target2と等価のノードオブジェクトを取得する
*/
public MyTreeNode findNodeInTree(MyTreeNode root1, MyTreeNode target2){
if(root1.equals(target2)){
return root1;
}
MyTreeNode parent2 = (MyTreeNode)target2.getParent();
MyTreeNode parent1 = findNodeInTree(root1, parent2);
int index = parent1.getNodeIndex(target2);
MyTreeNode target1 = parent1.getChildAt(index);
return target1;
}