12.12.2 使用JPasswordField

JPasswordFieldJTextField的一个子类,它是SwingMVC设计的产品:JPasswordFieldJTextField的各种特征几乎完全一样,只是当用户向JPasswordField输入内容时,JPasswordField并不会显示出用户输入的内容,而是以echo字符(通常是星号和黑点)来代替用户输入的所有字符

JPasswordFieldJTextField的用法几乎完全一样,连构造器的个数和参数都完全一样。但是JPasswordField多了一个setEchoChar(Char ch)方法,该方法用于设置该密码框的echo字符——当用户在密码输入框内输入时,每个字符都会使用该echo字符代替。

除此之外,JPasswordField重写了JTextComponentgetText()方法,并且不再推荐使用getText方法返回字符串密码框的字符串,因为getText方法所返回的字符串会一直停留在虚拟机中,直到垃圾回收,这可能导致存在一些安全隐患,所以JPasswordField提供了一个getPassword()方法,该方法返回个字符数组,而不是返回字符串,从而提供了更好的安全机制.

当程序使用完getPassword()方法返回的字符数组后,应该立即清空该字符数组的内容,以防该数组泄露密码信息.

JPasswordField类的方法 描述
void setEchoChar(char c) Sets the echo character for this JPasswordField.
char[] getPassword() Returns the text contained in this TextComponent.
String getText() Deprecated. As of Java 2 platform v1.2, replaced by getPassword.

12.12 使用FormattedTextField和JTextPanel创建格式文本

这里有一张图片

Swing使用JtextComponent作为所有文本输入组件的父类,从图12.1中可以看出:
SwingJtextComponent类提供了三个子类:JTextAreaJTextFieldJEditorPane.

JEditorPane

JEditorPane提供了一个JTextPane子类,JEditorPaneJTextPane是两个典型的格式文本编辑器,也是本节介绍的重点。

JTextAreaJTextField是两个常见的文本组件,比较简单,本节不会再次介绍它们。

JTextField

JTextField派生了两个子类:JPasswordFieldJFormattedTextField,它们代表密码输入框格式化文本输入框

文本输入组件的model是Document接口

与其他的Swing组件类似,所有的文本输入组件也遵循了MVC的设计模式,即每个文本输入组件都有对应的model来保存其状态数据;

与其他的Swing组件不同的是,文本输入组件的model接口不是XxxModel接口,而是Document接口。

Document既包括有格式的文本,也包括无格式的文本。不同的文本输入组件对应的Document不同。

12.12.1 监听Document的变化

如果希望检测到任何文本输入组件里所输入内容的变化,则可以通过监听该组件对应的Document来实现。

如何获取文本输入组件的Document对象

JTextComponent类里提供了一个getDocument()方法,该方法用于获取所有文本输入组件对应的Document对象。

JTextComponent类的方法 描述
Document getDocument() Fetches the model associated with the editor.

为Document添加监听器

Document提供了一个addDocumentListener()方法来为Document添加监听器,该监听器必须实现DocumentListener接口.

Document接口的方法 描述
void addDocumentListener(DocumentListener listener) Registers the given observer to begin receiving notifications when changes are made to the document.

DocumentListener接口方法

DocumentListener接口里提供了如下三个方法。

方法 描述
void changedUpdate(DocumentEvent e) Document里的属性或属性集发生了变化时触发该方法。
void insertUpdate(DocumentEvent e) 当向Document中插入文本时触发该方法。
void removeUpdate(DocumentEvent e) 当从Document中删除文本时触发该方法。

对于上面的三个方法而言,如果仅需要检测文本的变化,则无须实现第一个方法。但Swing并没有为DocumentListener接口提供适配器,所以程序依然要为第一个方法提供空实现。

撤销监听器

除此之外,还可以为文件输入组件添加一个撤销监听器,这样就允许用户撤销以前的修改。添加撤销监听器的方法是addUndoableEditListener:

Document接口的方法 描述
void addUndoableEditListener(UndoableEditListener listener) Registers the given observer to begin receiving notifications when undoable edits are made to the document.

UndoableEditListener

该方法需要接收一个UndoableEditListener监听器,该监听器里包含了undoableEditHappened()方法:

UndoableEditListener接口的方法 描述
void undoableEditHappened(UndoableEditEvent e) 当文档里发生了可撤销的编辑操作时将会触发该方法

程序 为文本域的Document添加监听器

下面程序示范了如何为一个普通文本域的Document添加监听器,当用户在目标文本域里输入、删除文本时,程序会显示出用户所做的修改。该文本域还支持撤销操作,当用户按“Ctrl+Z”键时,该文本域会撤销用户刚刚输入的内容。

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
import java.util.LinkedList;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.text.*;
import javax.swing.undo.*;
import javax.swing.event.*;

public class MonitorText {
JFrame mainWin = new JFrame("监听Document对象");
JTextArea target = new JTextArea(4, 35);
JTextArea msg = new JTextArea(5, 35);
JLabel label = new JLabel("文本域的修改信息");
Document doc = target.getDocument();
// 保存撤销操作的List对象
LinkedList<UndoableEdit> undoList = new LinkedList<>();
// 最多允许撤销多少次
final int UNDO_COUNT = 20;

public void init() {
msg.setEditable(false);
// 添加DocumentListener
doc.addDocumentListener(new DocumentListener() {
// 当Document的属性或属性集发生了改变时触发该方法
public void changedUpdate(DocumentEvent e) {
}

// 当向Document中插入文本时触发该方法
public void insertUpdate(DocumentEvent e) {
int offset = e.getOffset();
int len = e.getLength();
// 取得插入事件的位置
msg.append("插入文本的长度:" + len + "\n");
msg.append("插入文本的起始位置:" + offset + "\n");
try {
msg.append("插入文本内容:" + doc.getText(offset, len) + "\n");
} catch (BadLocationException evt) {
evt.printStackTrace();
}
}

// 当从Document中删除文本时触发该方法
public void removeUpdate(DocumentEvent e) {
int offset = e.getOffset();
int len = e.getLength();
// 取得插入事件的位置
msg.append("删除文本的长度:" + len + "\n");
msg.append("删除文本的起始位置:" + offset + "\n");
}
});
// 添加可撤销操作的监听器
doc.addUndoableEditListener(e -> {
// 每次发生可撤销操作都会触发该代码块 // ①
UndoableEdit edit = e.getEdit();
if (edit.canUndo() && undoList.size() < UNDO_COUNT) {
// 将撤销操作装入List内
undoList.add(edit);
}
// 已经达到了最大撤销次数
else if (edit.canUndo() && undoList.size() >= UNDO_COUNT) {
// 弹出第一个撤销操作
undoList.pop();
// 将撤销操作装入List内
undoList.add(edit);
}
});
// 为Ctrl+Z添加监听器
target.addKeyListener(new KeyAdapter() {
public void keyTyped(KeyEvent e) // ②
{
System.out.println(e.getKeyChar() + 0);
// 如果按键是Ctrl + Z或Ctrl + z
if (e.getKeyChar() == 26) {
if (undoList.size() > 0) {
// 移出最后一个可撤销操作,并取消该操作
undoList.removeLast().undo();
}
}
}
});
Box box = new Box(BoxLayout.Y_AXIS);
box.add(new JScrollPane(target));
JPanel panel = new JPanel();
panel.add(label);
box.add(panel);
box.add(new JScrollPane(msg));
mainWin.add(box);
mainWin.pack();
mainWin.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
mainWin.setVisible(true);
}

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

代码分析

上面程序中的两段粗体字代码实现了Document中插入文本、删除文本的事件处理器,当用户向Document中插入文本、删除文本时,程序将会把这些修改信息添加到下面的一个文本域里

添加可撤销操作监听器

程序中①号代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 添加可撤销操作的监听器
doc.addUndoableEditListener(e -> {
// 每次发生可撤销操作都会触发该代码块 // ①
UndoableEdit edit = e.getEdit();
if (edit.canUndo() && undoList.size() < UNDO_COUNT) {
// 将撤销操作装入List内
undoList.add(edit);
}
// 已经达到了最大撤销次数
else if (edit.canUndo() && undoList.size() >= UNDO_COUNT) {
// 弹出第一个撤销操作
undoList.pop();
// 将撤销操作装入List内
undoList.add(edit);
}
});

是可撤销操作的事件处理器,当用户在该文本域内进行可撤销操作时,这段代码将会被触发,这段代码把用户刚刚进行的可撤销操作以List保存起来,以便在合适的时候撤销用户所做的修改。

添加Ctrl+Z监听器

程序中②号代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 为Ctrl+Z添加监听器
target.addKeyListener(new KeyAdapter() {
public void keyTyped(KeyEvent e) // ②
{
System.out.println(e.getKeyChar() + 0);
// 如果按键是Ctrl + Z或Ctrl + z
if (e.getKeyChar() == 26) {
if (undoList.size() > 0) {
// 移出最后一个可撤销操作,并取消该操作
undoList.removeLast().undo();
}
}
}
});

主要用于为“Ctrl+Z”按键添加按键监听器,当用户按下“Ctrl+Z”键时,程序从保存可撤销操作的List中取岀最后一个可撤销操作,并撤销该操作的修改。

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

12.11.6 编辑单元格內容

如果用户双击JTable表格的指定单元格,系统将会开始编辑该单元格的内容。在默认情况下,系统会使用文本框来编辑该单元格的内容,包括如图12.54所示表格的图标单元格。

与此类似的是,如果用户双击JTree的节点,默认也会采用文本框来编辑节点的内容。

但如果单元格内容不是文字内容,而是如图12.54所示的图形类型时,用户当然不希望使用文本编辑器来编辑该单元格的内容,因为这种编辑方式非常不直观,用户体验相当差。为了避免这种情况,可以实现自己的单元格编辑器,从而可以给用户提供更好的操作界面。

实现JTable的单元格编辑器应该实现TableCellEditor接口,实现JTree的节点编辑器需要实现TreeCelleditor接口,TableCellEditorTreeCelleditor这两个接口有非常紧密的联系。它们有一个共同的父接口:CellEditor;而且它们有一个共同的实现类:DefaultCellEditor

关于TableCellEditorTreeCellEditor两个接口及其实现类之间的关系如图12.55所示。

从图12.55中可以看出,SwingTableCellEditor提供了DefaultCellEditor实现类(也可作为TreeCellEditor的实现类),DefaultCellEditor类有三个构造器,它们分别使用文本框、复选框和ComboBox作为单元格编辑器,其中

