12.10.2 拖动 编辑树节点

12.10.2 拖动 编辑树节点

JTree默认不可编辑

JTree生成的树默认是不可编辑的,不可以添加、删除节点,也不可以改变节点数据;
如果想让某个JTree对象变成可编辑状态,则可以调用JTreesetEditable(boolean b)方法,传入true即可把这棵树变成可编辑的树(可以添加、删除节点,也可以改变节点数据)。

方法 描述
void setEditable(boolean flag) Determines whether the tree is editable.

一旦将JTree对象设置成可编辑状态后,程序就可以为指定节点添加子节点、兄弟节点,也可以修改、删除指定节点。
前面简单提到过,JTree处理节点有两种方式:

  • 一种是根据TreePath;
  • 另一种是根据节点的行号.

所有JTree显示的节点都有一个唯一的行号(从0开始)。只有那些被显示出来的节点才有行号,这就带来一个潜在的问题:
如果该节点之前的节点被展开、折叠或增加、删除后,那么该节点的行号就会发生变化,因此通过行号来识别节点可能有一些不确定的地方;相反,使用TreePath来识别节点则会更加稳定。

可以使用文件系统来类比JTree,从图12.38中可以看出,实际上所有的文件系统都采用树状结构,其中

  • Windows的文件系统是森林,因为Windows包含C、D等多个根路径,
  • UNIXLinux的文件系统是一棵树,只有一个根路径。

如果直接给岀abc文件夹(类似于JTree中的节点),系统不能准确地定位该路径;
如果给出D:\xyz\abc,系统就可以准确地定位到该路径,这个D:\xyz\abc实际上由三个文件夹组成:D:xyzabc,其中D:是该路径的根路径。类似地,TreePath也采用这种方式来唯地标识节点。

TreePath保持着从根节点到指定节点的所有节点,TreePath由一系列节点组成,而不是单独的一个节点。

获取被选中的节点

JTree的很多方法都用于返回一个TreePath对象,当程序得到一个TreePath后,可以调用TreePathgetLastPathComponent​方法获取最后一个节点

例如:需要获得JTree中被选定的节点,则可以通过如下两行代码来实现。

1
2
3
4
//获取选中节点所在的 Treepath
TreePath path = tree.getselectionPath();
//获取指定 Treepath的最后一个节点
TreeNode target = (TreeNode) path.getLastPathcomponent();
TreePath方法 描述
Object getLastPathComponent() Returns the last element of this path.

获取选中的节点

又因为JTree经常需要查询被选中的节点,所以JTree提供了一个getLastSelectedPathComponent方法来获取选中的节点。
比如采用下面代码也可以获取选中的节点。

JTree方法 描述
Object getLastSelectedPathComponent() Returns the last path component of the selected path.
1
2
//获取选中的节点
TreeNode target =(TreeNode) tree.getLastselectedPathComponent();

可能有读者对上面这行代码感到奇怪,getLastSelectedPathComponent()方法返回的不是TreeNode吗?getLastSelectedPathComponent方法返回的不一定是TreeNode,该方法的返回值是Object。因为SwingJTree设计得非常复杂,JTree把所有的状态数据都交给TreeModel管理,而JTree本身并没有与TreeNode发生关联(从图12.40可以看出这一点),只是因为DefaultTreeModel需要TreeNode而已,如果开发者自己提供一个TreeModel实现类,这个TreeModel实现类完全可以与TreeNode没有任何关系。当然,对于大部分Swing开发者而言,无须理会JTree的这些过于复杂的设计。

TreeNode转成TreePath

如果已经有了从根节点到当前节点的一系列节点所组成的节点数组,也可以通过TreePath提供的构造器将这些节点转换成TreePath对象,如下代码所示。

1
2
//将一个节点数组转换成 Treepath对象
TreePath tp = new TreePath(nodes);

获取了选中的节点之后,即可通过DefaultTreeModel(它是SwingTreeModel提供的唯一一个实现类)提供的系列方法来插入、删除节点。DefaultTreeModel类有一个非常优秀的设计,当使用DefaultTreeModel插入、删除节点后,该DefaultTreeModel会自动通知对应的JTree重绘所有节点,用户可以立即看到程序所做的修改

也可以直接通过TreeNode提供的方法来添加、删除和修改节点。

但通过TreeNode改变节点时,程序必须显式调用JTreeupdateUI()通知JTree重绘所有节点,让用户看到程序所做的修改。

程序 增加 修改 删除节点 拖动节点

下面程序实现了增加、修改和删除节点的功能,并允许用户通过拖动将一个节点变成另一个节点的子节点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
import java.awt.BorderLayout;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.tree.*;

