第20章 树
Swing树使用人们所熟悉的文件夹和树叶图来显示分层的数据。应用最广泛的树组件(树组件又称为轮廓控件)。无疑是Windows Explorer,它包含一个用于导航目录的树组件。
与表格类似,树由许多类和接口组成,这些类和接口在它们自己的包——swing.tree包中定义,swing包中的JTree类代表树组件。
树由节点组成,节点可以是文件夹,也可以是树叶。文件夹可以有子节点,除根节点之外的所有节点都只有一个父节点。空的文件夹与树叶的不同之处就在于它允许有子节点。
图20-1显示的是JTree类的一个扩展,该扩展可用于导航目录和文件。文件夹和树叶由不同的图标表示,这些图标都是彼此独立的。在文件夹上双击,或单击文件夹的句柄,就可以展开或折叠文件夹,根节点句柄的可见性可以被设置。例如,图20-1所示的树的根节点就没有显示句柄。
除父节点和子节点外,树的节点还有一个用户对象(当使用DefaultTreeModel时就会呈现一个用户对象)。用户对象是Object类型,因此它提供了一个将任意对象与节点相关联的办法。
树有一个简单的模型,每一个JTree实例都要维护对绘制器和编辑器的引用,这个绘制器和编辑器被树中所有的节点所使用。在表20-1中列出了swing.tree包中的主要类。
表20-1 Swing.tree包中的主要类
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
名 称 实 现
─────────────────────────────────
DefaultMutableTreeNode 一个具有一个父节点、(可能)许多子节点和
一个用户对象的可变节点,为相关联的节点提
供了访问方法。如果没有任何子节点,这个节
点就是树叶
DefaultTreeModel 一个激发TreeModel Events事件的简单可变的模型。
提供对子节点的访问方法,但不是提供对父节点的访
问方法
DefaultTreeCellEditor 绘制器和编辑器的包装器,它把一个“真正”的
编辑器组件放在节点图标的旁边
DefaultTreeCellRenderer 具有字体、颜色和图标访问方法的JLabel扩
展,它提供图标的缺省值
TreePath 由一个节点到另一个节点的路径。路径中的节点存储在一个数
组中。路径用于在选取内容之间进行通信
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
20.1 创建树
图20-2示出的小应用程序包含一个表格,这个表格是用JTree无参数构件方法创建的。
图20-2中所示的小应用程序向应用程序的内容空格中添加了一个树,这个树包裹在一个滚动空格中。在例20-1中列出了这个小应用程序的代码。在缺省情况下显示一个树中所示的树在程序开始运行时,它的文件夹都是展开的。
例20-1 一个简单的树的例子
─────────────────────────────────
import javax.swing.*;
public class Test extends JApplet {
public void init(){
getContentPane().add(new JScrollPane(new JTree()));
}
}
─────────────────────────────────
如果在构造时没有显式地指定模型或节点,则JTree的实例就以图20-2所示的节点来构造。
几乎所有的树都是以下面的方式来构造的:选创建一个根节点,然后建立分层结构或创建一个树模型。例如,在创建图20-2所示的节点的缺省分层结构时,JTree缺省的构造方法就调用了JTree.getDefaultTreeModel()。
//From JTree.java:
protected static TreeModel getDefaultTreeModel(){
DefaultMutableTreeNode root=
new Default Mutable Tree Node ("JTree");
DefaultMutableTreeNode parent;
parent = new DefaultMutableTreeNode("colors");
root.add(parent);
parent.add(new DefaultMutableTreeNode("blue"));
parent.add(new DefaultMutableTreeNode("violet"));
parent.add(new DefaultMutableTreeNode("red"));
parent.add(new DefaultMutableTreeNode("yellow"));
parent = new DefaultMutableTreeNode("sports");
root.add(parent);
parent.add(new DefaultMutableTreeNode("basketball"));
parent.add(new DefaultMutableTreeNode("soccer"));
parent.add(new DefaultMutableTreeNode("football"));
parent.add(new DefaultMutableTreeNode("hockey"));
...
return new DefalutTreeModel(root);
}
用户对象的带有字符串JTree的节点被实例化,而且最终指定为树模型的根节点,节点colors有四个子节点,且指定为根节点的唯一节点。节点sports也有四个节点,而且也被添加到根节点中。
JTree类还提供了用Object数组、哈希表和矢量创建树的构造方法,如图20-3所示。
与构造分层节点相比,用数据来构造树,需要注意两点:
第一点,数据很少(几乎没有)用来构造树,通常,树都是用节点一配置的,图20-2中所示的小应用程序即是一例。
第二点,由于对象添加到哈希表中的顺序和对象在哈希表中存放的顺序没有任何联系,因此,用哈希表创建的树的节点顺序是难以预料的。
例20-2列出了图20-3中所示小应用程序的完整代码。
例20-2用对象、矢量和哈希表来创建树
─────────────────────────────────
import javax.swing.*;
import javax.swing.tree.TreePath;
import java.awt.*;
import java.awt.event.*;
import java.util.*;
public class Test extends JApplet {
Hashtable ht = new Hashtable(), ht2 = new Hashtable();
Vector vector = new Vector();
Object[] objs = new Object[] {
"array item 1", "array item 2", "array item 3"
};
public void init() {
Container contentPane = getContentPane();
vector.addElement("vector element 1");
vector.addElement("vector element 2");
vector.addElement("vector element 3");
vector.addElement("vector element 4");
vector.addElement("vector element 5");
ht.put("another hashtable", ht2);
ht.put("vector", vector);
ht.put("Object[]", objs);
ht2.put("Object[]", objs);
ht2.put("vector", vector);
ht2.put("one", new Integer(1));
ht2.put("two", new Integer(2));
ht2.put("three", new Integer(3));
// trees must be created after data is populated
JTree hashTree = new JTree(ht);
JTree vectorTree = new JTree(vector);
JTree objectTree = new JTree(objs);
JScrollPane objPane = new JScrollPane(objectTree);
JScrollPane hashPane = new JScrollPane(hashTree);
JScrollPane vectorPane = new JScrollPane(vectorTree);
objPane.setPreferredSize(new Dimension(150,200));//w,h
hashPane.setPreferredSize(new Dimension(250,200));
vectorPane.setPreferredSize(new Dimension(150,200));
objPane.setBorder(
BorderFactory.createTitledBorder("Object[]"));
hashPane.setBorder(
BorderFactory.createTitledBorder("Hashtable"));
vectorPane.setBorder(
BorderFactory.createTitledBorder("Vector"));
hashTree.expandPath(new TreePath(
hashTree.getModel().getRoot()));
contentPane.setLayout(new FlowLayout());
contentPane.add(objPane);
contentPane.add(hashPane);
contentPane.add(vectorPane);
}
}
─────────────────────────────────
20.2 树节点
在Swing树中,树节点是关键的组成部分,如同列是表格的主干一样。树节点由TreeNode接口定义,TreeNode接口被MutableTreeNode接口扩展,而MutableTreeNode接口又由DefaultMutableTreeNode类来实现。
20.2.1 TreeNode接口
TreeNode接口定义了(固定)树节点的实质,接口总结20-1总结了树节点。
接口总结20-1 TreeNode |
public abstract Enumeration children()
public abstract TreeNode getParent()
public abstract TreeNode getChildAt(int)
public abstract int getChildCount()
public abstract int getIndex(TreeNode)
public abstract abstract boolean getAllowsChildren()
public abstract boolean isLeaf()
上面列出的前两组方法是对一个节点的父节点和子节点的访问方法。访问一个节点的子节点,可以通过枚举子节点的父节点来实现,也可以通过索引来访问子节点。另外,还定义了获取节点索引的方法和获取一个节点包含的子节点数目的方法。
上面列出的最后两个方法用来确定一个节点是文件夹,还是树叶。
开发人员很少直接实现TreeNode接口,这是因为Swing在DefaultMutableTreeNode类中提供了TreeNode接口的一个常用的缺省实现。数目众多的树节点扩展了DefaultMutableTreeNode。
20.2.2 MutableTreeNode接口
MutableTreeNode接口扩展TreeNode,它除了定义指定用户对象的方法外,还定义了修改一个节点的父节点和子节点的方法。接口总结20-2总结了MutableNode接口。
接口总结20-2 MutableTreeNode |
扩展:TreeNode
public abstract void insert(MutableTreeNode child,int index)
public abstract void remove(int index)
public abstract void remove(MutableTreeNode child)
public abstract void removeFromParent()
public abstract void setParent(MutableTreeNode)
public abstract void setUserObject(Object)
上面列出的第一组方法用来插入和删除子节点,子节点可以通过索引或引用来删除。removeFromParent方法用来将节点从父节点中删除,并更新父节点的子节点数目。
上面列出的最后两个方法用来设置一个节点的父节点和用户对象。需要注意的是,MutableTreeNode继承了getParent方法,而没有继承getUserObject方法,这是一个疏漏,在以后发布的Swing中将予以更正。在实际应用中,因为没有getUserObject方法而造成的影响几乎为零,这是因为该方法已在defaultMutableTreeNode类中得到了实现。
20.2.3 DefaultMutableTreeNode类
在实际应用各,很少直接实现MutableTreeNode接口,这是因为Swing以DefaultMutableTreeNode类的形式提供了一个合理又强壮的MutableTreeNode接口的实现。
图20-4示出了DefaultMutableTreeNode接口,并维护对其父节点、用户对象和子节点的引用。通过维护对其父节点和子节点的一个引用,DefaultMutableTreeNode类实现了组合设计样式(注:内容略),该样式允许对文件夹和树叶进行嵌套。AWT的Component类Container类也是组合设计样式的一个样例(注:内容略)。
1.使用DefaultMutableTreeNode
树节点几乎总是DefaultMutableTreeNode类的实例或它的扩展。例如,由一个树创建的缺省节点就是DefaultMutableTreeNode的实例。
除了那些在TreeNode和MutableTreeNode接口中定义的方法外,DefaultMutableTreeNode类还提供了许多其他的方法来访问相关的节点。例如,它提供了getFirstLeaf和getNextLeaf方法,前一种方法返回第一个树叶,第二种方法返回某一个给定树叶的下一个兄弟节点。
DefaultMutableTreeNode类还提供了一些方法,这些方法以尝试优先或宽度优先遍历方式来返回由一个节点派生的子节点的一个枚举,深度优先和宽度优先这两种遍历方式的区别同图20-5。例20-3的小应用程序显示树的节点,这些节点是通过对一个树的根节点调用DefaultMutableTreeNode.depthFirstEmumeration和DefaultMutableTreeNode.breadthFirstEnumeration来获取的。
例20-3 深度优先遍历与宽度优先遍历之间的比较
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.tree.*;
import java.util.*;
public class Test extends JApplet {
private JTree tree = new JTree();
private JButton button = new JButton("show traversals");
private DefaultMutableTreeNode root =
(DefaultMutableTreeNode)tree.getModel().getRoot();
public void init() {
getContentPane().add(new JScrollPane(tree),
BorderLayout.CENTER);
getContentPane().add(button, BorderLayout.NORTH);
button.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
Enumeration df = root.depthFirstEnumeration();
Enumeration bf = root.breadthFirstEnumeration();
while(df.hasMoreElements()) {
System.out.println(
df.nextElement().toString());
}
System.out.println("");
System.out.println("");
while(bf.hasMoreElements()) {
System.out.println(
bf.nextElement().toString());
}
}
});
}
}
2.扩展DefaultMutableTreeNode
可能对目录和文件提供导航的文件查看器可能是树组件最自然的应用,图20-6所示的应用程序就包含一个可用作文件查看器的JTree的一个实例。
Swing树的设计基本是简便易行的,图20-6所示的应用程序就是一个证明,它有一个简单的实现方法。树包含有定制节点,这些节点是FileNode类的一些实例,而FileNode类维护作为自己的用户对象的一个File实例。
例 20-4 JTree文件查看器
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.tree.*;
import java.awt.*;
import java.awt.event.*;
import java.io.File;
import java.util.EventObject;
public class Test extends JFrame {
public Test() {
final JTree tree = new JTree(createTreeModel());
JScrollPane scrollPane = new JScrollPane(tree);
getContentPane().add(scrollPane, BorderLayout.CENTER);
getContentPane().add(GJApp.getStatusArea(),
BorderLayout.SOUTH);
tree.addTreeExpansionListener(new TreeExpansionListener(){
public void treeCollapsed(TreeExpansionEvent e) {
}
public void treeExpanded(TreeExpansionEvent e) {
UpdateStatus updateThread;
TreePath path = e.getPath();
FileNode node = (FileNode)
path.getLastPathComponent();
if( ! node.isExplored()) {
DefaultTreeModel model = (DefaultTreeModel)tree.getModel();
GJApp.updateStatus("exploring ...");
UpdateStatus us = new UpdateStatus();
us.start();
node.explore();
model.nodeStructureChanged(node);
}
}
class UpdateStatus extends Thread {
public void run() {
try { Thread.currentThread().sleep(450); }
catch(InterruptedException e) { }
SwingUtilities.invokeLater(new Runnable() {
public void run() {
GJApp.updateStatus(" ");
}
});
}
}
});
}
private DefaultTreeModel createTreeModel() {
File root = new File("E:/");
FileNode rootNode = new FileNode(root);
rootNode.explore();
return new DefaultTreeModel(rootNode);
}
public static void main(String args[]) {
GJApp.launch(new Test(),"JTree File Explorer",
300,300,450,400);
}
}
class FileNode extends DefaultMutableTreeNode {
private boolean explored = false;
public FileNode(File file) {
setUserObject(file);
}
public boolean getAllowsChildren() { return isDirectory(); }
public boolean isLeaf() { return !isDirectory(); }
public File getFile() { return (File)getUserObject(); }
public boolean isExplored() { return explored; }
public boolean isDirectory() {
File file = getFile();
return file.isDirectory();
}
public String toString() {
File file = (File)getUserObject();
String filename = file.toString();
int index = filename.lastIndexOf(File.separator);
return (index != -1 && index != filename.length()-1) ?
filename.substring(index+1) :
filename;
}
public void explore() {
if(!isDirectory())
return;
if(!isExplored()) {
File file = getFile();
File[] children = file.listFiles();
for(int i=0; i < children.length; ++i)
add(new FileNode(children[i]));
explored = true;
}
}
}
class GJApp extends WindowAdapter {
static private JPanel statusArea = new JPanel();
static private JLabel status = new JLabel(" ");
public static void launch(final JFrame f, String title,
final int x, final int y,
final int w, int h) {
f.setTitle(title);
f.setBounds(x,y,w,h);
f.setVisible(true);
statusArea.setBorder(BorderFactory.createEtchedBorder());
statusArea.setLayout(new FlowLayout(FlowLayout.LEFT,0,0));
statusArea.add(status);
status.setHorizontalAlignment(JLabel.LEFT);
f.setDefaultCloseOperation(
WindowConstants.DISPOSE_ON_CLOSE);
f.addWindowListener(new WindowAdapter() {
public void windowClosed(WindowEvent e) {
System.exit(0);
}
});
}
static public JPanel getStatusArea() {
return statusArea;
}
static public void updateStatus(String s) {
status.setText(s);
}
}
20.3 树路径
例20-5 使用树路径
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.tree.*;
import java.awt.*;
//import java.awt.event.*;
public class Test extends JApplet {
JTree tree = new JTree();
DefaultTreeModel model = (DefaultTreeModel)tree.getModel();
TreeSelectionModel selectionModel = tree.getSelectionModel();
public void init() {
getContentPane().add(tree, BorderLayout.CENTER);
tree.addTreeSelectionListener(
new TreeSelectionListener() {
public void valueChanged(TreeSelectionEvent e) {
TreePath path = e.getNewLeadSelectionPath();
if(path == null)
System.out.println("Selection Cleared");
else {
TreePath parentPath = path.getParentPath();
Object
lastNode = path.getLastPathComponent(),
firstNode= path.getPathComponent(0);
System.out.println("Path: " + path +
" has " +
path.getPathCount() +
" nodes");
System.out.println("Last Path Component: " +
lastNode.toString());
System.out.println("First Path Component: " +
firstNode.toString());
System.out.println("Parent Path: " +
parentPath);
// the following if statement is always true
if(parentPath.isDescendant(path)) {
System.out.println(parentPath +
" is a descendant of " + path);
}
DefaultMutableTreeNode last =
(DefaultMutableTreeNode)lastNode;
DefaultMutableTreeNode first =
(DefaultMutableTreeNode)lastNode;
if(first.isNodeDescendant(last)) {
System.out.println(
"last is descendant of first");
}
System.out.println("");
}
}
});
}
}
20.4 树模型
例20-6 添加和删除节点
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.tree.*;
import java.awt.*;
import java.awt.event.*;
public class Test extends JFrame {
JTree tree = new JTree();
DefaultTreeModel model = (DefaultTreeModel)tree.getModel();
TreeSelectionModel selectionModel = tree.getSelectionModel();
JButton removeButton = new JButton("Remove selected node");
JButton addButton = new JButton("Add node");
public Test() {
Container contentPane = getContentPane();
selectionModel.setSelectionMode(
TreeSelectionModel.SINGLE_TREE_SELECTION);
contentPane.add(new ControlPanel(), BorderLayout.NORTH);
contentPane.add(tree, BorderLayout.CENTER);
tree.addTreeSelectionListener(
new TreeSelectionListener() {
public void valueChanged(TreeSelectionEvent e) {
TreePath path = e.getNewLeadSelectionPath();
boolean nodesAreSelected = (path != null);
addButton.setEnabled(nodesAreSelected);
removeButton.setEnabled(nodesAreSelected);
}
});
model.addTreeModelListener(new TreeModelListener() {
public void treeNodesInserted(TreeModelEvent e) {
showInsertionOrRemoval(e, " added to ");
}
public void treeNodesRemoved(TreeModelEvent e) {
showInsertionOrRemoval(e, " removed from ");
}
private void showInsertionOrRemoval(TreeModelEvent e,
String s) {
Object[] parentPath = e.getPath();
int[] indexes = e.getChildIndices();
Object[] children = e.getChildren();
Object parent = parentPath[parentPath.length-1];
JOptionPane.showMessageDialog(Test.this,
"Node /"" + children[0].toString() +
"/"" + s + parent.toString() +
" at index " + indexes[0],
"Node Added or Removed",
JOptionPane.INFORMATION_MESSAGE);
}
public void treeNodesChanged(TreeModelEvent e) {}
public void treeStructureChanged(TreeModelEvent e) {}
});
}
class ControlPanel extends JPanel {
public ControlPanel() {
addButton.setEnabled(false);
removeButton.setEnabled(false);
add(addButton);
add(removeButton);
addButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
TreePath path =
selectionModel.getSelectionPath();
MutableTreeNode parent, node =
(MutableTreeNode)path.getLastPathComponent();
if(path.getPathCount() > 1)
parent = (MutableTreeNode)node.getParent();
else
parent = (MutableTreeNode)node;
int index = parent.getIndex(node) + 1;
String s = JOptionPane.showInputDialog(
Test.this,
"Enter a name for the new node:",
"New Tree Node",
JOptionPane.QUESTION_MESSAGE);
MutableTreeNode newNode =
new DefaultMutableTreeNode(s);
model.insertNodeInto(newNode, parent, index);
}
});
removeButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
TreePath path =
selectionModel.getSelectionPath();
if(path.getPathCount() == 1) {
JOptionPane.showMessageDialog(ControlPanel.this,
"Can't remove root node!");
return;
}
MutableTreeNode node =
(MutableTreeNode)path.getLastPathComponent();
model.removeNodeFromParent(node);
}
});
}
}
public static void main(String args[]) {
GraphicJavaApplication.launch(new Test(),
"Tree Model Example",300,300,450,300);
}
}
class GraphicJavaApplication extends WindowAdapter {
public static void launch(final JFrame f, String title,
final int x, final int y,
final int w, int h) {
f.setTitle(title);
f.setBounds(x,y,w,h);
f.setVisible(true);
f.setDefaultCloseOperation(
WindowConstants.DISPOSE_ON_CLOSE);
f.addWindowListener(new WindowAdapter() {
public void windowClosed(WindowEvent e) {
System.exit(0);
}
});
}
}
20.5 树选取
例20-7 树选取模式
import javax.swing.*;
import javax.swing.tree.*;
import java.awt.*;
import java.awt.event.*;
public class Test extends JApplet {
JTree tree = new JTree();
TreeSelectionModel selectionModel = tree.getSelectionModel();
String modes[] = {
"CONTIGUOUS_TREE_SELECTION",
"DISCONTIGUOUS_TREE_SELECTION",
"SINGLE_TREE_SELECTION"
};
int modeIds[] = {
TreeSelectionModel.CONTIGUOUS_TREE_SELECTION,
TreeSelectionModel.DISCONTIGUOUS_TREE_SELECTION,
TreeSelectionModel.SINGLE_TREE_SELECTION,
};
public void init() {
Container contentPane = getContentPane();
contentPane.add(new ControlPanel(), BorderLayout.NORTH);
contentPane.add(new JScrollPane(tree),
BorderLayout.CENTER);
}
class ControlPanel extends JPanel {
JComboBox combo = new JComboBox();
JButton button = new JButton("clear selection");
public ControlPanel() {
for(int i=0; i < modes.length; ++i) {
combo.addItem(modes[i]);
}
add(new JLabel("Selection Mode:"));
add(combo);
add(button);
int initialMode = selectionModel.getSelectionMode();
if(initialMode == modeIds[0])
combo.setSelectedIndex(0);
else if(initialMode == modeIds[1])
combo.setSelectedIndex(1);
else if(initialMode == modeIds[2])
combo.setSelectedIndex(2);
combo.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
int index = combo.getSelectedIndex();
selectionModel.setSelectionMode(
modeIds[index]);
}
});
button.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
selectionModel.clearSelection();
}
});
}
}
}
20.6 树单元绘制
20.6.1 DefaultTreeCellRenderer
例20-8 使用DefaultTreeCellRenderer
import javax.swing.*;
import javax.swing.tree.*;
import java.awt.*;
import java.awt.event.*;
public class Test extends JApplet {
static private Icon
openFolder = new ImageIcon("button_lit.jpg"),
closedFolder = new ImageIcon("button.jpg"),
leafIcon = new ImageIcon("leaf.gif");
public void init() {
JTree tree = new JTree();
JScrollPane scrollPane = new JScrollPane(tree);
DefaultTreeCellRenderer renderer =
new DefaultTreeCellRenderer();
renderer.setClosedIcon(closedFolder);
renderer.setOpenIcon(openFolder);
renderer.setLeafIcon(leafIcon);
renderer.setFont(new Font("Serif", Font.ITALIC, 12));
tree.setCellRenderer(renderer);
tree.setEditable(true);
getContentPane().add(scrollPane);
}
}
例20-9 用UIManager类设置树图标的缺省值
import javax.swing.*;
import javax.swing.tree.*;
import java.awt.*;
import java.awt.event.*;
public class Test extends JApplet {
static private Icon
openFolder = new ImageIcon("button_lit.jpg"),
closedFolder = new ImageIcon("button.jpg"),
leafIcon = new ImageIcon("leaf.gif");
public void init() {
UIManager.put("Tree.closedIcon", closedFolder);
UIManager.put("Tree.openIcon", openFolder);
UIManager.put("Tree.leafIcon", leafIcon);
JTree tree = new JTree();
JScrollPane scrollPane = new JScrollPane(tree);
getContentPane().add(scrollPane);
}
}
例20-10 扩展DefalutTreeCellRenderer
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.tree.*;
import java.io.File;
public class Test extends JFrame {
public Test() {
final JTree tree = new JTree(createTreeModel());
JScrollPane scrollPane = new JScrollPane(tree);
FileNodeRenderer renderer = new FileNodeRenderer();
tree.setEditable(true);
tree.setCellRenderer(renderer);
getContentPane().add(scrollPane, BorderLayout.CENTER);
tree.addTreeExpansionListener(new TreeExpansionListener(){
public void treeCollapsed(TreeExpansionEvent e) {
}
public void treeExpanded(TreeExpansionEvent e) {
TreePath path = e.getPath();
FileNode node = (FileNode)
path.getLastPathComponent();
if( ! node.isExplored()) {
DefaultTreeModel model =
(DefaultTreeModel)tree.getModel();
node.explore();
model.nodeStructureChanged(node);
}
}
});
}
private DefaultTreeModel createTreeModel() {
File root = new File("E:/");
FileNode rootNode = new FileNode(root), node;
rootNode.explore();
return new DefaultTreeModel(rootNode);
}
public static void main(String args[]) {
GJApp.launch(new Test(),"JTree File Explorer",
300,300,450,400);
}
}
class FileNode extends DefaultMutableTreeNode {
private boolean explored = false, selected = false;
public FileNode(File file) {
setUserObject(file);
}
public boolean getAllowsChildren() { return isDirectory(); }
public boolean isLeaf() { return !isDirectory(); }
public File getFile() { return (File)getUserObject(); }
public void explore() { explore(false); }
public boolean isExplored() { return explored; }
public void setSelected(boolean s) { selected = s; }
public boolean isSelected() { return selected; }
public boolean isDirectory() {
File file = (File)getUserObject();
return file.isDirectory();
}
public String toString() {
File file = (File)getUserObject();
String filename = file.toString();
int index = filename.lastIndexOf("//");
return (index != -1 && index != filename.length()-1) ?
filename.substring(index+1) :
filename;
}
public void explore(boolean force) {
if(!isExplored() || force) {
File file = getFile();
File[] children = file.listFiles();
for(int i=0; i < children.length; ++i)
add(new FileNode(children[i]));
explored = true;
}
}
}
class FileNodeRenderer extends DefaultTreeCellRenderer {
protected JCheckBox checkBox = new JCheckBox("backup");
private Component strut = Box.createHorizontalStrut(5);
private JPanel panel = new JPanel();
public FileNodeRenderer() {
panel.setBackground(
UIManager.getColor("Tree.textBackground"));
setOpaque(false);
checkBox.setOpaque(false);
panel.setOpaque(false);
panel.setLayout(new FlowLayout(FlowLayout.CENTER,0,0));
panel.add(this);
panel.add(strut);
panel.add(checkBox);
}
public Component getTreeCellRendererComponent(
JTree tree, Object value,
boolean selected, boolean expanded,
boolean leaf, int row,
boolean hasFocus) {
FileNode node = (FileNode)value;
String s = tree.convertValueToText(value, selected,
expanded, leaf, row, hasFocus);
super.getTreeCellRendererComponent(
tree, value, selected, expanded,
leaf, row, hasFocus);
checkBox.setVisible(node.isDirectory());
checkBox.setSelected(node.isSelected());
return panel;
}
}
class GJApp extends WindowAdapter {
static private JPanel statusArea = new JPanel();
static private JLabel status = new JLabel(" ");
public static void launch(final JFrame f, String title,
final int x, final int y,
final int w, int h) {
f.setTitle(title);
f.setBounds(x,y,w,h);
f.setVisible(true);
statusArea.setBorder(BorderFactory.createEtchedBorder());
statusArea.setLayout(new FlowLayout(FlowLayout.LEFT,0,0));
statusArea.add(status);
status.setHorizontalAlignment(JLabel.LEFT);
f.setDefaultCloseOperation(
WindowConstants.DISPOSE_ON_CLOSE);
f.addWindowListener(new WindowAdapter() {
public void windowClosed(WindowEvent e) {
System.exit(0);
}
});
}
static public JPanel getStatusArea() {
return statusArea;
}
static public void updateStatus(String s) {
status.setText(s);
}
}
例20-11 把用户对象设置为货币模式
import javax.swing.*;
import javax.swing.tree.*;
import java.awt.*;
import java.awt.event.*;
import java.text.*;
public class Test extends JApplet {
public void init() {
JTree tree = new JTree();
JScrollPane scrollPane = new JScrollPane(tree);
DefaultMutableTreeNode root =
new DefaultMutableTreeNode("prices");
root.add(new DefaultMutableTreeNode(new Double(10.99)));
root.add(new DefaultMutableTreeNode(new Double(8.99)));
root.add(new DefaultMutableTreeNode(new Double(6.95)));
root.add(new DefaultMutableTreeNode(new Double(8.00)));
root.add(new DefaultMutableTreeNode(new Double(7.59)));
root.add(new DefaultMutableTreeNode(new Double(2.49)));
DefaultTreeModel model =
(DefaultTreeModel)tree.getModel();
model.setRoot(root);
tree.setCellRenderer(new FormattingRenderer());
tree.setEditable(true);
getContentPane().add(scrollPane);
}
}
class FormattingRenderer extends DefaultTreeCellRenderer {
public Component getTreeCellRendererComponent(
JTree tree, Object value,
boolean selected, boolean expanded,
boolean leaf, int row,
boolean hasFocus) {
// initialize renderer component (this) ...
super.getTreeCellRendererComponent(
tree, value, selected, expanded,
leaf, row, hasFocus);
// now format label text ...
DefaultMutableTreeNode n = (DefaultMutableTreeNode)value;
Object userObject = n.getUserObject();
if(userObject instanceof Double) {
Double d = (Double)userObject;
Format format = NumberFormat.getCurrencyInstance();
setText(value == null ? "" : format.format(d));
}
return this;
}
}
20.6.2 Metal界面样式
20.6.3 根结点和根句柄
例20-12 显示根节点和根节点句柄
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.util.*;
public class Test extends JApplet {
JTree tree = new JTree();
public void init() {
Container contentPane = getContentPane();
JScrollPane scrollPane = new JScrollPane(tree);
contentPane.add(new ControlPanel(), BorderLayout.NORTH);
contentPane.add(scrollPane, BorderLayout.CENTER);
}
class ControlPanel extends JPanel{
JCheckBox showRoot = new JCheckBox("show root node");
JCheckBox showRootHandles = new JCheckBox(
"show root handle");
public ControlPanel(){
initializeCheckBoxes();
setLayout(new FlowLayout());
add(showRoot);
add(showRootHandles);
showRoot.addActionListener(new ActionListener(){
public void actionPerformed(ActionEvent e){
tree.setRootVisible(showRoot.isSelected());
}
});
showRootHandles.addActionListener(
new ActionListener(){
public void actionPerformed(ActionEvent e){
tree.setShowsRootHandles(
showRootHandles.isSelected());
}
});
}
private void initializeCheckBoxes(){
showRoot.setSelected(tree.isRootVisible());
showRootHandles.setSelected(
tree.getShowsRootHandles());
}
}
}
20.7 树单元编辑
20.7.1 扩展DefaultCellEditor
例20-13 一个扩展DefaultCellEditor的编辑器
import javax.swing.*;
import javax.swing.tree.*;
import java.awt.*;
import java.awt.event.*;
import java.util.*;
public class Test extends JApplet {
public void init() {
JTree tree = new JTree();
JScrollPane scrollPane = new JScrollPane(tree);
JComboBox combo = new JComboBox();
combo.addItem("red");
combo.addItem("blue");
combo.addItem("green");
combo.addItem("orange");
combo.addItem("yellow");
combo.addItem("magenta");
tree.setCellEditor(new ColorEditor(tree, combo));
tree.setEditable(true);
getContentPane().add(scrollPane);
}
}
class ColorEditor extends DefaultCellEditor {
private JTree tree;
public ColorEditor(JTree tree, JComboBox comboBox) {
super(comboBox);
this.tree = tree;
}
public boolean isCellEditable(EventObject e) {
boolean rv = false; // return value
if(e instanceof MouseEvent) {
MouseEvent me = (MouseEvent)e;
if(me.getClickCount() == 3) {
TreePath path =
tree.getPathForLocation(me.getX(), me.getY());
if(path.getPathCount() == 1) // root node
return false;
DefaultMutableTreeNode node =
(DefaultMutableTreeNode)
path.getLastPathComponent();
rv = node.getParent().toString().equals("colors");
}
}
return rv;
}
}
20.7.2 DefaultTreeCellEditor
例20-14 使用DefaultTreeCellEditor
import javax.swing.*;
import javax.swing.tree.*;
import java.awt.*;
import java.awt.event.*;
import java.util.*;
public class Test extends JApplet {
public void init() {
JTree tree = new JTree();
JScrollPane scrollPane = new JScrollPane(tree);
JComboBox combo = new JComboBox();
combo.addItem("red");
combo.addItem("blue");
combo.addItem("green");
combo.addItem("orange");
combo.addItem("yellow");
combo.addItem("magenta");
tree.setCellEditor(new DefaultTreeCellEditor(
tree, new DefaultTreeCellRenderer(),
new ColorEditor(tree, combo)));
tree.setEditable(true);
getContentPane().add(scrollPane);
}
}
class ColorEditor extends DefaultCellEditor {
private JTree tree;
public ColorEditor(JTree tree, JComboBox comboBox) {
super(comboBox);
this.tree = tree;
}
public boolean isCellEditable(EventObject e) {
boolean rv = false; // return value
if(e instanceof MouseEvent) {
MouseEvent me = (MouseEvent)e;
if(me.getClickCount() == 3) {
TreePath path =
tree.getPathForLocation(me.getX(), me.getY());
if(path.getPathCount() == 1) // root node
return false;
DefaultMutableTreeNode node =
(DefaultMutableTreeNode)
path.getLastPathComponent();
rv = node.getParent().toString().equals("colors");
}
}
return rv;
}
}
20.8 绘制和编辑:学习一个样例
20.8.1 Test类
例20-15 绘制和编辑:一个学习样例
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.tree.*;
import java.awt.*;
import java.awt.event.*;
import java.io.File;
import java.util.EventObject;
public class Test extends JFrame {
public Test() {
JTree tree = new JTree(createTreeModel());
JScrollPane scrollPane = new JScrollPane(tree);
FileNodeRenderer renderer = new FileNodeRenderer();
FileNodeEditor editor = new FileNodeEditor();
tree.setEditable(true);
tree.setCellRenderer(renderer);
tree.setCellEditor(new ImmediateEditor(tree,
renderer, editor));
getContentPane().add(scrollPane, BorderLayout.CENTER);
tree.addTreeExpansionListener(new TreeExpansionListener(){
public void treeCollapsed(TreeExpansionEvent e) {
}
public void treeExpanded(TreeExpansionEvent e) {
TreePath path = e.getPath();
FileNode node = (FileNode)
path.getLastPathComponent();
if( ! node.isExplored())
node.explore();
}
});
tree.getCellEditor().addCellEditorListener(
new CellEditorListener() {
public void editingCanceled(ChangeEvent e) {
CellEditor cellEditor = (CellEditor)e.getSource();
SelectableFile sf =
(SelectableFile)
cellEditor.getCellEditorValue();
System.out.println("editing canceled: " +
sf.toString());
}
public void editingStopped(ChangeEvent e) {
CellEditor cellEditor = (CellEditor)e.getSource();
SelectableFile sf =
(SelectableFile)
cellEditor.getCellEditorValue();
System.out.println("editing stopped: " +
sf.toString());
}
});
}
private DefaultTreeModel createTreeModel() {
File root = new File("E:/");
FileNode rootNode = new FileNode(root), node;
rootNode.explore();
return new DefaultTreeModel(rootNode);
}
public static void main(String args[]) {
GJApp.launch(new Test(),"JTree File Explorer",
300,300,450,400);
}
}
class SelectableFile {
private File file;
private boolean selected = false;
public SelectableFile(File file) {
this.file = file;
}
public String toString() {
return file.toString() + " selected: " + selected;
}
public void setSelected(boolean s) { selected = s; }
public boolean isSelected() { return selected; }
public File getFile() { return file; }
}
class FileNode extends DefaultMutableTreeNode {
private boolean explored = false;
public FileNode(File file) {
setUserObject(new SelectableFile(file));
}
public boolean getAllowsChildren() { return isDirectory(); }
public boolean isLeaf() { return !isDirectory(); }
public File getFile() {
SelectableFile sf = (SelectableFile)getUserObject();
return sf.getFile();
}
public boolean isSelected() {
SelectableFile sf = (SelectableFile)getUserObject();
return sf.isSelected();
}
public void setSelected(boolean b) {
SelectableFile sf = (SelectableFile)getUserObject();
sf.setSelected(b);
}
public boolean isDirectory() {
File file = getFile();
return file.isDirectory();
}
public String toString() {
File file = getFile();
String filename = file.toString();
int index = filename.lastIndexOf("//");
return (index != -1 && index != filename.length()-1) ?
filename.substring(index+1) :
filename;
}
public void explore() { explore(false); }
public boolean isExplored() { return explored; }
public void explore(boolean force) {
if(!isExplored() || force) {
File file = getFile();
File[] children = file.listFiles();
for(int i=0; i < children.length; ++i)
add(new FileNode(children[i]));
explored = true;
}
}
}
class FileNodeRenderer extends DefaultTreeCellRenderer {
protected JCheckBox checkBox = new JCheckBox("backup");
private Component strut = Box.createHorizontalStrut(5);
private JPanel panel = new JPanel();
public FileNodeRenderer() {
panel.setBackground(
UIManager.getColor("Tree.textBackground"));
setOpaque(false);
checkBox.setOpaque(false);
panel.setOpaque(false);
panel.setLayout(new FlowLayout(FlowLayout.CENTER,0,0));
panel.add(this);
panel.add(strut);
panel.add(checkBox);
}
public Component getTreeCellRendererComponent(
JTree tree, Object value,
boolean selected, boolean expanded,
boolean leaf, int row,
boolean hasFocus) {
FileNode node = (FileNode)value;
super.getTreeCellRendererComponent(
tree, value, selected, expanded,
leaf, row, hasFocus);
checkBox.setVisible(node.isDirectory());
checkBox.setSelected(node.isSelected());
return panel;
}
public Dimension getCheckBoxOffset() {
Graphics g = panel.getGraphics();
int xoffset = 0;
if(g != null) {
try {
FontMetrics fm = g.getFontMetrics();
xoffset = fm.stringWidth(getText()) +
strut.getPreferredSize().width;
}
finally {
g.dispose();
}
}
return new Dimension(xoffset, 0);
}
}
class FileNodeEditorRenderer extends FileNodeRenderer {
public Component getTreeCellRendererComponent(
JTree tree, Object value,
boolean selected, boolean expanded,
boolean leaf, int row,
boolean hasFocus) {
Component c = super.getTreeCellRendererComponent(tree,
value, selected, expanded,
leaf, row, hasFocus);
setIcon(null);
return c;
}
public JCheckBox getCheckBox() {
return checkBox;
}
}
class FileNodeEditor extends AbstractCellEditor {
FileNodeEditorRenderer renderer;
FileNode lastEditedNode;
JCheckBox checkBox;
public FileNodeEditor() {
renderer = new FileNodeEditorRenderer();
checkBox = renderer.getCheckBox();
checkBox.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
lastEditedNode.setSelected(checkBox.isSelected());
stopCellEditing();
}
});
}
public Component getTreeCellEditorComponent(
JTree tree, Object value,
boolean selected, boolean expanded,
boolean leaf, int row) {
lastEditedNode = (FileNode)value;
return renderer.getTreeCellRendererComponent(tree,
value, selected, expanded,
leaf, row, true); // hasFocus ignored
}
public Object getCellEditorValue() {
return lastEditedNode.getUserObject();
}
}
class ImmediateEditor extends DefaultTreeCellEditor {
private FileNodeRenderer renderer;
public ImmediateEditor(JTree tree,
FileNodeRenderer renderer,
FileNodeEditor editor) {
super(tree, renderer, editor);
this.renderer = renderer;
}
protected boolean canEditImmediately(EventObject e) {
boolean rv = false; // rv = return value
if(e instanceof MouseEvent) {
MouseEvent me = (MouseEvent)e;
rv = inCheckBoxHitRegion(me);
}
return rv;
}
public boolean shouldSelectCell(EventObject e) {
boolean rv = false; // only mouse events
if(e instanceof MouseEvent) {
MouseEvent me = (MouseEvent)e;
TreePath path = tree.getPathForLocation(me.getX(),
me.getY());
FileNode node = (FileNode)
path.getLastPathComponent();
rv = node.isLeaf() || !inCheckBoxHitRegion(me);
}
return rv;
}
public boolean inCheckBoxHitRegion(MouseEvent e) {
TreePath path = tree.getPathForLocation(e.getX(),
e.getY());
FileNode node = (FileNode)path.getLastPathComponent();
boolean rv = false;
if(node.isDirectory()) {
// offset and lastRow DefaultTreeCellEditor
// protected members
Rectangle bounds = tree.getRowBounds(lastRow);
Dimension checkBoxOffset =
renderer.getCheckBoxOffset();
bounds.translate(offset + checkBoxOffset.width,
checkBoxOffset.height);
rv = bounds.contains(e.getPoint());
}
return rv;
}
}
class GJApp extends WindowAdapter {
static private JPanel statusArea = new JPanel();
static private JLabel status = new JLabel(" ");
public static void launch(final JFrame f, String title,
final int x, final int y,
final int w, int h) {
f.setTitle(title);
f.setBounds(x,y,w,h);
f.setVisible(true);
statusArea.setBorder(BorderFactory.createEtchedBorder());
statusArea.setLayout(new FlowLayout(FlowLayout.LEFT,0,0));
statusArea.add(status);
status.setHorizontalAlignment(JLabel.LEFT);
f.setDefaultCloseOperation(
WindowConstants.DISPOSE_ON_CLOSE);
f.addWindowListener(new WindowAdapter() {
public void windowClosed(WindowEvent e) {
System.exit(0);
}
});
}
static public JPanel getStatusArea() {
return statusArea;
}
static public void updateStatus(String s) {
status.setText(s);
}
}
20.8.2 SelectableFile类和FileNode类
20.8.3 绘制器
20.8.4 编辑器
20.8.5 JTree属性
20.8.6 树事件
例20-16 处理树鼠标事件
import javax.swing.*;
import javax.swing.tree.*;
import java.awt.*;
import java.awt.event.*;
public class Test extends JApplet {
public Test() {
JTree tree = new JTree();
getContentPane().add(new JScrollPane(tree));
tree.addMouseListener(new MouseAdapter() {
public void mousePressed(MouseEvent e) {
String s = null;
JTree t = (JTree)e.getSource();
int row = t.getRowForLocation(e.getX(), e.getY());
if(e.getClickCount() == 2)
s = "double click in row " + row;
else
s = "single click in row " + row;
if(row != -1) {
TreePath path = t.getPathForRow(row);
TreeNode node = (TreeNode)
path.getLastPathComponent();
s += ", node = " + node.toString();
}
showStatus(s);
}
});
}
}
例20-17 处理树编辑事件
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.tree.*;
import java.awt.*;
import java.awt.event.*;
import java.util.*;
public class Test extends JApplet {
public void init() {
JTree tree = new JTree();
getContentPane().add(tree, BorderLayout.CENTER);
// must invoke setEditable, or the call below to
// getCellEditor() will return null.
tree.setEditable(true);
tree.getCellEditor().addCellEditorListener(
new CellEditorListener() {
public void editingCanceled(ChangeEvent e) {
CellEditor editor = (CellEditor)e.getSource();
String s = (String)editor.getCellEditorValue();
showStatus("editing cancelled: " + s);
}
public void editingStopped(ChangeEvent e) {
CellEditor editor = (CellEditor)e.getSource();
String s = (String)editor.getCellEditorValue();
showStatus("editing stopped: " + s);
}
});
}
}
例20-18 处理树选取事件
import javax.swing.*;
import javax.swing.tree.*;
import javax.swing.event.*;
import java.awt.*;
import java.awt.event.*;
public class Test extends JApplet {
JTree tree = new JTree();
JTextArea textArea = new JTextArea();
JSplitPane splitPane = new JSplitPane(
JSplitPane.HORIZONTAL_SPLIT,
new JScrollPane(tree),
new JScrollPane(textArea));
public void init() {
splitPane.setDividerLocation(150);
textArea.setFont(new Font("Serif", Font.PLAIN, 17));
getContentPane().add(splitPane, BorderLayout.CENTER);
tree.addTreeSelectionListener(
new TreeSelectionListener() {
public void valueChanged(TreeSelectionEvent e) {
TreePath path = e.getNewLeadSelectionPath();
String s = new String();
if(path != null) {
s += "New lead selection path: " +
path.toString() + "/n";
}
else
s += "selection cleared/n";
path = e.getOldLeadSelectionPath();
if(path != null) {
s += "Old lead selection path: " +
path.toString() + "/n";
}
else
s += "No previous lead selection/n";
textArea.append(s + "/n");
printSelectionInformation(e);
}
void printSelectionInformation(TreeSelectionEvent e) {
showPaths(e);
showRows();
textArea.append("/n----------------------------");
textArea.append("----------------------------/n");
}
private void showPaths(TreeSelectionEvent e) {
TreePath[] paths = e.getPaths();
textArea.append("Number of Paths: " +
paths.length + "/n");
for(int i=0; i < paths.length; ++i) {
TreePath path = paths[i];
boolean wasAdded = e.isAddedPath(path);
textArea.append(" path " + i + ": ");
textArea.append(path +
(wasAdded ? " added to selection" :
" removed from selection") + "/n");
}
}
private void showRows() {
int[] rows = tree.getSelectionRows();
if(rows != null && rows.length > 0) {
textArea.append("/nSelected Rows: ");
for(int i=0; i < rows.length; ++i) {
textArea.append(
Integer.toString(rows[i]));
if(i != rows.length-1)
textArea.append(",");
}
}
}
});
}
}
例20-19 处理树展开事件
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.tree.*;
import java.awt.*;
import java.awt.event.*;
public class Test extends JApplet {
public void init() {
Container contentPane = getContentPane();
JTree tree = new JTree();
contentPane.add(tree);
tree.addTreeExpansionListener(
new TreeExpansionListener() {
public void treeCollapsed(TreeExpansionEvent e) {
TreePath path = e.getPath();
TreeNode node = (TreeNode)
path.getLastPathComponent();
showStatus("node " + "/"" + node.toString() +
"/"" + " collapsed");
}
public void treeExpanded(TreeExpansionEvent e) {
TreePath path = e.getPath();
TreeNode node = (TreeNode)
path.getLastPathComponent();
showStatus("node " + "/"" + node.toString() +
"/"" + " expanded");
}
});
}
}
例20-20 否决节点的展开和折叠
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.tree.*;
import java.awt.*;
import java.awt.event.*;
public class Test extends JFrame {
public Test() {
JTree tree = new JTree();
getContentPane().add(tree);
tree.addTreeWillExpandListener(
new TreeWillExpandListener() {
public void treeWillExpand(TreeExpansionEvent e)
throws ExpandVetoException {
TreePath path = e.getPath();
TreeNode node = (TreeNode)
path.getLastPathComponent();
if(node.toString().equals("colors")) {
JOptionPane.showMessageDialog(Test.this,
"Can't Expand Colors",
"Expansion Vetoed",
JOptionPane.INFORMATION_MESSAGE);
throw new ExpandVetoException(e);
}
}
public void treeWillCollapse(TreeExpansionEvent e)
throws ExpandVetoException {
TreePath path = e.getPath();
TreeNode node = (TreeNode)
path.getLastPathComponent();
if(node.toString().equals("food")) {
JOptionPane.showMessageDialog(Test.this,
"Can't Collapse Food",
"Collapse Vetoed",
JOptionPane.INFORMATION_MESSAGE);
throw new ExpandVetoException(e);
}
}
});
}
public static void main(String args[]) {
GraphicJavaApplication.launch(new Test(),
"Vetoing Node Expansion/Collapse",300,300,300,200);
}
}
class GraphicJavaApplication extends WindowAdapter {
public static void launch(final JFrame f, String title,
final int x, final int y,
final int w, int h) {
f.setTitle(title);
f.setBounds(x,y,w,h);
f.setVisible(true);
f.setDefaultCloseOperation(
WindowConstants.DISPOSE_ON_CLOSE);
f.addWindowListener(new WindowAdapter() {
public void windowClosed(WindowEvent e) {
System.exit(0);
}
});
}
}
20.8.7 JTree类总结
20.8.8 AWT兼容
20.9 本章回顾