木難しいのはツリー (2) [JTreeのにほへ: JTree, TreeNode, TreePath]
2002-11-26
前回に引き続き、JTreeを使うためのヒントです。前回は、JTreeの木構造の組み立て方やTreePathの使い方といったJTreeの基本についてご紹介しました。今回は、TreePathとTreeNodeの関係など、JTreeを使う上で今まで悩んできたことをベースにお話ししたいと思います。また、JTreeはノード(node:
節)にあらゆるオブジェクトを格納することができるため、JTableと並んで、データを保持するための構造として機能することができます。このような観点でもJTreeを見ていきます。
■小道と枝の見えない関係
JTreeで操作を行っている中でTreePathを利用することには問題ないのですが、TreeModelで木の構造を変更したりし始めると、困った状況に出くわします。TreePathを取得しているのに、メソッドの引数にTreeNodeを渡さないといけない場合や、その逆の場合です。TreePathとTreeNodeは、無関係でないことは疑う余地もないのですが、その接点がどこにあるのか、APIドキュメントからはわかりません。なんと言っても、それぞれのAPIドキュメントを眺めても、一方に他方のクラス名が全く出てこないのです。
それを調べるためのプログラムを作ってみました(memo10ex1.java)。このプログラムでは、ウインドウ内のJTreeで選択された枝を格納したTreePathを分解して、中身を表示します。表示の中身は、以下のようになっています。
- "Added..."で始まる行
TreePathのtoStringメソッドの実行結果
- "Path Component: "で始まる行
TreePathのgetPathメソッドで取得したObject型配列の要素のそれぞれについて、クラス名及び、toStringメソッドの実行結果
- "LastPathComponent: "で始まる行
TreePathのgetLastPathComponentメソッドで取得したObjectのクラス名及び、toStringメソッドの実行結果
|
E:\develop\test\jtree>java memo10ex1
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
|
| memo10ex1.java |
TreePathの中身 |
選択した部分からルート(root: 根)までの一連のTreeNodeが、Path Compoentの行に出てきています。LastPathComponentは、選択の操作をしたノードになっています。TreePathがあれば、getLastPathComponentメソッドを呼び出すことで、選択されたTreeNodeを取り出せることになります。なお、TreePathのgetLastPathComponentメソッドは、Object型を返しますから、型キャストが必要になります。
その逆は、どうすればいいのでしょうか。TreeNodeがわかっているときに、TreePathを取得する方法を考えてみましょう。TreePathのコンストラクタには、Object型の配列を引数としてとるものがありますから、これがTreeNodeの配列ではないか、というのは容易に想像がつきます。素直にJTreeのソースを見てしまうと、まさしくTreeNodeの配列をTreePathのコンストラクタの引数に渡して、インスタンスを取得しています。TreePathはTreeNodeの配列をWrapしたクラスということになります。
TreePathとTreeNodeを相互に変換する方法をまとめると、以下のようになります。JTreeのgetSelectionPathメソッドでTreePathを取得した場合、TreePathのgetLastPathComponentメソッドを使用すれば、選択されているTreeNodeを知ることができます。
| TreePath
→ TreeNode |
| TreePath.getPath() |
戻り値のObject型の配列がTreeNodeを格納。添え字の0がルートのノード |
| TreePath.getLastPathComponent() |
格納している一連のTreeNodeの末端のノードを返す |
| TreeNode
→ TreePath |
| DefaultTreeModel.getPathToRoot() |
ルートまでのTreeNodeの配列を返すので、TreePathのコンストラクタに渡してTreePath生成 |
| DefaultMutableTreeNode.getPath() |
ルートまでのTreeNodeの配列を返すので、TreePathのコンストラクタに渡してTreePath生成 |
| TreeNode.getParent() |
親のノードを取得することを繰り返し、ルートにたどり着いたら、配列に格納して、TreePathを生成 |
このようにして、相互に変換はできるわけですが、プログラムを書く上では、どちらかに統一してしまった方がわかりやすいと言えます。MorePasteの1.1でJTreeを使っている部分では、JTreeの処理に合わせて、TreePathでやりとりを行うようにしています。どちらを使うべきか、ということについては、JTreeとTreeModelどちらに関係した処理が多いのかによるので、一概には言えません。
TreeNodeはTreeModelとともに木構造を保持すること自体に使われていますから、あえて生成しなくてもJTreeがあれば既に存在しています。一方、TreePathは生成する必要があり、その際には、木構造をトラバースする必要がありますから、比較的重い処理だと言えます。こう考えると、TreeNodeを使った方がいいようにも思えますが、TreeNodeに統一してしまうと、TreePath→TreeNode→TreePathという変換が発生する可能性があり、かえってオーバーヘッドになる可能性もあります。
このようにTreePathとTreeNodeは、相互に変換できるわけですが、APIドキュメントからは、それはうかがい知れません。TreePathのgetPathメソッドなどは、TreeNodeの配列を返してもいいはずで、必要以上に使われているObject型での受け渡しが、わかりにくくさせていると言えます。TreePathからは、TreeNodeのメソッドを一切呼び出すことはなく、中身がTreeNodeであろうとなかろうと問題ない、ということでObject型なのかもしれませんが、理解するには時間がかかってしまいます。
■枝の中身
Object型が多用されていることの問題点を指摘したわけですが、それは内部的にはTreeNodeなど型にルールがあるにもかかわらず、それを明示的にしていないからです。一方で、型を定めておく必然性が無く、自由度を与えるために、Object型にしているところがあります。プログラム上は、型のチェックとキャストが必要になり、煩雑にはなりますが、データを格納する、という最も単純な使い方をする部分です。
Windowsのエクスプローラのように、フォルダ階層をツリー表示して、何らかのファイル操作を行う場合を考えてみます。TreeNodeに、単純に、フォルダやファイルの名称を文字列で保存するのではなく、Fileのインスタンスそのものを格納したり、URLを保持したり、と考えるのは容易に思いつくことです。また、いろいろなフラグの状態に応じて、ツリーのアイコンを変えたい場合に、その状態をどこに保持するかと考えてみると、ツリーに格納するということに素直に行き着くでしょう。
データを格納する構造としてのJTreeについて考えてみることにしましょう。DefaultMutableTreeNodeは、コンストラクタでObject型のuserObjectを引数にとることができます。これ自体は、Stringである必要はなく、どんなクラスのインスタンスでも問題ないわけです。ノードにFileのオブジェクトを格納して、ディレクトリツリーを表示するプログラムを作ってみました(memo10ex2.java)。
001: ...
002: public memo10ex2() {
003: File
root = new File("./root");
004: Hashtable
ret = new Hashtable();
005:
006: if(
root.isDirectory() )
007: ret.put(
root, getChildren( root )
);
008: else
009: ret.put(
root, root );
010: JTree
tree = new JTree( ret );
011: ...
012: }
013:
014: public Hashtable
getChildren( File parent ) {
015: File[]
dirs = parent.listFiles();
016: Hashtable
ret = new Hashtable();
017: for(
int i = 0; i < dirs.length; i++ ) {
018: if(
dirs[i].isDirectory() )
019: ret.put(
dirs[i], getChildren( dirs[i] )
);
020: else
021: ret.put(
dirs[i], dirs[i] );
022: }
023: return
ret;
024: }
025: ...
|
| memo10ex2.java |
Fileを格納したJTree |
003では、Fileクラスのインスタンスを作成しており、これがJTreeに格納されるフォルダ階層のルートになります。木の構造はHashtableに格納してJTreeに渡すことにし、004ではそのためのHashtableインスタンスを生成しています。006でルートとなるFileがフォルダを指している場合には、getChildrenメソッドを呼び出して、それ以下の階層を調べていくようにしてます。
getChildrenメソッドは、フォルダ指すFileオブジェクトを引数にとり、その直下を調べます。フォルダがあった場合には、再帰的にgetChildrenを呼び出し、Hashtableに格納して結果を返します。015で、引数で渡されて来たフォルダの中にあるFileオブジェクトの配列を取り出しています。017以降で、配列のそれぞれについて、それがフォルダの場合、再度、getChildrenメソッドを呼び出しています。(プログラム中の色のついた部分にマウスを合わせると注釈が表示されます。)
プログラムを実行すると、ルートのフォルダだけの状態でウインドウが開かれます。実行画面では、すべてのフォルダを開き、ウインドウのサイズを変えています。ルートからのパスが全て表示されており、あまり、うれしくない状態になっています。これは、表示用の文字列を取得する際に、FileオブジェクトのtoStringメソッドが使われているためです。これを解決するためには、以下のようなやり方が考えられます。
- TreeNodeに格納されたuserObjectの
toStringメソッドを定義する
- TreeNodeの
toStringメソッドを定義する
- JTreeを継承して、独自の
convertValueToTextメソッドを定義する
- javax.swing.tree.TreeCellRendererを実装したクラスで独自の
getTreeCellRendererComponentメソッドを定義する
最も容易なのは、userObjectのtoStringメソッドの動作を変えることで、これは、ラッパークラスを用意して、希望する文字列を返す処理をするように、toStringメソッドを宣言すれば事足ります。このようにすれば、userObject自体のtoStringメソッドが別の目的で使用されている場合にも対処できます。JTreeやTreeCellRendererで独自の処理を定義するのは、表示用の文字列を取得するためだけに行うのであれば、少し大げさです。当然、全てのノードが、そのように動作するようになりますから、影響を考えないと、思わぬ結果になってしまいます。
ラッパークラスでtoStringメソッドを定義して、表示を整えるようにしたプログラムを作ってみました(memo10ex3.java)。このプログラムでは、HashtableにFileを格納する際に、ラッパークラスとして作成したNodeWrapperで常にFileを包むようにしています。ラッパークラスを使うと、Hashtableから取り出す順序が変わってしまうため、使っていない場合とはツリーの構成が一致しなくなっています。順序と表示を完全にコントロールするためには、前回お話ししたとおり、TreeModelとTreeNodeを使うことになります。
JTreeをデータを保持する機能から見た場合、特徴的なのは、木構造という親子関係をデータ間で持たせながら格納することができるということです。Fileクラスのインスタンス同士は、パスを見れば親子関係はわかるにはわかります。では、すべてのFileオブジェクトで毎回調べるかというと、とてもではありませんがやっていられません。親子関係の保持のためだけにJTreeを使うとなるとそれはそれで問題がありますが、そういった点を意識して利用すれば、より効果的に使用することができるのではないでしょうか。

|