木難しいのはツリー (1) [JTreeのいろは: JTree, TreeNode, TreePath]
2002-07-07
Windowsのエクスプローラのフォルダツリーのようなインタフェースを作る時に使用するのが、SwingのJTreeです。木構造を操作するインタフェースは、いろいろなところで使用されているため、あんなことがしたい、こんな機能を組み込みたいと、イメージがふくれがちになって、多くの期待を持ってしまいます。JTreeを使えば、そのうちのかなりの部分は実現できるでしょう。高い自由度を持たせた作りがそれを可能にしているわけですが、しかし、それだけに少し凝った機能を実現しようと思うと、途端に理解するのが難しくなるのも事実です。
JTree上に木の構造を組み立てたり、選択されている枝を取得したり、といった、基本的な機能であっても、最初は結構、苦労させられるでしょう。さらに、表示されているイメージやツールティップ、フォーカスのコントロールなど、ちょっと凝ろうと思うと、多くの関連したクラスが芋蔓式に出てきてしまいます。
ということで、JTreeを使うためのヒントをメモしておきましょう。
■木を茂らせる
JTree上に木を組み立てられないと何も始まりませんから、まずはそのやり方から見ていくことにしましょう。JTree上に木の構造を組み立てる際には、以下のようなクラスを使います。JTreeでは、1つの親のノード(node:
節)が、複数の子供のノードを持つ構造を繰り返すことで、全体として木を構成します。ノードの追加や削除など、木構造の変更は一般的に、親のノードのメソッドを呼び出して、子供のノードを引数に指定して実行します。
| TreeNode |
一つの親と複数の子供を持つ、木のノード一つ一つの構造を保持するためのインタフェース |
| MutableTreeNode |
子供の追加と削除、親の指定といった、木の構造を変更するための操作をTreeNodeに追加したインタフェース |
| DefaultMutableTreeNode |
MutableTreeNodeのデフォルトの実装 |
| JTree.DynamicUtilTreeNode |
子供を内部に格納し、必要に応じて生成する機能を持つノードの実装 |
| TreeModel |
木構造を格納するデータモデルを定義するためのインタフェース |
| DefaultTreeModel |
TreeModelのデフォルトの実装 |
TreeNodeはノードを構成する最も基本的なインタフェースです。MutableTreeNodeは、TreeNodeに木の構造を変更するためのメソッドを追加したインタフェースです。木の構造を別途作成しておいて、他のノードの子供としてはめ込むことで、枝の追加や付け替えを行うことができます。
MutableTreeNodeの実装には、DefaultMutableTreeNodeとJTree.DynamicUtilTreeNodeの2つのクラスがあります。後者は、前者を継承しており、子供のノードの生成に関する処理を中心に機能が追加されています。JTreeは、コンストラクタで初期の木構造を組み立てることができますが、その際にどちらが使用されるかは、コンストラクタへの引数によって変わってきます。引数として、配列やVector、Hashtableを渡した場合、木の構造は、JTree.DynamicUtilTreeNodeで保持されます。それ以外の場合には、DefaultMutableTreeNodeのインスタンスが使われます。
JTree.DynamicUtilTreeNodeを用いて、木構造を組み立てる処理の例を作成してみました(memo09ex1.java)。入れ子になったHashtableで、木構造を作成し、JTreeに登録するという手順です。TreeNodeのインスタンスは、全く作成しなくても木構造を組み立てることができます。
001: ...
002: Hashtable
root
= new Hashtable(), nodes1,
nodes2;
003: root.put(
"SwingSwinging.com", (
nodes1 = new Hashtable() ) );
004: nodes1.put(
"Column", ( nodes2 = new Hashtable() ) );
005: nodes2.put(
"JavaMemo", "JavaMemo" );
006: nodes2.put(
"Diary", "Diary" );
007: nodes1.put(
"Desktop Java", ( nodes2 = new Hashtable() ) );
008: nodes2.put(
"Not Readme", "Not Readme" );
009
010: JTree
tree = new JTree( root
);
011: ...
|
| memo09ex1.java |
HashtableとJTree.DynamicUtilTreeNodeで木構造を組み立てる |
002では、Hashtableの変数の宣言を行っています。処理の都合上、3つ宣言しています。003から008では、Hashtableの要素にHashtableを入れ子状に格納しています。これを繰り返すことで、木を構成しています。010では、木の全体を格納するHashtableを引数に渡して、JTreeのコンストラクタを呼び出しています。(プログラム中の色のついた部分にマウスを合わせると注釈が表示されます。)
このプログラムを実行すると、直後には、ルート(root: 根)の枝だけが表示されたウインドウを開きます。実行画面では、ノードをすべて開き、ウインドウのサイズを変えています。
一方、DefaultMutableTreeNodeを用いて、木構造を組み立てる処理は、以下のようになります(memo09ex2.java)。親のノードに子供のノードを追加する、という操作を繰り返し行っています。
001: ...
002: DefaultMutableTreeNode
root = new
DefaultMutableTreeNode( "SwingSwinging.com" );
003: MutableTreeNode
node = new DefaultMutableTreeNode( "Column" );
004: node.insert(
new DefaultMutableTreeNode( "JavaMemo" ), 0 );
005: node.insert(
new DefaultMutableTreeNode( "Diary" ), 1 );
006: root.add(
node );
007: node
= new DefaultMutableTreeNode( "Desktop Java" );
008: ( (DefaultMutableTreeNode)node
).add( new DefaultMutableTreeNode( "Not Readme" ) );
009: root.add(
node );
010:
011: JTree
tree = new JTree( root
);
012: ...
|
| memo09ex2.java |
DefaultMutableTreeNodeで木構造を組み立てる |
002から009で木の構造を組み立てています。004で、MutableTreeNodeのinsertメソッドで子供のノードを追加しています。006では、DefaultMutableTreeNodeのaddメソッドで子供を追加しています。MutableTreeNodeには、最低限のメソッドしか用意されていませんが、DefaultMutableTreeNodeには、変更のための様々なメソッドが実装されています。010で、JTreeのコンストラクタの引数にルートとなるDefaultMutableTreeNodeのインスタンスを渡しています。(プログラム中の色のついた部分にマウスを合わせると注釈が表示されます。)
プログラムを実行すると、2階層目までを開いた状態でウインドウを開きます。実行画面では、すべてのフォルダを開き、ウインドウのサイズを変更しています。
■ビミョーでキミョーな木
2つのプログラムの実行画面を比べると、いくつか違いがあることがわかります。
- ルートフォルダのハンドル
Hashtableで木を構成したmemo09ex1の方はルートフォルダのハンドルが表示されているが、memo09ex2の方は表示されていない
- ノードの表示順序
Hashtableで木を構成する場合、プログラムとは違う順序でノードが表示されている
- フォルダの開き方
起動直後のフォルダの状態は、Hashtableで木を構成した場合はルートだけだが、そうでない場合は2階層目まで表示される
JTreeのコンストラクタに配列やVector、Hashtableを指定する場合、JTreeは見かけ上、複数のルートを持つことができます。見かけ上、というのは、本来のルートを非表示にして、2段目の子供から表示することで実現しているためです。だからでしょうか、このような仕様になっているようです。だからといって、ルートのハンドルの表示を変える、というのも変な話しですね。このルートのハンドルの表示/非表示の切り替えは、JTreeのsetShowsRootHandlesメソッドで設定することができます。
ノードの表示順序は、Hashtableの特性上、順序を保持することができないため、しかたのない動作です。Java2SDKの1.4からは、インサートの順序性を保持できるMapができますから、HashtableではなくMapインタフェースを渡せれば、解消できる可能性はあります。しかしながら、1.4でもJTreeはMapを渡せるようにはなっていませんでした。
フォルダの開き方のちがいは、JTree.DynamicUtilTreeNodeは、子供のノードが必要になるまでその生成を行わないために起こります。子供のノードの生成タイミングをJTree.DynamicUtilTreeNodeに任せることのトレードオフと言えるでしょう。
JTree.DynamicUtilTreeNodeを使用するように処理を書くか、DefaultMutableTreeNodeを使うかは、以下のような要素をどこまで細かくコントロールしたいかによります。JTree.DynamicUtilTreeNodeを使用すると、どれかが制約を受けることになると言えます。
- 木構造の明確化、決定のタイミング
- ノードを表示する際の順序性
- ノード生成のタイミング
JTree.DynamicUtilTreeNodeを使って、かつ、順序性を保持した木構造は、JTree.DynamicUtilTreeNodeのcreateChildrenメソッドを使えば、組み立てることができます。同メソッドに配列やVectorを渡して、順次、木構造を定義していくわけですが、DefaultMutableTreeNodeを使う場合とそれほど変わらなくなってしまいますね。また、JTree.DynamicUtilTreeNodeは、必要になるまで子供のノードを生成しませんから、それまではcreateChildrenメソッドを呼び出すことはできません。
なお、JTree.DynamicUtilTreeNodeとDefaultMutableTreeNodeを混在させて使用すると、思わぬ木構造ができあがる可能性があります。前者が子供のノードを生成した後であれば、それほど問題はないかもしれませんが、どちらかに統一した方がわかりやすいと言えます。
■選んだり開いたり
JTreeで、選択されているノードを取得したり、ノードが開いて子供の枝が表示状態になっているかどうか調べたり、といったことをする場合に、TreePathというオブジェクトが登場してきます。
| TreePath |
一連のTreeNodeのつながりを格納 |
TreePathは、ルートからの一連のノードのつながり(パス)を格納して、1本の枝のように表現します。ノードを保持するTreeNodeは、親と子という関係しか持ちませんから、木構造の中でそれがどこに位置するのかはわかりません。TreePathは、ルートからの経路という形で、木構造の中での位置を特定するものだと言えます。
JTreeでは、TreePathを使用することで、木構造の中でどこが選択されているかを取得したり、また、木構造のある部分が表示されているかどうかをチェックしたりすることができるようになっているわけです。また、JTreeのイベントを処理するリスナでも、イベントの発生する操作が行われた部分を特定するために、TreePathが用いられます。
よく使いそうなTreePathに関連するJTreeのメソッドには、以下のようなものがあります。
| JTreeのTreePathを使うメソッド |
| collapsePath() |
引数で渡されたTreePathを閉じている状態にして、子供のノードが表示されないようにする |
| expandPath() |
引数で渡されたTreePathを開いている状態にして、子供のノードを表示状態にする |
| getPathForRow() |
行を指定して、TreePathを取得する |
| getSelectionPath() |
選択されているTreePathを取得する |
| isCollapsed() |
引数のTreePathが閉じているかどうか調べる |
| isExpanded() |
引数のTreePathが開いているかどうか調べる |
| isVisible() |
引数のTreePathが表示されているかどうか調べる |
| makeVisible() |
TreePathを表示状態にする |
| setSelectionPath() |
引数で渡されたTreePathを選択する |
Selectionに関するメソッドは、Pathの部分がPathsと複数形になったメソッドも用意されており、その場合、TreePathの配列を取得したり、渡したりします。
上記のメソッドのいくつかを使用して、JTreeの開いている枝をすべて表示するプログラムを作ってみました(memo09ex3.java)。枝を開いたり、閉じたりするごとに処理が呼び出され、枝の状態をチェックします。JTreeから、getPathForRowメソッドでTreePathを取得して、isExpandedメソッドで状態を調べています。isExpandedメソッドは、行を指定することもできますが、TreePathを使うためにあえて無駄な処理をしています。
JTree自体は、木の構造を表示して、操作するためのコンポーネントのクラスです。コンポーネントのクラスですから、基本的なことですが、タブ(JTabbedPane)やパネル(JPanel)などコンテナに追加して、レイアウトやスクロールなどコントロールされる対象となります。JTreeは、MVC(Model-View-Controller)と呼ばれるクラスの役割分担では、ViewとControllerに相当するといえます。ModelはTreeModelとして別にありますから、JTreeから直接Modelにアクセスして、木構造を変えるメソッドは用意されていないのは、役割分担上の理由、ということになります。
基本の延長線上、と表現した部分だけで、長くなってきてしまいましたので、一旦、この辺で終わりにしたいと思います。。表示されているイメージやツールティップについては、SwingのチュートリアルのJTreeに関する節がわかりやすいでしょう。次回では、TreeNodeとTreePathの関係や、TreeNodeに格納されるオブジェクトと枝の同一性に関する話し、TreeModelについてのトピックに触れていきたいと思います。

|