  • 使用文本框编辑器是最常见的情形,
  • 如果单元格的值是Boolean类型,则系统默认使用复选框编辑器(如图12.54中最右边一列所示),这两种情形都是前面见过的情形。
  • 如果想指定某列使用JComboBox作为单元格编

实现TableCellEditor接口可以开发自己的单元格编辑器,但这种做法比较烦琐:通常会使用扩展DefaultCellEditor类的方式,这种方式比较简单。

TableCellEditor接口定义了一个getTableCellEditorComponent方法,该方法返回一个Component对象,该对象就是该单元格的编辑器。

安装单元格编辑器的两种方式

一旦实现了自己的单元格编辑器,就可以为JTable对象安装该单元格编辑器,与安装单元格绘制器类似,安装单元格编辑器也有两种方式

  • 局部方式(列级):为特定列指定单元格编辑器,通过调用TableColumnsetCellEditor()方法为该列安装单元格编辑器
  • 全局方式(表级):调用JTablesetDefaultEditor()方法为该表格安装默认的单元格编辑器。该方法需要两个参数,即列类型和单元格编辑器,这两个参数表明对于指定类型的数据列使用该单元格编辑器。

局部覆盖全局

与单元格绘制器相似的是,如果有一列同时满足列级单元格编辑器和表级单元格编辑器的要求,系统将采用列级单元格编辑器。

程序

下面程序实现了一个ImageCellEditor编辑器,该编辑器由一个不可直接编辑的文本框和一个按钮组成,当用户单击该按钮时,该编辑器弹出一个文件选择器,方便用户选择图标文件。除此之外,下面程序还创建了一个基于JComboBoxDefaultcellEditor类,该编辑器允许用户通过下拉列表来选择图标。

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
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
import java.io.File;
import java.awt.*;
import javax.swing.*;
import javax.swing.table.*;
import javax.swing.filechooser.*;

public class TableCellEditorTest
{
JFrame jf = new JFrame("使用单元格编辑器");
JTable table;
// 定义二维数组作为表格数据
Object[][] tableData =
{
new Object[]{"李清照" , 29 , "女" , new ImageIcon("icon/3.gif")
, new ImageIcon("icon/3.gif") , true},
new Object[]{"苏格拉底", 56 , "男" , new ImageIcon("icon/1.gif")
, new ImageIcon("icon/1.gif") , false},
new Object[]{"李白", 35 , "男" , new ImageIcon("icon/4.gif")
, new ImageIcon("icon/4.gif") , true},
new Object[]{"弄玉", 18 , "女" , new ImageIcon("icon/2.gif")
, new ImageIcon("icon/2.gif") , true},
new Object[]{"虎头" , 2 , "男" , new ImageIcon("icon/5.gif")
, new ImageIcon("icon/5.gif") , false}
};
// 定义一维数据作为列标题
String[] columnTitle = {"姓名" , "年龄" , "性别" , "主头像"
, "次头像" , "是否中国人"};
public void init()
{
// 以二维数组和一维数组来创建一个ExtendedTableModel对象
ExtendedTableModel model = new ExtendedTableModel(
columnTitle , tableData);
// 以ExtendedTableModel来创建JTable
table = new JTable(model);
table.setRowSelectionAllowed(false);
table.setRowHeight(40);
// 为该表格指定默认的编辑器
table.setDefaultEditor(ImageIcon.class, new ImageCellEditor());
// 获取第五列
TableColumn lastColumn = table.getColumnModel().getColumn(4);
// 创建JComboBox对象,并添加多个图标列表项
JComboBox<ImageIcon> editCombo = new JComboBox<>();
for (int i = 1; i <= 10; i++)
{
editCombo.addItem(new ImageIcon("icon/" + i + ".gif"));
}
// 设置第五列使用基于JComboBox的DefaultCellEditor
lastColumn.setCellEditor(new DefaultCellEditor(editCombo));
// 将JTable对象放在JScrollPane中,并将该JScrollPane放在窗口中显示出来
jf.add(new JScrollPane(table));
jf.pack();
jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
jf.setVisible(true);
}
public static void main(String[] args)
{
new TableCellEditorTest().init();
}
}
class ExtendedTableModel extends DefaultTableModel
{
// 重新提供一个构造器,该构造器的实现委托给DefaultTableModel父类
public ExtendedTableModel(String[] columnNames , Object[][] cells)
{
super(cells , columnNames);
}
// 重写getColumnClass方法,根据每列的第一个值返回该列真实的数据类型
public Class getColumnClass(int c)
{
return getValueAt(0 , c).getClass();
}
}
// 扩展DefaultCellEditor来实现TableCellEditor类
class ImageCellEditor extends DefaultCellEditor
{
// 定义文件选择器
private JFileChooser fDialog = new JFileChooser(); ;
private JTextField field = new JTextField(15);
private JButton button = new JButton("...");
public ImageCellEditor()
{
// 因为DefaultCellEditor没有无参数的构造器
// 所以这里显式调用父类有参数的构造器。
super(new JTextField());
initEditor();
}
private void initEditor()
{
field.setEditable(false);
// 为按钮添加监听器,当用户单击该按钮时,
// 系统将出现一个文件选择器让用户选择图标文件。
button.addActionListener(e -> browse());
// 为文件选择器安装文件过滤器
fDialog.addChoosableFileFilter(new FileFilter()
{
public boolean accept(File f)
{
if (f.isDirectory())
{
return true;
}
String extension = Utils.getExtension(f);
if (extension != null)
{
if (extension.equals(Utils.tiff)
|| extension.equals(Utils.tif)
|| extension.equals(Utils.gif)
|| extension.equals(Utils.jpeg)
|| extension.equals(Utils.jpg)
|| extension.equals(Utils.png))
{
return true;
}
else
{
return false;
}
}
return false;
}
public String getDescription()
{
return "有效的图片文件";
}
});
fDialog.setAcceptAllFileFilterUsed(false);
}
// 重写TableCellEditor接口的getTableCellEditorComponent方法
// 该方法返回单元格编辑器,该编辑器是一个JPanel,
// 该容器包含一个文本框和一个按钮
public Component getTableCellEditorComponent(JTable table
, Object value , boolean isSelected , int row , int column) // ①
{
this.button.setPreferredSize(new Dimension(20, 20));
JPanel panel = new JPanel();
panel.setLayout(new BorderLayout());
field.setText(value.toString());
panel.add(this.field, BorderLayout.CENTER);
panel.add(this.button, BorderLayout.EAST);
return panel;
}
public Object getCellEditorValue()
{
return new ImageIcon(field.getText());
}
private void browse()
{
// 设置、打开文件选择器
fDialog.setCurrentDirectory(new File("icon"));
int result = fDialog.showOpenDialog(null);
// 如果单击了文件选择器的“取消”按钮
if (result == JFileChooser.CANCEL_OPTION)
{
// 取消编辑
super.cancelCellEditing();
return;
}
// 如果单击了文件选择器的“确定”按钮
else
{
// 设置field的内容
field.setText("icon/" + fDialog.getSelectedFile().getName());
}
}
}
class Utils
{
public final static String jpeg = "jpeg";
public final static String jpg = "jpg";
public final static String gif = "gif";
public final static String tiff = "tiff";
public final static String tif = "tif";
public final static String png = "png";
// 获取文件扩展名的方法
public static String getExtension(File f)
{
String ext = null;
String s = f.getName();
int i = s.lastIndexOf('.');
if (i > 0 && i < s.length() - 1)
{
ext = s.substring(i + 1).toLowerCase();
}
return ext;
}
}

上面程序中实现了一个ImageCellEditor编辑器,程序中的粗体字代码将该单元格编辑器注册成Imagelcon类型的单元格编辑器,如果某一列的数据类型是ImageIcon,则默认使用该单元格编辑器。ImageCellEditor扩展了DefaultCellEditor基类,重写getTableCellEditorComponent方法返回一个JPanel,该JPanel里包含一个文本框和一个按钮。

除此之外,程序中的粗体字代码还为最后一列安装了一个基于JComboBoxDefaultCellEditor

运行上面程序,双击倒数第3列的任意单元格,开始编辑该单元格,将看到如图12.56所示的窗口。

双击第5列的任意单元格,开始编辑该单元格,将看到如图12.57所示的窗口。

通过图12.56和图12.57可以看岀,如果单元格的值需要从多个枚举值之中选择,则使用DefaultCellEditor即可。使用自定义的单元格编辑器则非常灵活,可以取得单元格编辑器的全部控制权。

12.11.5 绘制单元格内容

前面看到的所有表格的单元格内容都是字符串,实际上表格的单元格内容也可以是更复杂的内容,JTable使用TableCellRenderer绘制单元格,Swing为该接口提供了一个实现类:DefaultTableCellRenderer,该单元格绘制器可以绘制如下三种类型的单元格值(根据其TableModelgetColumnClass方法来决定该单元格值的类型)。

