文档

Java™教程
隐藏目录
如何使用树形结构
路径: 使用Swing创建GUI
课程: 使用Swing组件
部分: 如何使用各种组件

如何使用树形结构

JTreeJTree
一棵树
JTree节点根节点分支节点叶子节点如何编写树展开监听器如何编写树将要展开监听器

创建树

TreeDemo

请尝试以下操作: 
  1. 点击“Launch”按钮使用Java™ Web Start运行Tree Demo(下载JDK 7或更高版本)。或者,如果你想自己编译和运行示例,请参考示例索引启动TreeDemo示例

  2. 展开一个或多个节点。
    你可以通过点击项左侧的圆圈来实现。
  3. 折叠一个节点。
    你可以通过点击已展开节点左侧的圆圈来实现。

以下代码取自TreeDemo.java,创建了JTree对象并将其放入滚动窗格中:

//在此处声明实例变量:
private JTree tree;
...
public TreeDemo() {
    ...
    DefaultMutableTreeNode top =
        new DefaultMutableTreeNode("Java系列");
    createNodes(top);
    tree = new JTree(top);
    ...
    JScrollPane treeView = new JScrollPane(tree);
    ...
}

该代码创建了一个DefaultMutableTreeNode的实例作为树的根节点。然后,它创建了树中的其他节点。然后,它创建了树,将根节点作为JTree构造函数的参数指定。最后,它将树放入滚动窗格中,这是一种常见的策略,因为显示完整的展开树会占用太多空间。

下面是创建根节点下的节点的代码:

private void createNodes(DefaultMutableTreeNode top) {
    DefaultMutableTreeNode category = null;
    DefaultMutableTreeNode book = null;
    
    category = new DefaultMutableTreeNode("面向Java程序员的图书");
    top.add(category);
    
    //原始教程
    book = new DefaultMutableTreeNode(new BookInfo
        ("Java教程:基础知识简短课程",
        "tutorial.html"));
    category.add(book);
    
    //继续教程
    book = new DefaultMutableTreeNode(new BookInfo
        ("Java教程继续:JDK的其他部分",
        "tutorialcont.html"));
    category.add(book);
    
    //Swing教程
    book = new DefaultMutableTreeNode(new BookInfo
        ("Swing教程:构建GUI的指南",
        "swingtutorial.html"));
    category.add(book);

    //...添加更多面向程序员的图书...

    category = new DefaultMutableTreeNode("面向Java实现者的图书");
    top.add(category);

    //虚拟机
    book = new DefaultMutableTreeNode(new BookInfo
        ("Java虚拟机规范",
         "vm.html"));
    category.add(book);

    //语言规范
    book = new DefaultMutableTreeNode(new BookInfo
        ("Java语言规范",
         "jls.html"));
    category.add(book);
}

DefaultMutableTreeNode构造函数的参数是用户对象,它是一个包含或指向与树节点相关联的数据的对象。用户对象可以是一个字符串,也可以是一个自定义对象。如果实现了自定义对象,应该实现它的toString方法,以返回要为该节点显示的字符串。默认情况下,JTree使用从toString返回的值来渲染每个节点,因此toString返回有意义的内容非常重要。有时,不可行重写toString;在这种情况下,可以重写JTree的convertValueToText方法,将模型中的对象映射为要显示的字符串。

例如,上面的代码片段中使用的BookInfo类是一个自定义类,它保存了两个数据:书名和描述该书的HTML文件的URL。toString方法被实现为返回书名。因此,与BookInfo对象相关联的每个节点显示一个书名。


注意: 您可以通过在节点的字符串中插入HTML标签来指定树节点的文本格式。详见在Swing组件中使用HTML

总结一下,您可以通过调用JTree构造函数并指定实现TreeNode的类作为参数来创建一个树。最好将树放在一个滚动窗格中,以便树不会占用太多空间。您不需要做任何操作使树节点在用户点击时展开和折叠。但是,当用户选择节点时(例如,点击节点),您需要添加一些代码来使树做出响应。

响应节点选择

