JTreeは、ツリー(HTMLの<ul><li>〜</ul>のような構造)を表示するコンポーネント。
|
|
|
JTree内で実際にデータを管理するのがTreeModel(インターフェース)。
基本的にはそれを実装しているDefaultTreeModelクラスが使われる。
TreeModelにはデータを取得する系統のメソッドしか宣言されていない。
データを操作(追加・削除)するメソッドはDefaultTreeModelの方にある。
個々のデータ(要素)はTreeNodeインターフェース(後から子ノードを増減する場合はMutableTreeNodeインターフェース)の実装クラス(デフォルトではDefaultMutableTreeNodeクラス)で保持する。
DefaultTreeModelはルートノード(一番上の要素)だけを保持し、子ノードの一覧(リスト)は各ノードがそれぞれ保持する。
メソッド | 概要 | 更新日 | ||
---|---|---|---|---|
JTree | expandPath(パス) | 指定されたパスを開く(展開する)。 | 2009-03-20 | |
collapsePath(パス) | 指定されたパスを閉じる(折りたたむ)。 | 2009-03-20 | ||
setSelectionPath(パス) setSelectionPaths(パス配列) addSelectionPath(パス) addSelectionPaths(パス配列) |
指定されたパスを選択状態にする。 | 2009-03-20 | ||
clearSelection() | 選択状態を解除する。 | 2009-03-20 | ||
isSelectionEmpty() | 選択されている状態かどうか判断する。 | 2009-03-20 | ||
getSelectionPath() getSelectionPaths() |
選択されているパスを取得する。 | 2009-03-20 | ||
getLeadSelectionPath() | フォーカスの当たっているパスを取得する。 →getSelectionPath()との違い |
2009-03-20 | ||
getPathForLocation(x, y) | 指定座標のノードのパスを返す。(マウスイベントで使用) | 2009-03-20 | ||
JTree | getModel() | TreeModelを取得する。 | 2009-03-20 | |
TreeModel | TreeModel | getRoot() | ルートノードを取得する。 →ルートノードのTreePathを作る |
2009-03-20 |
getChildCount(ノード) | 指定ノードが保持している子ノードの個数を取得する。 →TreeNode#getChildCount() |
2009-03-20 | ||
getChild(ノード, n) | 指定ノードが保持しているn番目の子ノードを取得する。 →TreeNode#getChildAt() |
2009-03-20 | ||
getIndexOfChild(ノード, 子ノード) | 指定ノード内の子ノードの位置(番号)を取得する。 →TreeNode#getIndex() |
2009-03-20 | ||
isLeaf(ノード) | 指定ノードが葉かどうかを返す。 →TreeNode#getAllowsChildren()、isLeaf() |
2009-03-20 | ||
DefaultTreeModel |
reload() reload(ノード) |
ノードの再表示(再描画)を行う。 引数を何も指定しないと、全体を再描画する。 (開いていたツリーは閉じてしまう) |
2009-03-28 | |
getPathToRoot(ノード) | そのノードまでのルートからのノード配列を返す。 →TreePathを作るのに使える |
2009-03-20 | ||
TreeNode | MutableTreeNode | insert(ノード, n) | 指定位置にノードを挿入する。 | 2009-03-20 |
remove(n) remove(ノード) |
ノードを削除する。 (削除後にreload()しないと画面の表示は変わらない) |
2009-11-23 | ||
setUserObject(オブジェクト) | ユーザーオブジェクトをセットする。 (取得メソッドはインターフェースに無い…→getUserObject()) |
2009-03-20 | ||
TreeNode | getChildCount() | 保持している子ノードの個数を返す。 | 2009-03-20 | |
getChildAt(n) | 保持しているn番目の子ノードを返す。 | 2009-03-20 | ||
getIndex(子ノード) | 子ノードの位置(番号)を返す。 | 2009-03-20 | ||
isLeaf() | 葉(末端・子ノードを保持しない)かどうかを返す。 | 2009-03-20 | ||
getAllowsChildren() | 子を持てるかどうかを返す。 | 2009-03-20 | ||
getParent() | 親ノードを返す。 (親ノードに追加した時に自動的に設定される) |
2009-03-20 | ||
chidren() | 子ノード一覧(Enumeration)を返す。 | 2011-04-16 | ||
DefaultMutableTreeNode | add(ノード) | ノードを追加する。 →insert() |
2009-03-20 | |
removeAllChildren() | 全ての子ノードを削除する。 (削除後にreload()しないと画面の表示は変わらない) |
2009-11-23 | ||
getUserObject() | ユーザーオブジェクトを取得する。 | 2009-03-20 | ||
preorderEnumeration() postorderEnumeration() breadthFirstEnumeration() depthFirstEnumeration() pathFromAncestorEnumeration(ノード) |
孫まで含めた全ノード一覧(Enumeration)を返す。 幅優先・深さ優先等。 |
2011-04-16 | ||
TreePath | getLastPathComponent() | 保持しているパスの一番末端のノードを返す。 | 2009-03-20 | |
getPathCount() | パスの階層の個数を返す。 | 2009-03-20 | ||
getPathComponent(n) | n階層目のノードを返す。 | 2009-03-20 | ||
getPath() | パス内の各階層のノードを配列にして返す。 | 2009-03-20 | ||
getParentPath() | 親ノードのTreePathを返す。 | 2009-03-20 | ||
JTree | getSelectionModel() | TreeSelectionModelを取得する。 | 2009-03-28 | |
TreeSelectionModel | setSelectionMode(モード) | 要素の選択方式を設定する。 | 2009-03-28 |
JTreeのインスタンスだけ作ればツリー表示は出来るのだが、デフォルトコンストラクター(引数なし)だとサンプルデータが表示される(爆) サンプル集じゃあるまいし、なんでデフォルトがそんな風になってるんだ!おかしいだろそれ!(苦笑)
private void initPane(Container c) { JTree tree = new JTree(); c.add(tree); }
という訳で、モデルから作るのがよい。
import javax.swing.JTree; import javax.swing.tree.DefaultMutableTreeNode; import javax.swing.tree.DefaultTreeModel;
private void initPane(Container container) { DefaultMutableTreeNode root = new DefaultMutableTreeNode("ルートオブジェクト"); for (int i = 0; i < 3; i++) { DefaultMutableTreeNode child = new DefaultMutableTreeNode("子要素" + i); root.add(child); } DefaultTreeModel model = new DefaultTreeModel(root); JTree tree = new JTree(model); JScrollPane scroll = new JScrollPane(tree); container.add(scroll); }
DefaultMutableTreeNodeは、1つのノードを管理するクラス。
コンストラクターに渡したデータは「ユーザーオブジェクト」となる。→getUserObject()
DefaultTreeModelを使うなら、JTreeのコンストラクターにルートノードを直接渡してもよい。
(ノードを引数にとるコンストラクターでは、内部で上記と同じ「new DefaultTreeModel(ノード)」を呼び出しているから)
JTreeにモデルを登録した後でルートノードに新しい子ノードを追加した場合、表示には反映されない。
DefaultTreeModel#reload()を呼び出して反映させる必要がある。
大きなツリーになる場合にスクロールさせたいなら、JScrollPaneを併用する。
ノード(要素)は、親ノード内のリストのn番目に存在する。これはすぐ上の親ノードからの位置でしかない。
ツリー全体の中のルートノードからの位置を表すのがTreePathクラス。
表したいノード | TreePathでの表現 |
---|---|
ルートノード | [ルートノード] |
子要素0 | [ルートノード, 子要素0] |
孫要素A | [ルートノード, 子要素1, 孫要素A] |
孫要素C | [ルートノード, 子要素2, 孫要素C] |
ノードからTreePathを導き出すには、DefaultTreeModel#getPathToRoot()を使う。
TreeNode node = 〜; DefaultTreeModel model = (DefaultTreeModel) tree.getModel(); TreePath path = new TreePath(model.getPathToRoot(node));
ルートノードのTreePathは以下のようにして作れる。
TreeModel model = tree.getModel(); Object root = model.getRoot(); TreePath path = new TreePath(root);
ノードをクリックしたりカーソルの上下で移動すると、ノードが選択できる。また、Shiftキーを押しながらだと範囲選択になる。
Ctrlキーを押しながらクリックすると追加選択・一部解除が出来る。
Ctrlキーを押しながらカーソルの上下で移動し、スペースキーを押すことによって、ノードの追加選択・一部解除が出来る。
デフォルトでは複数選択可能状態だが、単独しか選択できないように変更することも出来る。
tree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
ノードがクリックされたりカーソルの上下で移動したりして選択されたり選択が解除されたりした場合、TreeSelectionListener#valueChanged()が呼ばれる。
tree.addTreeSelectionListener(new SelectionListener());
import javax.swing.event.TreeSelectionEvent; import javax.swing.event.TreeSelectionListener;
class SelectionListener implements TreeSelectionListener { @Override public void valueChanged(TreeSelectionEvent e) { TreePath[] ps = e.getPaths(); for (int i = 0; i < ps.length; i++) { boolean add = e.isAddedPath(i); System.out.printf("%-5b %s%n", add, ps[i]); } } }
getPaths()で返されるパスには、新たに選択されたパスと今回の操作で選択解除されたパスの両方が含まれる。
isAddPath()がtrueを返すパスが選択された(選択に追加された)パス。
複数選択されている場合でも、新しく選択されたパスが含まれるだけで、今までに選択されている全パスが含まれているわけではない。
ツリーの全データを構築しようとしたら時間がかかるような場合、最小限の階層だけまず表示し、ノードツリーを開こうとした時点でそのノード内のデータを探しに行くようなパターンが考えられる。
ノードを開いたり閉じたり(ツリーを展開したり折りたたんだり)する際にはTreeWillExpandListenerやTreeExpansionListenerが呼ばれる。
(ノードのアイコンをダブルクリックしたり、選択中にカーソルの左右(左:ツリーを閉じる、右:ツリーを開く)を押す)
クラス | メソッド | 呼ばれるタイミング |
---|---|---|
TreeWillExpandListener | treeWillExpand() | ノード(ツリー)が開かれる前に呼ばれる。 |
treeWillCollapse() | ノード(ツリー)が閉じられる前に呼ばれる。 | |
TreeExpansionListener | treeExpanded() | ノード(ツリー)が開かれた時に呼ばれる。 |
treeCollapsed() | ノード(ツリー)が閉じられた時に呼ばれる。 |
したがって、treeWillExpand()(ノードが開かれる前のイベント処理)でツリーにデータを追加してやればよい。
ただしデフォルトのJTreeでは、ノードが葉の時(つまりisLeaf()がtrueの場合)はtreeWillExpand()やtreeExpanded()は呼ばれない。
DefaultMutableTreeNodeは子ノード数が0の時は葉という扱いになるので、データ数が0の場合でもisLeaf()がfalseを返すようにしてやる必要がある。
なお、treeWillExpand()やtreeWillCollapse()において、開いたり閉じたりしたくない場合はExpandVetoExceptionをスローすればよい。
が、子ノードが0件の場合(子が無いから開くことが出来ないわけだが)に処理を続行しても特に問題は無い為、例外をスローさせる必要は無い。
(むしろ、0件だと“ツリーが展開できる”ことを意味するアイコンが消えてくれるので、そのまま処理を続行する方がいい)
import javax.swing.event.TreeExpansionEvent; import javax.swing.event.TreeWillExpandListener; import javax.swing.tree.ExpandVetoException;
private void initPane(Container c) { DefaultMutableTreeNode root = new DefaultMutableTreeNode("ルートオブジェクト"); root.add(new LazyTreeNode("A0", null)); root.add(new LazyTreeNode("A1", new Object[] { "B0", "B1" })); root.add(new LazyTreeNode("A2", new Object[] { "C0", "C1", "C2" })); DefaultTreeModel model = new DefaultTreeModel(root); JTree tree = new JTree(model); TreeWillExpandListener tel = new ExpandListener(); tree.addTreeWillExpandListener(tel); c.add(tree); }
class LazyTreeNode extends DefaultMutableTreeNode { private static final long serialVersionUID = 8985822376802918245L; protected Object[] addObjects; /** * コンストラクター * * @param userObject 当ノードのユーザーオブジェクト * @param addObjects 子ノードのユーザーオブジェクト一覧 */ public LazyTreeNode(Object userObject, Object[] addObjects) { super(userObject); this.addObjects = addObjects; } @Override public boolean isLeaf() { return false; } /** * 初回だけ、子ノードにデータを追加する */ public void addChildNodes() { if (addObjects != null) { for (Object obj : addObjects) { DefaultMutableTreeNode child = new DefaultMutableTreeNode(obj); this.add(child); } addObjects = null; } } }
class ExpandListener implements TreeWillExpandListener { // ツリーを開く際に呼ばれる @Override public void treeWillExpand(TreeExpansionEvent event) throws ExpandVetoException { TreePath path = event.getPath(); Object obj = path.getLastPathComponent(); //一番末端の(今回開こうとしている)ノード if (obj instanceof LazyTreeNode) { LazyTreeNode node = (LazyTreeNode) obj; node.addChildNodes(); //子ノードにデータを追加する } } // ツリーを閉じる際に呼ばれる @Override public void treeWillCollapse(TreeExpansionEvent event) throws ExpandVetoException { } }
ノード(葉)がダブルクリックされた場合に何かの処理をしたい場合は、マウスのリスナーを登録しておく。
また、Enterキーが押された場合も現在選択されているノードに対してダブルクリックと同じ処理をするだろう。キーボードのリスナーを登録しておく。
ShowObjectHandler listener = new ShowObjectHandler(); tree.addMouseListener(listener); //マウスイベント tree.addKeyListener(listener); //キーイベント
class ShowObjectHandler extends MouseAdapter implements KeyListener { //マウスイベント @Override public void mouseClicked(MouseEvent e) { if (SwingUtilities.isLeftMouseButton(e) && e.getClickCount() == 2) { JTree tree = (JTree) e.getSource(); TreePath path = tree.getPathForLocation(e.getX(), e.getY()); showObject(path); } }
//キーイベント @Override public void keyPressed(KeyEvent e) { if (e.getKeyCode() == KeyEvent.VK_ENTER) { JTree tree = (JTree) e.getSource(); // TreePath path = tree.getSelectionPath(); //選択範囲の先頭 TreePath path = tree.getLeadSelectionPath(); //フォーカスのある要素 showObject(path); e.consume(); } } @Override public void keyReleased(KeyEvent e) { } @Override public void keyTyped(KeyEvent e) { }
↑キー押下時(pressed)でなく離した時(released)でもいいんだけど、ダイアログを終了させる為のEnterキー入力が長押しとなって、再度キー入力を拾ってしまう確率が高くなる。
(typedの場合は、getKeyCode()にはVK_ENTERが入っていない。getKeyChar()なら'\n'が入っているので、そちらで判定する)
getSelectionPath()は選択されている範囲の先頭のパスを返す。
getLeadSelectionPath()はフォーカスのある要素のパスを返す。
通常は選択している要素のどれかがフォーカスのある要素だが、Ctrlキーを押しながらカーソルの上下を押したりCtrlキー+スペースキーで選択解除したりした場合、選択していない要素でもフォーカスが当たる。
protected void showObject(TreePath path) { if (path == null) { return; } DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent(); if (node.isLeaf()) { // 匿名クラスにしたいところだったが、匿名クラスは引数つきコンストラクターを用意できないので 局所クラスで代用 class Show implements Runnable { protected DefaultMutableTreeNode node; Show(DefaultMutableTreeNode node) { this.node = node; } @Override public void run() { // ダイアログでユーザーデータを表示 JOptionPane.showMessageDialog(null, node.getUserObject()); } } SwingUtilities.invokeLater(new Show(node)); } } }
JTreeでは、要素をマウスでドラッグすることが出来る。[2009-03-28]
ただし、デフォルトではドラッグできない。以下のようにして、ドラッグを許可する必要がある。
tree.setDragEnabled(true);
ドラッグした要素は、デフォルトではJTree内ではドロップできない。(→JTreeでドロップを受け付ける方法)
他のコンポーネントやアプリケーションがドロップを受け付ける場合にドロップできる。例えばJEditorPaneやワードパッドやMS-Wordにドロップすることが出来る。
ドロップ元データ | ドロップ先イメージ | ||||||
---|---|---|---|---|---|---|---|
コンポーネント アプリケーション |
JTree | new JEditorPane() ワードパッド |
new JEditorPane("text/html","") MS-Word |
MS-Excel | |||
データ例 |
|
food hot dogs pizza |
|
|
|||
渡されるデータ | food\n |
<html>\n<body>\n<ul>\n |
JEditorPane(HTMLテキスト)やMS-WordやMS-ExcelはHTMLを解釈できるので、HTMLの<ul><li>〜</ul>という形で文字列が渡される。
HTMLを解釈できないワードパッドやJEditorPane(プレーンテキスト)では、単なる文字列が渡される。
どちらも、ツリーの木構造は保持されない。
ドロップ元データは、ドロップ開始時に選択されている要素。
デフォルトでは複数の要素を選択することが出来るが、変更することも出来る。
import javax.swing.tree.TreeSelectionModel;
TreeSelectionModel sm = tree.getSelectionModel(); sm.setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION); // 一行だけ選択可能 // sm.setSelectionMode(TreeSelectionModel.CONTIGUOUS_TREE_SELECTION); // 連続した複数要素を選択可能 // sm.setSelectionMode(TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION); // 無制限に複数要素を選択可能(デフォルト)
JTreeでは、Ctrl+Cによるクリップボードへのコピー処理もサポートしている。[2009-03-28]
コピーできる内容は ドラッグ処理と同じ。(TransferHandlerの機能を使用しているので、同じになる)
JEditorPaneやワードパッド・MS-Word等にペースト(Ctrl+Vによる貼り付け)することが出来る。
なお、JTreeのデフォルトのTransferHandlerであるBasicTreeUI$TreeTransferHandlerのgetSourceActions()はCOPYしか返さないので、カット(Ctrl+Xによる切り取り)は使用できない。
JTreeでは、デフォルトのドロップ処理は用意されていない。[2009-03-28]
(ドロップされた時にどういう処理をすればいいかの仕様が汎用的に決められないからだろう。
例えば要素上にドロップされた時、その要素の位置に挿入するのか、その要素の子要素として追加するのか?)
ドロップ処理を行うには、TransferHandlerを自分で作成してJTreeインスタンスに登録する。
この場合、ドラッグ中にドロップ位置の要素の背景色を反転させたり枝要素上でしばらく待つとツリーを開いたりする機能は用意されている。
DropMode | ターゲット(ドロップできる位置) | 参考 |
---|---|---|
ON | ノード上のみ | |
INSERT | ノードとノードの間のみ(枝の場合はノード上も可) | |
ON_OR_INSERT | 上記両方 | |
USE_SELECTION | JDK1.5までの動作。デフォルト。 動作に違和感があるので使わない方がいいらしい。 |
ITproのSwingでのドラッグ&ドロップ その2 |
import javax.swing.DropMode;
tree.setDropMode(DropMode.ON_OR_INSERT);
TransferHandlerでなくDropTargetを使うことも出来るが、その場合はドロップ先の要素の背景色を反転させるような機能は使えないので、それも自作する羽目になってしまう。
(JTreeのデフォルトのDropTargetがそういった処理を行っているので、自分でDropTargetを登録してしまうとデフォルトの機能が呼び出されなくなる為)
ただ、自作TransferHandlerをJTreeインスタンスに登録すると、JTreeのデフォルトのTransferHandlerであるBasicTreeUI$TreeTransferHandlerの機能(つまりドラッグやコピー)が使えなくなってしまう。
(TransferHandlerはドロップだけでなくドラッグやコピーも処理する為。なお、TreeTransferHandlerはpublicでないので継承することは出来ない)
とは言っても、ドロップを受け付けるからにはドロップ用のデータ構造(Transferable)が必要なはずで、ドラッグやコピーもそのデータ構造を作り出す必要があるので、普通なら両方とも自分でコーディングすることになるだろう。
エクスプローラーからファイル一覧をドロップしてツリーに追加する例。(ツリーからのドラッグはサポートしない)
tree.setTransferHandler(new FileTransHandler()); tree.setDropMode(DropMode.ON_OR_INSERT); TreeSelectionModel sm = tree.getSelectionModel(); sm.setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION);
class FileTransHandler extends TransferHandler { private static final long serialVersionUID = 5279395553614357670L; @Override public int getSourceActions(JComponent c) { return COPY; }
@Override public boolean canImport(TransferSupport support) { Transferable t = support.getTransferable(); if (t.isDataFlavorSupported(DataFlavor.javaFileListFlavor)) { return true; // ファイル一覧しか受け付けない } return false; }
@SuppressWarnings("unchecked") @Override public boolean importData(TransferSupport support) { if (!canImport(support)) { return false; } Transferable t = support.getTransferable(); List<File> flist; try { // ファイル一覧取得 flist = (List<File>) t.getTransferData(DataFlavor.javaFileListFlavor); } catch (UnsupportedFlavorException e) { return false; } catch (IOException e) { return false; } JTree tree = (JTree) support.getComponent(); TreePath path; int cindex; if (support.isDrop()) { //ドロップの時 JTree.DropLocation loc = (JTree.DropLocation) support.getDropLocation(); path = loc.getPath(); cindex = loc.getChildIndex(); } else { //ペーストの時 path = tree.getSelectionPath(); cindex = -1; } if (path != null) { DefaultTreeModel model = (DefaultTreeModel) tree.getModel(); DefaultMutableTreeNode node = (DefaultMutableTreeNode) path.getLastPathComponent(); if (cindex < 0) { // ターゲットは要素上 for (File f : flist) { DefaultMutableTreeNode fnode = new DefaultMutableTreeNode(f); node.add(fnode); //子要素として末尾に追加 } } else { // ターゲットは要素と要素の間 for (File f : flist) { DefaultMutableTreeNode fnode = new DefaultMutableTreeNode(f); node.insert(fnode, cindex++); //間に挿入 } } model.reload(node); return true; } return false; } }
importData()を実装したTransferHandlerをJTreeインスタンスに登録すると、Ctrl+Vによるクリップボードからのペースト(貼り付け)も出来るようになる。[2009-03-28]
(TransferHandlerのimportData()は、ドロップとペーストで共通な為)
canImport()はペースト時には呼ばれないので、importData()の先頭で自分でcanImport()を呼び出し、受け付けられるデータ型かどうか調べている。
TransferSupport#getDropLocation()等のドロップ処理時専用のメソッドは、ペースト時に呼び出すとIllegalStateException("Not
a drop")の例外が発生する。
そこで、isDrop()を使って処理を分岐させる。
もしペースト時には処理を行いたくないのであれば、ここでreturn falseしてしまえばよい。