Java Memo <prev : index : next>

木難しいのはツリー (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

Image of memo10ex2 running. Creating jtree including File Objects.003では、Fileクラスのインスタンスを作成しており、これがJTreeに格納されるフォルダ階層のルートになります。木の構造はHashtableに格納してJTreeに渡すことにし、004ではそのためのHashtableインスタンスを生成しています。006でルートとなるFileがフォルダを指している場合には、getChildrenメソッドを呼び出して、それ以下の階層を調べていくようにしてます。

getChildrenメソッドは、フォルダ指すFileオブジェクトを引数にとり、その直下を調べます。フォルダがあった場合には、再帰的にgetChildrenを呼び出し、Hashtableに格納して結果を返します。015で、引数で渡されて来たフォルダの中にあるFileオブジェクトの配列を取り出しています。017以降で、配列のそれぞれについて、それがフォルダの場合、再度、getChildrenメソッドを呼び出しています。(プログラム中の色のついた部分にマウスを合わせると注釈が表示されます。)

プログラムを実行すると、ルートのフォルダだけの状態でウインドウが開かれます。実行画面では、すべてのフォルダを開き、ウインドウのサイズを変えています。ルートからのパスが全て表示されており、あまり、うれしくない状態になっています。これは、表示用の文字列を取得する際に、FileオブジェクトのtoStringメソッドが使われているためです。これを解決するためには、以下のようなやり方が考えられます。

  • TreeNodeに格納されたuserObjecttoStringメソッドを定義する
  • TreeNodeのtoStringメソッドを定義する
  • JTreeを継承して、独自のconvertValueToTextメソッドを定義する
  • javax.swing.tree.TreeCellRendererを実装したクラスで独自のgetTreeCellRendererComponentメソッドを定義する

Image of memo10ex3 running. Creating jtree with wrapper class.最も容易なのは、userObjecttoStringメソッドの動作を変えることで、これは、ラッパークラスを用意して、希望する文字列を返す処理をするように、toStringメソッドを宣言すれば事足ります。このようにすれば、userObject自体のtoStringメソッドが別の目的で使用されている場合にも対処できます。JTreeやTreeCellRendererで独自の処理を定義するのは、表示用の文字列を取得するためだけに行うのであれば、少し大げさです。当然、全てのノードが、そのように動作するようになりますから、影響を考えないと、思わぬ結果になってしまいます。

ラッパークラスでtoStringメソッドを定義して、表示を整えるようにしたプログラムを作ってみました(memo10ex3.java)。このプログラムでは、HashtableにFileを格納する際に、ラッパークラスとして作成したNodeWrapperで常にFileを包むようにしています。ラッパークラスを使うと、Hashtableから取り出す順序が変わってしまうため、使っていない場合とはツリーの構成が一致しなくなっています。順序と表示を完全にコントロールするためには、前回お話ししたとおり、TreeModelとTreeNodeを使うことになります。


JTreeをデータを保持する機能から見た場合、特徴的なのは、木構造という親子関係をデータ間で持たせながら格納することができるということです。Fileクラスのインスタンス同士は、パスを見れば親子関係はわかるにはわかります。では、すべてのFileオブジェクトで毎回調べるかというと、とてもではありませんがやっていられません。親子関係の保持のためだけにJTreeを使うとなるとそれはそれで問題がありますが、そういった点を意識して利用すれば、より効果的に使用することができるのではないでしょうか。

おしまい

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

Versions & Requirements.
Java2 Standard Edition Version 1.3.1_04
MorePaste 1.0/1.1
Keywords.
javax.swing.JTree, javax.swing.tree, javax.swing.tree.TreePath, javax.swing.tree.DefaultMutableTreeNode, javax.swing.tree.TreeCellRenderer, java.util.Hashtable
See also.
Swing Tutorial [Top]: How to Use Trees

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

 

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