I have a JTable
where the last row is the total row that aggregates all other rows. When the user clicks a column header on the table, rows are sorted by that c
Based on this example, the complete example example below illustrates the approach in the accepted answer. In particular,
The implementation of getRowCount()
in the TableModel
returns the size()
of the List<Employee>
, which is one less than the number of rows displayed in the table.
The implementation of getValueAt()
in the TableModel
illustrates the call to sum()
in the COLUMN_SALARY
and some possible display choices for the remaining columns in the extra row at the bottom.
The implementation of getViewRowCount()
in the TableRowSorter
enables the display of the extra row but precludes reasonable filtering.
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.text.NumberFormat;
import java.util.ArrayList;
import java.util.List;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.table.TableModel;
import javax.swing.table.TableRowSorter;
/**
* @see https://stackoverflow.com/a/37913520/230513
* @see https://stackoverflow.com/a/37892395/230513
*/
public class JTableColumnTotalExample {
public static void main(String[] args) {
EventQueue.invokeLater(JTableColumnTotalExample::display);
}
public static void display() {
JFrame f = new JFrame("JTable Sorting Example");
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
List<Employee> listEmployees = createListEmployees();
TableModel model = new EmployeeTableModel(listEmployees);
JTable table = new JTable(model) {
@Override
public Dimension getPreferredScrollableViewportSize() {
return new Dimension(500, getRowCount() * getRowHeight());
}
};
table.getColumnModel().getColumn(3).setCellRenderer(new CurrencyFormatter());
TableRowSorter sorter = new TableRowSorter<TableModel>(model) {
@Override
public int convertRowIndexToModel(int index) {
int maxRow = super.getViewRowCount();
if (index >= maxRow) {
return index;
}
return super.convertRowIndexToModel(index);
}
@Override
public int convertRowIndexToView(int index) {
int maxRow = super.getModelRowCount();
if (index > maxRow) {
return index;
}
return super.convertRowIndexToView(index);
}
@Override
public int getViewRowCount() {
return super.getViewRowCount() + 1;
}
};
table.setRowSorter(sorter);
f.add(new JScrollPane(table), BorderLayout.CENTER);
f.pack();
f.setLocationRelativeTo(null);
f.setVisible(true);
}
private static List<Employee> createListEmployees() {
List<Employee> listEmployees = new ArrayList<>();
listEmployees.add(new Employee("Peter", "Manager", 40000));
listEmployees.add(new Employee("Paul", "Programmer", 25000));
listEmployees.add(new Employee("Mary", "Designer", 25000));
listEmployees.add(new Employee("Donald", "Leader", 30000));
listEmployees.add(new Employee("Tom", "Designer", 28000));
listEmployees.add(new Employee("Samantha", "Analyst", 50000));
listEmployees.add(new Employee("Jerome", "Programmer", 32000));
listEmployees.add(new Employee("Jonathon", "Developer", 29000));
listEmployees.add(new Employee("Kevin", "Programmer", 23000));
listEmployees.add(new Employee("Anthony", "Programmer", 23000));
listEmployees.add(new Employee("John", "Designer", 33000));
listEmployees.add(new Employee("David", "Developer", 28000));
listEmployees.add(new Employee("Harry", "Designer", 31000));
listEmployees.add(new Employee("Charles", "Programmer", 26000));
listEmployees.add(new Employee("Joseph", "Manager", 40000));
return listEmployees;
}
private static class EmployeeTableModel extends AbstractTableModel {
private static final int COLUMN_NUM = 0;
private static final int COLUMN_NAME = 1;
private static final int COLUMN_JOB = 2;
private static final int COLUMN_SALARY = 3;
private final String[] columnNames = {"No", "Name", "Job", "Salary"};
private final List<Employee> listEmployees;
public EmployeeTableModel(List<Employee> listEmployees) {
this.listEmployees = listEmployees;
int indexCount = 1;
for (Employee employee : listEmployees) {
employee.setIndex(indexCount++);
}
}
@Override
public int getColumnCount() {
return columnNames.length;
}
@Override
public int getRowCount() {
return listEmployees.size();
}
@Override
public String getColumnName(int columnIndex) {
return columnNames[columnIndex];
}
@Override
public Class<?> getColumnClass(int columnIndex) {
return getValueAt(0, columnIndex).getClass();
}
@Override
public Object getValueAt(int rowIndex, int columnIndex) {
if (rowIndex == getRowCount()) {
switch (columnIndex) {
case COLUMN_NUM:
return 999_999_999;
case COLUMN_NAME:
return "Total";
case COLUMN_JOB:
return "Salary";
case COLUMN_SALARY:
return sum();
}
}
Employee employee = listEmployees.get(rowIndex);
switch (columnIndex) {
case COLUMN_NUM:
return employee.getIndex();
case COLUMN_NAME:
return employee.getName();
case COLUMN_JOB:
return employee.getJob();
case COLUMN_SALARY:
return employee.getSalary();
default:
throw new IllegalArgumentException("Invalid column index");
}
}
private int sum() {
int sum = 0;
for (int r = 0; r < getRowCount(); r++) {
sum += (int) getValueAt(r, COLUMN_SALARY);
}
return sum;
}
}
private static class Employee {
private int index;
private String name;
private String job;
private int salary;
public Employee(String name, String job, int salary) {
this.name = name;
this.job = job;
this.salary = salary;
}
public int getIndex() {
return index;
}
public void setIndex(int index) {
this.index = index;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getJob() {
return job;
}
public void setJob(String job) {
this.job = job;
}
public int getSalary() {
return salary;
}
public void setSalary(int age) {
this.salary = age;
}
}
private static class CurrencyFormatter extends DefaultTableCellRenderer {
private NumberFormat numberFormat = NumberFormat.getCurrencyInstance();
@Override
public Component getTableCellRendererComponent(JTable jTable, Object value,
boolean isSelected, boolean hasFocus, int row, int column) {
Component c = super.getTableCellRendererComponent(jTable, value,
isSelected, hasFocus, row, column);
if (c instanceof JLabel && value instanceof Number) {
JLabel label = (JLabel) c;
label.setHorizontalAlignment(JLabel.RIGHT);
Number num = (Number) value;
String text = numberFormat.format(num);
label.setText(text);
}
return c;
}
}
}
Personally, I would create a single-row 2nd table with the header removed, and place it directly underneath the main table, so as to create the illusion of a last row.
Besides it solving your sorting problem, it will also persist as the user scrolls that main table, which is probably a good thing since it's a "totals" row.
You could even add a ColumnModelListener
to the main table's TableColumnModel
to synch up column resizing.
EDIT: Here's the general idea:
import java.awt.*;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.table.*;
public class TestFrame implements Runnable
{
JTable mainTable;
JTable fixedTable;
public static void main(String[] args)
{
SwingUtilities.invokeLater(new TestFrame());
}
public void run()
{
mainTable = new JTable(8, 3);
mainTable.getTableHeader().setReorderingAllowed(false);
mainTable.setAutoCreateRowSorter(true);
for (int r = 0; r < 8; r++)
{
for (int c = 0; c < 3; c++)
{
mainTable.setValueAt((int)(Math.random()*100), r, c);
}
}
mainTable.getColumnModel().addColumnModelListener(
new TableColumnModelListener()
{
public void columnAdded(TableColumnModelEvent e) {}
public void columnRemoved(TableColumnModelEvent e) {}
public void columnMoved(TableColumnModelEvent e) {}
public void columnSelectionChanged(ListSelectionEvent e) {}
public void columnMarginChanged(ChangeEvent e)
{
synchColumnSizes();
}
});
setVisibleRowCount(mainTable, 5);
JScrollPane scroll = new JScrollPane(mainTable);
scroll.setVerticalScrollBarPolicy(JScrollPane.VERTICAL_SCROLLBAR_ALWAYS);
fixedTable = new JTable(1, 3);
fixedTable.setValueAt("will not sort or", 0, 0);
fixedTable.setValueAt("scroll but will", 0, 1);
fixedTable.setValueAt("resize with main", 0, 2);
JPanel p = new JPanel(new GridBagLayout());
p.setBorder(BorderFactory.createTitledBorder("Fixed Last Row"));
GridBagConstraints gbc = new GridBagConstraints();
gbc.anchor = GridBagConstraints.WEST;
p.add(scroll, gbc);
gbc.gridy = 1;
p.add(fixedTable, gbc);
JFrame f = new JFrame();
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
f.getContentPane().add(p, BorderLayout.CENTER);
f.pack();
f.setLocationRelativeTo(null);
f.setVisible(true);
}
private void synchColumnSizes()
{
TableColumnModel tcmMain = mainTable.getColumnModel();
TableColumnModel tcmFixed = fixedTable.getColumnModel();
for (int i = 0; i < tcmMain.getColumnCount(); i++)
{
int width = tcmMain.getColumn(i).getWidth();
tcmFixed.getColumn(i).setPreferredWidth(width);
}
}
public static void setVisibleRowCount(JTable table, int rows)
{
table.setPreferredScrollableViewportSize(new Dimension(
table.getPreferredScrollableViewportSize().width,
rows * table.getRowHeight()));
}
}
The following solution worked for me....
In your table model update the getRowCount()
member to return 1 row less than required.
Then modify the index and row counts reported by your sorter as follows...
TableRowSorter<TableModel> sorter =
new DefaultTableRowSorter<TableModel>(this.getModel())
{
public int convertRowIndexToModel(int index)
{
int maxRow = super.getViewRowCount();
if (index >= maxRow)
return index;
return super.convertRowIndexToModel(index);
}
public int convertRowIndexToView(int index)
{
int maxRow = super.getModelRowCount();
if (index > maxRow)
return index;
return super.convertRowIndexToView(index);
}
public int getViewRowCount()
{
return super.getViewRowCount() + 1;
}
};
myTable.setRowSorter(sorter);
@Das: maybe this version is better since you do not need to override the getRowCount(), which may be used and maybe causing problems by other functions
public class TableRowSorterTotal<MyTableModel extends TableModel> extends TableRowSorter<TableModel>
{
TableRowSorterTotal(MyTableModel model)
{
super(model);
}
public int convertRowIndexToModel(int index)
{
int maxRow = super.getViewRowCount();
int currModel = super.convertRowIndexToModel(index);
int maxModel = super.convertRowIndexToModel(maxRow-1);
if(currModel == maxModel)
return maxRow - 1;
if(currModel > maxModel)
return currModel- 1;
return currModel;
}
public int convertRowIndexToView(int index)
{
int maxRow = super.getModelRowCount();
int currView= super.convertRowIndexToView(index);
int maxView = super.convertRowIndexToView(maxRow-1);
if(currView == maxView)
return maxRow - 1;
if(currView > maxView)
return currView- 1;
return currView;
}
}
Is there a simple way to implement this with TableRowSorter?
not isn't simple
good idea, quite is possible to set flag for RowSorter, its SortingKeys
there is bug for all ClassTypes except String instance