We have a JTable
extension (actually, more than one).
Being a JTable
, it supports RowSorter
s. Typically, it's a TableRowSorter
.
First, the table is set up (e.g. columns are added), then it is filled (or re-filled).
The sorter is an exception, though. Even though, it seems like part of setup logic, it is reset on each refill. Before a new sorter instance is set, it is populated with Comparator
s stored in the table's collection.
At first, I didn't get it, so I tried to set the sorter only during the setup. However, I encountered one problem: when you add Comparator
s to a TableRowSorter
, the latter checks with a TableModel
(not TableColumnModel
) if it has such a column index. If not, it throws.
Keep in mind the setup and fill cannot be mixed together: the table may be refreshed, GUI elements may be reconfigured — all of those would trigger a refill. In other words, the data is fluid, it cannot be set just once on setup.
I would prefer to have comparators set in the table's overridden addColumn()
. It would check the column's type and set a matching comparator to the table's row sorter automatically (our columns are sadly not parameterized but have magic constants for types). I tried adding new columns to both column and data models (see javax.swing.table.DefaultTableModel#addColumn(java.lang.Object)
), aiming to keep the two in sync. However, manipulating a TableModel
that way results in erasing previously set comparators (see javax.swing.DefaultRowSorter#allChanged
). The same happens if I simply update the data model's column count on each added column. I will include an MRE for that too, if necessary.
I also considered extending TableRowSorter
and tweaking its default behavior a little bit. Unfortunately, it doesn't seem as straightforward as I hoped: my idea was to override javax.swing.DefaultRowSorter#getModelWrapper
so that it checks with a TableColumnModel
only to find the method is final
.
Our app is highly customizable: it has user rights, app settings and whatnot — some of those determine the number of columns in a table (e.g. the user can add an extra column in GUI). The app may not know the total column count, let alone their identifiers before the user starts interaction. That is another reason why it does not seem feasible to fully initialize a TableModel
beforehand, before its first fill (e.g. set its column count).
It appears there are only two options:
- Somehow break the connection between a
RowSorter
andTableModel
. - Accept the fact that I would need to reset comparators on each refill.
So my question is, is it the way you deal with a TableRowSorter
, you reset it on each fill? Or is there a way to do it just once on setup, as I think it should be?
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"), ComparatorparingInt(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)
We have a JTable
extension (actually, more than one).
Being a JTable
, it supports RowSorter
s. Typically, it's a TableRowSorter
.
First, the table is set up (e.g. columns are added), then it is filled (or re-filled).
The sorter is an exception, though. Even though, it seems like part of setup logic, it is reset on each refill. Before a new sorter instance is set, it is populated with Comparator
s stored in the table's collection.
At first, I didn't get it, so I tried to set the sorter only during the setup. However, I encountered one problem: when you add Comparator
s to a TableRowSorter
, the latter checks with a TableModel
(not TableColumnModel
) if it has such a column index. If not, it throws.
Keep in mind the setup and fill cannot be mixed together: the table may be refreshed, GUI elements may be reconfigured — all of those would trigger a refill. In other words, the data is fluid, it cannot be set just once on setup.
I would prefer to have comparators set in the table's overridden addColumn()
. It would check the column's type and set a matching comparator to the table's row sorter automatically (our columns are sadly not parameterized but have magic constants for types). I tried adding new columns to both column and data models (see javax.swing.table.DefaultTableModel#addColumn(java.lang.Object)
), aiming to keep the two in sync. However, manipulating a TableModel
that way results in erasing previously set comparators (see javax.swing.DefaultRowSorter#allChanged
). The same happens if I simply update the data model's column count on each added column. I will include an MRE for that too, if necessary.
I also considered extending TableRowSorter
and tweaking its default behavior a little bit. Unfortunately, it doesn't seem as straightforward as I hoped: my idea was to override javax.swing.DefaultRowSorter#getModelWrapper
so that it checks with a TableColumnModel
only to find the method is final
.
Our app is highly customizable: it has user rights, app settings and whatnot — some of those determine the number of columns in a table (e.g. the user can add an extra column in GUI). The app may not know the total column count, let alone their identifiers before the user starts interaction. That is another reason why it does not seem feasible to fully initialize a TableModel
beforehand, before its first fill (e.g. set its column count).
It appears there are only two options:
- Somehow break the connection between a
RowSorter
andTableModel
. - Accept the fact that I would need to reset comparators on each refill.
So my question is, is it the way you deal with a TableRowSorter
, you reset it on each fill? Or is there a way to do it just once on setup, as I think it should be?
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"), ComparatorparingInt(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)
Share
Improve this question
edited Mar 3 at 11:43
Cagepi
asked Mar 3 at 7:07
CagepiCagepi
3171 silver badge7 bronze badges
5
|
2 Answers
Reset to default 1If the underlying model structure changes (the modelStructureChanged method is invoked) the following are reset to their default values: Comparators by column, current sort order, and whether each column is sortable. The default sort order is natural (the same as the model), and columns are sortable by default.
From https://docs.oracle/javase/8/docs/api/javax/swing/table/TableRowSorter.html
The documentation says that the state of the TableRowSorter would be reset if there is a change in the structure of the model. This means anytime you cause a change in the structure, you must set the comparators again. Things like changing the number of columns would cause a structure change, whereas things like just setting the value of a cell should not cause a structure change.
So the right way seems to be to set the comparators again whenever you cause a structure change. A possibly hackish way to do that would be to override modelStructureChanged() on TableRowSorter to set the comparators again.
Looking at source code:
private void checkColumn(int column) {
if (column < 0 || column >= getModelWrapper().getColumnCount()) {
throw new IndexOutOfBoundsException(
"column beyond range of TableModel");
}
}
here ModelWrapper is modelWrapper DefaultRowSorter has and this model clearly does not have such column (since it has no connection to your table). This leads me to class documentation where it is written:
"The setModelWrapper method must be invoked soon after the constructor is called, ideally from within the subclass's constructor. Undefined behavior will result if you use a DefaultRowSorter without specifying a ModelWrapper."
and to the TableRowSorter constructor you are using:
public TableRowSorter() { this(null); }
So the issue is your table and your row sorter are using different table models. Instead you should create row sorter with the model from your table:
new TableRowSorter<>(table.getModel());
Also since model does not reflect column count from columnModel, you need to initialize your JTable with model that does already know column count like:
JTable table = new JTable(new DefaultTableModel(0, 3));
or the one with column names:
JTable table = new JTable(new DefaultTableModel(new String[]{"Name", "Age", "City"}, 0));
For DefaultTableModel you can even set column count later, not in constructor.
DefaultTableModel model = (DefaultTableModel) table.getModel();
model.setColumnCount(3);
TableModel
, notTableColumnModel
– Cagepi Commented Mar 3 at 7:08JTable
so if that code is meant to be a minimal reproducible example, I don't see how it is. What am I missing? – Abra Commented Mar 3 at 8:05