12.11.2 TableModel和监听器

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实例。