响应树节点选择非常简单。你需要实现一个树选择监听器并将其注册到树上。下面的代码展示了TreeDemo程序中与选择相关的代码:

//树的初始化:
    tree.getSelectionModel().setSelectionMode
            (TreeSelectionModel.SINGLE_TREE_SELECTION);

    //监听选择变化。
    tree.addTreeSelectionListener(this);
...
public void valueChanged(TreeSelectionEvent e) {
//返回选择的最后一个路径元素。
//只有在选择模型允许单选时,此方法才有用。
    DefaultMutableTreeNode node = (DefaultMutableTreeNode)
                       tree.getLastSelectedPathComponent();

    if (node == null)
    //未选择任何节点。     
    return;

    Object nodeInfo = node.getUserObject();
    if (node.isLeaf()) {
        BookInfo book = (BookInfo)nodeInfo;
        displayURL(book.bookURL);
    } else {
        displayURL(helpURL); 
    }
}

上述代码执行了以下任务:

有关处理树选择事件的更多详细信息,请参阅如何编写树选择监听器

自定义树的显示

下图显示了一些树节点的样式,它们是由Java、Windows和Mac OS的外观实现绘制的。

TreeDemo with angled lines A tree in the Windows look and feel A tree in the MacOS look and feel
Java外观 Windows外观 Mac OS外观

如上图所示,树形结构通常会为每个节点显示一个图标和一些文本。您可以自定义这些内容,稍后我们将进行演示。

树形结构通常还会执行一些与外观相关的绘制操作,以显示节点之间的关系。您可以以有限的方式自定义此绘制操作。首先,您可以使用 tree.setRootVisible(true) 来显示根节点,或者使用 tree.setRootVisible(false) 来隐藏根节点。其次,您可以使用 tree.setShowsRootHandles(true) 来请求树的顶层节点(如果根节点可见)或其子节点(如果根节点不可见)具有展开或折叠的手柄。

如果您使用的是Java外观,您可以自定义绘制树节点之间关系的连线。默认情况下,Java外观会在节点之间绘制带角度的连线。通过设置树的 JTree.lineStyle 客户端属性,您可以指定其他约定。例如,要求Java外观仅使用水平线来分组节点,请使用以下代码:

tree.putClientProperty("JTree.lineStyle", "Horizontal");

要指定Java外观不绘制连线,请使用以下代码:

tree.putClientProperty("JTree.lineStyle", "None");

以下快照展示了在使用Java外观时设置 JTree.lineStyle 属性的结果。

带有角度线的TreeDemo
带有水平线的TreeDemo
没有连线的TreeDemo
"Angled"(默认) "Horizontal" "None"

无论外观如何,节点显示的默认图标取决于节点是否为叶节点,以及如果不是叶节点,则取决于其是否展开。例如,在Windows和Motif外观实现中,每个叶节点的默认图标是一个点;在Java外观中,默认的叶节点图标是一个纸张样式的符号。在我们展示的所有外观实现中,分支节点都使用类似文件夹的符号进行标记。某些外观可能会为展开的分支和折叠的分支使用不同的图标。

您可以轻松地更改用于叶节点、展开的分支节点或折叠的分支节点的默认图标。要做到这一点,首先创建一个DefaultTreeCellRenderer的实例。您还可以从头开始创建自己的TreeCellRenderer实现,重用您喜欢的任何组件。接下来,通过调用渲染器上的一个或多个以下方法来指定要使用的图标:setLeafIcon(用于叶节点)、setOpenIcon(用于展开的分支节点)、setClosedIcon(用于折叠的分支节点)。如果您希望树显示某种类型节点的无图标,则指定null为图标。一旦设置了图标,使用树的setCellRenderer方法指定DefaultTreeCellRenderer来绘制其节点。下面是一个例子,取自TreeIconDemo.java

ImageIcon leafIcon = createImageIcon("images/middle.gif");
if (leafIcon != null) {
    DefaultTreeCellRenderer renderer = 
        new DefaultTreeCellRenderer();
    renderer.setLeafIcon(leafIcon);
    tree.setCellRenderer(renderer);
}

