Java Swing JTable仅初始化一次RowSorter的可行方案问询
我们有多个JTable扩展类,这类组件支持RowSorter(通常用TableRowSorter)。现有流程是先完成表格配置(比如添加列),再填充或重填数据。
排序器是例外:它属于配置逻辑,但每次重填数据时都得重置,重置前要从表格集合加载Comparator。
最初尝试只在配置阶段设置排序器,遇到问题:向TableRowSorter添加Comparator时,它会检查TableModel(而非TableColumnModel)是否存在对应列索引,不存在就抛出异常。
需要注意:配置与填充流程不能混合——表格可能被刷新、GUI元素可能被重新配置,这些都会触发重填,数据是动态的,没法只在初始化时设置一次。
我希望在重写的addColumn()方法里自动根据列类型设置匹配的Comparator(我们的列未参数化,用魔术常量标识类型)。尝试同步TableColumnModel与TableModel的列(调用DefaultTableModel#addColumn),但修改TableModel会触发DefaultRowSorter#allChanged,清除已设置的Comparator;仅更新TableModel列数也会出现同样问题。
还考虑过扩展TableRowSorter调整默认行为,但发现javax.swing.DefaultRowSorter#getModelWrapper是final方法,没法重写,这个方案行不通。
我们的应用高度定制:用户权限、应用设置等决定表格列数(比如用户可在GUI中添加额外列),应用在用户交互前没法预知总列数及列标识,因此没法提前完全初始化TableModel。
目前似乎只有两种选择:
- 断开RowSorter与TableModel的关联;
- 接受每次重填数据时重置Comparator的现状。
想请教:处理TableRowSorter时,是否普遍采用每次重填都重置的方式?还是存在仅在配置阶段初始化一次的可行方案?
最小可复现示例(MRE)
import javax.swing.JFrame; import javax.swing.JPanel; import javax.swing.JScrollPane; import javax.swing.JTable; import javax.swing.RowSorter; import javax.swing.table.DefaultTableModel; import javax.swing.table.TableColumn; import javax.swing.table.TableColumnModel; import javax.swing.table.TableModel; import javax.swing.table.TableRowSorter; import java.awt.BorderLayout; import java.util.ArrayList; import java.util.Comparator; import java.util.List; public class SortedTableDemo { public static void main(String[] args) { JFrame frame = new JFrame("JTable Example"); frame.setContentPane(createMainPanel()); frame.pack(); frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); frame.setLocationRelativeTo(null); frame.setVisible(true); } private static JPanel createMainPanel() { JPanel panel = new JPanel(new BorderLayout()); panel.add(createTableScroller()); return panel; } private static JScrollPane createTableScroller() { JTable table = createTable(); return new JScrollPane(table); } private static JTable createTable() { JTable table = new JTable(); table.setAutoCreateColumnsFromModel(false); TableColumnModel columnModel = table.getColumnModel(); createColumns().forEach(columnModel::addColumn); RowSorter<? extends TableModel> rowSorter = createRowSorter(columnModel); table.setRowSorter(rowSorter); return table; } private static RowSorter<? extends TableModel> createRowSorter(TableColumnModel columnModel) { TableRowSorter<? extends TableModel> sorter = new TableRowSorter<>(); sorter.setComparator(columnModel.getColumnIndex("Age"), Comparator.comparingInt(Integer::intValue)); return sorter; } private static List<TableColumn> createColumns() { List<TableColumn> columns = new ArrayList<>(); TableColumn nameColumn = new TableColumn(); nameColumn.setHeaderValue("Name"); columns.add(nameColumn); TableColumn ageColumn = new TableColumn(); ageColumn.setHeaderValue("Age"); columns.add(ageColumn); TableColumn cityColumn = new TableColumn(); cityColumn.setHeaderValue("City"); columns.add(cityColumn); return columns; } }
异常信息
Exception in thread "main" java.lang.IndexOutOfBoundsException: column beyond range of TableModel at javax.swing.DefaultRowSorter.checkColumn(DefaultRowSorter.java:1219) at javax.swing.DefaultRowSorter.setComparator(DefaultRowSorter.java:761) at demos.table.SortedTableDemo.createRowSorter(SortedTableDemo.java:54)
解决方案
1. 普遍做法:数据重填时重置Comparator
在Swing生态里,每次重填数据后重置TableRowSorter的Comparator是比较普遍的做法。因为TableRowSorter紧密绑定TableModel的结构,当TableModel的列结构或数据发生大变化时,排序器的内部状态很容易失效,重置能避免各种潜在的索引不匹配问题。
你可以把Comparator的配置逻辑封装成一个独立方法,比如setupColumnComparators(TableRowSorter sorter, TableColumnModel columnModel),每次数据填充完成后调用这个方法重新设置所有列的Comparator。
2. 仅配置阶段初始化的可行方案:自定义ModelWrapper替代方案
虽然DefaultRowSorter#getModelWrapper是final方法,但可以通过自定义TableModel来适配,让它在列配置阶段就同步列结构,同时避免触发排序器的allChanged事件:
- 自定义一个
ConfigurableTableModel,继承DefaultTableModel,添加一个addColumnSilently方法,该方法在添加列时不触发tableChanged事件(暂时关闭事件通知),等所有列配置完成后再统一触发一次事件。 - 在表格配置阶段,先通过
addColumnSilently同步TableModel与TableColumnModel的列结构,然后再设置TableRowSorter的Comparator,此时TableModel已经有对应的列索引,就不会抛出异常。 - 后续重填数据时,只更新TableModel的行数据,不修改列结构,这样排序器的Comparator不会被清除,无需重复设置。
示例代码片段:
public class ConfigurableTableModel extends DefaultTableModel { public void addColumnSilently(Object columnName) { // 暂时关闭事件通知 setFireTableStructureChanges(false); addColumn(columnName); setFireTableStructureChanges(true); } // 配置完成后手动触发结构变更事件 public void notifyStructureChanged() { fireTableStructureChanged(); } }
使用时:
private static JTable createTable() { ConfigurableTableModel model = new ConfigurableTableModel(); JTable table = new JTable(model); table.setAutoCreateColumnsFromModel(false); TableColumnModel columnModel = table.getColumnModel(); List<TableColumn> columns = createColumns(); columns.forEach(columnModel::addColumn); // 同步TableModel列(静默添加) columns.forEach(col -> model.addColumnSilently(col.getHeaderValue())); // 手动触发结构变更 model.notifyStructureChanged(); RowSorter<? extends TableModel> rowSorter = createRowSorter(columnModel); table.setRowSorter(rowSorter); return table; }
3. 另一种思路:绑定列标识而非索引
TableRowSorter支持通过列名(如果TableModel的getColumnIdentifiers有设置)来关联Comparator,但需要确保TableModel的列标识和TableColumn的headerValue一致。不过这种方式需要TableModel提前设置好列标识,对你的场景来说可能需要调整列配置流程,但能避免索引不匹配的问题。
内容的提问来源于stack exchange,提问作者Cagepi

