木難しいのはツリー (1) [JTreeのいろは: JTree, TreeNode, TreePath]
2002-06-23
Windowsのエクスプローラのフォルダツリーのようなインタフェースを作る時に使用するのが、SwingのJTreeです。木構造を操作するインタフェースは、いろいろなところで使用されているため、こんなことがしたい、あんな機能を組み込みたいと、イメージがふくれがちになり、多くの期待を持ってしまいます。JTreeを使えば、そのうちのかなりの部分は実現できるでしょう。高い自由度を持たせた作りになっているわけですが、しかし、それだけに少し凝った機能を実現しようと思うと、途端に理解するのが難しくなるのも事実です。
たとえば、JTreeの枝の追加や付け替えくらいですと、基本の延長線上だと言えます。ですが、表示されているイメージやツールティップ、フォーカスのコントロールなど、ちょっと凝ろうと思うと、多くの関連したクラスが芋蔓式に出てきてしまいます。
ということで、JTreeを使うためのヒントに関するトピックをメモしておきましょう。
■木を茂らせる
JTree自体は、木の構造を表示して、操作できるようにするためのコンポーネントのクラスです。コンポーネントのクラスですから、基本的なことですが、タブ(JTabbedPane)やパネル(JPanel)などコンテナに追加して、レイアウトやスクロールなどコントロールされる対象となります。JTree自身には、コンストラクタで木の一番根本とその直下の子供を作る以外に、木構造を構築する機能は備わっていません。
JTree上に木の構造を組み立てる際には、以下のようなクラスを使います。TreeNodeは木の構造にアクセスするためのインタフェースです。1つの親の節(node)が、複数の子供の節を持つ構造を繰り返すことで、全体として木を構成します。TreeNodeの操作は、木構造を取得するために、JTreeも呼び出します。一方で、MutableTreeNodeの操作は主に、JTree以外から呼び出されることになります。JTreeでは、TreeNodeはすべて、DefaultMutableTreeNodeのインスタンスになります。
| TreeNode |
一つの親と複数の子供を持ち、木の枝一つ一つの構造を保持するためのインタフェース |
| MutableTreeNode |
子供の追加と削除、親の指定といった、木の構造を変更するための操作をTreeNodeに追加したインタフェース |
| DefaultMutableTreeNode |
MutableTreeNodeのデフォルトの実装 |
JTreeでは、枝の追加や削除は一般的に、親のTreeNodeのメソッドを呼び出して、子供のTreeNodeを指定して実行します。また、別途作成したTreeNodeを既存のTreeNodeの子供としてはめ込むことで、枝の追加や付け替えを実現することができます。木構造を組み立てる処理の例は、以下のようになります(memo09ex1.java)。
001: ...
002: MutableTreeNode
column = new
DefaultMutableTreeNode( "Column" );
003: column.insert(
new DefaultMutableTreeNode( "JavaMemo" ), 0 );
004: column.insert(
new DefaultMutableTreeNode( "Diary" ), 1 );
005:
006: DefaultMutableTreeNode
root = new DefaultMutableTreeNode( "SwingSwinging.com" );
007: root.add(
column );
008: root.add(
new DefaultMutableTreeNode( "Desktop Java" ) );
009:
010: JTree
tree = new JTree( root
);
011:
012: JFrame
frame = new JFrame( "Create Tree" );
013: frame.getContentPane().add(
tree );
014: ...
|
| memo09ex1.java |
TreeNodeで木構造を組み立てる |
002では、あえて、MutableTreeNodeの変数にDefaultMutableTreeNodeのインスタンスを代入しています。003で、MutableTreeNodeのinsertメソッドで子供の枝を追加しています。007では、DefaultMutableTreeNodeのaddメソッドで子供を追加しています。MutableTreeNodeには、最低限のメソッドしか用意されていませんが、DefaultMutableTreeNodeには、変更のための様々なメソッドが実装されています。010で、JTreeのコンストラクタの引数に作成したDefaultMutableTreeNodeのインスタンスを渡しています。
JTreeは、MVC(Model-View-Controller)と呼ばれるクラスの役割分担では、ViewとControllerに相当するといえます。ModelはTreeModelとして別に用意されており、役割分担上、JTreeから直接Modelにアクセスして、木構造を変えるメソッドは準備されていません。
既にある木構造に枝を追加する場合には、Modelから木の根(root)となるノードを取得し、親となる枝を探すことになります。Modelの取得にはJTreeのgetModelメソッドを使用し、TreeModelのgetRootメソッドで木の根のTreeNodeを取得します。Modelに関するクラスには、以下のようなものがあります。
| TreeModel |
木構造を格納するデータモデルを定義するためのインタフェース |
| DefaultTreeModel |
TreeModelのデフォルトの実装 |
枝を選択してボタンを押下すると、選択された枝が親の枝と交換される、というプログラムを作ってみました(memo09ex2.java)。以下はそのコンストラクタでの処理で、説明のために、一部、ソースを変更しています。003でTreeModelを取得し、005で木の根であるTreeNodeを取り出しています。009から017で、処理対象となる枝を探して枝を追加する、ということを行っています。020のreloadメソッドを呼び出して、枝の変更をJTreeに反映させます。
001: ...
002: JTree
tree = new JTree( new DefaultMutableTreeNode( "SwingSwinging.com"
) );
003: TreeModel
model = tree.getModel();
004:
005: TreeNode
root = (TreeNode)model.getRoot();
006:
007: ( (DefaultMutableTreeNode)root
).add( new DefaultMutableTreeNode( "Column" ) );
008: ...
009: Enumeration
enum = root.children();
010: DefaultMutableTreeNode
node;
011: while(
enum.hasMoreElements() ) {
012: node
= (DefaultMutableTreeNode)enum.nextElement();
013:
014: if(
node.getUserObject().equals(
"Column" ) ) {
015: node.add(
new DefaultMutableTreeNode( "JavaMemo" ) );
016: node.add(
new DefaultMutableTreeNode( "Diary" ) );
017: }
018: ...
019: }
020: ( (DefaultTreeModel)model
).reload();
|
| memo09ex2.java |
TreeModelからTreeNodeを取得して木構造を組み立てる |
TreeModelのgetRootメソッドは、なぜかObject型で返すため、TreeNodeに代入するためには、キャストが必要になります。JTreeの理解を難しくさせている理由として、Object型での受け渡しが多数行われる、ということが挙げられます。中には、Object型である必然性が薄いと思われる部分もいくつかあります。
■小道と枝の見えない関係
JTreeで、選択されている枝を取得したり、枝が開いて子供の枝が表示状態になっているかどうか調べたり、といったことをする場合に、TreePathというオブジェクトが登場してきます。
| TreePath |
一連のTreeNodeのつながりを格納 |
JTreeで操作を行っている中でTreePathを利用することには問題ないのですが、TreeModelで木の構造を変更したりし始めると、思わぬ疑問をもつことになります。TreePathとTreeNodeの接点がどこにあるのか、という疑問です。それぞれのAPIドキュメントを眺めても、一方に他方のクラス名が全く出てきません。
それを調べるためのプログラムを作ってみました(memo09ex3.java)。このプログラムでは、ウインドウ内のJTreeで選択された枝を格納したTreePathを分解して、中身を表示します。表示の中身は、以下のようになっています。
- "Added..."で始まる行
TreePathのtoStringメソッドの実行結果
- "Path Component: "で始まる行
TreePathのgetPathメソッドで取得したObject型配列の要素のそれぞれについて、クラス名及び、toStringメソッドの実行結果
- "LastPathComponent: "で始まる行
TreePathのgetLastPathComponentメソッドで取得したObjectのクラス名及び、toStringメソッドの実行結果
|
E:\develop\test\jtree>java memo09ex3
Added New to Selection: [SwingSwinging.com]
Path Component: javax.swing.tree.DefaultMutableTreeNode,
Value: SwingSwinging.com
LastPathComponent: javax.swing.tree.DefaultMutableTreeNode,
Value: SwingSwinging .com
Added New to Selection: [SwingSwinging.com, Desktop
Java, Not ReadMe]
Path Component: javax.swing.tree.DefaultMutableTreeNode,
Value: SwingSwinging.com
Path Component: javax.swing.tree.DefaultMutableTreeNode,
Value: Desktop Java
Path Component: javax.swing.tree.DefaultMutableTreeNode,
Value: Not ReadMe
LastPathComponent: javax.swing.tree.DefaultMutableTreeNode,
Value: Not ReadMe
Added New to Selection: [SwingSwinging.com, Desktop
Java]
Path Component: javax.swing.tree.DefaultMutableTreeNode,
Value: SwingSwinging.com
Path Component: javax.swing.tree.DefaultMutableTreeNode,
Value: Desktop Java
LastPathComponent: javax.swing.tree.DefaultMutableTreeNode,
Value: Desktop Java
|
| memo09ex3.java |
TreePathの中身 |
TreePathやDefaultTreeModelなどのソースを見るとよりよくわかるのですが、TreePathはTreeNodeの配列をWrapしたクラスということになります。TreePathとTreeNodeを相互に変換する方法をまとめると、以下のようになります。JTreeのgetSelectionPathメソッドでTreePathを取得した場合、TreePathのgetLastPathComponentメソッドを使用すれば、選択されているTreeNodeを知ることができます。
| TreePath
→ TreeNode |
| TreePath.getPath() |
戻り値のObject型の配列がTreeNodeを格納。添え字の0が根(root)のノード。 |
| TreePath.getLastPathComponent() |
格納している一連のTreeNodeの末端のノードを返す |
| TreeNode
→ TreePath |
| TreeNode.getParent() |
親のノードを取得することを繰り返し、根(root)にたどり着いたら、配列に格納して、TreePathを生成 |
| DefaultMutableTreeNode.getPath() |
根(root)までのTreeNodeの配列を返すので、そのままTreePathを生成 |
このようにして、相互に変換はできるわけですが、プログラムを書く上では、どちらかに統一してしまった方がわかりやすいと言えます。MorePasteの1.1でJTreeを使っている部分では、JTreeの処理に合わせて、TreePathでやりとりを行うようにしています。どちらを使うべきか、ということについては、JTreeとTreeModelどちらに関係した処理が多いのかによるので、一概には言えません。
TreePathは生成する必要があり、その際には、木構造をトラバースする必要がありますから、比較的重い処理だと言えます。一方で、TreeNodeは木構造を保持すること自体に使われていますから、あえて生成しなくても木構造があれば既に存在しています。ただし、TreeNodeに統一すると、TreePath→TreeNode→TreePathという変換が発生する可能性があり、オーバーヘッドが発生します。
このようにTreePathとTreeNodeは、相互に変換できるわけですが、APIドキュメントからは、それはうかがい知れません。TreePathのgetPathメソッドなどは、TreeNodeの配列を返してもいいはずで、必要以上に使われているObject型での受け渡しが、わかりにくくさせていると言えます。TreePathからは、TreeNodeのメソッドを一切呼び出すことはなく、中身がTreeNodeであろうとなかろうと問題ない、ということでObject型なのかもしれませんが、何となく、納得できないものがあります。
基本の延長線上、と表現した部分だけで、長くなってきてしまいましたので、一旦、この辺で終わりにしたいと思います。。表示されているイメージやツールティップについては、SwingのチュートリアルのJTreeに関する節がわかりやすいでしょう。次回では、TreeNodeに格納されるオブジェクトと枝の同一性に関する話しや、キーストロークへの反応、TreeModelについてのトピックに触れていきたいと思います。

|