  • Icon:默认的单元格绘制器会把该类型的单元格值绘制成该Icon对象所代表的图标。
  • Boolean:默认的单元格绘制器会把该类型的单元格值绘制成复选按钮
  • Object:默认的单元格绘制器在单元格内绘制出该对象的toString()方法返回的字符串

在默认情况下,如果程序直接使用二维数组或Vector来创建JTable,程序将会使用JTable的匿名内部类或DefaultTableModel充当该表格的mode对象,这两个TableModelgetColumnClass方法的返回值都是Object。这意味着,即使该二维数组里值的类型是Icon,但由于两个默认的TableModel实现类的getColumnClass方法总是返回Object,这将导致默认的单元格绘制器把Icon值当成Object值处理——只是绘制出其toString方法返回的字符串。

为了让默认的单元格绘制器可以将Icon类型的值绘制成图标,把Boolean类型的值绘制成复选框,创建JTable时所使用的TableModel绝不能采用默认的TableModel,必须采用扩展后的TableModel类,如下所示。

1
2
3
4
5
6
7
8
9
//定义一个DefaultTableCellRenderer类的子类
class ExtendedTableModel extends DefaultTableCellRenderer
{
//重写 getco1 umnclass方法,根据每列的第一个值来返回每列真实的数据类型
public Class getColumnClass(int c){
return getValueAt(0,c).getclass();
}

}

提供了上面的ExtendedTableModel类之后,程序应该先创建ExtendedTableModel对象,再利用该对象来创建Table,这样就可以保证JTablemodel对象的getColumnClass方法会返回每列真实的数据类型,默认的单元格绘制器就会将Icon类型的单元格值绘制成图标,将Boolean类型的单元格值绘制成复选框。

自定义表格单元格绘制器

如果希望程序采用自己定制的单元格绘制器,则必须实现自己的单元格绘制器,单元格绘制器必须实现TableCellRenderer接口。与前面的TreeCellRenderer接口完全相似,该接口里也只包含一个getTableCellrendererComponent()方法,该方法返回的Component将会作为指定单元格绘制的组件。

单元格绘制器模型

Swing提供了一致的编程模型,不管是JListJTree还是JTable,它们所使用的单元格绘制器都有一致的编程模型,分别需要扩展ListCellRendererTreeCellRendererTableCellRenderer,扩展这三个基类时都需要重写getXxeCellRendererComponent()方法,该方法的返回值将作为被绘制的组件

单元格绘制器的两种方式

一旦实现了自己的单元格绘制器之后,还必须将该单元格绘制器安装到指定的JTable对象上,为指定的JTable对象安装单元格绘制器有如下两种方式。