下面是TreeIconDemo的屏幕截图:

TreeIconDemo

试一试: 

如果您希望对节点图标有更精细的控制,或者希望提供工具提示,您可以创建DefaultTreeCellRenderer的子类,并重写getTreeCellRendererComponent方法。因为DefaultTreeCellRendererJLabel的子类,您可以使用任何JLabel方法,如setIcon,来自定义DefaultTreeCellRenderer

以下代码来自 TreeIconDemo2.java,它创建了一个单元格渲染器,根据节点的文本数据中是否包含单词“教程”来变化叶子图标。渲染器还指定了工具提示文本,如粗体所示。


请尝试: 
//...树初始化的地方:
    //启用工具提示。
    ToolTipManager.sharedInstance().registerComponent(tree);
    
    ImageIcon tutorialIcon = createImageIcon("images/middle.gif");
    if (tutorialIcon != null) {
        tree.setCellRenderer(new MyRenderer(tutorialIcon));
    }
...
class MyRenderer extends DefaultTreeCellRenderer {
    Icon tutorialIcon;

    public MyRenderer(Icon icon) {
        tutorialIcon = icon;
    }

    public Component getTreeCellRendererComponent(
                        JTree tree,
                        Object value,
                        boolean sel,
                        boolean expanded,
                        boolean leaf,
                        int row,
                        boolean hasFocus) {

        super.getTreeCellRendererComponent(
                        tree, value, sel,
                        expanded, leaf, row,
                        hasFocus);
        if (leaf && isTutorialBook(value)) {
            setIcon(tutorialIcon);
            setToolTipText("这本书是教程系列的。");
        } else {
            setToolTipText(null); //没有工具提示
        } 

        return this;
    }

    protected boolean isTutorialBook(Object value) {
        DefaultMutableTreeNode node =
                (DefaultMutableTreeNode)value;
        BookInfo nodeInfo =
                (BookInfo)(node.getUserObject());
        String title = nodeInfo.bookName;
        if (title.indexOf("Tutorial") >= 0) {
            return true;
        }

        return false;
    }
}

这是结果:

TreeIconDemo2

你可能想知道一个单元格渲染器是如何工作的。当树绘制每个节点时,不是JTree或其特定于外观的实现实际上包含绘制节点的代码。相反,树使用单元格渲染器的绘制代码来绘制节点。例如,要绘制一个叶节点,其字符串为"The Java Programming Language",树会要求其单元格渲染器返回一个可以绘制带有该字符串的叶节点的组件。如果单元格渲染器是DefaultTreeCellRenderer,则返回一个标签,该标签绘制默认的叶节点图标,然后是字符串。

单元格渲染器只能进行绘制,不能处理事件。如果要向树添加事件处理程序,需要在树或(如果处理仅在选择节点时发生)树的单元格编辑器上注册处理程序。有关单元格编辑器的信息,请参见概念:编辑器和渲染器。该部分讨论了与树单元格编辑器和渲染器类似的表格单元格编辑器和渲染器。

动态更改树

下图显示了一个名为DynamicTreeDemo的应用程序,允许您向可见树中添加节点和删除节点。您还可以编辑每个节点中的文本。

DynamicTreeDemo

该应用程序是基于教程读者Richard Stanford提供的示例。


试一试: 

下面是初始化树的代码:

rootNode = new DefaultMutableTreeNode("根节点");
treeModel = new DefaultTreeModel(rootNode);
treeModel.addTreeModelListener(new MyTreeModelListener());

tree = new JTree(treeModel);
tree.setEditable(true);
tree.getSelectionModel().setSelectionMode
        (TreeSelectionModel.SINGLE_TREE_SELECTION);
tree.setShowsRootHandles(true);

通过显式创建树的模型,代码保证树的模型是 DefaultTreeModel 的一个实例。这样,我们就知道树模型支持的所有方法。例如,我们知道我们可以调用模型的 insertNodeInto 方法,即使该方法不是 TreeModel 接口所要求的。

