JTree使用实例
From:http://download.oracle.com/javase/tutorial/uiswing/components/tree.html
如何使用Jtree
利用JTree类,你可以显示等级体系的数据。一个JTree对象并没有包含实际的数据;它只是提供了数据的一个视图。像其他非平凡的(nontrivial)Swing组件一样,这种Jtree通过查询她的数据模型获得数据。
Jtree垂直显示它的数据。树中显示的每一行包含一项数据,称之为节点(node)。每颗树有一个根节点(root node),其他所有节点是它的子孙。默认情况下,树只显示根节点,但是你可以设置改变默认显示方式。一个节点可以拥有孩子也可以不拥有任何子孙。我们称那些可以拥有孩子(不管当前是否有孩子)的节点为“分支节点”(branch nodes),而不能拥有孩子的节点为“叶子节点”(leaf nodes)。
分支节点可以有任意多个孩子。通常,用户可以通过点击实现展开或者折叠分支节点,使得他们的孩子可见或者不可见。默认情况下,除了根节点以外的所有分支节点默认呈现折叠状态。程序中,通过监听 tree expansion 或者 tree-will-expand事件可以检测分支节点的展开状态。监听事件在下面两节内容中描述How to Write a Tree Expansion Listener and How to Write a Tree-Will-Expand Listener.
- 在树中,一个节点可以通过TreePath(一个囊括该节点和他所有祖先节点的路径对象)或者他的折叠行来识别。
- 展开节点(expanded node)就是一个非叶子节点,当他的所有祖先都展开时,他将显示他的孩子。
- 折叠节点(collapsed node)是隐藏了孩子们得的节点。
- 隐藏节点(hidden node)就是折叠节点下的一个孩子
创建树
这里是一个应用程序的截图,上半部分展示了一个滚动面板(scroll pane)中的树(Jtree)。
代码来自http://download.oracle.com/javase/tutorial/uiswing/examples/components/TreeDemoProject/src/components/TreeDemo.java获得,创建了一个JTree对象,并将之放到一个scroll pane上
1 //Where instance variables are declared: 2 private JTree tree; 3 ... 4 public TreeDemo() { 5 ... 6 DefaultMutableTreeNode top = new DefaultMutableTreeNode("The Java Series"); 7 createNodes(top); 8 tree = new JTree(top); 9 ... 10 JScrollPane treeView = new JScrollPane(tree); 11 ... 12 }
这段代码创建了一个DefaultMutableTreeNode实例作为根节点。接着创建树中剩下的其他节点。创建完节点后,通过指定刚才创建的根节点为JTree构造函数的参数,创建一棵树。最后,将树放到滚动面板中,这是一个通常的策略,因为需要显示完一个树,而展开树需要另外比较大的空间。
以下代码创建根节点以下的节点
1 private void createNodes(DefaultMutableTreeNode top) { 2 DefaultMutableTreeNode category = null; 3 DefaultMutableTreeNode book = null; 4 5 category = new DefaultMutableTreeNode("Books for Java Programmers"); 6 top.add(category); 7 8 //original Tutorial 9 book = new DefaultMutableTreeNode(new BookInfo 10 ("The Java Tutorial: A Short Course on the Basics", 11 "tutorial.html")); 12 category.add(book); 13 14 //Tutorial Continued 15 book = new DefaultMutableTreeNode(new BookInfo 16 ("The Java Tutorial Continued: The Rest of the JDK", 17 "tutorialcont.html")); 18 category.add(book); 19 20 //JFC Swing Tutorial 21 book = new DefaultMutableTreeNode(new BookInfo 22 ("The JFC Swing Tutorial: A Guide to Constructing GUIs", 23 "swingtutorial.html")); 24 category.add(book); 25 26 //...add more books for programmers... 27 28 category = new DefaultMutableTreeNode("Books for Java Implementers"); 29 top.add(category); 30 31 //VM 32 book = new DefaultMutableTreeNode(new BookInfo 33 ("The Java Virtual Machine Specification", 34 "vm.html")); 35 category.add(book); 36 37 //Language Spec 38 book = new DefaultMutableTreeNode(new BookInfo 39 ("The Java Language Specification", 40 "jls.html")); 41 category.add(book); 42 }
DefaultMutableTreeNode构造函数的参数是一个用户自定义的类对象,它包含或指向了关联树节点的数据。这个用户对象可以是一个字符串,或者是一个自定义的类。如果它实现了一个自定义对象,你应该要重新实现覆盖他的toString方法,这样他才能返回对应字符串作为节点显示的字符串。Jtree默认情况下,每个节点都是用toString的返回值作为显示。所以,让toString返回一些有意义的值是很重要的。有时候,覆盖toString方法是不可行的;在某些场景你可以通过重写Jtree的convertValueToText方法,映射模型对象到一个可显示的字符串。
例如,前面demo中的BookInfo类是一个自定义类,它包含了两个字段:书名和描述该书本的HTML文件的URL路径。toString方法也重新实现,返回书名。从而,每个节点关联了一个BookInfo对象,并且显示书名。
总之,你可以调用Jtree的构造函数创建一棵树,指定一个实现了TreeNode的类作为参数。你应该尽量把这棵树放到一个滚动面板中(scroll pane),这样树就不会占用太大的空间。对于树节点相应用户点击而展开和折叠的功能,你不需要做任何事情。但是,你一定要添加一些代码使得树在用户点击选择一个节点时能够作出反应,例如:
对节点的选择做出响应
对于树节点的选择做出响应是简单的。你可以实现一个树节点选择监听器,并且注册在这棵树上。接下来的代码显示了TreeDemo.java中有关选择的代码:
1 //Where the tree is initialized: 2 tree.getSelectionModel().setSelectionMode(TreeSelectionModel.SINGLE_TREE_SELECTION); 3 4 //Listen for when the selection changes. 5 tree.addTreeSelectionListener(this); 6 ... 7 public void valueChanged(TreeSelectionEvent e) { 8 //Returns the last path element of the selection. 9 //This method is useful only when the selection model allows a single selection. 10 DefaultMutableTreeNode node = (DefaultMutableTreeNode)tree.getLastSelectedPathComponent(); 11 12 if (node == null) { 13 //Nothing is selected. 14 return; 15 } 16 17 Object nodeInfo = node.getUserObject(); 18 if (node.isLeaf()) { 19 BookInfo book = (BookInfo)nodeInfo; 20 displayURL(book.bookURL); 21 } else { 22 displayURL(helpURL); 23 } 24 }
上面的代码执行了一下任务:
- 获得树的默认TreeSelectionModel(节点选择模式),然后设置它,使得在某一时刻只有一个节点被选中。
- 注册了一个事件处理器。事件处理器是一个实现了TreeSelectionListener接口的对象。
- 在事件处理器中,通过调用Tree的getLastSelectedPathComponent方法获得选中的节点。
- 使用getUserObject方法获得节点关联的数据。(节点node是一个非平凡组件,要通过它关联的数据模型获得真正的数据)
通过设置Look,可以获得Java、Windows和MacOS样式窗口风格。
像之前图片显示一样,一棵树按照惯例,对于每个基点显示了一个图标和一些文字。像我们简短的展示一样,你可以指定这些样式。
一棵树通常表现一些外观和样式特效,通过不同的绘制图形指示节点间的关系。你可以在限制范围内自定义这些图形。首先,你可以使用tree.setRootVisible(true)设置显示根节点或者tree.setRootVisible(false)隐藏根节点。其次,你可以使用tree.setShowsRootHandles(true)请求设置树的顶层节点具有句柄(+-图标,点击句柄使其展开折叠)。如果顶层节点是根节点的话,需要保证它是可视的,如果是顶层节点则每个孩子都显示句柄。
如果你使用Java样式,你可以自定是否在节点间显示行线来表现他们的关系。默认情况下,Java样式使用“角线”(类似“L”)。通过设置Jtree.lineStyle的客户端属性,你可以指定一种不同的标准。例如,通过以下代码,这只JAVA样式仅使用水平线隔开一组节点:
1 tree.putClientProperty(“Jtree.lineStyle”, “Horizontal”);
指定JAVA样式在节点间不显示任何行线,则使用以下代码:
1 tree.putClientProperty(“Jtree.lineStyle”, “None”);
自定义树的外观表现
设置不同的Jtree.lineStyle属性(使用JAVA样式)可以得到不同的外观。
不管你使用那种样式(java、windows、mac),默认情况下,节点显示的图标决定于节点是否为叶子节点和是否可展开。例如,在windwos样式中,每个叶子节点的默认图标是一个点;在JAVA样式中,叶子节点默认图标是一个类似白纸的符号。在所有样式中,分支节点被一个文件夹符号所标识。不同样式对于可展开分支和对应的可折叠分支,可能有不同的图标。
你可以很容易的改变叶子节点、可展开分支节点和可折叠分支节点的默认图标。如果要这样做的话,首先,你要创建一个DefaultTreeCellRenderer实例。你总是可以创建自己的TreeCellRender,让你喜欢的任何组件重复利用。接着,通过调用以下一个或多个方法去指定图标:setLeafIcon(对于叶子节点),setOpenIcon(对于可展开分支节点),setClosedIcon(对于可折叠节点)。如果你想要这棵树中各种节点都不显示图标,你就要指定图标为null。
一定你创建了这些图标,使用树的setCellRender方法去指定这个DefaultTreeCellRender来绘制它的节点。这里有一个来自TreeIconDemo的例子
1 ImageIcon leafIcon = createImageIcon("images/middle.gif"); 2 if (leafIcon != null) { 3 DefaultTreeCellRenderer renderer = 4 new DefaultTreeCellRenderer(); 5 renderer.setLeafIcon(leafIcon); 6 tree.setCellRenderer(renderer); 7 }
如果你想更精巧的控制节点图标,或者你想提供一些工具,你可以创建DefaultTreeCellRender的子类,然后覆盖他的getTreeCellRendererComponent方法。因为DefaultTreeCellRenderer是Jlabel的一个子类,你可以使用任何Jlabel的方法,例如setIcon。
下面代码来自TreeIconDemo2.java,创建了一个单元绘制器(cell renderer),它根据节点的文本数据是否包含单词“Tutorial”来改变了叶子节点的图标。这个renderer同样可以指定提示文本(tool-tip)---鼠标移到上面,出现提示。
1 //...where the tree is initialized: 2 //Enable tool tips. 3 ToolTipManager.sharedInstance().registerComponent(tree); 4 5 ImageIcon tutorialIcon = createImageIcon("images/middle.gif"); 6 if (tutorialIcon != null) { 7 tree.setCellRenderer(new MyRenderer(tutorialIcon)); 8 } 9 ... 10 class MyRenderer extends DefaultTreeCellRenderer { 11 Icon tutorialIcon; 12 13 public MyRenderer(Icon icon) { 14 tutorialIcon = icon; 15 } 16 17 public Component getTreeCellRendererComponent( 18 JTree tree, 19 Object value, 20 boolean sel, 21 boolean expanded, 22 boolean leaf, 23 int row, 24 boolean hasFocus) { 25 26 super.getTreeCellRendererComponent( 27 tree, value, sel, 28 expanded, leaf, row, 29 hasFocus); 30 if (leaf && isTutorialBook(value)) { 31 setIcon(tutorialIcon); 32 setToolTipText("This book is in the Tutorial series."); 33 } else { 34 setToolTipText(null); //no tool tip 35 } 36 37 return this; 38 } 39 40 protected boolean isTutorialBook(Object value) { 41 DefaultMutableTreeNode node = 42 (DefaultMutableTreeNode)value; 43 BookInfo nodeInfo = 44 (BookInfo)(node.getUserObject()); 45 String title = nodeInfo.bookName; 46 if (title.indexOf("Tutorial") >= 0) { 47 return true; 48 } 49 50 return false; 51 } 52 }
你可能会疑惑单元绘制器(cell renderer)是如何工作的。当一个tree在话每个节点的时候,不管是Jtree或是他的样式表现都包含了绘制节点的代码。Tree可以使用cell renderer的绘图代码代替前者去绘制节点。例如,画一个包含字符串“The Java Programming Language”的叶子节点,tree会要求cell renderer返回一个组件,该组件能够绘制一个包含该字符串的叶子节点。如果这个cell renderer是一个DefaultTreeCellRender,它就返回一个label(DefaultTreeCellRender继承于Jlabel),它绘制默认的叶子节点图标,紧随一段字符串。
一个cell renderer仅绘制而不处理事件。如果你想要对一颗tree增加事件处理器,你需要在树上注册监听器,如果事件紧发生在某个节点被选择时,可以选择注册在tree的cell editor上。有关cell editors的资料可以参考Concepts: Editors and Renderers.这节将讨论table cell editors 和 renderers,他们类似于tree cell editors 和 renderers。
动态地改变一颗树
接下来的图片展示了一个叫DynamicTreeDemo的应用程序,它允许你从一颗可视tree中增加或者移除节点。你也可以编辑每个节点的文本。
这里给出了树初始化的代码:
1 rootNode = new DefaultMutableTreeNode("Root Node"); 2 treeModel = new DefaultTreeModel(rootNode); 3 treeModel.addTreeModelListener(new MyTreeModelListener()); 4 5 tree = new JTree(treeModel); 6 tree.setEditable(true); 7 tree.getSelectionModel().setSelectionMode 8 (TreeSelectionModel.SINGLE_TREE_SELECTION); 9 tree.setShowsRootHandles(true);
通过明确的创建tree的模型(model),这段代码保证tree的model是DefaultTreeModel的实例。这样,我们知道所有tree model支持的方法。例如,我们可以调用model的insertNodeInto方法,几时这个方法不是TreeModel接口要求的。
为使得树中节点的文本值可编辑,我们调用对tree调用setEditable(true)。当用户完成一个节点的编辑时,这个model产生一个tree model事件,它会告诉所有监听者(包括Jtree):树节点被改变了。注意:尽管DefaultMutableTreeNode拥有改变一个节点内容的方法,但是改变还是需要通过DefaultTreeModel上面的方法。否则,tree model事件就不能产生,事件的监听者(例如tree)就不能知道这些更新。
为了通知“节点改变”,我们可以实现一个TreeModelListener。这里有一个关于tree model监听器的例子,当用户为一个树节点输入一个新名字时,事件会被检测到。
1 class MyTreeModelListener implements TreeModelListener { 2 public void treeNodesChanged(TreeModelEvent e) { 3 DefaultMutableTreeNode node; 4 node = (DefaultMutableTreeNode) 5 (e.getTreePath().getLastPathComponent()); 6 7 /* 8 9 * If the event lists children, then the changed 10 * node is the child of the node we have already 11 * gotten. Otherwise, the changed node and the 12 * specified node are the same. 13 */ 14 try { 15 int index = e.getChildIndices()[0]; 16 node = (DefaultMutableTreeNode) 17 (node.getChildAt(index)); 18 } catch (NullPointerException exc) {} 19 20 System.out.println("The user has finished editing the node."); 21 System.out.println("New value: " + node.getUserObject()); 22 } 23 public void treeNodesInserted(TreeModelEvent e) { 24 } 25 public void treeNodesRemoved(TreeModelEvent e) { 26 } 27 public void treeStructureChanged(TreeModelEvent e) { 28 } 29 }
这里是一些增加按钮事件处理器(用于增加节点)的代码:
1 treePanel.addObject("New Node " + newNodeSuffix++); 2 ... 3 public DefaultMutableTreeNode addObject(Object child) { 4 DefaultMutableTreeNode parentNode = null; 5 TreePath parentPath = tree.getSelectionPath(); 6 7 if (parentPath == null) { 8 //There is no selection. Default to the root node. 9 parentNode = rootNode; 10 } else { 11 parentNode = (DefaultMutableTreeNode) 12 (parentPath.getLastPathComponent()); 13 } 14 15 return addObject(parentNode, child, true); 16 } 17 ... 18 public DefaultMutableTreeNode addObject(DefaultMutableTreeNode parent, 19 Object child, 20 boolean shouldBeVisible) { 21 DefaultMutableTreeNode childNode = 22 new DefaultMutableTreeNode(child); 23 ... 24 treeModel.insertNodeInto(childNode, parent, 25 parent.getChildCount()); 26 27 //Make sure the user can see the lovely new node. 28 if (shouldBeVisible) { 29 tree.scrollPathToVisible(new TreePath(childNode.getPath())); 30 } 31 return childNode; 32 }
用tree model做为JTree的构造函数的参数,节点的文本改变监听器是注册在model上,而节点增删是通过ActionListener监听按钮事件
这段代码创建一个节点,插入tree model中。如果可以的话,讲请求该节点的上层节点展开,tree滚动,这样新节点就可视了。这段代码使用DefaultTreeModel类提供的insertNodeInto方法向tree model插入新节点。
创建一个数据模型
如果DefaultTreeModel不能符合你的需求,则需要你自定义一个data model。你的data model必须实现TreeModel接口。TreeModel指定获取树中特定节点、获取特定节的孩子数量、确定一个节点是否为叶子、通知model树的改变和增加删除tree model监听器的方法。
有趣的是,TreeModel接口接受各种对象作为树节点。这就不需要通过TreeNode对象来表现节点,节点甚至不需要实现TreeNode接口。因此,如果TreeNode接口不适合你的tree model,大可自由的设计自己的节点表现形式。例如,如果一个事前存在的阶级数据结构(hierarchical data structure),你就不需要复制或者强制把他放进TreeNode模子中。你只需实现你的tree model,这样你就可以使用已经存在的数据结构。
下面图片展示了一个叫GenealogyExample(家谱例子),他展示了某一个人的子孙和祖先。
在GenealogyModel.java
中,你可以找到这个自定义的tree model的实现。因为这个model通过一个DefaultTreeModel的子类实现,他必须实现TreeModel接口。这就需要实现获得节点信息的一系列方法,例如,哪个是根节点、某个节点的子孙是哪些节点。在GenealogyModel的例子中,每个节点表现为一个Person类型的对象,这是一个未实现TreeNode接口的自定义类。
一个tree model一定要实现一些方法,用于增删tree model listeners(监听器),当树的数据结构或者数据被改变时,必须把TreeModelEvents(tree model事件)响应到这些监听器。例如,当用户指示GenealogyExample从“显示子孙”改变为“显示祖先”时,tree model实现这些改变,然后产生一个事件并通知它的监听器。
(这里涉及的四个java文件都挺值得读,里面的编程思想跟技巧很值得学习)
“懒”加载孩子
懒加载(lazy loading)是一种应用程序特征:当一个类实例的实际加载和实例化延迟到这个实例使用前才进行。
通过懒加载我们得到任何东西了吗?当然,这将肯定增加了应用程序的性能。通过懒加载,你能够在使用一个类前,利用内存资源加载和实例化它。这样避免了应用程序的初始化时占用更多的类加载跟实例化时间,加快了应用程序的初始化加载时间。
有一种办法可以懒加载一棵树的孩子:利用TreeWillExpandListener接口。例如,你可以声明和加载根节点,祖父双亲和双亲的显示包含在以下代码中:(树的上层为祖先)
我们声明了根节点root,祖父双亲和双亲如下所示:
1 class DemoArea extends JScrollPane 2 3 implements TreeWillExpandListener { 4 ....... 5 ....... 6 7 private TreeNode createNodes() { 8 DefaultMutableTreeNode root; 9 DefaultMutableTreeNode grandparent; 10 DefaultMutableTreeNode parent; 11 12 root = new DefaultMutableTreeNode("San Francisco"); 13 grandparent = new DefaultMutableTreeNode("Potrero Hill"); 14 root.add(grandparent); 15 16 parent = new DefaultMutableTreeNode("Restaurants"); 17 grandparent.add(parent); 18 19 dummyParent = parent; 20 return root; 21 22 }
你还可以像下面代码一样,把之前声明的节点加载到树tree上(这里只是显示而已)
1 TreeNode rootNode = createNodes(); 2 tree = new JTree(rootNode); 3 tree.addTreeExpansionListener(this); 4 tree.addTreeWillExpandListener(this); 5 ....... 6 ....... 7 setViewportView(tree);
现在,你可以你可以懒加载孩子了,无论双亲节点Restaurant是否可视。如上所述,我们在一个方法中声明两个孩子:
1 private void LoadLazyChildren(){ 2 DefaultMutableTreeNode child; 3 child = new DefaultMutableTreeNode("Thai Barbeque"); 4 dummyParent.add(child); 5 child = new DefaultMutableTreeNode("Goat Hill Pizza"); 6 dummyParent.add(child); 7 textArea.append(" Thai Barbeque and Goat Hill Pizza are loaded lazily"); 8 } 9 10 ....... 11 ....... 12 13 public void treeWillExpand(TreeExpansionEvent e) 14 throws ExpandVetoException { 15 saySomething("You are about to expand node ", e); 16 int n = JOptionPane.showOptionDialog( 17 this, willExpandText, willExpandTitle, 18 JOptionPane.YES_NO_OPTION, 19 JOptionPane.QUESTION_MESSAGE, 20 null, 21 willExpandOptions, 22 willExpandOptions[1]); 23 24 LoadLazyChildren(); 25 }
如何写Tree Expansion Listener(监听器)
有时候,我们使用一棵tree,当分支变成展开或者折叠状态的时候,或许需要作出某些反映。例如,你或许需要加载或者保存数据。
有两种监听器可以负责响应展开或折叠事件:tree expansion listeners(我理解为:已展开事件监听器)和tree-will-expand listeners.(将来可以展开事件监听器)这节讨论tree expansion listeners。
一个tree expansion 监听器侦测在展开或者折叠已经发生。一般来说,你应该实现一个tree expansion监听器,除非你需要阻止展开或折叠。
这个例子演示了一个简单的tree expansion监听器。窗口底部的文字区域展示关于每次tree expansion事件发生的消息。这是一个简单易懂的演示。
下面的代码展示了程序如何处理expansion 事件
1 private void LoadLazyChildren(){ 2 DefaultMutableTreeNode child; 3 child = new DefaultMutableTreeNode("Thai Barbeque"); 4 dummyParent.add(child); 5 child = new DefaultMutableTreeNode("Goat Hill Pizza"); 6 dummyParent.add(child); 7 textArea.append(" Thai Barbeque and Goat Hill Pizza are loaded lazily"); 8 } 9 10 ....... 11 ....... 12 13 public void treeWillExpand(TreeExpansionEvent e) 14 throws ExpandVetoException { 15 saySomething("You are about to expand node ", e); 16 int n = JOptionPane.showOptionDialog( 17 this, willExpandText, willExpandTitle, 18 JOptionPane.YES_NO_OPTION, 19 JOptionPane.QUESTION_MESSAGE, 20 null, 21 willExpandOptions, 22 willExpandOptions[1]); 23 24 LoadLazyChildren(); 25 }
如何写Tree-Will-Expand Listener
Tree-will-expand listener 挡住了节点的展开或折叠(就是监听器侦听在展开折叠正在发生之前)。如果仅仅要在展开和折叠发生后通知监听器,那你就应该使用expansion listener代替它。
这个demo增加了tree-will-expand监听器。在你每次要展开一个节点前,监听器会提示你给出确认信息,确认是否展开。