public class EditJTree {
JFrame jf;
JTree tree;
// 上面JTree对象对应的model
DefaultTreeModel model;
// 定义几个初始节点
DefaultMutableTreeNode root = new DefaultMutableTreeNode("中国");
DefaultMutableTreeNode guangdong = new DefaultMutableTreeNode("广东");
DefaultMutableTreeNode guangxi = new DefaultMutableTreeNode("广西");
DefaultMutableTreeNode foshan = new DefaultMutableTreeNode("佛山");
DefaultMutableTreeNode shantou = new DefaultMutableTreeNode("汕头");
DefaultMutableTreeNode guilin = new DefaultMutableTreeNode("桂林");
DefaultMutableTreeNode nanning = new DefaultMutableTreeNode("南宁");
// 定义需要被拖动的TreePath
TreePath movePath;
JButton addSiblingButton = new JButton("添加兄弟节点");
JButton addChildButton = new JButton("添加子节点");
JButton deleteButton = new JButton("删除节点");
JButton editButton = new JButton("编辑当前节点");

public void init() {
guangdong.add(foshan);
guangdong.add(shantou);
guangxi.add(guilin);
guangxi.add(nanning);
root.add(guangdong);
root.add(guangxi);
jf = new JFrame("可编辑节点的树");
tree = new JTree(root);
// 获取JTree对应的TreeModel对象
model = (DefaultTreeModel) tree.getModel();
// 设置JTree可编辑
tree.setEditable(true);
MouseListener ml = new MouseAdapter() {
// 按下鼠标时获得被拖动的节点
public void mousePressed(MouseEvent e) {
// 如果需要唯一确定某个节点,必须通过TreePath来获取。
TreePath tp = tree.getPathForLocation(e.getX(), e.getY());
if (tp != null) {
movePath = tp;
}
}

// 鼠标松开时获得需要拖到哪个父节点
public void mouseReleased(MouseEvent e) {
// 根据鼠标松开时的TreePath来获取TreePath
TreePath tp = tree.getPathForLocation(e.getX(), e.getY());
if (tp != null && movePath != null) {
// 阻止向子节点拖动
if (movePath.isDescendant(tp) && movePath != tp) {
JOptionPane.showMessageDialog(jf, "目标节点是被移动节点的子节点,无法移动!", "非法操作", JOptionPane.ERROR_MESSAGE);
return;
}
// 既不是向子节点移动,而且鼠标按下、松开的不是同一个节点
else if (movePath != tp) {
// add方法先将该节点从原父节下删除,再添加到新父节点下
((DefaultMutableTreeNode) tp.getLastPathComponent())
.add((DefaultMutableTreeNode) movePath.getLastPathComponent());
movePath = null;
tree.updateUI();
}
}
}
};
// 为JTree添加鼠标监听器
tree.addMouseListener(ml);
JPanel panel = new JPanel();
// 实现添加兄弟节点的监听器
addSiblingButton.addActionListener(event -> {
// 获取选中节点
DefaultMutableTreeNode selectedNode = (DefaultMutableTreeNode) tree.getLastSelectedPathComponent();
// 如果节点为空,直接返回
if (selectedNode == null)
return;
// 获取该选中节点的父节点
DefaultMutableTreeNode parent = (DefaultMutableTreeNode) selectedNode.getParent();
// 如果父节点为空,直接返回
if (parent == null)
return;
// 创建一个新节点
DefaultMutableTreeNode newNode = new DefaultMutableTreeNode("新节点");
// 获取选中节点的选中索引
int selectedIndex = parent.getIndex(selectedNode);
// 在选中位置插入新节点
model.insertNodeInto(newNode, parent, selectedIndex + 1);
// --------下面代码实现显示新节点(自动展开父节点)-------
// 获取从根节点到新节点的所有节点
TreeNode[] nodes = model.getPathToRoot(newNode);
// 使用指定的节点数组来创建TreePath
TreePath path = new TreePath(nodes);
// 显示指定TreePath
tree.scrollPathToVisible(path);
});
panel.add(addSiblingButton);
// 实现添加子节点的监听器
addChildButton.addActionListener(event -> {
// 获取选中节点
DefaultMutableTreeNode selectedNode = (DefaultMutableTreeNode) tree.getLastSelectedPathComponent();
// 如果节点为空,直接返回
if (selectedNode == null)
return;
// 创建一个新节点
DefaultMutableTreeNode newNode = new DefaultMutableTreeNode("新节点");
// 通过model来添加新节点,则无须通过调用JTree的updateUI方法
// model.insertNodeInto(newNode, selectedNode
// , selectedNode.getChildCount());
// 通过节点添加新节点,则需要调用tree的updateUI方法
selectedNode.add(newNode);
// --------下面代码实现显示新节点(自动展开父节点)-------
TreeNode[] nodes = model.getPathToRoot(newNode);
TreePath path = new TreePath(nodes);
tree.scrollPathToVisible(path);
tree.updateUI();
});
panel.add(addChildButton);
// 实现删除节点的监听器
deleteButton.addActionListener(event -> {
DefaultMutableTreeNode selectedNode = (DefaultMutableTreeNode) tree.getLastSelectedPathComponent();
if (selectedNode != null && selectedNode.getParent() != null) {
// 删除指定节点
model.removeNodeFromParent(selectedNode);
}
});
panel.add(deleteButton);
// 实现编辑节点的监听器
editButton.addActionListener(event -> {
TreePath selectedPath = tree.getSelectionPath();
if (selectedPath != null) {
// 编辑选中节点
tree.startEditingAtPath(selectedPath);
}
});
panel.add(editButton);
jf.add(new JScrollPane(tree));
jf.add(panel, BorderLayout.SOUTH);
jf.pack();
jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
jf.setVisible(true);
}

public static void main(String[] args) {
new EditJTree().init();
}
}

上面程序中实现拖动节点也比较容易:
当用户按下鼠标时获取鼠标事件发生位置的树节点,并把该节点赋给movePath变量;
当用户松开鼠标时获取鼠标事件发生位置的树节点,作为目标节点需要拖到的父节点,把movePath从原来的节点中删除,添加到新的父节点中即可(TreeNodeadd()方法可以同时完成这两个操作)。

运行上面程序,会看到如图12.42所示的效果。


选中图12.42中的某个节点并双击,或者单击“编辑当前节点”按钮,就可以进入该节点的编辑状态,系统启动默认的单元格编辑器来编辑该节点,JTree的单元格编辑器与JTable的单元格编辑器都实现了相同的CellEditor接口。