为了使树节点中的文本可编辑,我们在树上调用 setEditable(true)。当用户完成对节点的编辑后,模型会生成一个树模型事件,告诉任何监听器(包括 JTree)树节点已经改变。请注意,尽管 DefaultMutableTreeNode 有改变节点内容的方法,但更改应通过 DefaultTreeModel 的封装方法进行。否则,树模型事件将不会生成,并且树等监听器将不会知道更新。

要接收节点更改的通知,我们可以实现一个 TreeModelListener。下面是一个树模型监听器的示例,可以检测用户是否为树节点键入了新名称:

class MyTreeModelListener implements TreeModelListener {
    public void treeNodesChanged(TreeModelEvent e) {
        DefaultMutableTreeNode node;
        node = (DefaultMutableTreeNode)
                 (e.getTreePath().getLastPathComponent());

        /*
         * 如果事件列出了子节点,则更改的节点是我们已经获取到的节点的子节点。
         * 否则,更改的节点和指定的节点是相同的。
         */
        try {
            int index = e.getChildIndices()[0];
            node = (DefaultMutableTreeNode)
                   (node.getChildAt(index));
        } catch (NullPointerException exc) {}

        System.out.println("用户已完成对节点的编辑。");
        System.out.println("新值:" + node.getUserObject());
    }
    public void treeNodesInserted(TreeModelEvent e) {
    }
    public void treeNodesRemoved(TreeModelEvent e) {
    }
    public void treeStructureChanged(TreeModelEvent e) {
    }
}

以下是添加按钮的事件处理程序使用的代码,用于向树中添加新节点:

treePanel.addObject("New Node " + newNodeSuffix++);
...
public DefaultMutableTreeNode addObject(Object child) {
    DefaultMutableTreeNode parentNode = null;
    TreePath parentPath = tree.getSelectionPath();

    if (parentPath == null) {
        // 没有选择。默认为根节点。
        parentNode = rootNode;
    } else {
        parentNode = (DefaultMutableTreeNode)
                     (parentPath.getLastPathComponent());
    }

    return addObject(parentNode, child, true);
}
...
public DefaultMutableTreeNode addObject(DefaultMutableTreeNode parent,
                                        Object child,
                                        boolean shouldBeVisible) {
    DefaultMutableTreeNode childNode =
            new DefaultMutableTreeNode(child);
    ...
    treeModel.insertNodeInto(childNode, parent,
                             parent.getChildCount());

    // 确保用户可以看到新节点。
    if (shouldBeVisible) {
        tree.scrollPathToVisible(new TreePath(childNode.getPath()));
    }
    return childNode;
}

这段代码创建一个节点,并将其插入到树模型中,然后,如果适用,请求展开其上方的节点并滚动树,以使新节点可见。要将节点插入到模型中,代码使用了DefaultTreeModel类提供的insertNodeInto方法。

创建数据模型

如果DefaultTreeModel不满足您的需求,那么您将需要编写一个自定义数据模型。您的数据模型必须实现TreeModel接口。TreeModel指定了获取树的特定节点、获取特定节点的子节点数、确定节点是否为叶节点、通知模型树发生变化以及添加和删除树模型监听器的方法。

有趣的是,TreeModel接口接受任何类型的对象作为树节点。它不要求节点由DefaultMutableTreeNode对象表示,甚至不要求节点实现TreeNode接口。因此,如果TreeNode接口不适合您的树模型,可以自由地设计自己的树节点表示形式。例如,如果您有一个现有的层次结构数据结构,您不需要复制它或强制其进入TreeNode模式。您只需要实现自己的树模型,使其使用现有数据结构中的信息。

下图显示了一个名为GenealogyExample的应用程序,显示特定人的后代或祖先。(感谢教程读者Olivier Berlanger提供此示例。)


试一试: 
GenealogyExample

您可以在 GenealogyModel.java 中找到自定义树模型的实现。由于该模型是作为 Object 子类而不是 DefaultTreeModel 的子类实现的,因此它必须直接实现 TreeModel 接口。这需要实现一些关于节点的信息的方法,例如根节点是哪个,特定节点的子节点是什么。在 GenealogyModel 的情况下,每个节点由一个 Person 类型的对象表示,这是一个不实现 TreeNode 的自定义类。

