S-JIS[2009-03-20/2011-04-16] 変更履歴

JTree(Swing)

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を併用する。


TreePath

ノード(要素)は、親ノード内のリストのn番目に存在する。これはすぐ上の親ノードからの位置でしかない。
ツリー全体の中のルートノードからの位置を表すのがTreePathクラス

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 {
	}
}

LazyTree


ダブルクリック時の動作

ノード(葉)がダブルクリックされた場合に何かの処理をしたい場合は、マウスのリスナーを登録しておく。
また、Enterキーが押された場合も現在選択されているノードに対してダブルクリックと同じ処理をするだろう。キーボードのリスナーを登録しておく。

参考:

ダブルクリック・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
hot dogs
pizza
  • food
  • hot dogs
  • pizza
 food
 hot dogs
 pizza
渡されるデータ   food\n
hot dogs\n
pizza
<html>\n<body>\n<ul>\n
<li>food\n
<li>hot dogs\n
<li>pizza\n
</ul>\n</body>\n</html>

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インスタンスに登録する。
この場合、ドラッグ中にドロップ位置の要素の背景色を反転させたり枝要素上でしばらく待つとツリーを開いたりする機能は用意されている。

ドロップ位置を示す方法(JDK1.6以降)
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);

FileTransHandler.java:

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;
	}
}

DropTargetでファイル一覧を受け取る方法


ペースト処理

importData()を実装したTransferHandlerをJTreeインスタンスに登録すると、Ctrl+Vによるクリップボードからのペースト(貼り付け)も出来るようになる。[2009-03-28]
(TransferHandlerのimportData()は、ドロップとペーストで共通な為)

canImport()はペースト時には呼ばれないので、importData()の先頭で自分でcanImport()を呼び出し、受け付けられるデータ型かどうか調べている。

TransferSupport#getDropLocation()等のドロップ処理時専用のメソッドは、ペースト時に呼び出すとIllegalStateException("Not a drop")の例外が発生する。
そこで、isDrop()を使って処理を分岐させる。
もしペースト時には処理を行いたくないのであれば、ここでreturn falseしてしまえばよい。


Swing目次へ戻る / Java目次へ戻る / 自作Swingクラスへ行く
メールの送信先:ひしだま