Java Memo <prev : index : next>

木難しいのはツリー (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で木構造を組み立てる

Creating Tree only manipulating with 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についてのトピックに触れていきたいと思います。

おしまい

Mail to author Mail to author. Top of this page.

Versions & Requirements.
Java2 Standard Edition Version 1.3.1_03
MorePaste 1.0/1.1
Keywords.
javax.swing.JTree, javax.swing.tree, javax.swing.tree.TreeNode, javax.swing.TreePath
Related Links.
Swing Tutorial [Top]: How to Use Trees
Java House ML Search [Top]: JTree

[This page was updated: 2003-03-09 ]

 

 
Copyright © 2001-2003 Takashi KOBAYASHI. All Rights Reserved.