树模型还必须实现添加和删除树模型监听器的方法,并在树的结构或数据发生更改时向这些监听器发送 TreeModelEvent。例如,当用户指示 GenealogyExample 从显示祖先切换到显示后代时,树模型进行更改,然后发送事件通知其监听器(例如树组件)。

如何延迟加载子节点

延迟加载是应用程序的一种特性,即在实例实际使用之前,实际加载和实例化类的操作被延迟到最后时刻。

通过延迟加载,我们能获得什么好处?是的,这肯定会提高应用程序的性能。通过延迟加载,您可以将内存资源专用于在实际使用时才加载和实例化对象。您还可以加快应用程序的初始加载时间。

您可以利用 TreeWillExpandListener 接口来延迟加载树的子节点。例如,您可以在应用程序中声明和加载树的根节点、祖父节点和父节点,如下面的代码所示:

让我们声明根节点、祖父节点和父节点,如下所示:

class DemoArea extends JScrollPane
                   implements TreeWillExpandListener {
        .......
        .......

        private TreeNode createNodes() {
            DefaultMutableTreeNode root;
            DefaultMutableTreeNode grandparent;
            DefaultMutableTreeNode parent;

            root = new DefaultMutableTreeNode("San Francisco");

            grandparent = new DefaultMutableTreeNode("Potrero Hill");
            root.add(grandparent);

            parent = new DefaultMutableTreeNode("Restaurants");
            grandparent.add(parent);
            
            dummyParent = parent;
            return root;
        }

您可以将上述声明的节点加载到树中,如下所示:

TreeNode rootNode = createNodes();
tree = new JTree(rootNode);
tree.addTreeExpansionListener(this);
tree.addTreeWillExpandListener(this);
.......
.......
setViewportView(tree);

现在,每当应用程序中的父节点Restaurants可见时,您可以延迟加载子节点到应用程序中。为此,让我们在一个单独的方法中声明两个子节点,并在以下代码中调用该方法:

private void 加载延迟子节点(){
            DefaultMutableTreeNode child;
            child = new DefaultMutableTreeNode("泰式烧烤");
            dummyParent.add(child);
            child = new DefaultMutableTreeNode("山羊山披萨");
            dummyParent.add(child);
            textArea.append(" 泰式烧烤和山羊山披萨已被延迟加载");
        }

        .......
        .......

public void treeWillExpand(TreeExpansionEvent e) 
                    throws ExpandVetoException {
            saySomething("您即将展开节点 ", e);
            int n = JOptionPane.showOptionDialog(
                this, willExpandText, willExpandTitle,
                JOptionPane.YES_NO_OPTION,
                JOptionPane.QUESTION_MESSAGE,
                null,
                willExpandOptions,
                willExpandOptions[1]);
           
        加载延迟子节点();
        }

有关Tree-Will-Expand监听器的描述,请参见如何编写Tree-Will-Expand监听器

树API

树API非常广泛。以下表格仅列出了API的一小部分,重点关注以下类别:

有关树API的更多信息,请参见JTree的API文档,以及tree package中各个类和接口的文档。还可以参考JComponent类中有关JTree继承自其超类的API的信息。

与树相关的类和接口JTreeTreePathTreeNode
MutableTreeNode
DefaultMutableTreeNodeTreeModel
DefaultTreeModelTreeCellRenderer
DefaultTreeCellRendererTreeCellEditor
DefaultTreeCellEditorTreeSelectionModel
DefaultTreeSelectionModelTreeSelectionListener
TreeSelectionEvent入门指南TreeModelListener
TreeModelEvent如何编写树模型监听器TreeExpansionListener
TreeWillExpandListener
TreeExpansionEvent如何编写树展开监听器如何编写树展开监听器ExpandVetoException如何编写树展开监听器
创建和设置树JTree(TreeNode)
JTree(TreeNode, boolean)
JTree(TreeModel)
JTree()
JTree(Hashtable)
JTree(Object[])
JTree(Vector)TreeNodeTreeModelHashtableVector

如果存在boolean参数,则指定树应如何确定节点是否应显示为叶节点。如果参数为false(默认值),则没有子节点的任何节点都显示为叶节点。如果参数为true,则只有当节点的getAllowsChildren方法返回false时,节点才是叶节点。

void setCellRenderer(TreeCellRenderer)void setEditable(boolean)
void setCellEditor(TreeCellEditor)void setRootVisible(boolean)void setShowsRootHandles(boolean)setShowsRootHandles(true)void setDragEnabled(boolean)
boolean getDragEnabled()dragEnabled拖放和数据传输
实现选择
方法 目的
void addTreeSelectionListener(TreeSelectionListener) 注册一个监听器来检测节点是否被选择或取消选择。
void setSelectionModel(TreeSelectionModel)
TreeSelectionModel getSelectionModel()
设置或获取用于控制节点选择的模型。可以使用setSelectionModel(null)完全关闭节点选择。
void setSelectionMode(int)
int getSelectionMode()
(在TreeSelectionModel中)
设置或获取选择模式。值可以是CONTIGUOUS_TREE_SELECTIONDISCONTIGUOUS_TREE_SELECTIONSINGLE_TREE_SELECTION(都在TreeSelectionModel中定义)。
Object getLastSelectedPathComponent() 获取表示当前选择节点的对象。这相当于在tree.getSelectionPath()返回的值上调用getLastPathComponent
void setSelectionPath(TreePath)
TreePath getSelectionPath()
设置或获取当前选择节点的路径。
void setSelectionPaths(TreePath[])
TreePath[] getSelectionPaths()
设置或获取当前选择节点的路径。
void setSelectionPath(TreePath)
TreePath getSelectionPath()
设置或获取当前选择节点的路径。

显示和隐藏节点
方法 目的
void addTreeExpansionListener(TreeExpansionListener)
void addTreeWillExpandListener(TreeWillExpandListener)
注册一个监听器来检测树节点的展开或折叠情况,或者将要展开或折叠的情况。为了阻止即将发生的展开或折叠,TreeWillExpandListener可以抛出ExpandVetoException。
void expandPath(TreePath)
void collapsePath(TreePath)
展开或折叠指定的树路径。
void scrollPathToVisible(TreePath) 确保指定路径的节点可见 - 即展开了通向它的路径,并且节点位于滚动窗格的可视区域内。
void makeVisible(TreePath) 确保指定路径的节点可见 - 即展开了通向它的路径。节点可能不在可视区域内。
void setScrollsOnExpand(boolean)
boolean getScrollsOnExpand()
设置或获取树是否尝试滚动以显示之前隐藏的节点。默认值为true。
void setToggleClickCount(int)
int getToggleClickCount()
设置或获取节点展开或关闭前需要的鼠标点击次数。默认值为2。
TreePath getNextMatch(String, int, Position.Bias) 返回以指定前缀开头的下一个树元素的TreePath。

使用树的示例

这个表格列出了使用JTree的示例以及这些示例的描述。

示例 描述位置 注释
TreeDemo 创建树响应节点选择自定义树的显示 创建一个响应用户选择的树。还有用于自定义Java外观下线条样式的代码。
TreeIconDemo 自定义树的显示 向TreeDemo添加自定义叶子图标。
TreeIconDemo2 自定义树的显示 自定义特定的叶子图标,还为某些树节点提供工具提示。
DynamicTreeDemo 动态改变树 演示了向树中添加和删除节点的过程。还允许编辑节点文本。
GenealogyExample 创建数据模型 实现了自定义的树模型和自定义节点类型。
TreeExpandEventDemo 如何编写树展开监听器 展示了如何检测节点的展开和折叠。
TreeExpandEventDemo2 如何编写树将展开监听器 展示了如何否决节点的展开。

如果你在使用JavaFX进行编程,请参阅树视图


上一页: 如何使用工具提示
下一页: 如何在Swing组件中使用HTML