Swing之JTable详解
https://how2j.cn/k/gui/gui-table/410.html#nowhere
JAVA 中级 / 图形界面 / 图形界面系列教材 (十)- Swing 使用 JTable详解
示例
1
:
基本表格
示例
2
:
JScrollPane
示例
3
:
列宽
示例
4
:
TableModel
示例
5
:
进一步理解TableModel
示例
6
:
TableModel 与DAO结合
示例
7
:
TableSelectionModel
示例
8
:
更新Table
示例
9
:
输入项验证
示例
10
:
选中指定行
1. 一维数组: String[]columnNames 表示表格的标题
2. 二维数组: String[][] heros 表格中的内容
默认情况下,表格的标题是不会显示出来了,除非使用了JScrollPane
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
|
package gui; import java.awt.BorderLayout; import javax.swing.JFrame; import javax.swing.JTable; public class TestGUI { public static void main(String[] args) { JFrame f = new JFrame( "LoL" ); f.setSize( 400 , 300 ); f.setLocation( 200 , 200 ); f.setLayout( new BorderLayout()); // 表格上的title String[] columnNames = new String[] { "id" , "name" , "hp" , "damage" }; // 表格中的内容,是一个二维数组 String[][] heros = new String[][] { { "1" , "盖伦" , "616" , "100" }, { "2" , "提莫" , "512" , "102" }, { "3" , "奎因" , "832" , "200" } }; JTable t = new JTable(heros, columnNames); f.add(t, BorderLayout.CENTER); f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); f.setVisible( true ); } } |
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
|
package gui; import java.awt.BorderLayout; import javax.swing.JFrame; import javax.swing.JScrollPane; import javax.swing.JTable; public class TestGUI { public static void main(String[] args) { JFrame f = new JFrame( "LoL" ); f.setSize( 400 , 300 ); f.setLocation( 200 , 200 ); f.setLayout( new BorderLayout()); String[] columnNames = new String[] { "id" , "name" , "hp" , "damage" }; String[][] heros = new String[][] { { "1" , "盖伦" , "616" , "100" }, { "2" , "提莫" , "512" , "102" }, { "3" , "奎因" , "832" , "200" } }; JTable t = new JTable(heros, columnNames); // 根据t创建 JScrollPane JScrollPane sp = new JScrollPane(t); //或则创建一个空的JScrollPane,再通过setViewportView把table放在JScrollPane中 // JScrollPane sp = new JScrollPane(t); // sp.setViewportView(t); // 把sp而非JTable加入到JFrame上, f.add(sp, BorderLayout.CENTER); f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); f.setVisible( true ); } } |
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
|
package gui; import java.awt.BorderLayout; import javax.swing.JFrame; import javax.swing.JScrollPane; import javax.swing.JTable; public class TestGUI { public static void main(String[] args) { JFrame f = new JFrame( "LoL" ); f.setSize( 400 , 300 ); f.setLocation( 200 , 200 ); f.setLayout( new BorderLayout()); String[] columnNames = new String[] { "id" , "name" , "hp" , "damage" }; String[][] heros = new String[][] { { "1" , "盖伦" , "616" , "100" }, { "2" , "提莫" , "512" , "102" }, { "3" , "奎因" , "832" , "200" } }; JTable t = new JTable(heros, columnNames); JScrollPane sp = new JScrollPane(t); // 设置列宽度 t.getColumnModel().getColumn( 0 ).setPreferredWidth( 10 ); f.add(sp, BorderLayout.CENTER); f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); f.setVisible( true ); } } |
使用TableModel的方式存放Table需要显示的数据
HeroTableModel 继承AbstractTableModel ,进而实现了接口TableModel
在HeroTableModel 中提供一个table显示需要的所有信息
1. getRowCount 返回一共有多少行
2. getColumnCount 返回一共有多少列
3. getColumnName 每一列的名字
4. isCellEditable 单元格是否可以修改
5. getValueAt 每一个单元格里的值
当图形界面需要渲染第一个单元格的数据的时候,就会调用方法TabelModel的getValueAt(0,0) ,把返回值拿到并显示
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
|
package gui; import javax.swing.table.AbstractTableModel; public class HeroTableModel extends AbstractTableModel { String[] columnNames = new String[] { "id" , "name" , "hp" , "damage" }; String[][] heros = new String[][] { { "1" , "盖伦" , "616" , "100" }, { "2" , "提莫" , "512" , "102" }, { "3" , "奎因" , "832" , "200" } }; // 返回一共有多少行 public int getRowCount() { // TODO Auto-generated method stub return heros.length; } // 返回一共有多少列 public int getColumnCount() { // TODO Auto-generated method stub return columnNames.length; } // 获取每一列的名称 public String getColumnName( int columnIndex) { // TODO Auto-generated method stub return columnNames[columnIndex]; } // 单元格是否可以修改 public boolean isCellEditable( int rowIndex, int columnIndex) { return false ; } // 每一个单元格里的值 public Object getValueAt( int rowIndex, int columnIndex) { // TODO Auto-generated method stub return heros[rowIndex][columnIndex]; } } |
这样的风格创建一个JTable的
所以实际上调用的是如下的构造方法:
如图所示,在JTable的的源代码中,它就会根据rowData和columnNames去创建一个TableModel对象
DAO使用HeroDAO
在TableModel中,使用从DAO返回的List作为TableModel的数据
只需要修改HeroTableModel,无需修改TestGUI。 这正好演绎了Model设计思想中的数据分离的好处,当只需要数据发生变化的时候,修改Model即可,界面GUI部分,不需要做任何改动
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
|
package gui; import java.util.List; import javax.swing.table.AbstractTableModel; import jdbc.HeroDAO; import charactor.Hero; public class HeroTableModel extends AbstractTableModel { String[] columnNames = new String[] { "id" , "name" , "hp" , "damage" }; // 使用从DAO返回的List作为TableModel的数据 public List<Hero> heros = new HeroDAO().list(); // heros.size返回一共有多少行 public int getRowCount() { // TODO Auto-generated method stub return heros.size(); } public int getColumnCount() { // TODO Auto-generated method stub return columnNames.length; } public String getColumnName( int columnIndex) { // TODO Auto-generated method stub return columnNames[columnIndex]; } public boolean isCellEditable( int rowIndex, int columnIndex) { return false ; } // 先通过heros.get(rowIndex)获取行对应的Hero对象 // 然后根据columnIndex返回对应的属性 public Object getValueAt( int rowIndex, int columnIndex) { Hero h = heros.get(rowIndex); if ( 0 == columnIndex) return h.id; if ( 1 == columnIndex) return h.name; if ( 2 == columnIndex) return h.hp; if ( 3 == columnIndex) return h.damage; return null ; } } |
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
|
package gui; import java.awt.BorderLayout; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JTable; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; import charactor.Hero; public class TestGUI { public static void main(String[] args) { JFrame f = new JFrame( "LoL" ); f.setSize( 400 , 300 ); f.setLocation( 200 , 200 ); f.setLayout( new BorderLayout()); final HeroTableModel htm = new HeroTableModel(); final JTable t = new JTable(htm); // 准备一个Panel上面放一个Label用于显示哪条被选中了 JPanel p = new JPanel(); final JLabel l = new JLabel( "暂时未选中条目" ); p.add(l); JScrollPane sp = new JScrollPane(t); // 使用selection监听器来监听table的哪个条目被选中 t.getSelectionModel().addListSelectionListener( new ListSelectionListener() { // 当选择了某一行的时候触发该事件 public void valueChanged(ListSelectionEvent e) { // 获取哪一行被选中了 int row = t.getSelectedRow(); // 根据选中的行,到HeroTableModel中获取对应的对象 Hero h = htm.heros.get(row); // 更新标签内容 l.setText( "当前选中的英雄是: " + h.name); } }); f.add(p, BorderLayout.NORTH); f.add(sp, BorderLayout.CENTER); f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); f.setVisible( true ); } } |
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
|
package gui; import java.awt.BorderLayout; import java.awt.Dimension; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import javax.swing.JButton; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JTable; import javax.swing.JTextField; import jdbc.HeroDAO; import charactor.Hero; public class TestGUI { public static void main(String[] args) { JFrame f = new JFrame( "LoL" ); f.setSize( 400 , 300 ); f.setLocation( 200 , 200 ); f.setLayout( new BorderLayout()); final HeroTableModel htm = new HeroTableModel(); final JTable t = new JTable(htm); // 增加 一个 panel用于放置名称,血量输入框和增加 按钮 JPanel p = new JPanel(); final JLabel lName = new JLabel( "名称" ); final JTextField tfName = new JTextField( "" ); final JLabel lHp = new JLabel( "血量" ); final JTextField tfHp = new JTextField( "" ); JButton bAdd = new JButton( "增加" ); tfName.setPreferredSize( new Dimension( 80 , 30 )); tfHp.setPreferredSize( new Dimension( 80 , 30 )); p.add(lName); p.add(tfName); p.add(lHp); p.add(tfHp); p.add(bAdd); // 为增加按钮添加监听 bAdd.addActionListener( new ActionListener() { @Override public void actionPerformed(ActionEvent e) { HeroDAO dao = new HeroDAO(); // 根据输入框数据创建一个Hero对象 Hero h = new Hero(); h.name = tfName.getText(); h.hp = Integer.parseInt(tfHp.getText()); // 通过dao把该对象加入到数据库 dao.add(h); // 通过dao更新tablemodel中的数据 htm.heros = dao.list(); // 调用JTable的updateUI,刷新界面。 // 刷新界面的时候,会到tablemodel中去取最新的数据 // 就能看到新加进去的数据了 t.updateUI(); } }); JScrollPane sp = new JScrollPane(t); f.add(p, BorderLayout.NORTH); f.add(sp, BorderLayout.CENTER); f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); f.setVisible( true ); } } |
“感觉上” 界面就卡住了。 这是不友好的人机交互行为。
所以需要加上输入项的验证,如果输入的数据不合格,应该弹出对话框提示用户具体原因。
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
|
package gui; import java.awt.BorderLayout; import java.awt.Dimension; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import javax.swing.JButton; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JTable; import javax.swing.JTextField; import jdbc.HeroDAO; import charactor.Hero; public class TestGUI { public static void main(String[] args) { final JFrame f = new JFrame( "LoL" ); f.setSize( 400 , 300 ); f.setLocation( 200 , 200 ); f.setLayout( new BorderLayout()); final HeroTableModel htm = new HeroTableModel(); final JTable t = new JTable(htm); JPanel p = new JPanel(); final JLabel lName = new JLabel( "名称" ); final JTextField tfName = new JTextField( "" ); final JLabel lHp = new JLabel( "血量" ); final JTextField tfHp = new JTextField( "" ); JButton bAdd = new JButton( "增加" ); tfName.setPreferredSize( new Dimension( 80 , 30 )); tfHp.setPreferredSize( new Dimension( 80 , 30 )); p.add(lName); p.add(tfName); p.add(lHp); p.add(tfHp); p.add(bAdd); bAdd.addActionListener( new ActionListener() { @Override public void actionPerformed(ActionEvent e) { HeroDAO dao = new HeroDAO(); Hero h = new Hero(); String name = tfName.getText(); // 通过name长度判断 名称是否为空 if (name.length() == 0 ) { // 弹出对话框提示用户 JOptionPane.showMessageDialog(f, "名称不能为空" ); // 名称输入框获取焦点 tfName.grabFocus(); return ; } String hp = tfHp.getText().trim(); try { // 把hp转换为浮点型,如果出现异常NumberFormatException表示不是浮点型格式 Float.parseFloat(hp); } catch (NumberFormatException e1) { JOptionPane.showMessageDialog(f, "血量只能是小数 " ); tfHp.grabFocus(); return ; } h.name = name; h.hp = Float.parseFloat(hp); dao.add(h); htm.heros = dao.list(); t.updateUI(); } }); JScrollPane sp = new JScrollPane(t); f.add(p, BorderLayout.NORTH); f.add(sp, BorderLayout.CENTER); f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); f.setVisible( true ); } } |
2. 增加数据后,也应该选中新增的这一条
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
|
package gui; import java.awt.BorderLayout; import java.awt.Dimension; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import javax.swing.JButton; import javax.swing.JFrame; import javax.swing.JLabel; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JTable; import javax.swing.JTextField; import javax.swing.ListSelectionModel; import jdbc.HeroDAO; import charactor.Hero; public class TestGUI { public static void main(String[] args) { final JFrame f = new JFrame( "LoL" ); f.setSize( 400 , 300 ); f.setLocation( 200 , 200 ); f.setLayout( new BorderLayout()); final HeroTableModel htm = new HeroTableModel(); final JTable t = new JTable(htm); // 设置选择模式为 只能选中一行 t.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); // 选中第一行 (基本0) t.getSelectionModel().setSelectionInterval( 0 , 0 ); JPanel p = new JPanel(); final JLabel lName = new JLabel( "名称" ); final JTextField tfName = new JTextField( "" ); final JLabel lHp = new JLabel( "血量" ); final JTextField tfHp = new JTextField( "" ); JButton bAdd = new JButton( "增加" ); tfName.setPreferredSize( new Dimension( 80 , 30 )); tfHp.setPreferredSize( new Dimension( 80 , 30 )); p.add(lName); p.add(tfName); p.add(lHp); p.add(tfHp); p.add(bAdd); bAdd.addActionListener( new ActionListener() { @Override public void actionPerformed(ActionEvent e) { HeroDAO dao = new HeroDAO(); Hero h = new Hero(); String name = tfName.getText(); if (name.length() == 0 ) { JOptionPane.showMessageDialog(f, "名称不能为空" ); tfName.grabFocus(); return ; } String hp = tfHp.getText().trim(); try { Float.parseFloat(hp); } catch (NumberFormatException e1) { JOptionPane.showMessageDialog(f, "血量只能是小数 " ); tfHp.grabFocus(); return ; } h.name = name; h.hp = Float.parseFloat(hp); dao.add(h); htm.heros = dao.list(); t.updateUI(); // 选中 第一行 ,因为 DAO是按照 ID倒排序查询,所以第一行就是新加入的数据 t.getSelectionModel().setSelectionInterval( 0 , 0 ); } }); JScrollPane sp = new JScrollPane(t); f.add(p, BorderLayout.NORTH); f.add(sp, BorderLayout.CENTER); f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); f.setVisible( true ); } } |
本段结束,下面还有另一篇,来源不同,内容不同
————————————————————
如何使用 Swing 组件 JTable
https://my.oschina.net/bairrfhoinn/blog/166850
https://blog.csdn.net/dudefu011/article/details/47320635
- 创建壹個简单的表格
- 增加壹個表格到容器中
- 设置和更改列宽度
- 用户的选择
- 创建壹個 Table Model
- 监控数据的更新
- 发起数据更新事件
- 相关内容:编辑器和渲染器
- 使用自定义的渲染器
- 为单元格指定工具提示信息
- 为表头指定工具提示信息
- 排序与过滤数据
- 使用 ComboBox 下拉列表作为编辑器
- 使用其它编辑器
- 使用编辑器验证用户输入的文本信息的合法性
- 打印表格内容
使用 JTable 类你可以显示表中的数据,也可以允许用户编辑表中的数据,JTable 不包含数据,也不缓存数据,它只是你的数据的壹個视图,下图是一个在滚动窗格中显示的一个典型的表格:
本节的其余部分告诉你如何完成一些表格相关的常用任务。
创建壹個简单的表格
1、点击 Launch 可以使用 Java Web Start 运行 SimpleTableDemo ,或者由你自己来编译运行 SimpleTableDemo.java ;
2、点击包含 "Snowboarding"
的单元格,这壹行都会被选中,说明你已经选择了 Kathy Smith 的数据。壹個特殊的高亮显示说明 "Snowboarding"
单元格是可以编辑的。通常来讲,你可以通过双击单元格来编辑该单元的文本信息。
3、将鼠标指向 "First
Name",现在点击鼠标并且拖到右边,如同你所看到的壹样,用户可以重新排列表格中的列名称。
4、将鼠标指向列头的右边缘,现在点击鼠标并且左右拖动,该列宽度发生了改变,并且其它列填充了多出来的空间。
5、调整包含表格的窗口大小,以便它比实际展示需要的空间更大壹些。所有的表格元素变宽了,水平的填充了多出来的额外空间。
SimpleTableDemo.java 中的表格在 String 数组中声明列名称:
String[] columnNames = {"First Name", "Last Name", "Sport", "# of Years", "Vegetarian"};
它的数据被初始化并且存储在壹個二维的对象数组中:
Object[][] data = {
{"Kathy", "Smith", "Snowboarding", new Integer(5), new Boolean(false)},
{"John", "Doe", "Rowing", new Integer(3), new Boolean(true)},
{"Sue", "Black", "Knitting", new Integer(2), new Boolean(false)},
{"Jane", "White", "Speed reading", new Integer(20), new Boolean(true)},
{"Joe", "Brown", "Pool", new Integer(10), new Boolean(false)}
};
然后表格对象使用 data 和 columnNames 完成构建。
JTable table = new JTable(data, columnNames);
壹共有两個 JTable 构造方法直接接受数据,SimpleTableDemo 使用第壹种:
JTable(Object[][] rowData, Object[] columnNames)
JTable(Vector rowData, Vector columnNames)
这些构造方法的优点就是它们非常易用,然而,这些构造方法也有它的缺点:
他们设置每個单元格可编辑;
他们以相同的方式对待所有的数据类型(全部识别为 String)。比如说,如果表中的某壹列包含 Boolean
数据,表格可以将该数据显示为壹個复选框。因此,如果你使用上述两個构造方法中任意壹個,你的 Boolean 数据都会被显示为字符串。你可以在上图中的
Vegetarian 列中查看这种差异。
他们要求你把所有的数据都存储在壹個二维数组或者 Vector
对象中,但是这样做对于某些情况下的数据是不合适的。比如说,如果你从数据库中初始化壹系列数据,你可能希望直接查询对象以获取结果,而不是把所有的结果都复制到数组或者
Vector 中。
如果你希望绕过这些限制,你需要实现自己的 Table Model,可以参考 创建壹個 Table Model。
增加壹個表格到容器中
如下是壹段典型的创建壹個卷动窗格作为表格的容器的代码:
JScrollPane scrollPane = new JScrollPane(table);
table.setFillsViewportHeight(true);
上面两行代码做了如下事情:调用 JScrollPane 构造方法并传入 table 对象作为其参数,这句话为 table 对象创建了壹個滚动窗格,table 被自动添加到该容器中;
调用 JTable.setFillsViewportHeight()
设置了fillsViewportHeight 属性,设置表格是否需要大到足以填充封闭视图的全部高度。当这個属性为 true
时,表格对象使用容器的所有高度,哪怕没有足够多的行来使用垂直的空间,这样做可以更加容易的使用表格作为拖拽的目标。
滚动空格自动的将表头放在了顶端,当表格中的数据滚动时仍然可以看到列名显示在顶上。
如果你使用表格却不包含滚动空格,那么你需要获取表头组件并且自己来放置它,比如说像下面这样:
container.setLayout(new BorderLayout());
container.add(table.getTableHeader(), BorderLayout.PAGE_START);
container.add(table, BorderLayout.CENTER);
设置和更改列宽度
默认情况下,表中所有的列开始时都是等宽的,并且所有的列会自动填充表格的整個宽度。当表格变宽或者变窄(当用户调整包含表格的窗口时会发生),所有的列宽会自动适应变化。
当用户通过拖动列的右边缘来调整列宽度时,其它列必须改变宽度,或者表格的宽度也必须改变。默认情况下,表格的宽度会保持原样,所有拖拽点右左边的列会调整以适应右边的空间增大,所有拖拽点左边的列会调整以适应左边的空间减少。
如果希望自定义初始的列宽,对于你的表格中的每壹列,你可以调用
setPreferredWidth 方法来进行设置。这会设置推荐宽度和他们的近似相对宽度。比如说:增加如下代码到 SimpleTableDemo
会让第三列比其它列要宽壹些。
TableColumn column = null;
for (int i = 0; i < 5; i++) {
column = table.getColumnModel().getColumn(i);
if (i == 2) {
column.setPreferredWidth(100); //第三列宽壹些
} else {
column.setPreferredWidth(50);
}
}
如同上述代码显示的壹样,表格的每壹列都由壹個 TableColumn 对象表示。TableColumn 为列宽度的最小值,推荐值,最大值提供了 getter 和 setter 方法,同样也提供了获取当前列宽度的方法。比如说,基于壹個近似的空间设置需要绘制的单元格元素的宽度,可以参考 TableRenderDemo.java 的 initColumnSizes() 方法。
当用户明确的调整宽度时,
列的首选宽度设置为用户指定的尺寸,同时变成列的当前宽度。然而,当表格自己调整时(通常因为窗口调整了)列的推荐宽度则不会改变,存在的首选宽度用于计算新的列宽以填充可用的空间。
你可以通过调用 setAutoResizeMode(int) 方法改变表格的宽度调整行为。
用户的选择
在这個默认配置下,表格支持选择壹行或者多行,用户可以选择连续的几行。最后一个单元格指示的用户得到一个特殊的迹象,在金属的视觉上看,这個元素超出了轮廓,它有时被称作焦点元素或者当前元素,用户使用鼠标或者键盘来做出选择,具体描述见下表:
操作 | 鼠标动作 | 键盘动作 |
选择单行 | 单击 | 上下光标键 |
延伸的连续选择 | Shift + 单击 或者在表格行上拖动 | Shift + 上下光标键 |
添加行选择/切换行选择 | Ctrl + 单击 | 移动控制向上箭头或下箭头选择控制铅,然后使用空格键添加到选择或控制,空格键切换行选择。 |
要查看选择行是如何执行的,请点击
Launch 以使用 Java™ Web Start 来运行 TableSelectionDemo.java,或者由你自己来编译运行这個 例子。
这個样例程序展示了相似的表格,允许用户操纵特定的 JTable 选项。它还有壹個文本面板记录选择事件。在下方的截图中,壹個用户运行程序,单击第壹行,然后 Ctrl + 单击第三行,注意点击的最后壹個单元格周围的轮廓,这就是突出率先选择的金属外观和感觉。
在"Selection Mode"下方有几個单选按钮,点击标签是"Single Selection"的按钮。现在你可以每次只选择壹行了。如果你点击标签是"Single Interval Selection"的单选按钮,你可以选择连续的壹组行记录。所有的"Selection Mode"下方的单选按钮都调用了方法 JTable.setSelectionMode 这個方法只接受壹個参数,且必须是类 javax.swing.ListSelectionModel 中的常量值之壹,如下所示:MULTIPLE_INTERVAL_SELECTION,SINGLE_INTERVAL_SELECTION 还有 SINGLE_SELECTION。
回到 TableSelectionDemo,注意在"Selection Options"下方的三個复选框,每個复选框控制壹個 boolean 型的由 JTable 定义的绑定变量:
"行选择" 控制行选择,对应的有两個方法 setRowSelectionAllowed() 和
getRowSelectionAllowed()。当这個绑定属性为 true,并且 columnSelectionAllowed 的属性值为 false
时,用户可以选择行记录;
"列选择" 控制列选择,对应的有两個方法 setColumnSelectionAllowed() 和
getColumnSelectionAllowed()。当这個绑定属性为 true,并且 rowSelectionAllowed 绑定属性为 false
时,用户可以选择列记录;
"单元格选择" 控制单元格选择,对应的有两個方法 setCellSelectionEnabled() 和
getCellSelectionEnabled()。当这個绑定属性为 true 时,用户可以选择单個单元格或者矩形块状的单元格;
注意:JTable
使用非常简单的选择概念管理行和列的交叉点,
它不是设计来处理完全独立的单元格选择的。
如果你清除所有的三個复选框,将三個绑定属性设置为 false,那麽就不会有选择。只有高亮元素显示。
你可能会注意到在间隔选择模式下 "Cell
Selection"
复选框被禁用。这是因为在演示中,不支持这种模式下的单元格选择。你可以在间隔选择模式下指定单元格的选择,但是结果集是壹個不会产生任何有效选择的表格。
你同样可能注意到更改任意三個选项都会影响到其它选项,这是因为同时允许行选择和列选择和允许单元格选择完全壹样,JTable 自动更新了三個绑定变量以保持它们的壹致性。
注意:设置 cellSelectionEnabled 到某個值会对 rowSelectionEnabled 和 columnSelectionEnabled 有边缘影响,它们也会被设置为这個值。同时设置 rowSelectionEnabled 和 columnSelectionEnabled 到某個值同样对 cellSelectionEnabled 有边缘影响,它也会被设置为那個值。设置 rowSelectionEnabled 和 columnSelectionEnabled 为不同的值会对 cellSelectionEnabled 有边缘影响,它会被设置为 false。
为了获取当前选择,使用 JTable.getSelectedRows 会返回选择的所有行下标,使用 JTable.getSelectedColumns 会返回所有的列下标。 要检索率先选择的坐标,参照表和表的列模型的选择模型。下面的代码格式化包含率先选择的行和列的字符串:
String.format("Lead Selection: %d, %d. ", table.getSelectionModel().getLeadSelectionIndex(), table.getColumnModel().getSelectionModel().getLeadSelectionIndex());
使用选择功能生成壹系列的事件。请参考 编写事件监听器 课程的 如何编写壹系列选择事件监听器 这壹节。
注意:选择数据实际描述为在视图中选择单元格 (表格数据就像它排序或者过滤之后显示出来的壹样) 而不是选择表格的 Model。这個区别不会影响你,除非你查看重新排列的数据,包括排序,过滤或者用户操作过的行。在这种情况下,你必须使用在排序和过滤中提到的转换方法转换选择坐标。
创建壹個 Table Model
每個表对象使用壹個 table model 对象来管理实际的表数据。table model 对象必须实现 TableModel 接口。如果开发人员不提供 table model 对象的定义,那么 JTable 会自动创建壹個 DefaultTableModel 类的实例,这种关系如下图所示。
用 SimpleTableDemo 作为 table model 构造 JTable 表格的示例代码如下:
new AbstractTableModel() {
public String getColumnName(int col) {
return columnNames[col].toString();
}
public int getRowCount() { return rowData.length; }
public int getColumnCount() { return columnNames.length; }
public Object getValueAt(int row, int col) {
return rowData[row][col];
}
public boolean isCellEditable(int row, int col)
{ return true; }
public void setValueAt(Object value, int row, int col) {
rowData[row][col] = value;
fireTableCellUpdated(row, col);
}
}
如同上述代码所示,实现壹個 table model 可以很简单。通常来讲,你可以在 AbstractTableModel 的子类里来实现你自己的 Table Model。你的 model 可能把数据保存在 Array,Vector 或者哈希表中,或者它也可以从外部源获取数据,比如说从数据库获取数据。它甚至可以在运行期间生成表数据。
这個表格与 SimpleTableDemo 在如下几個地方存在不同点:
1、TableDemo 的自定义 table model
哪怕很简单,它也可以决定数据的类型,帮助 JTable 以更好的格式展示数据。从另壹方面讲,SimpleTableDemo 自动创建的 table model
并不知道列 # of Years 包含靠右对齐的数字,并且具有特殊的格式。它不知道列 Vegerarian 包含布尔型值,可以将其展示成复选框。
2、在
TableDemo 中实现的自定义 table model 不允许你编辑姓名列,然而它允许你编辑其它列。在 SimpleTableDemo
中,所有的元素都是可以编辑的。
可以看到来自 TableDemo.java 的如下代码和来自 SimpleTableDemo.java 的不壹样。黑体字显示创建表格的 model 与为创建 SimpleTableDemo 而自动生成的 table model 不壹样。
public TableDemo() {
...
JTable table = new JTable(new MyTableModel());
...
}
class MyTableModel extends AbstractTableModel {
private String[] columnNames = ...//和以前壹样
private Object[][] data = ...//和以前壹样
public int getColumnCount() {
return columnNames.length;
}
public int getRowCount() {
return data.length;
}
public String getColumnName(int col) {
return columnNames[col];
}
public Object getValueAt(int row, int col) {
return data[row][col];
}
public Class getColumnClass(int c) {
return getValueAt(0, c).getClass();
}
//如果你的表格不可编辑就不要实现这個方法
public boolean isCellEditable(int row, int col) {
return col >= 2;
}
//如果你的表格中的数据不改变,则不需要实现这個方法
public void setValueAt(Object value, int row, int col) {
data[row][col] = value;
fireTableCellUpdated(row, col);
}
...
}
监控数据的更新
当表格中的数据发生改变时,每個 table model 都可以有壹系列的监听器,监听器都是 TableModelListener 的实例。在如下代码中,SimpleTableDemo 继承了这样壹個监听器。
import javax.swing.event.*;
import javax.swing.table.TableModel;
public class SimpleTableDemo ... implements TableModelListener {
...
public SimpleTableDemo() {
...
table.getModel().addTableModelListener(this);
...
}
public void tableChanged(TableModelEvent e) {
int row = e.getFirstRow();
int column = e.getColumn();
TableModel model = (TableModel)e.getSource();
String columnName = model.getColumnName(column);
Object data = model.getValueAt(row, column);
...//对数据做壹些事情...
}
...
}
发起数据更新事件
为了发起数据更新的事件,table model 必须知道如何创建壹個 TableModelEvent 对象。这可以是壹個复杂的过程,但是它现在已经在 DefaultTableModel 中实现了。你既可以允许 JTable 使用默认的DefaultTableModel 实例,也可以创建你自己的 DefaultTableModel 的自定义的子类。我的理解,发起数据更新事件的含义就是,当数据改变时,通知所有相关的监听器,让它们知道某行某列的数据发生了改变,就是这样。如果 DefaultTableModel 对于你自定义的 table model 类而言不适用,你就可以考虑继承 AbstractTableModel 类实现壹個子类。这個类为创建 TableModelEvent 对象实现了壹個简单的框架。你的自定义类只需要在每次表数据被外部源改变时,简单的调用如下的 AbstractTableModel 方法。
方法名 | 发生的变化 |
fireTableCellUpdated(int row, int column) | 更新了某個单元格,其坐标为(row, column) |
fireTableRowsUpdated(int firstRow, int lastRow) | 更新了从 firstRow 到 lastRow 的行记录,包括 firstRow 和 lastRow |
fireTableDataChanged() | 更新了整個表格中的数据 |
fireTableRowsInserted(int firstRow, int lastRow) | 在行的范围[firstRow, lastRow]内已经插入新的数据,包含 firstRow 和 lastRow |
fireTableRowsDeleted(int firstRow, int lastRow) | 在行范围[firstRow, lastRow]内存在的行被删除了,包含 firstRow 和 lastRow |
fireTableStructureChanged() | 整個表格失效,包括表中的数据和表的结构 |
相关内容:编辑器和渲染器
在你开始继续接下来几個任务之前,你需要理解表格是如何画出它们的单元格。你可能期望每個表格中的单元格都是壹個组件。然而,出于性能方面考虑,Swing 的表格的实现是不壹样的。
相反,单独壹個单元格渲染器通常可以用来画出所有的包含相同数据类型的单元格。你可以将渲染器想象成壹個可配置的墨水印章,表格使用它来给适当格式化的数据盖章。当用户开始编辑单元格中的数据时,壹個单元格渲染器接手单元格,控制单元格的编辑行为。
举例来说,TableDemo
中的每個# of Years 列的单元格包含 Number 型数据,比较特殊,是 Integer
对象。默认情况下,单元格渲染器对于壹個包含数字的列,在每個列中的单元格上都使用壹個 JLable
实例来画出合适的数字,靠右对齐。如果用户开始编辑其中壹個单元格,默认的单元格编辑器使用靠右对齐的 JTextField
来控制单元格的编辑。
为了选择渲染器来显示列中的单元,你首先需要决定表格是否需要为特定的列使用特定的渲染器。如果不需要,那么表格调用 table
model 的 getColumnClass
方法,后者将会获取列中元素的数据类型,下壹步,表格中列的数据类型与壹個数据类型列表比较,数据类型列表中的元素渲染器已经注册过。这個列表由表格来完成初始化,但是你可以增加或者改变它。目前,表格将如下类型的数据放进列表中:
Boolean — 被渲染成壹個复选框;
Number — 被渲染成壹個靠右对齐的标签;
Double, Float — 和 Number 类型壹样,但是壹個 NumberFormat 实例会完成对象到文本的转换,针对当前语言使用默认的数字格式;
Date — 被渲染成壹個标签,但是壹個 DateFormat 实例会完成对象到文本的转换,针对时间和日期使用壹种简短的风格;
ImageIcon, Icon — 被渲染成壹個居中对齐的标签;
Object — 被渲染成壹個显示对象的字符串值的标签;
单元格编辑器的选择使用类似的算法。
记住,如果你允许壹個表格使用它自己的 model ,它就会使用 Object 作为每個列的类型。为了指定更加精确的列类型,table model 必须定义恰当的 getColumnClass() 方法。就像 TableDemo.java 的演示代码那样。牢记尽管渲染器决定每個单元格或者列头部如何展示,以及它们的 tool tip 文本是什么,但是渲染器不处理事件。如果你需要获取发生在表格中的事件,你需要使用技术不同的你感兴趣的事件排序:
情况 |
如何获取事件 |
检测单元格被编辑的事件 | 使用单元格编辑器,或者在单元格编辑器上注册壹個监听器 |
检测行、列或者单元格的选择与反选事件 | 使用 Detecting User Selections 中提到的选择监听器 |
在列的头部检测鼠标事件 | 在表格的 JTableHeader 对象中注册 mouse listener 的合适类型。请查阅 TableSorter.java 中的实例。 |
检测其它事件 | 为 JTable 对象注册合适的监听器 |
下述几章会告诉你如何自定义显示,以及通过特殊的渲染器和编辑器进行编辑。你要么在列上指定单元格渲染器和编辑器,要么在数据类型上指定单元格渲染器和编辑器。
使用自定义的渲染器
这壹章节告诉我们如何创建和指定壹個单元格的渲染器。你可以使用 JTable 的方法 setDefaultRenderer() 来设置壹個指定类型的单元格渲染器。为了让某個特殊列里面的单元格使用指定的渲染器,你可以使用 TableColumn 的 setCellRenderer() 方法。你甚至可以通过创建 JTable 的子类来指定壹個特定于单元格的渲染器。
使用 DefaultTableCellRenderer
可以很容易的自定义文本或者图片的默认渲染器,你只需要创建壹個子类并且实现 setValue() 方法以便它可以使用合适的字符串或者图片来调用 setText()
或者 setIcon() 方法。举個例子,这里是壹個默认的日期类渲染器的实现:
static class DateRenderer extends DefaultTableCellRenderer {
DateFormat formatter;
public DateRenderer() { super(); }
public void setValue(Object value) {
if (formatter==null) {
formatter = DateFormat.getDateInstance();
}
setText((value == null) ? "" : formatter.format(value));
}
}
如果扩展 DefaultTableCellRenderer 还不够,你可以使用另外壹個子类创建壹個渲染器。最简单的方法就是创建壹個已经存在的组件的子类,实现 TableCellRenderer 接口来创建你自己的子类。TableCellRenderer 只需要实现壹個方法:getTableCellRendererComponent()。你对这個方法的实现应该设置渲染组件,以反映传入的状态,然后返回组件。
在 TableDialogEditDemo 的截图中,用于 Favorite Color 单元格上的渲染器是壹個 JLabel 的子类,被称作 ColorRenderer。这里有壹個来自于 ColorRenderer.java 的片段用于展示它是如何实现的。
public class ColorRenderer extends JLabel implements TableCellRenderer {
...
public ColorRenderer(boolean isBordered) {
this.isBordered = isBordered;
//将背景设置为不透明,为了能够显示出来必须这样做
setOpaque(true);
}
public Component getTableCellRendererComponent(
JTable table, Object color,
boolean isSelected, boolean hasFocus,
int row, int column) {
Color newColor = (Color)color;
setBackground(newColor);
if (isBordered) {
if (isSelected) {
...
//selectedBorder 是颜色的固定边缘
//table.getSelectionBackground().
setBorder(selectedBorder);
} else {
...
//unselectedBorder 是颜色的固定边缘
//table.getBackground().
setBorder(unselectedBorder);
}
}
setToolTipText(...); //留待后继讨论
return this;
}
}
这里是来自 TableDialogEditDemo.java 的壹段代码注册了壹個 ColorRenderer 实例作为所有颜色的数据的默认渲染器:
table.setDefaultRenderer(Color.class, new ColorRenderer(true));
为了给特定单元格指定渲染器,你需要定义壹個 JTable 子类,重载 getCellRenderer() 方法。举個例子,下述的代码让表格中的第壹列的第壹個单元格使用特定的渲染器:
TableCellRenderer weirdRenderer = new WeirdRenderer();
table = new JTable(...) {
public TableCellRenderer getCellRenderer(int row, int column) {
if ((row == 0) && (column == 0)) {
return weirdRenderer;
}
// else...
return super.getCellRenderer(row, column);
}
};
为单元格指定工具提示信息
默认情况下,显示在表中单元格上的工具提示文本由单元格的渲染器决定。然而,有时候通过重载 JTable 的实现中的 getToolTipText(MonseEvent e) 方法可以更加简单指定工具提示文本。这壹章节向你展示如何使用这两個技术。
为了在壹個单元格的渲染器上增加工具提示,你首先需要获得或者创建壹個单元格渲染器。然后,确保渲染器组件是壹個
JComponent,调用它的 setToolTipText() 方法即可。
TableRenderDemo 中有壹個为单元格设置工具提示的例子。点击这里可以运行该样例代码,当然你也可以自己编译运行它的源代码。它使用如下代码在 Sport 列中为单元格添加了工具提示:
DefaultTableCellRenderer renderer = new DefaultTableCellRenderer();
renderer.setToolTipText("Click for combo box");
sportColumn.setCellRenderer(renderer);
虽然上個例子里的工具提示文本样例是静态的,但是你也可以实现随着单元格状态改变而改变的工具提示文本程序,这里有两种实现的方法:
1、在渲染器的
getTableCellRendererComponent() 方法实现中增加壹些代码;
2、重载 JTable 的
getToolTipText(MonseEvent e)方法;
壹個在单元格渲染器中添加代码的例子是 TableDialogEditDemo。点击启动按钮可以运行这個例子,或者你也可以自行编译运行这個例子。TableDialogEditDemo 使用了壹個用于颜色的渲染器,并且在 ColorRender.java 中实现了它。
public class ColorRenderer extends JLabel
implements TableCellRenderer {
...
public Component getTableCellRendererComponent(
JTable table, Object color,
boolean isSelected, boolean hasFocus,
int row, int column) {
Color newColor = (Color)color;
...
setToolTipText("RGB value: " + newColor.getRed() + ", "
+ newColor.getGreen() + ", "
+ newColor.getBlue());
return this;
}
}
下图是工具栏提示运行时的效果:
通过重载 JTable 的 getToolTipText(MouseEvent e) 方法你可以指定工具栏提示文本,程序 TableToolTipsDemo 展示了该如何进行。Sport 和 Vegetarian 两個列中的元素具有工具栏提示,它们的演示效果如下:
这是来自 TableToolTipsDemo 中的壹段代码,它实现了在 Sport 和 Vegetarian 列中的元素上添加工具栏提示的功能。
JTable table = new JTable(new MyTableModel()) {
//Implement table cell tool tips.
public String getToolTipText(MouseEvent e) {
String tip = null;
java.awt.Point p = e.getPoint();
int rowIndex = rowAtPoint(p);
int colIndex = columnAtPoint(p);
int realColumnIndex = convertColumnIndexToModel(colIndex);
if (realColumnIndex == 2) { //Sport column
tip = "This person's favorite sport to "
+ "participate in is: "
+ getValueAt(rowIndex, colIndex);
} else if (realColumnIndex == 4) { //Veggie column
TableModel model = getModel();
String firstName = (String)model.getValueAt(rowIndex,0);
String lastName = (String)model.getValueAt(rowIndex,1);
Boolean veggie = (Boolean)model.getValueAt(rowIndex,4);
if (Boolean.TRUE.equals(veggie)) {
tip = firstName + " " + lastName
+ " is a vegetarian";
} else {
tip = firstName + " " + lastName
+ " is not a vegetarian";
}
} else { //another column
//You can omit this part if you know you don't
//have any renderers that supply their own tool
//tips.
tip = super.getToolTipText(e);
}
return tip;
}
...
}
该代码是相当简单的,除非是调用convertColumnIndexToModel()。该调用是必要的,因为如果用户移动周围的列,列的视图的索引将不匹配模型的索引列。例如,用户可以拖动 Vegetarian 列(假设该模型的列在索引4),使得它可以显示作为第一列 - 此时视图索引为0。由于prepareRenderer提供视图索引,您需要转换视图索引到模型索引,这样你才可以确信该列已选定。
为表头指定工具提示信息
通过为你的表格的 JTableHeader设置工具提示文本,你可以增加壹個工具提示到列的头部。通常不同的列头部需要不同的工具提示信息。通过重载表格头的 getToolTipText() 方法你可以改变文本信息。你可以交替的调用 TableColumn.setHeaderRenderer() 方法来为表头提供自定义的渲染器。
在 TableSorterDemo.java
中有壹個为所有列的头部使用工具提示文本的例子,如下是如何设置工具提示信息的代码:
table.getTableHeader().setToolTipText("Click to sort; Shift-Click to sort in reverse order");
TableToolTipsDemo.java 中实现了壹個根据列的头部给出不同的工具提示信息的例子。除了最初的两個以外,当你的鼠标划过任何列的头部时,你会看见工具提示信息。因为他们似乎不言自明,所以没有为这些名称列提供工具提示信息。
接下来的代码实现了工具提示功能,基本上,它创建了壹個 JTableHeader 的子类并且重载了 getToolTipText(MonseEvent e)方法,以便它可以为当前列返回文本。为了关联修改表的表头,JTable 中的 createDefaultTableHeader() 方法被重载了,以便它返回壹個 JTableHeader 子类的实例。
protected String[] columnToolTips = {
null, // "First Name" assumed obvious
null, // "Last Name" assumed obvious
"The person's favorite sport to participate in",
"The number of years the person has played the sport",
"If checked, the person eats no meat"};
...
JTable table = new JTable(new MyTableModel()) {
...
//实现表头工具栏提示
protected JTableHeader createDefaultTableHeader() {
return new JTableHeader(columnModel) {
public String getToolTipText(MouseEvent e) {
String tip = null;
java.awt.Point p = e.getPoint();
int index = columnModel.getColumnIndexAtX(p.x);
int realIndex =
columnModel.getColumn(index).getModelIndex();
return columnToolTips[realIndex];
}
};
}
};
排序与过滤数据
表格的排序和过滤由壹個 sorter 对象进行管理,最简单的提供 sorter 对象的方法是设置 autoCreateRowSorter 绑定属性为 true:
JTable table = new JTable();
table.setAutoCreateRowSorter(true);
这次我们定义壹個行排序 sorter,它是 javax.swing.table.TableRowSorter 的壹個实例。当用户点击列的头部时,排序工具提供给表格壹個简单的基于当地的排序。TableSortDemo.java 就是这样壹個演示的例子,下图是截屏:
要想获取更多关于排序的控制,你可以创建壹個 TableRowSorter 的实例并且指定它是你的表格中的排序对象。
TableRowSorter<TableModel> sorter = new TableRowSorter<TableModel>(table.getModel());
table.setRowSorter(sorter);
TableRowSorter 使用 java.util.Comparator 对象来完成行排序。壹個实现了这样接口的类提供壹個 compare() 方法用于定义任意两個对象为了完成排序应该如何进行比较。举例来说,下述的代码创建了壹個比较器,用于对比壹系列的字符串通过对比每個字符串最后壹個字母来进行排序:
Comparator<String> comparator = new Comparator<String>() {
public int compare(String s1, String s2) {
String[] strings1 = s1.split("\\s");
String[] strings2 = s2.split("\\s");
return strings1[strings1.length - 1]
.compareTo(strings2[strings2.length - 1]);
}
};
这個例子是特意简化过的,更有代表性的,壹個比较器的实现是壹個 java.text.Collator 的子类。你可以定义你自己的子类,使用在 Collator 类中的工厂方法来根据指定的语言获得壹個 Comparator 对象,或者使用 java.text.RuleBasedCollator 类。
这個例子是特地简化过的;更常见情况下,Comparator 的实现是 java.text.Collator 的壹個子类。你可以有你自己的子类,使用在 Collator 类的工厂方法可以获取壹個基于本地环境的 Comparator 实例,或者使用 java.text.RuleBasedCollator 。
对于每壹列应该使用哪种 Comparator ,TableRowSorter 会尝试应用如下规则。规则会按照如下的顺序被遵循;第壹条规则如果已经使用,则后继的规则会被忽略。
1、如果已经通过 setComparator() 指定了壹個比较器,则使用该比较器。
2、如果 table model 已经声明列数据由字符串组成,就是说对于指定列调用 TableModel.getColumnClass() 方法返回 String.class,使用基于本地环境的字符串顺序进行排序。
3、如果针对列调用 TableModel.getColumnClass() 之后的返回值实现了 Comparable 接口,则使用 Comparable.compareTo() 方法的返回值的字符串顺序进行排序。
4、如果表格调用了 setStringConverter() 方法指定了字符串转换器,则使用基于本地环境的字符串的表现形式的字符串排序器排序。
5、如果以上规则均不适用,则先调用列中数据的 toString() 方法将其转换成字符串类型,然后针对转换后的字符串使用基于本地环境的字符串顺序进行排序。
对于更多更加复杂的排序类型,可以继承 TableRowSorteror 的父类 javax.swing.DefaultRowSorter 。
为调用 setSortKeys() 可以指定列的排序顺序和排序优先级。这里有壹個针对表格的前两列进行排序的例子。优先的列可以在排序键的列表中指定。在这种情况下,第二列拥有第壹個排序键,所以这些行先按照 first name 排序,然后再按照 second name 排序。
List <RowSorter.SortKey> sortKeys = new ArrayList<RowSorter.SortKey>();
sortKeys.add(new RowSorter.SortKey(1, SortOrder.ASCENDING));
sortKeys.add(new RowSorter.SortKey(0, SortOrder.ASCENDING));
sorter.setSortKeys(sortKeys);
除了重新排序的结果,也可以指定壹個表分拣机指定显示哪些行,这就是所谓的过滤。TableRowSorter 使用 javax.swing.RowFilter 对象实现过滤。RowFilter 实现了几個工厂方法用于创建常用的过滤器类型。举例来说,regexFilter 返回壹個基于正则表达式过滤的 RowFilter。
在如下的样例代码中,你明确的创建壹個排序器对象以便你可以在稍后使用它作为过滤器。
MyTableModel model = new MyTableModel();
sorter = new TableRowSorter<MyTableModel>(model);
table = new JTable(model);
table.setRowSorter(sorter);
然后你可以基于文本域的当前值过滤:
private void newFilter() {
RowFilter<MyTableModel, Object> rf = null;
try {
rf = RowFilter.regexFilter(filterText.getText(), 0);
} catch (java.util.regex.PatternSyntaxException e) {
return;
}
sorter.setRowFilter(rf);
}
在后面的例子中,每次文本域改变时都会调用 newFilter() 方法。当用户输入难懂的正则表达式时,try...catch 会防止干扰和阻止语法端的输入异常。当表格使用了排序器,用户看到的数据可能呈现壹种与 table model 指定的不同的顺序,也有可能包含 table model 所指定的所有数据。用户实际看到的数据被称作视图,它有自己的坐标系。JTable 提供了从 model 坐标系到视图坐标系的转换方法: convertColumnIndexToView() 和 convertRowIndexToView() ,也提供了从视图坐标系到 model 坐标系的转换方法:convertColumnIndexToModel() 和 convertRowIndexToModel()。
注意:当使用排序器时,总要记住转换单元格的坐标系。
接下来的例子把我们这节讨论过的所有想法都带来了。TableFilterDemo.java 给 TableDemo 做了壹些小的改变。它们包含在这节之前的代码片段中,作用是为主表提供了壹個排序器,并且使用壹個文本框来提供过滤用的正则表达式。如下的截屏展示了 TableFilterDemo 在排序和过滤操作之前的样子。注意模型中的第三行在视图中也在相同的第三行:
如果用户在第二列上点击两次,第四列就变成了第壹列,但只是在视图中:
就像之前提到的那样,用户在"Filter Text"中输入的文本决定哪些行会显示,同样对于排序,过滤引起视图坐标系偏离模型坐标系:
这里是更新状态文本来映射当前选择的代码:
table.getSelectionModel().addListSelectionListener(
new ListSelectionListener() {
public void valueChanged(ListSelectionEvent event) {
int viewRow = table.getSelectedRow();
if (viewRow < 0) {
statusText.setText("");
} else {
int modelRow = table.convertRowIndexToModel(viewRow);
statusText.setText(String.format("Selected Row in view: %d. " + "Selected Row in model: %d.", viewRow, modelRow));
}
}
}
);
使用 ComboBox 下拉列表作为编辑器
启用壹個下拉列表作为编辑器很容易,如同下述样例展示的壹样。
TableColumn sportColumn = table.getColumnModel().getColumn(2);
...
JComboBox comboBox = new JComboBox();
comboBox.addItem("Snowboarding");
comboBox.addItem("Rowing");
comboBox.addItem("Chasing toddlers");
comboBox.addItem("Speed reading");
comboBox.addItem("Teaching high school");
comboBox.addItem("None");
sportColumn.setCellEditor(new DefaultCellEditor(comboBox));
这是下拉列表在使用时的效果:
上述代码来自于 TableRenderDemo.java 。
使用其它编辑器
无论你是为单個的列或者单元格设置编辑器(使用TableColumnsetCellEditor的方法)还是为特定的数据类型设置编辑器(使用JTablesetDefaultEditor的方法),你都应该让这個编辑器实现 TableCellEditor 接口。幸运的是,DefaultCellEditor 类实现这个接口,并允许你指定编辑组成部分是一个JTextField,JCheckBox 或者 JComboBox 的构造函数。通常,您不需要明确指定一个复选框作为壹個编辑器,因为布尔数据的列会自动使用复选框渲染器和编辑器。
如果你想指定复选框、下拉列表、文本框编辑器以外的文本字段该如何做呢?由于DefaultCellEditor不支持其他类型的组件,您必须做更多一点的工作。您需要创建一个类实现 TableCellEditor 的接口。 AbstractCellEditor 类是一个很好的超类使用。它实现了 TableCellEditor 的超级接口,CellEditor,减少了您的麻烦同时实现了事件触发单元格编辑器所必需的代码。
你的单元格编辑器需要定义至少两個方法:getCellEditorValue() 和 getTableCellEditorComponent()。 方法 getCellEditorValue() 的定义是 CellEditor 所必需的,它返回单元格的当前值。方法 getTableCellEditorComponent() 则是 TableCellEditor 所必需的,应该配置为返回你希望使用的那個编辑器的组件。
这里有壹個用对话框作为单元格编辑器(不是直接的出现)的表格的图片。当用户开始编辑列 Favorite Color 列中的单元格时,壹個按钮(真正的单元格编辑器)出现并且显示出对话框,在对话框中用户可以选择壹個不壹样的颜色值。
上述代码参见 TableDialogEditDemo.java 。
这是来自 ColorEditor.java 的源代码,它实现了单元格编辑器。
public class ColorEditor extends AbstractCellEditor
implements TableCellEditor,
ActionListener {
Color currentColor;
JButton button;
JColorChooser colorChooser;
JDialog dialog;
protected static final String EDIT = "edit";
public ColorEditor() {
button = new JButton();
button.setActionCommand(EDIT);
button.addActionListener(this);
button.setBorderPainted(false);
//Set up the dialog that the button brings up.
colorChooser = new JColorChooser();
dialog = JColorChooser.createDialog(button,
"Pick a Color",
true, //模型
colorChooser,
this, //确认按钮的 handler
null); //没有取消按钮的 handler
}
public void actionPerformed(ActionEvent e) {
if (EDIT.equals(e.getActionCommand())) {
//用户点击了单元格,所以触发了弹出对话框
button.setBackground(currentColor);
colorChooser.setColor(currentColor);
dialog.setVisible(true);
fireEditingStopped(); //让渲染器重新可见
} else { //用户点击了对话框中的 OK 按钮
currentColor = colorChooser.getColor();
}
}
//实现了壹個 AbstractCellEditor 没有实现的 CellEditor 方法
public Object getCellEditorValue() {
return currentColor;
}
//实现了 TableCellEditor 定义的壹個方法
public Component getTableCellEditorComponent(JTable table,
Object value,
boolean isSelected,
int row,
int column) {
currentColor = (Color)value;
return button;
}
}
就像你看到的壹样,代码很简单。略有点棘手的地方是在编辑器按钮动作处理程序结束的时候需要调用 fireEditingStopped() 方法。没有这個调用,编辑器会仍然保持活动状态,尽管模态对话框已经不再可见。通过调用 fireEditingStopped() 方法可以让表格知道它可以关闭编辑器,让单元格可以再次被渲染器处理。
使用编辑器验证用户输入的文本信息的合法性
如果一个单元格的默认编辑器允许文本输入,如果单元格的类型指定为字符串或对象以外的东西,你会得到一些免费的错误检查。错误检查的副作用是将输入的文字转换成一个对象的正确类型。
自动检查用户输入的字符串时发生在默认编辑器试图创建一个新的单元格的列相关联的类的实例的时候。默认编辑器使用一个String作为参数的构造函数创建这个实例。例如,在一列Integer类型的单元格中,当用户输入“123”的默认编辑器创建相应的整数,使用相当于新的整数(“123”)的代码。如果构造函数抛出异常,单元格的轮廓变成红色,并拒绝让焦点移动的单元格。如果实现列的数据类型作为一类,你可以使用默认的编辑器,如果你的类提供了一个接受一个参数类型为String的构造函数。
如果你想使用壹個文本框作为单元格的编辑器,但是想自定义它 — 可能需要更加严格的检查用户输入,或者当输入文本不合法时用不同的方法重构 — 你可以使用壹個 formatted text field 改变单元格编辑器。用户表明打字(如按Enter键)结束之后,格式化的文本框可以连续检查用户键入后的值。
接下来的代码来自于 TableFTFEditDemo.java ,它设置了壹個格式化文本框作为编辑器,后者限制所有的整型值都必须在 0 到 100 之间。接下来的代码为所有的列创建了壹個包含 Integer 类型数据的格式化文本框:
table.setDefaultEditor(Integer.class, new IntegerEditor(0, 100));
IntegerEditor 类是作为 DefaultCellEditor 的壹個子类实现的,并且使用了壹個 JFormattedTextField 代替了 JTextField,后者是由 DefaultCellEditor 支持的。它是通过首先设置壹個使用整型格式的格式化文本框,并且指定最小和最大值,然后使用 How to Use Formatted Text Fields 中提到的 API 实现的。之后它重载了 DefaultCellEditor 的getTableCellEditorComponent(),getCellEditorValue(), 和 stopCellEditing() 方法实现,为格式化文本框增加了必要的操作。
覆盖getTableCellEditorComponent()设置格式的文本字段的值属性(不仅仅是文本的属性,它继承自JTextField的)编辑器显示之前。覆盖getCellEditorValue()保持作为一个整数单元格的值,而不是,比方说,Long值,格式化文本字段的解析器趋于恢复。最后,覆盖stopCellEditing()让你检查文本是否是有效的,可能停止编辑被解雇。如果文本是无效的,你stopCellEditing()的实施提出了一个对话框,让用户选择继续编辑或恢复到最后一个良好的价值。源代码是太长了一点,包括在这里,你也可以查看 IntegerEditor.java 。
打印表格内容
JTable 提供了打印表格的壹個简单的 API。最简单的打印出表格的方法就是不带参数调用 JTable.print() 。
try {
if (! table.print()) {
System.err.println("User cancelled printing");
}
} catch (java.awt.print.PrinterException e) {
System.err.format("Cannot print %s%n", e.getMessage());
}
在壹個正常的 Swing 应用上调用打印功能会打开壹個标准的打印对话框。在壹個没有表格头的应用中,则简单的打印它。它的返回值表示用户是否继续打印的任务还是放弃打印。JTable.print() 可以抛出 java.awt.print.PrinterException 异常,它是壹個 受检异常 ,这就是为什么我们在例子中要使用壹個 try ... catch 语句来包围它。
JTable 提供了几個 print() 的重载方法,可以使用各种各样的选项来完成打印。如下来自 TablePrintDemo.java 的代码展示了如何定义页面头部:
MessageFormat header = new MessageFormat("Page {0,number,integer}");
try {
table.print(JTable.PrintMode.FIT_WIDTH, header, null);
} catch (java.awt.print.PrinterException e) {
System.err.format("Cannot print %s%n", e.getMessage());
}
如果想了解更多更复杂的表格打印功能,可以使用 JTable.getPrintable() 来从表格中获取壹個 Printable 对象。
图形界面系列教材 (十)- Swing 使用 JTable详解
如何使用 Swing 组件 JTable