之前写过一遍关于JTable添加行号显示功能的文章。在后期使用中发现交互体验很不友好,出现很多问题:点击行号没有选中相应的行、点击标题没有选择全列(或多选列)、删除行后行号没有相对应的删除或有残影现象、多选单元格的功能没有了等等。于是~最后的最后,我对添加行号显示这一实现进行了重写。
P.S…由于网上找不到相关问题的资料,最后自己花了很长时间去摸索解决的。今天除了分享之外,同时记录下解决的思路。
问题
- 点击行号没有选中相应的行
- 点击标题没有选择全列(或多选列)
- 多选单元格的功能没有了
- 删除行后行号没有相对应的删除或有残影现象
思路
总得来说就是行号与表格在互动过程中出现的问题。在最初通过表格的容器 jScrollPane.setRowHeaderView(Component view) 添加行号组件的时候就要让行号和表格建立起很好的紧密互动联系。上面问题的最后就是——怎么建立这个互动联系。
探索与解决
首先创建一个继承JTable的类,亦是最终添加的行号组件。
public class RowHeader extends JTable {
/** 与行号产生联系的JTable */
private final JTable refTable;
/** 为JTable添加RowHeader,
* @param refTable 与行号产生联系的JTable
* @param width 行号的宽度
*/
public RowHeaderTable(JTable refTable, int width) {
super(new DefaultTableModel() {
// 与JTable的行数同步
private int rowCount = refTable.getRowCount();
@Override
public void setRowCount(int rowCount) {
this.rowCount = rowCount;
}
@Override
public boolean isCellEditable(int row, int column) {
return false;
}
@Override
public int getRowCount() {
return rowCount;
}
@Override
public int getColumnCount() {
return 1;
}
@Override
public Object getValueAt(int row, int column) {
return row+1; // 行号的显示值从1开始
}
});
this.refTable = refTable;
setAutoResizeMode(JTable.AUTO_RESIZE_OFF);//不允许调整列宽
getColumnModel().getColumn(0).setPreferredWidth(width);
setDefaultRenderer(Object.class, new RowHeaderRenderer(this.refTable, this));//设置渲染器
setPreferredScrollableViewportSize(new Dimension(width, 0));
}
}
上面代码是对行号组件进行一些基础配置,核心在 setDefaultRenderer 中的RowHeaderRenderer(this.refTable, this) ——这个自建类将为行号组件与表格提供互动支持。(先分析,最后给出完整代码)
// RowHeaderRenderer 的继承关系,关键是后面的两个接口
class RowHeaderRenderer extends JLabel implements TableCellRenderer, ListSelectionListener
表格组件存在三处点击事件——单元格、行号、标题,三者应该相互影响。在RowHeaderRenderer类的实现中,针对这三个事件进行分析与实现。
点击单元格事件 —— 对表格组件的维持行选择状态的模型添加相关的事件处理。
ListSelectionModel lsm = reftable.getSelectionModel();
// 点击单元格事件。实现当在reftable中选择行时,RowHeader会发生颜色变化
lsm.addListSelectionListener(new ListSelectionListener() {
private boolean flag = true; // 记录有效值的开关
private int rowIndex; // 记录有效的行值
private int colIndex; // 记录有效的列值
@Override
public void valueChanged(ListSelectionEvent e) {
// 一次点击事件会多次触发这个事件。随之出现了一种情况————
// 点击行号后再点击单元格,会出现“无响应”。
// 通过调取各数值后发现只有第一次调用valueChanged时,各属性值才是真实值
if (flag) { // 只在第一次运行
flag = false;
colIndex = reftable.getSelectedColumnCount();
rowIndex = lsm.getLeadSelectionIndex();
}
// getValueIsAdjusting()返回此事件是否是仍然在更改的多个不同事件之一。
if (!e.getValueIsAdjusting()) { // 一次点击触发多次。最后一次getValueIsAdjusting()为false
flag = true;
// 判断点击事件是否源于标题
if (header) {
header = false;
} else { // 点击单元格
if (colIndex > 1 || reftable.getSelectedRowCount() > 1) {
// 多选单元格时,不执行任何操作
return;
}
if (rowIndex > -1) { // 选中对应行号
tableShow.setRowSelectionInterval(rowIndex, rowIndex);
}
}
}
}
});
点击行号事件 —— 在RowHeaderRenderer类中覆写getTableCellRendererComponent方法
// 点击行号触发。行号状态每改变一次就调用一次
@Override
public Component getTableCellRendererComponent(JTable table, Object obj,
boolean isSelected, boolean hasFocus, int row, int col) {
((DefaultTableModel) table.getModel()).setRowCount(reftable.getRowCount());
if (isSelected) {
// 不能与 isSelected 并用,容易导致显示异常
if (table.getSelectedRow() < reftable.getRowCount()) {
// 选中对应行号的表格行
reftable.setRowSelectionInterval(row, row);
reftable.setColumnSelectionInterval(0, reftable.getColumnCount()-1);
setForeground(Color.WHITE);
setBackground(tableSeleBg);
}
} else {
setForeground(Color.BLACK);
if (row % 2 == 0) {
setBackground(Color.WHITE);
} else {
// 背景色为标题的背景色
setBackground(reftable.getTableHeader().getBackground());
}
}
setText(String.valueOf(row + 1));
return this;
}
点击标题事件 —— 对表格组件的TableHeader添加相关的事件处理。
// 点击标头选中列
reftable.getTableHeader().addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
if (col.isEmpty()) { // 清除选择的列
header = true;
tableShow.clearSelection();
reftable.clearSelection(); // 调用该方法会立即触发SelectionListener,需提前设置header
}
int p = reftable.columnAtPoint(e.getPoint());
header = true;
// 选择多个列
if (col.contains(p)) {
col.removeIf((t) -> {
return t == p;
});
reftable.removeColumnSelectionInterval(p, p);
} else {
col.add(p);
reftable.addColumnSelectionInterval(p, p);
reftable.addRowSelectionInterval(0, reftable.getRowCount()-1);
}
}
});
到此~单元格、行号、标题三者的点击联动就实现了。同时提到的问题也基本得到解决。
但是当对表格进行删除行操作时,问题出现了 —— 删除行后行号没有相对应的删除或有残影现象,如果处于选择状态下的话,删除某行后~行号仍然处于选择状态。
解决关键是为表格添加删除行的事件,通知行号组件更新。
// 表格删除行后更新行号
reftable.getModel().addTableModelListener(new TableModelListener() {
@Override
public void tableChanged(TableModelEvent e) {
if (e.getType() == javax.swing.event.TableModelEvent.DELETE) {
((DefaultTableModel)tableShow.getModel()).setRowCount(reftable.getRowCount());
tableShow.getSelectionModel().clearSelection();
tableShow.revalidate(); // 更新行号组件。消除残影
}
}
});
到这里~开始提到的问题都在这四个方法中得到了解决。而最终得到的行号组件跟表格间有了很友好紧密的联动。
源码
最后 —— 附上完整的代码:
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import javax.swing.JLabel;
import javax.swing.JTable;
import javax.swing.ListSelectionModel;
import javax.swing.SwingConstants;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.event.TableModelEvent;
import javax.swing.event.TableModelListener;
import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.table.DefaultTableModel;
import javax.swing.table.TableCellRenderer;
import sun.swing.table.DefaultTableCellHeaderRenderer;
/** 用于显示行号的RowHeader组件
* @author Bson Hoang
*/
public class RowHeader extends JTable {
private final JTable refTable;
public RowHeader(JTable refTable, int width) {
super(new DefaultTableModel() {
private int rowCount = refTable.getRowCount();
@Override
public void setRowCount(int rowCount) {
this.rowCount = rowCount;
}
@Override
public boolean isCellEditable(int row, int column) {
return false;
}
@Override
public int getRowCount() {
return rowCount;
}
@Override
public int getColumnCount() {
return 1;
}
@Override
public Object getValueAt(int row, int column) {
return row+1;
}
});
this.refTable = refTable;
setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
getColumnModel().getColumn(0).setPreferredWidth(width);
setDefaultRenderer(Object.class, new RowHeaderRenderer(this.refTable, this));
setPreferredScrollableViewportSize(new Dimension(width, 0));
}
}
class RowHeaderRenderer extends JLabel implements TableCellRenderer, ListSelectionListener {
private boolean header = false;
private final ArrayList<Integer> col = new ArrayList<>();
private final JTable reftable;
private final JTable tableShow;
private Color tableSeleBg;
public RowHeaderRenderer(JTable reftable, JTable tableShow) {
this.reftable = reftable;
this.tableShow = tableShow;
setOpaque(true);
setBorder(javax.swing.UIManager.getBorder("TableHeader.cellBorder"));
setHorizontalAlignment(CENTER);
initConfig();
}
private void initConfig() {
tableSeleBg = reftable.getSelectionBackground();
((DefaultTableCellHeaderRenderer)reftable.getTableHeader().getDefaultRenderer()).setHorizontalAlignment(SwingConstants.CENTER);
DefaultTableCellRenderer dtcr = new DefaultTableCellRenderer();
dtcr.setHorizontalAlignment(SwingConstants.CENTER);
reftable.setDefaultRenderer(Object.class, dtcr);
reftable.getTableHeader().addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
if (col.isEmpty()) {
header = true;
tableShow.clearSelection();
reftable.clearSelection();
}
int p = reftable.columnAtPoint(e.getPoint());
header = true;
if (col.contains(p)) {
col.removeIf((t) -> {
return t == p;
});
reftable.removeColumnSelectionInterval(p, p);
} else {
col.add(p);
reftable.addColumnSelectionInterval(p, p);
reftable.addRowSelectionInterval(0, reftable.getRowCount()-1);
}
}
});
reftable.getTableHeader().addMouseMotionListener(new java.awt.event.MouseAdapter() {
@Override
public void mouseMoved(java.awt.event.MouseEvent e) {
int c = reftable.getColumnModel().getColumnIndexAtX(e.getX());
if (c > -1) {
String s = reftable.getColumnName(c);
reftable.getTableHeader().setToolTipText(s == null || s.isEmpty() ? null : s);
}
}
});
reftable.addMouseMotionListener(new java.awt.event.MouseAdapter() {
@Override
public void mouseMoved(java.awt.event.MouseEvent e) {
int r = reftable.rowAtPoint(e.getPoint());
int c = reftable.columnAtPoint(e.getPoint());
if (r > -1 && c > -1) {
Object o = reftable.getValueAt(r, c);
reftable.setToolTipText(o == null || o.toString().isEmpty() ? null : o.toString());
}
}
});
reftable.getModel().addTableModelListener(new TableModelListener() {
@Override
public void tableChanged(TableModelEvent e) {
if (e.getType() == javax.swing.event.TableModelEvent.DELETE ||
e.getType() == javax.swing.event.TableModelEvent.INSERT) {
((DefaultTableModel)tableShow.getModel()).setRowCount(reftable.getRowCount());
tableShow.getSelectionModel().clearSelection();
tableShow.revalidate();
}
}
});
ListSelectionModel lsm = reftable.getSelectionModel();
lsm.addListSelectionListener(new ListSelectionListener() {
private boolean flag = true;
private int rowIndex;
private int colIndex;
@Override
public void valueChanged(ListSelectionEvent e) {
if (flag) {
flag = false;
colIndex = reftable.getSelectedColumnCount();
rowIndex = lsm.getLeadSelectionIndex();
}
if (!e.getValueIsAdjusting()) {
flag = true;
if (header) {
header = false;
} else {
if (colIndex > 1 || reftable.getSelectedRowCount() > 1) {
return;
}
if (!col.isEmpty()) {
col.clear();
}
if (rowIndex > -1) {
tableShow.setRowSelectionInterval(rowIndex, rowIndex);
}
}
}
}
});
}
@Override
public Component getTableCellRendererComponent(JTable table, Object obj,
boolean isSelected, boolean hasFocus, int row, int col) {
((DefaultTableModel) table.getModel()).setRowCount(reftable.getRowCount());
if (isSelected) {
if (table.getSelectedRow() < reftable.getRowCount()) {
reftable.setRowSelectionInterval(row, row);
reftable.setColumnSelectionInterval(0, reftable.getColumnCount()-1);
setForeground(Color.WHITE);
setBackground(tableSeleBg);
}
} else {
setForeground(Color.BLACK);
if (row % 2 == 0) {
setBackground(Color.WHITE);
} else {
setBackground(reftable.getTableHeader().getBackground());
}
}
setText(String.valueOf(row + 1));
return this;
}
@Override
public void valueChanged(ListSelectionEvent e) {
tableShow.revalidate();
}
}
你会发现该类被高度封装。为了便于大家使用,对外只暴露了一个构造函数,所以使用起来非常简单方便:
// 首先将上面的源码保存下来
// 你的JTable及其容器
javax.swing.JTable jt = new javax.swing.JTable();
javax.swing.JScrollPane jsp = new javax.swing.JScrollPane();
jsp.setViewportView(jt);
// 添加宽度为30的行号组件
jsp.setRowHeaderView(new RowHeader(jt, 30));
功能&效果
- 显示行号(实现点击行号,选中相应行)
- 点击单元格,相应行号被选中
- 单双行不同颜色显示(浏览效果更佳)
- 单击表头选中列,支持任意多选
- 添加或删除行后同步更新行号
- 表头悬浮提示、单元格悬浮提示
- 表头内容、单元格内容居中显示
P.S…最近发现一篇为JTable实现合并单元格的文章,有兴趣的看官可以去了解一下。