  • 局部方式(列级):调用TableColumnsetCellRenderer()方法为指定列安装指定的单元格绘制器
  • 全局方式(表级):调用JTablesetDefaultRenderer()方法为指定的JTable对象安装单元格绘制器。setDefaultRenderer()方法需要传入两个参数,即列类型和单元格绘制器,表明指定类型的数据列才会使用该单元格绘制器。

局部绘制器优先级更高

当某一列既符合全局绘制器的规则,又符合局部绘制器的规则时,局部绘制器将会负责绘制该单元格,全局绘制器不会产生任何作用。

列头单元格绘制器

除此之外,TableColumn还包含了一个setHeaderRenderer()方法,该方法可以为指定列的列头安装单元格绘制器。

程序

下面程序提供了一个ExtendedTablemodel类,该类扩展了DefaultTableModel,重写了父类的getColumnClass()方法,该方法根据每列的第一个值来决定该列的数据类型;下面程序还提供了一个定制的单元格绘制器,它使用图标来形象地表明每个好友的性别

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
import java.awt.*;
import javax.swing.*;
import javax.swing.table.*;

public class TableCellRendererTest
{
JFrame jf = new JFrame("使用单元格绘制器");
JTable table;
// 定义二维数组作为表格数据
Object[][] tableData =
{
new Object[]{"李清照" , 29 , "女"
, new ImageIcon("icon/3.gif") , true},
new Object[]{"苏格拉底", 56 , "男"
, new ImageIcon("icon/1.gif") , false},
new Object[]{"李白", 35 , "男"
, new ImageIcon("icon/4.gif") , true},
new Object[]{"弄玉", 18 , "女"
, new ImageIcon("icon/2.gif") , true},
new Object[]{"虎头" , 2 , "男"
, new ImageIcon("icon/5.gif") , false}
};
// 定义一维数据作为列标题
String[] columnTitle = {"姓名" , "年龄" , "性别"
, "主头像" , "是否中国人"};
public void init()
{
// 以二维数组和一维数组来创建一个ExtendedTableModel对象
ExtendedTableModel model = new ExtendedTableModel(columnTitle
, tableData);
// 以ExtendedTableModel来创建JTable
table = new JTable( model);
table.setRowSelectionAllowed(false);
table.setRowHeight(40);
// 获取第三列
TableColumn lastColumn = table.getColumnModel().getColumn(2);
// 对第三列采用自定义的单元格绘制器
lastColumn.setCellRenderer(new GenderTableCellRenderer());
// 将JTable对象放在JScrollPane中,并将该JScrollPane显示出来
jf.add(new JScrollPane(table));
jf.pack();
jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
jf.setVisible(true);
}
public static void main(String[] args)
{
new TableCellRendererTest().init();
}
}
class ExtendedTableModel extends DefaultTableModel
{
// 重新提供一个构造器,该构造器的实现委托给DefaultTableModel父类
public ExtendedTableModel(String[] columnNames , Object[][] cells)
{
super(cells , columnNames);
}
// 重写getColumnClass方法,根据每列的第一个值来返回其真实的数据类型
public Class getColumnClass(int c)
{
return getValueAt(0 , c).getClass();
}
}
// 定义自定义的单元格绘制器
class GenderTableCellRenderer extends JPanel
implements TableCellRenderer
{
private String cellValue;
// 定义图标的宽度和高度
final int ICON_WIDTH = 23;
final int ICON_HEIGHT = 21;
public Component getTableCellRendererComponent(JTable table
, Object value , boolean isSelected , boolean hasFocus
, int row , int column)
{
cellValue = (String)value;
// 设置选中状态下绘制边框
if (hasFocus)
{
setBorder(UIManager.getBorder("Table.focusCellHighlightBorder"));
}
else
{
setBorder(null);
}
return this;
}
// 重写paint()方法,负责绘制该单元格内容
public void paint(Graphics g)
{
// 如果表格值为"男"或"male",则绘制一个男性图标
if (cellValue.equalsIgnoreCase("男")
|| cellValue.equalsIgnoreCase("male"))
{
drawImage(g , new ImageIcon("icon/male.gif").getImage());
}
// 如果表格值为"女"或"female",则绘制一个女性图标
if (cellValue.equalsIgnoreCase("女")
|| cellValue.equalsIgnoreCase("female"))
{
drawImage(g , new ImageIcon("icon/female.gif").getImage());
}
}
// 绘制图标的方法
private void drawImage(Graphics g , Image image)
{
g.drawImage(image, (getWidth() - ICON_WIDTH ) / 2
, (getHeight() - ICON_HEIGHT) / 2 , null);
}
}

上面程序中没有直接使用二维数组和一维数组来创建JTable对象,而是采用ExtendedTableModel对象来创建JTable对象(如第一段粗体字代码所示)。ExtendedTableModel类重写了父类的getColumnClass方法,该方法将会根据每列实际的值来返回该列的类型(如第二段粗体字代码所示)。

程序提供了一个GenderTableCellRenderer类,该类实现了Tablecellrenderer接口,可以作为单元格绘制器使用。该类继承了JPanel容器,重写getTableCelIRendererComponent()方法时返回this,这表明它会使用JPanel对象作为单元格绘制器。

读者可以将ExtendedTableModel补充得更加完整——主要是将DefaultTableModel中的几个构造器重新暴露出来,以后程序中可以使用ExtendedTablemodel类作为JTablemodel类,这样创建的JTable就可以将Icon列、Boolean列绘制成图标和复选框。

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

12.11.4 实现排序

使用JTable实现的表格并没有实现根据指定列排序的功能,但开发者可以利用AbstractTableModel类来实现该功能。由于TableModel不强制要求保存表格里的数据,只要TableModel实现了getValueAt(),getColumnCount()getRowCount()三个方法,JTable就可以根据该TableModel生成表格。因此可以创建个SortableTableModel实现类,它可以将原TableModel包装起来,并实现根据指定列排序的功能。

程序创建的SortableTableModel实现类会对原TableModel进行包装,但它实际上并不保存仼何数据,它会把所有的方法实现委托给原TableModel完成。SortableTableModel仅保存原TableModel里每行的行索引,当程序对SortableTableModel的指定列排序时,实际上仅仅对SortableTableModel里的行索引进行排序,这样造成的结果是:SortableTableModel里的数据行的行索引与原TableModel里数据行的行索引不一致,所以对于TableModel的那些涉及行索引的方法都需要进行相应的转换。

程序 表格排序

下面程序实现了SortableTableModel类,并使用该类来实现对表格根据指定列排序的功能。

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
import java.awt.event.*;
import javax.swing.*;
import javax.swing.table.*;

public class SortTable
{
JFrame jf = new JFrame("可按列排序的表格");
// 定义二维数组作为表格数据
Object[][] tableData =
{
new Object[]{"李清照" , 29 , "女"},
new Object[]{"苏格拉底", 56 , "男"},
new Object[]{"李白", 35 , "男"},
new Object[]{"弄玉", 18 , "女"},
new Object[]{"虎头" , 2 , "男"}
};
// 定义一维数据作为列标题
Object[] columnTitle = {"姓名" , "年龄" , "性别"};
// 以二维数组和一维数组来创建一个JTable对象
JTable table = new JTable(tableData , columnTitle);
// 将原表格里的model包装成新的SortableTableModel对象
SortableTableModel sorterModel = new SortableTableModel(
table.getModel());
public void init()
{
// 使用包装后SortableTableModel对象作为JTable的model对象
table.setModel(sorterModel);
// 为每列的列头增加鼠标监听器
table.getTableHeader().addMouseListener(new MouseAdapter()
{
public void mouseClicked(MouseEvent event) // ①
{
// 如果单击次数小于2,即不是双击,直接返回
if (event.getClickCount() < 2)
{
return;
}
// 找出鼠标双击事件所在的列索引
int tableColumn = table.columnAtPoint(event.getPoint());
// 将JTable中的列索引转换成对应TableModel中的列索引
int modelColumn = table.convertColumnIndexToModel(tableColumn);
// 根据指定列进行排序
sorterModel.sort(modelColumn);
}
});
// 将JTable对象放在JScrollPane中,并将该JScrollPane显示出来
jf.add(new JScrollPane(table));
jf.pack();
jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
jf.setVisible(true);
}
public static void main(String[] args)
{
new SortTable().init();
}
}
class SortableTableModel extends AbstractTableModel
{
private TableModel model;
private int sortColumn;
private Row[] rows;
// 将一个已经存在TableModel对象包装成SortableTableModel对象
public SortableTableModel(TableModel m)
{
// 将被封装的TableModel传入
model = m;
rows = new Row[model.getRowCount()];
// 将原TableModel中的每行记录的索引使用Row数组保存起来
for (int i = 0; i < rows.length; i++)
{
rows[i] = new Row(i);
}
}
// 实现根据指定列进行排序
public void sort(int c)
{
sortColumn = c;
java.util.Arrays.sort(rows);
fireTableDataChanged();
}
// 下面三个方法需要访问model中的数据,所以涉及本model中数据
// 和被包装model数据中的索引转换,程序使用rows数组完成这种转换。
public Object getValueAt(int r, int c)
{
return model.getValueAt(rows[r].index, c);
}
public boolean isCellEditable(int r, int c)
{
return model.isCellEditable(rows[r].index, c);
}
public void setValueAt(Object aValue, int r, int c)
{
model.setValueAt(aValue, rows[r].index, c);
}
// 下面方法的实现把该model的方法委托为原封装的model来实现
public int getRowCount()
{
return model.getRowCount();
}
public int getColumnCount()
{
return model.getColumnCount();
}
public String getColumnName(int c)
{
return model.getColumnName(c);
}
public Class getColumnClass(int c)
{
return model.getColumnClass(c);
}
// 定义一个Row类,该类用于封装JTable中的一行
// 实际上它并不封装行数据,它只封装行索引
private class Row implements Comparable<Row>
{
// 该index保存着被封装Model里每行记录的行索引
public int index;
public Row(int index)
{
this.index = index;
}
// 实现两行之间的大小比较
public int compareTo(Row other)
{
Object a = model.getValueAt(index, sortColumn);
Object b = model.getValueAt(other.index, sortColumn);
if (a instanceof Comparable)
{
return ((Comparable)a).compareTo(b);
}
else
{
return a.toString().compareTo(b.toString());
}
}
}
}

上面程序是在SimpleTable程序的基础上改变而来的,改变的部分就是增加了两行粗体字代码和①号粗体字代码块。其中粗体字代码负责把原JTablemodel对象包装成SortableTableModel实例,并设置原JTable使用SortableTableModel实例作为对应的mode对象;

而①号粗体字代码部分则用于为该表格的列头增加鼠标监听器:当用鼠标双击指定列时,SortableTableModel对象根据指定列进行排序。
程序中还使用了convertColumnIndexToModel()方法把JTable中的列索引转换成TableModel中的列索引。这是因为JTable中的列允许用户随意拖动,因此可能造成JTable中的列索引与TableModel中的列索引不一致。

运行上面程序,并双击“年龄”列头,将看到如图12.53所示的排序效果。

图12.53

实际上,上面程序的关键在于SortableTableModel类,该类使用rows数组来保存原TableModel里的行索引。为了让程序可以对rows数组元素根据指定列排序,程序使用了Row类来封装行索引,并实现了compareTo()方法,该方法实现了根据指定列来比较两行大小的功能,从而允许程序根据指定列对rows[数组元素进行排序。

12.11.3 TableColumnMode和监听器

JTable使用TableModel来保存该表格所有数据列的状态数据,如果程序需要访问JTable的所有列状态信息,则可以通过获取该JTableTableColumnModel来实现。TableColumnModel提供了如下几个方法来增加、删除和移动数据列:

方法 描述
void addColumn(TableColumn aColumn) 该方法用于为TableModel添加一列。该方法主要用于将原来隐藏的数据列显示出来
void moveColumn(int columnIndex, int newIndex) 该方法用于将指定列移动到其他位置。
void removeColumn(TableColumn column) 该方法用于从TableModel中删除指定列。实际上,该方法并未真正删除指定列,只是将该列在TableColumnModel中隐藏起来,使之不可见。

注意:当调用removeColumn删除指定列之后,调用TableColumnModelgetColumnCount()方法也会看到返回的列数减少了,看起来很像真正删除了该列。但使用setValueAt()方法为该列设置值时,依然可以设置成功,这表明调用removeColumn删除指定列之后,这些列依然是存在的

实际上,JTable也提供了对应的方法来增加、删除和移动数据列,不过JTable的这些方法实际上还是需要委托给它所对应的TableColumnModel来完成。

图12.51显示了JTable及其主要辅助类之间的关系:

图12.51 JTable及其主要辅助类之间的关系

程序

下面程序示范了如何通过DefaultTableModelTableColumnModel动态地改变表格的行、列。

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
import java.util.ArrayList;
import java.awt.BorderLayout;
import javax.swing.*;
import javax.swing.table.*;

public class DefaultTableModelTest {
JFrame mainWin = new JFrame("管理数据行、数据列");
final int COLUMN_COUNT = 5;
DefaultTableModel model;
JTable table;
// 用于保存被隐藏列的List集合
ArrayList<TableColumn> hiddenColumns = new ArrayList<>();

public void init() {
model = new DefaultTableModel(COLUMN_COUNT, COLUMN_COUNT);
for (int i = 0; i < COLUMN_COUNT; i++) {
for (int j = 0; j < COLUMN_COUNT; j++) {
model.setValueAt("老单元格值 " + i + " " + j, i, j);
}
}
table = new JTable(model);
mainWin.add(new JScrollPane(table), BorderLayout.CENTER);
// 为窗口安装菜单
JMenuBar menuBar = new JMenuBar();
mainWin.setJMenuBar(menuBar);
JMenu tableMenu = new JMenu("管理");
menuBar.add(tableMenu);
JMenuItem hideColumnsItem = new JMenuItem("隐藏选中列");
hideColumnsItem.addActionListener(event -> {
// 获取所有选中列的索引
int[] selected = table.getSelectedColumns();
TableColumnModel columnModel = table.getColumnModel();
// 依次把每一个选中的列隐藏起来,并使用List保存这些列
for (int i = selected.length - 1; i >= 0; i--) {
TableColumn column = columnModel.getColumn(selected[i]);
// 隐藏指定列
table.removeColumn(column);
// 把隐藏的列保存起来,确保以后可以显示出来
hiddenColumns.add(column);
}
});
tableMenu.add(hideColumnsItem);
JMenuItem showColumnsItem = new JMenuItem("显示隐藏列");
showColumnsItem.addActionListener(event -> {
// 把所有隐藏列依次显示出来
for (TableColumn tc : hiddenColumns) {
// 依次把所有隐藏的列显示出来
table.addColumn(tc);
}
// 清空保存隐藏列的List集合
hiddenColumns.clear();
});
tableMenu.add(showColumnsItem);
JMenuItem addColumnItem = new JMenuItem("插入选中列");
addColumnItem.addActionListener(event -> {
// 获取所有选中列的索引
int[] selected = table.getSelectedColumns();
TableColumnModel columnModel = table.getColumnModel();
// 依次把选中的列添加到JTable之后
for (int i = selected.length - 1; i >= 0; i--) {
TableColumn column = columnModel.getColumn(selected[i]);
table.addColumn(column);
}
});
tableMenu.add(addColumnItem);
JMenuItem addRowItem = new JMenuItem("增加行");
addRowItem.addActionListener(event -> {
// 创建一个String数组作为新增行的内容
String[] newCells = new String[COLUMN_COUNT];
for (int i = 0; i < newCells.length; i++) {
newCells[i] = "新单元格值 " + model.getRowCount() + " " + i;
}
// 向TableModel中新增一行。
model.addRow(newCells);
});
tableMenu.add(addRowItem);
JMenuItem removeRowsItem = new JMenuItem("删除选中行");
removeRowsItem.addActionListener(event -> {
// 获取所有选中行
int[] selected = table.getSelectedRows();
// 依次删除所有选中行
for (int i = selected.length - 1; i >= 0; i--) {
model.removeRow(selected[i]);
}
});
tableMenu.add(removeRowsItem);
mainWin.pack();
mainWin.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
mainWin.setVisible(true);
}

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

上面程序中的粗体字代码部分就是程序控制隐藏列、显示隐藏列、增加数据行和删除数据行的代码。除此之外,程序还实现了一个功能:当用户选中某个数据列之后,还可以将该数据列添加到该表格的后面——但不要忘记了add()方法的功能,add方法只是将已有的数据列显示出来,并不是真正添加数据列。运行上面程序,会看到如图12.52所示的界面。

图12.52
图12.52

从图12.52中可以看出,虽然程序新增了一列,但新增列的列名依然是B,如果修改新增列内的单元格的值时,看到原来的B列的值也随之改变。
如图12.52.2所示:

图12.52.2

由此可见,addColumn方法只是将原有的列显示出来而已。程序还允许新增数据行,当执行addRows()方法时需要传入数组或Vector参数,该参数里包含的多个数值将作为新增行的数据。

监听JTable里列状态的改变

如果程序需要监听JTable列状态的改变,例如监听列的增加、删除、移动等改变,则必须使用该JTable所对应的TableColumnModel对象。

TableColumnModel对象提供了一个addColumnModelListener()方法来添加监听器

TableColumnModelListener监听器接口里包含如下几个方法:

TableColumnModelListener方法 描述
void columnAdded(TableColumnModelEvent e) 当向TableColumnModel里添加数据列时将会触发该方法
void columnMarginChanged(ChangeEvent e) 当由于页面距(Margin)的改变引起列状态改变时将会触发该方法
void columnMoved(TableColumnModelEvent e) 当移动TableColumnMode里的数据列时将会触发该方法
void columnRemoved(TableColumnModelEvent e) 当删除TableColumnModel里的数据列时将会触发该方法。
void columnSelectionChanged(ListSelectionEvent e) 当改变表格的选择模式时将会触发该方法。

但表格的数据列通常需要程序来控制增加、删除,用户操作通常无法直接为表格增加、删除数据列,所以使用监听器来监听TableColumnModel改变的情况比较少见。

12.11.2 TableModel和监听器

JListJTree类似的是,JTable采用了TableModel来保存表格中的所有状态数据;与ListModel类似的是,TableModel也不强制保存该表格显示的数据。虽然在前面程序中看到的是直接利用一个二维数组来创建JTable对象,但也可以通过TableModel对象来创建表格

使用TableModel创建表格

如果需要利用TableModel来创建表格对象,则可以利用Swing提供的AbstractTableModel抽象类,该抽象类已经实现了TableModel接口里的大部分方法,程序只需要为该抽象类实现如下三个抽象方法即可。

  • getColumnCount():返回该TableModel对象的列数量。
  • getRowCount():返回该TableModel对象的行数量。
  • getValueAt():返回指定行、指定列的单元格值。

重写这三个方法后只是告诉JTable生成该表格所需的基本信息,如果想指定JTable生成表格的列名,还需要重写getColumnName(int c)方法,该方法返回一个字符串,该字符串将作为第c+1列的列名。

设置单元格内容可修改

在默认情况下,AbstractTableModelisCellEditable方法返回false,表明该表格的单元格处于不可编辑状态,如果想让用户直接修改单元格的内容,则需要重写该方法,并让该方法返回true。重写该方法后,只实现了界面上单元格的可编辑,如果需要控制实际的编辑操作,还需要重写该类的setValueAt方法。

方法 描述
boolean isCellEditable(int rowIndex, int columnIndex) Returns false.
void setValueAt(Object aValue, int rowIndex, int columnIndex) This empty implementation is provided so users don’t have to implement this method if their data model is not editable.

TableModel典型应用 封装JDBCResultset

关于TableModel的典型应用就是用于封装JDBC编程里的Resultset,程序可以利用TableModel来封装数据库査询得到的结果集,然后使用JTable把该结果集显示岀来。还可以允许用户直接编辑表格的单元格,当用户编辑完成后,程序将用户所做的修改写入数据库。

程序

下面程序简单实现了这种功能——当用户选择了指定的数据表后,程序将显示该数据表中的全部数据,用户可以直接在该表格内修改数据表的记录

mysql.sql

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
drop database if exists auction;

create database auction;

use auction;

#用户表
create table auction_user(
user_id int(11) auto_increment,
username varchar(50) not null,
userpass varchar(50) not null,
email varchar(100) not null,
primary key(user_id),
unique(username)
);

INSERT INTO auction_user (username,userpass,email) VALUES ('tomcat','tomcat','spring_test@163.com');
INSERT INTO auction_user (username,userpass,email) VALUES ('mysql','mysql','spring_test@163.com');

#物品种类表
create table kind(
kind_id int(11) auto_increment,
kind_name varchar(50) not null,
kind_desc varchar(255) not null,
primary key(kind_id)
);

INSERT INTO kind (kind_name,kind_desc) VALUES ('电脑硬件','这里并不是很主流的产品,但价格绝对令你心动');
INSERT INTO kind (kind_name,kind_desc) VALUES ('房产','提供非常稀缺的房源');

#物品状态表
create table state(
state_id int(11) auto_increment,
state_name varchar(10),
primary key(state_id)
);

INSERT INTO state (state_name) VALUES ('拍卖中');
INSERT INTO state (state_name) VALUES ('拍卖成功');
INSERT INTO state (state_name) VALUES ('流拍');

#物品表
create table item(
item_id int(11) auto_increment,
item_name varchar(255) not null,
item_remark varchar(255),
item_desc varchar(255),
kind_id int(11) not null,
addtime date not null,
endtime date not null,
init_price double not null,
max_price double not null,
owner_id int(11) not null,
winer_id int(11),
state_id int(11) not null,
primary key(item_id),
FOREIGN KEY(kind_id) REFERENCES kind(kind_id),
FOREIGN KEY(owner_id) REFERENCES auction_user(user_id),
FOREIGN KEY(winer_id) REFERENCES auction_user(user_id),
FOREIGN KEY(state_id) REFERENCES state(state_id)
);

# 拍卖中的物品
INSERT INTO item ( item_name , item_remark , item_desc, kind_id, addtime , endtime, init_price, max_price, owner_id, winer_id, state_id)
VALUES ( '主板', '老式主板', '老主板,还可以用', 1, ADDDATE(CURDATE(), -5), ADDDATE(CURDATE(), 30) , 230, 250, 1, null, 1);
# 流派的物品
INSERT INTO item ( item_name , item_remark , item_desc, kind_id, addtime , endtime, init_price, max_price, owner_id, winer_id, state_id)
VALUES ( '显卡', '老式显卡', '老显卡,还可以用', 1, ADDDATE(CURDATE(), -9), ADDDATE(CURDATE(), -2), 210, 210, 2, null, 3);
# 被竞得的物品
INSERT INTO item ( item_name , item_remark , item_desc, kind_id, addtime , endtime, init_price, max_price, owner_id, winer_id, state_id)
VALUES ( '老房子', '老式房子', '40年的老房子', 2, ADDDATE(CURDATE(), -9), ADDDATE(CURDATE(), -5), 21000, 25000, 2, 1, 2);

#竞标历史表
create table bid(
bid_id int(11) auto_increment,
user_id int(11) not null,
item_id int(11) not null,
bid_price double not null,
bid_date date not null,
primary key(bid_id),
unique(item_id , bid_price),
FOREIGN KEY(user_id) REFERENCES auction_user(user_id),
FOREIGN KEY(item_id) REFERENCES item(item_id)
);

INSERT INTO bid ( user_id , item_id , bid_price, bid_date)
VALUES ( 2, 1, 250, ADDDATE(CURDATE(), -2));
INSERT INTO bid ( user_id , item_id , bid_price, bid_date)
VALUES ( 1, 3, 25000, ADDDATE(CURDATE(), -6));
;

conn.ini

1
2
3
4
jdbc.drivers=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost/auction
jdbc.username=root
jdbc.password=32147

TableModelTest.java

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
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
import java.sql.*;
import java.io.*;
import java.util.*;
import java.awt.BorderLayout;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.table.*;

public class TableModelTest {
JFrame jf = new JFrame("数据表管理工具");
private JScrollPane scrollPane;
private ResultSetTableModel model;
// 用于装载数据表的JComboBox
private JComboBox<String> tableNames = new JComboBox<>();
private JTextArea changeMsg = new JTextArea(4, 80);
private ResultSet rs;
private Connection conn;
private Statement stmt;

public void init() {
// 为JComboBox添加事件监听器,当用户选择某个数据表时,触发该方法
tableNames.addActionListener(event -> {
try {
// 如果装载JTable的JScrollPane不为空
if (scrollPane != null) {
// 从主窗口中删除表格
jf.remove(scrollPane);
}
// 从JComboBox中取出用户试图管理的数据表的表名
String tableName = (String) tableNames.getSelectedItem();
// 如果结果集不为空,则关闭结果集
if (rs != null) {
rs.close();
}
String query = "select * from " + tableName;
// 查询用户选择的数据表
rs = stmt.executeQuery(query);
// 使用查询到的ResultSet创建TableModel对象
model = new ResultSetTableModel(rs);
// 为TableModel添加监听器,监听用户的修改
model.addTableModelListener(evt -> {
int row = evt.getFirstRow();
int column = evt.getColumn();
changeMsg.append("修改的列:" + column + ",修改的行:" + row + "修改后的值:" + model.getValueAt(row, column));
});
// 使用TableModel创建JTable,并将对应表格添加到窗口中
JTable table = new JTable(model);
scrollPane = new JScrollPane(table);
jf.add(scrollPane, BorderLayout.CENTER);
jf.validate();
} catch (SQLException e) {
e.printStackTrace();
}
});
JPanel p = new JPanel();
p.add(tableNames);
jf.add(p, BorderLayout.NORTH);
jf.add(new JScrollPane(changeMsg), BorderLayout.SOUTH);
try {
// 获取数据库连接
conn = getConnection();
// 获取数据库的MetaData对象
DatabaseMetaData meta = conn.getMetaData();
// 创建Statement
stmt = conn.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE, ResultSet.CONCUR_UPDATABLE);
// 查询当前数据库的全部数据表
ResultSet tables = meta.getTables(null, null, null, new String[] { "TABLE" });
// 将全部数据表添加到JComboBox中
while (tables.next()) {
tableNames.addItem(tables.getString(3));
}
tables.close();
} catch (IOException e) {
e.printStackTrace();
} catch (Exception e) {
e.printStackTrace();
}
jf.addWindowListener(new WindowAdapter() {
public void windowClosing(WindowEvent event) {
try {
if (conn != null)
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
});
jf.pack();
jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
jf.setVisible(true);
}

private static Connection getConnection() throws SQLException, IOException, ClassNotFoundException {
// 通过加载conn.ini文件来获取数据库连接的详细信息
Properties props = new Properties();
FileInputStream in = new FileInputStream("conn.ini");
props.load(in);
in.close();
String drivers = props.getProperty("jdbc.drivers");
String url = props.getProperty("jdbc.url");
String username = props.getProperty("jdbc.username");
String password = props.getProperty("jdbc.password");
// 加载数据库驱动
Class.forName(drivers);
// 取得数据库连接
return DriverManager.getConnection(url, username, password);
}

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

// 扩展AbstractTableModel,用于将一个ResultSet包装成TableModel
class ResultSetTableModel extends AbstractTableModel // ①
{
private ResultSet rs;
private ResultSetMetaData rsmd;

// 构造器,初始化rs和rsmd两个属性
public ResultSetTableModel(ResultSet aResultSet) {
rs = aResultSet;
try {
rsmd = rs.getMetaData();
} catch (SQLException e) {
e.printStackTrace();
}
}

// 重写getColumnName方法,用于为该TableModel设置列名
public String getColumnName(int c) {
try {
return rsmd.getColumnName(c + 1);
} catch (SQLException e) {
e.printStackTrace();
return "";
}
}

// 重写getColumnCount方法,用于设置该TableModel的列数
public int getColumnCount() {
try {
return rsmd.getColumnCount();
} catch (SQLException e) {
e.printStackTrace();
return 0;
}
}

// 重写getValueAt方法,用于设置该TableModel指定单元格的值
public Object getValueAt(int r, int c) {
try {
rs.absolute(r + 1);
return rs.getObject(c + 1);
} catch (SQLException e) {
e.printStackTrace();
return null;
}
}

// 重写getColumnCount方法,用于设置该TableModel的行数
public int getRowCount() {
try {
rs.last();
return rs.getRow();
} catch (SQLException e) {
e.printStackTrace();
return 0;
}
}

// 重写isCellEditable返回true,让每个单元格可编辑
public boolean isCellEditable(int rowIndex, int columnIndex) {
return true;
}

// 重写setValueAt()方法,当用户编辑单元格时,将会触发该方法
public void setValueAt(Object aValue, int row, int column) {
try {
// 结果集定位到对应的行数
rs.absolute(row + 1);
// 修改单元格多对应的值
rs.updateObject(column + 1, aValue);
// 提交修改
rs.updateRow();
// 触发单元格的修改事件
fireTableCellUpdated(row, column);
} catch (SQLException evt) {
evt.printStackTrace();
}
}
}

上面程序的关键在于①号粗体字代码所扩展的ResultsetTableModel类,该类继承了AbstractTableModel父类,根据其Resultset来重写getColumnCountgetRowCountgetValueAt三个方法,从而允许该表格可以将该Resultset里的所有记录显示出来。除此之外,该扩展类还重写了isCellEditabled方式用来实现允许用户编辑单元格的功能,重写了setValueAt两个方法用来实现当用户编辑单元格时将所做的修改同步到数据库的功能。

程序中的粗体字代码使用Resultset创建了一个TableModel对象,并为该TableModel添加事件监听器,然后把该TableModel使用JTable显示出来。当用户修改该JTable对应表格里单元格的内容时,该监听器会检测到这种修改,并将这种修改信息通过下面的文本域显示出来。

提示:上面程序大量使用了JDBC编程中的JDBC连接数据库、获取可更新的结果集、ResultSetMetaData、DatabaseMetaData等知识,读者可能一时难以读懂,可以参考本书第13章的内容来阅读本程序。该程序的运行需要底层数据库的支持,所以读者应按第13章的内容正常安装MySQI数据库,并将 codes\12\12.11路径下的mysql.sql脚本导入数据库,修改conn.ini文件中的数据库连接信息才可运行该程序。使用JDBC连接数据库还需要加载JDBC驱动,所以本章为运行该程序提供了一个run.cmd批处理文件,读者可以通过该文件来运行该程序。不要直接运行该程序,否则可能出现 java.lang.ClassNotFoundException:com.mysql.jdbc.Driver 。

运行上面程序,会看到如图12.50所示的界面。

图12.50

从图12.50中可以看出,当修改指定单元格的记录时,添加在TableModel上的监听器就会被触发。当修改JTable单元格里的内容时,底层数据表里的记录也会做出相应的改变,如图12.50.1所示:

图12.50.1

DefaultTableModel

不仅用户可以扩展AbstractTableModel抽象类,Swing本身也为AbstractTableModel提供了一个DefaultTableModel实现类,程序可以通过使用DefaultTableModel实现类来创建Table对象。通过DefaultTableModel对象创建JTable对象后,就可以调用它提供的方法来添加数据行、插入数据行、删除数据行和移动数据行

DefaultTableModel提供了如下几个方法来控制数据行操作。

DefaultTableModel控制数据行的方法 描述
addColumn() 该方法用于为TableModel增加一列,该方法有三个重载的版本,实际上该方法只是将原来隐藏的数据列显示出来。
addRow() 该方法用于为TableModel增加一行,该方法有两个重载的版本
insertRow() 该方法用于在TableModel的指定位置插入一行,该方法有两个重载的版本。
removeRow(int start, int end,int to) 该方法用于移动TableModel中指定范围的数据行

通过DefaultTableModel提供的这样几个方法,程序就可以动态地改变表格里的数据行

SwingTableModel提供了两个实现类,其中一个是DefaultTableModel,另一个是JTable的匿名内部类。

  • 如果直接使用二维数组来创建JTable对象,维护该JTable状态信息的model对象就是JTable匿名内部类的实例;
  • 当使用Vector来创建JTable对象时,维护该JTable状态信息的modl对象就是DefaultTableModel实例

提示:Swing为 TableModel 提供了两个实现类,其中一个是 DefaultTableModel,另一个是JTable的匿名内部类。如果直接使用二维数组来创建JTable对象,维护该 JTable状态信息的 model对象就是 JTable 匿名内部类的实例;当使用 Vector 来创建 JTable对象时,维护该 JTable 状态信息的 model对象就是 DefaultTableModel实例。

12.11 使用JTable和TableModel创建表格

表格也是GUI程序中常用的组件,表格是一个由多行、多列组成的二维显示区。SwingJTable以及相关类提供了这种表格支持,通过使用JTable以及相关类,程序既可以使用简单的代码创建岀表格来显示二维数据,也可以开发出功能丰富的表格,还可以为表格定制各种显示外观、编辑特性。

12.11.1 创建表格

使用JTable来创建表格是非常容易的事情,JTable可以把一个二维数据包装成一个表格,这个二维数据既可以是一个二维数组,也可以是集合元素为VectorVector对象(Vector里包含Vector形成二维数据)。除此之外,为了给该表格的每一列指定列标题,还需要传入一个一维数据作为列标题,这一个维数据既可以是一维数组,也可以是Vector对象。

程序 使用二维数组和一维数组创建简单表格

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
import javax.swing.*;

public class SimpleTable {
JFrame jf = new JFrame("简单表格");
JTable table;
// 定义二维数组作为表格数据
Object[][] tableData = {
new Object[] { "李清照", 29, "女" },
new Object[] { "苏格拉底", 56, "男" },
new Object[] { "李白", 35, "男" },
new Object[] { "弄玉", 18, "女" },
new Object[] { "虎头", 2, "男" }
};
// 定义一维数据作为列标题
Object[] columnTitle = { "姓名", "年龄", "性别" };

public void init() {
// 以二维数组和一维数组来创建一个JTable对象
table = new JTable(tableData, columnTitle);
// 将JTable对象放在JScrollPane中,
// 并将该JScrollPane放在窗口中显示出来
jf.add(new JScrollPane(table));
jf.pack();
jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
jf.setVisible(true);
}

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

上面程序中的粗体字代码创建了两个Object数组,第一个二维数组作为JTable的数据,第二个一维数组作为JTable的列标题。创建二维数组时利用了JDK1.5提供的自动装箱功能——虽然直接指定的数组元素是int类型的整数,但系统会将它包装成Integer对象。

JTable表格数据是字符串

在默认情况下,JTable的表格数据、表格列标题全部是字符串内容,因此JTable会使用这些Object对象的toString()方法的返回值作为表格数据、表格列标题。

通过TableModel处理特殊的表格数据

如果需要特殊对待某些表格数据,例如把它们当成图标或其他类型的对象来处理,则可以通过特定的TableModel或指定自己的单元格绘制器来实现。

在默认情况下,JTable的所有单元格、列标题显示的全部是字符串内容。除此之外,通常应该将JTable对象放在JScrollPane容器中,由JScrollPaneJTable提供ViewPort

运行上面程序,会看到如图12.47所示的简单表格。

图12.47

  • 虽然生成如图12.47所示表格的代码非常简单,但这个表格已经表现出丰富的功能。该表格具有如下几个功能。
  • 当表格高度不足以显示所有的数据行时,该表格会自动显示滚动条。
  • 当把鼠标移动到两列之间的分界符时,鼠标形状会变成可调整大小的形状,表明用户可以自由调整表格列的大小。
  • 当在表格列上按下鼠标并拖动时,可以将表格的整列拖动到其他位置
  • 当单击某一个单元格时,系统会自动选中该单元格所在的行。
  • 当双击某一个单元格时,系统会自动进入该单元格的修改状态

设置JTable列宽调整方式 setAutoResizeMode方法

在上面的程序中,当拖动两列分界线来调整某列的列宽时,将看到该列后面的所有列的列宽都会发生相应的改变,但该列前面的所有列的列宽都不会发生改变,整个表格的宽度不会发生改变

setAutoResizeMode方法

JTable提供了一个setAutoResizeMode()方法来控制这种调整方式:

方法 描述
void setAutoResizeMode(int mode) Sets the table’s auto resize mode when the table is resized.

setAutoResizeMode方法的mode参数值

setAutoResizeMode方法mode参数值 描述
JTable.AUTO_RESIZE_OFF 关闭JTable的自动调整功能,当调整某一列的宽度时,其他列的宽度不会发生改变,只有表格的宽度会随之改变
JTable.AUTO_RESIZE_NEXT_COLUMN 只调整下一列的宽度,其他列及表格的宽度不会发生改变。
JTable.AUTO_RESIZE_SUBSEQUENT_COLUMNS 平均调整当前列后面所有列的宽度,当前列的前面所有列及表格的宽度都不会发生变化,这是默认的调整方式
JTable.AUTO_RESIZE_LAST_COLUMN 只调整最后一列的宽度,其他列及表格的宽度不会发生改变。
JTable.AUTO_RESIZE_ALL_COLUMNS 平均调整表格中所有列的宽度,表格的宽度不会发生改变。

JTable默认采用平均调整当前列后面所有列的宽度的方式,这种方式允许用户从左到右依次调整每列的宽度,以达到最好的显示效果。

尽量避免使用平均调整表格中所有列的宽度的方式,这种方式将会导致用户调整某列时,其余所有列都随之发生改变,从而使得用户很难把毎一列的宽度都调整到具有最好的显示效果。

TableColumn

如果需要精确控制每一列的宽度,则可通过TableColumn对象来实现。JTable使用TableColumn来表示表格中的每一列,JTable中表格列的所有属性,如最佳宽度、是否可调整宽度、最小和最大宽度等都保存在该TableColumn。此外,TableColumn还允许为该列指定特定的单元格绘制器和单元格编辑器(这些内容将在后面讲解)。

TableColumn对象方法

TableColumn具有如下方法。

TableColumn类方法 描述
void setMaxWidth(int maxWidth) 设置该列的最大宽度。如果指定的maxWidth小于该列的最小宽度,则maxWidth被设置成最小宽度。
void setMinWidth(int minWidth) 设置该列的最小宽度。
void setPreferredWidth(int preferredWidth) 设置该列的最佳宽度。
void setResizable(boolean isResizable) 设置是否可以调整该列的宽度。
void sizeWidthToFit() 调整该列的宽度,以适合其标题单元格的宽度。

设置是否选中整行

在默认情况下,当用户单击JTable的任意一个单元格时,系统默认会选中该单元格所在行的的x也就是说,JTable表格默认的选择单元是行。当然也可通过JTable提供的setRowSelectionAllowed()方来改变这种设置,如果为该方法传入false参数,则可以关闭这种每次选择一行的方式。

设置是否选中整列

除此之外,JTable还提供了一个setColumnSelectionAllowed()方法,该方法用于控制选择单元是否是列,如果为该方法传入true参数,则当用户单击某个单元格时,系统会选中该单元格所在的列。

选中单元格

如果同时调用setColumnSelectionAllowed(true)setRowSelectionAllowed(true)方法,则该表格的选择单元是单元格。

实际上,同时调用这两个方法相当于调用setCellSelectionEnabled(true)方法。与此相反,如果调用setCellSelectionEnabled(false)方法,则相当于同时调用setColumnSelectionAllowed(false)setRow Allowed(false)方法,即用户无法选中该表格的任何地方。

JTable的方法 描述
void setRowSelectionAllowed(boolean rowSelectionAllowed) Sets whether the rows in this model can be selected.
void setColumnSelectionAllowed(boolean columnSelectionAllowed) Sets whether the columns in this model can be selected.
void setCellSelectionEnabled(boolean cellSelectionEnabled) Sets whether this table allows both a column selection and a row selection to exist simultaneously.

ListSelectionModel

JListJTree类似的是,JTable使用了一个ListSelectionModel表示该表格的选择状态,程序可以通过ListSelectionModel来控制JTable的选择模式。

注意:保存JTable选择状态的model类就是ListselectionModel,这并不是笔误。

JTable的选择模式

JTable的选择模式有如下三种

ListselectionModel静态属性 描述
ListSelectionModel.MULTIPLE_INTERVAL_SELECTION 没有任何限制,可以选择表格中任何表格单元,这是默认的选择模式。通过ShiftCtrl辅助键的帮助可以选择多个表格单元
ListSelectionModel.SINGLE_INTERVAL_SELECTION 选择单个连续区域,该选项可以选择多个表格单元,但多个表格单元之间必须是连续的。通过Shift辅助键的帮助来选择连续区域。
ListSelectionModel.SINGLE_SELECTION 只能选择单个表格单元。

代码 改变JTable的选择模式

程序通常通过如下代码来改变JTable的选择模式。

1
2
//设置该表格只能选中单个表格单元
jTable.getSelectionModel().setSelectionMode(ListselectionMode.SINGLE_SELECTION);

程序

下面程序示范了如何控制每列的宽度、控制表格的宽度调整模式、改变表格的选择单元和表格的选择模式。

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 javax.swing.*;
import javax.swing.table.*;

public class AdjustingWidth
{
JFrame jf = new JFrame("调整表格列宽");
JMenuBar menuBar = new JMenuBar();
JMenu adjustModeMenu = new JMenu("调整方式");
JMenu selectUnitMenu = new JMenu("选择单元");
JMenu selectModeMenu = new JMenu("选择方式");
// 定义5个单选框按钮,用以控制表格的宽度调整方式
JRadioButtonMenuItem[] adjustModesItem = new JRadioButtonMenuItem[5];
// 定义3个单选框按钮,用以控制表格的选择方式
JRadioButtonMenuItem[] selectModesItem = new JRadioButtonMenuItem[3];
JCheckBoxMenuItem rowsItem = new JCheckBoxMenuItem("选择行");
JCheckBoxMenuItem columnsItem = new JCheckBoxMenuItem("选择列");
JCheckBoxMenuItem cellsItem = new JCheckBoxMenuItem("选择单元格");
ButtonGroup adjustBg = new ButtonGroup();
ButtonGroup selectBg = new ButtonGroup();
// 定义一个int类型的数组,用于保存表格所有的宽度调整方式
int[] adjustModes = new int[]{
JTable.AUTO_RESIZE_OFF
, JTable.AUTO_RESIZE_NEXT_COLUMN
, JTable.AUTO_RESIZE_SUBSEQUENT_COLUMNS
, JTable.AUTO_RESIZE_LAST_COLUMN
, JTable.AUTO_RESIZE_ALL_COLUMNS
};
int[] selectModes = new int[]{
ListSelectionModel.MULTIPLE_INTERVAL_SELECTION
, ListSelectionModel.SINGLE_INTERVAL_SELECTION
, ListSelectionModel.SINGLE_SELECTION
};
JTable table;
// 定义二维数组作为表格数据
Object[][] tableData =
{
new Object[]{"李清照" , 29 , "女"},
new Object[]{"苏格拉底", 56 , "男"},
new Object[]{"李白", 35 , "男"},
new Object[]{"弄玉", 18 , "女"},
new Object[]{"虎头" , 2 , "男"}
};
// 定义一维数据作为列标题
Object[] columnTitle = {"姓名" , "年龄" , "性别"};
public void init()
{
// 以二维数组和一维数组来创建一个JTable对象
table = new JTable(tableData , columnTitle);
// -----------为窗口安装设置表格调整方式的菜单-----------
adjustModesItem[0] = new JRadioButtonMenuItem("只调整表格");
adjustModesItem[1] = new JRadioButtonMenuItem("只调整下一列");
adjustModesItem[2] = new JRadioButtonMenuItem("平均调整余下列");
adjustModesItem[3] = new JRadioButtonMenuItem("只调整最后一列");
adjustModesItem[4] = new JRadioButtonMenuItem("平均调整所有列");
menuBar.add(adjustModeMenu);
for (int i = 0; i < adjustModesItem.length ; i++)
{
// 默认选中第三个菜单项,即对应表格默认的宽度调整方式
if (i == 2)
{
adjustModesItem[i].setSelected(true);
}
adjustBg.add(adjustModesItem[i]);
adjustModeMenu.add(adjustModesItem[i]);
final int index = i;
// 为设置调整方式的菜单项添加监听器
adjustModesItem[i].addActionListener(evt ->
{
// 如果当前菜单项处于选中状态,表格使用对应的调整方式
if (adjustModesItem[index].isSelected())
{
table.setAutoResizeMode(adjustModes[index]); //①
}
});
}
// -----------为窗口安装设置表格选择方式的菜单-----------
selectModesItem[0] = new JRadioButtonMenuItem("无限制");
selectModesItem[1] = new JRadioButtonMenuItem("单独的连续区");
selectModesItem[2] = new JRadioButtonMenuItem("单选");
menuBar.add(selectModeMenu);
for (int i = 0; i < selectModesItem.length ; i++)
{
// 默认选中第一个菜单项,即对应表格默认的选择方式
if (i == 0)
{
selectModesItem[i].setSelected(true);
}
selectBg.add(selectModesItem[i]);
selectModeMenu.add(selectModesItem[i]);
final int index = i;
// 为设置选择方式的菜单项添加监听器
selectModesItem[i].addActionListener(evt -> {
// 如果当前菜单项处于选中状态,表格使用对应的选择方式s
if (selectModesItem[index].isSelected())
{
table.getSelectionModel().setSelectionMode
(selectModes[index]); //②
}
});
}
menuBar.add(selectUnitMenu);
// -----为窗口安装设置表格选择单元的菜单-----
rowsItem.setSelected(table.getRowSelectionAllowed());
columnsItem.setSelected(table.getColumnSelectionAllowed());
cellsItem.setSelected(table.getCellSelectionEnabled());
rowsItem.addActionListener(event -> {
table.clearSelection();
// 如果该菜单项处于选中状态,设置表格的选择单元是行
table.setRowSelectionAllowed(rowsItem.isSelected());
// 如果选择行、选择列同时被选中,其实质是选择单元格
cellsItem.setSelected(table.getCellSelectionEnabled());
});
selectUnitMenu.add(rowsItem);
columnsItem.addActionListener(event -> {
table.clearSelection();
// 如果该菜单项处于选中状态,设置表格的选择单元是列
table.setColumnSelectionAllowed(columnsItem.isSelected());
// 如果选择行、选择列同时被选中,其实质是选择单元格
cellsItem.setSelected(table.getCellSelectionEnabled());
});
selectUnitMenu.add(columnsItem);
cellsItem.addActionListener(event -> {
table.clearSelection();
// 如果该菜单项处于选中状态,设置表格的选择单元是单元格
table.setCellSelectionEnabled(cellsItem.isSelected());
// 该选项的改变会同时影响选择行、选择列两个菜单
rowsItem.setSelected(table.getRowSelectionAllowed());
columnsItem.setSelected(table.getColumnSelectionAllowed());
});
selectUnitMenu.add(cellsItem);
jf.setJMenuBar(menuBar);
// 分别获取表格的三个表格列,并设置三列的最小宽度,最佳宽度和最大宽度
TableColumn nameColumn = table.getColumn(columnTitle[0]);
nameColumn.setMinWidth(40);
TableColumn ageColumn = table.getColumn(columnTitle[1]);
ageColumn.setPreferredWidth(50);
TableColumn genderColumn = table.getColumn(columnTitle[2]);
genderColumn.setMaxWidth(50);
// 将JTable对象放在JScrollPane中,并将该JScrollPane放在窗口中显示出来
jf.add(new JScrollPane(table));
jf.pack();
jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
jf.setVisible(true);
}
public static void main(String[] args)
{
new AdjustingWidth().init();
}
}

上面程序中的①号粗体字代码根据单选钮菜单来设置表格的宽度调整方式,②号粗体字代码根据单选钮菜单来设置表格的选择模式,最后一段粗体字代码通过JTablegetColumn()方法获取指定列,并分别设置三列的最佳、最大、最小宽度。如果选中“只调整表格”菜单项,并把第一列宽度拖大,将看到如图12.48所示的界面

上面程序中还有三段粗体字代码,分别用于为三个复选框菜单添加监听器,根据复选框菜单的选中状态来决定表格的选择单元。如果程序采用JTable默认的选择模式(无限制的选择模式),并设置表格的选择单元是单元格,则可看到如图12.49所示的界面。

12.10.6 实现TreeCellRenderer改变节点外观

这种方式是最灵活的方式,程序实现TreeCellRenderer接口时同样需要实现getTreeCellRendererComponent()方法,该方法可以返回任意类型的组件,该组件将作为JTree的节点。通过这种方式可以最大程度地改变JTree的节点外观。
与前面实现ListCellRenderer接口类似的是,本实例程序同样通过扩展JPanel来实现TreeCellRenderer,实现TreeCellRenderer的方式与前面实现ListCellRenderer的方式基本相似,所以读者将会看到一个完全不同的JTree

程序 实现TreeCellRenderer改变节点外观

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
import java.awt.*;
import javax.swing.*;
import javax.swing.tree.*;

public class CustomTreeNode {
JFrame jf = new JFrame("定制树的节点");
JTree tree;
// 定义几个初始节点
DefaultMutableTreeNode friends = new DefaultMutableTreeNode("我的好友");
DefaultMutableTreeNode qingzhao = new DefaultMutableTreeNode("李清照");
DefaultMutableTreeNode suge = new DefaultMutableTreeNode("苏格拉底");
DefaultMutableTreeNode libai = new DefaultMutableTreeNode("李白");
DefaultMutableTreeNode nongyu = new DefaultMutableTreeNode("弄玉");
DefaultMutableTreeNode hutou = new DefaultMutableTreeNode("虎头");

public void init() {
// 通过add()方法建立树节点之间的父子关系
friends.add(qingzhao);
friends.add(suge);
friends.add(libai);
friends.add(nongyu);
friends.add(hutou);
// 以根节点创建树
tree = new JTree(friends);
// 设置是否显示根节点的“展开/折叠”图标,默认是false
tree.setShowsRootHandles(true);
// 设置节点是否可见,默认是true
tree.setRootVisible(true);
// 设置使用定制的节点绘制器
tree.setCellRenderer(new ImageCellRenderer());
jf.add(new JScrollPane(tree));
jf.pack();
jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
jf.setVisible(true);
}

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

// 实现自己的节点绘制器
class ImageCellRenderer extends JPanel implements TreeCellRenderer {
private ImageIcon icon;
private String name;
// 定义绘制单元格时的背景色
private Color background;
// 定义绘制单元格时的前景色
private Color foreground;

public Component getTreeCellRendererComponent(JTree tree, Object value, boolean sel, boolean expanded, boolean leaf,
int row, boolean hasFocus) {
icon = new ImageIcon("icon/" + value + ".gif");
name = value.toString();
background = hasFocus ? new Color(140, 200, 235) : new Color(255, 255, 255);
foreground = hasFocus ? new Color(255, 255, 3) : new Color(0, 0, 0);
// 返回该JPanel对象作为单元格绘制器
return this;
}

// 重写paintComponent方法,改变JPanel的外观
public void paintComponent(Graphics g) {
int imageWidth = icon.getImage().getWidth(null);
int imageHeight = icon.getImage().getHeight(null);
g.setColor(background);
g.fillRect(0, 0, getWidth(), getHeight());
g.setColor(foreground);
// 绘制好友图标
g.drawImage(icon.getImage(), getWidth() / 2 - imageWidth / 2, 10, null);
g.setFont(new Font("SansSerif", Font.BOLD, 18));
// 绘制好友用户名
g.drawString(name, getWidth() / 2 - name.length() * 10, imageHeight + 30);
}

// 通过该方法来设置该ImageCellRenderer的最佳大小
public Dimension getPreferredSize() {
return new Dimension(80, 80);
}
}

上面程序中的粗体字代码设置JTree对象使用定制的节点绘制器:ImageCellRenderer,该节点绘制器实现了TreeCellRenderer接口的getTreeCellRendererComponent()方法,该方法返回thi,也就是一个特殊的JPanel对象,这个特殊的JPanel重写了paintComponent()方法,重新绘制了JPanel的外观—根据节点数据来绘制图标和文本。运行上面程序,会看到如图12.46所示的树。


这看上去似乎不太像一棵树,但可从每个节点前的连接线、表示节点的展开/折叠的图标中看出这依然是一棵树。

12.10.5 扩展DefaultTreeCellRenderer改变节点外观

DefaultTreeCellRenderer实现类实现了TreeCellRenderer接口,该接口里只有一个负责绘制JTree节点的
getTreeCellRendererComponent()方法:

方法 描述
Component getTreeCellRendererComponent(JTree tree, Object value, boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus) Sets the value of the current tree cell to value.

如果读者还记得前面介绍的绘制JList的列表项外观的内容,应该对该方法非常熟悉,与Listcellrenderer接口类似的是,getTreeCellRendererComponent()方法返回一个Component对象,该对象就是JTree的节点组件。
DefaultTreeCellRenderer类继承了JLabel,实现getTreeCellRendererComponent方法时返回this,即返回一个特殊的JLabel对象。如果需要根据节点内容来改变节点的外观,则可以再次扩展DefaultTreeCellRenderer类,并再次重写它提供的getTreeCellRendererComponent方法。

程序 数据库对象导航树

下面程序模拟了一个数据库对象导航树,程序可以根据节点的类型来绘制节点的图标。在本程序中为了给每个节点指定节点类型,程序不再使用String作为节点数据,而是使用NodeData来封装节点数据,并重写了NodeDatatoString方法。

使用Object类型的对象来创建TreeNode对象时,DefaultTreeCellRenderer默认使用该对象的toString方法返回的字符串作为该节点的标签。

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
import java.awt.*;
import javax.swing.*;
import javax.swing.tree.*;

public class ExtendsDefaultTreeCellRenderer {
JFrame jf = new JFrame("根据节点类型定义图标");
JTree tree;
// 定义几个初始节点
DefaultMutableTreeNode root = new DefaultMutableTreeNode(new NodeData(DBObjectType.ROOT, "数据库导航"));
DefaultMutableTreeNode salaryDb = new DefaultMutableTreeNode(new NodeData(DBObjectType.DATABASE, "公司工资数据库"));
DefaultMutableTreeNode customerDb = new DefaultMutableTreeNode(new NodeData(DBObjectType.DATABASE, "公司客户数据库"));
// 定义salaryDb的两个子节点
DefaultMutableTreeNode employee = new DefaultMutableTreeNode(new NodeData(DBObjectType.TABLE, "员工表"));
DefaultMutableTreeNode attend = new DefaultMutableTreeNode(new NodeData(DBObjectType.TABLE, "考勤表"));
// 定义customerDb的一个子节点
DefaultMutableTreeNode contact = new DefaultMutableTreeNode(new NodeData(DBObjectType.TABLE, "联系方式表"));
// 定义employee的三个子节点
DefaultMutableTreeNode id = new DefaultMutableTreeNode(new NodeData(DBObjectType.INDEX, "员工ID"));
DefaultMutableTreeNode name = new DefaultMutableTreeNode(new NodeData(DBObjectType.COLUMN, "姓名"));
DefaultMutableTreeNode gender = new DefaultMutableTreeNode(new NodeData(DBObjectType.COLUMN, "性别"));

public void init() {
// 通过add()方法建立树节点之间的父子关系
root.add(salaryDb);
root.add(customerDb);
salaryDb.add(employee);
salaryDb.add(attend);
customerDb.add(contact);
employee.add(id);
employee.add(name);
employee.add(gender);
// 以根节点创建树
tree = new JTree(root);
// 设置该JTree使用自定义的节点绘制器
tree.setCellRenderer(new MyRenderer());
// 设置是否显示根节点的“展开/折叠”图标,默认是false
tree.setShowsRootHandles(true);
// 设置节点是否可见,默认是true
tree.setRootVisible(true);
try {
// 设置使用Windows风格外观
UIManager.setLookAndFeel("com.sun.java.swing.plaf." + "windows.WindowsLookAndFeel");
} catch (Exception ex) {
}
// 更新JTree的UI外观
SwingUtilities.updateComponentTreeUI(tree);
jf.add(new JScrollPane(tree));
jf.pack();
jf.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
jf.setVisible(true);
}

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

// 定义一个NodeData类,用于封装节点数据
class NodeData {
public int nodeType;
public String nodeData;

public NodeData(int nodeType, String nodeData) {
this.nodeType = nodeType;
this.nodeData = nodeData;
}

public String toString() {
return nodeData;
}
}

// 定义一个接口,该接口里包含数据库对象类型的常量
interface DBObjectType {
int ROOT = 0;
int DATABASE = 1;
int TABLE = 2;
int COLUMN = 3;
int INDEX = 4;
}

class MyRenderer extends DefaultTreeCellRenderer {
// 初始化5个图标
ImageIcon rootIcon = new ImageIcon("icon/root.gif");
ImageIcon databaseIcon = new ImageIcon("icon/database.gif");
ImageIcon tableIcon = new ImageIcon("icon/table.gif");
ImageIcon columnIcon = new ImageIcon("icon/column.gif");
ImageIcon indexIcon = new ImageIcon("icon/index.gif");

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);
DefaultMutableTreeNode node = (DefaultMutableTreeNode) value;
NodeData data = (NodeData) node.getUserObject();
// 根据数据节点里的nodeType数据决定节点图标
ImageIcon icon = null;
switch (data.nodeType) {
case DBObjectType.ROOT:
icon = rootIcon;
break;
case DBObjectType.DATABASE:
icon = databaseIcon;
break;
case DBObjectType.TABLE:
icon = tableIcon;
break;
case DBObjectType.COLUMN:
icon = columnIcon;
break;
case DBObjectType.INDEX:
icon = indexIcon;
break;
}
// 改变图标
this.setIcon(icon);
return this;
}
}

上面的程序中码强制JTree使用自定义的节点绘制器:MyRenderer,该节点绘制器继承了DefaultTreeCellRenderer类,并重写了getTreeCellRendererComponent()方法。该节点绘制器重写该节点时根据节点的nodeType属性改变其图标。运行上面程序,会看到如图12.45所示的效果。

从图12.45中可以看出,JTree中表示节点展开、折叠的图标已经改为了“+”和“-”,这是因为本程序强制JTree使用了Windows风格。