How to remove a JTable column from both TableModel and TableColumnModel with setAutoCreateColumnsFromModel(false)?

Serg :

See at the end my SSCCE code. What I'm trying to achieve is:

  • Equal number of columns in the data model and columns model
  • Using setAutoCreateColumnsFromModel(false) to avoid recreation of columns model when a column is added or removed by the data/table model
  • Ability to move columns

The large button adds a new column to the end. Each new column gets a unique identifier.

enter image description here

The header has a right click menu HeaderMenu to remove columns. A timer calls table.tableModel.addRow() to insert a new row on the top. The data for each column is generated by class Column. In this demo the value is simply a rows counter with column's identifier.

enter image description here

In the actual table (not this demo)

  • each column is a subclass of Column and generates meaningful data
  • the menu also contains insert left/right and replace. This is achieved using similar as in the demo add/remove methods and by moving a column to the desired position
  • the data model may contain a dozen of columns and over a million rows
  • rows may be added with a time interval between a millisecond to several seconds, i.e. performance matters

This demo demonstrates the problem with deletion of columns which generates errors like this:

Exception in thread "AWT-EventQueue-0" java.lang.ArrayIndexOutOfBoundsException: 2 >= 2
at java.util.Vector.elementAt(Vector.java:477)
at javax.swing.table.DefaultTableModel.getValueAt(DefaultTableModel.java:649)
at javax.swing.JTable.getValueAt(JTable.java:2720)
at javax.swing.JTable.prepareRenderer(JTable.java:5712)
at javax.swing.plaf.basic.BasicTableUI.paintCell(BasicTableUI.java:2114)
...

Please advise how to fix it. Here is the entire code:

import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.Point;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import java.util.Vector;

import javax.swing.BorderFactory;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JMenuItem;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.SwingUtilities;
import javax.swing.table.DefaultTableModel;
import javax.swing.table.TableColumn;

@SuppressWarnings("serial")
public class TableDemo extends JTable {

    private static class Column {

        private int rowsCounter = 0;
        private final String identifier;

        public Column(String identifier) {
            this.identifier = identifier;
        }

        private String nextCellValue() {
            return (rowsCounter++) + ", id: " + identifier;
        }
    }

    private static class MyTableModel extends DefaultTableModel {

        private final List<Column> columns = new ArrayList<>();
        private int nextColumnIdentifier = 0;

        private void addRow() {
            Object[] row = columns.stream().map(Column::nextCellValue).toArray();
            insertRow(0, row);
        }

        private TableColumn addColumn() {
            String identifier = String.valueOf(nextColumnIdentifier++);
            columns.add(new Column(identifier));
            addColumn(identifier);
            TableColumn tc = new TableColumn();
            tc.setIdentifier(identifier);
            tc.setHeaderValue(identifier);
            tc.setModelIndex(columns.size() - 1);
            return tc;
        }

        private void removeColumn(int idx) {
            columns.remove(idx);
            columnIdentifiers.remove(idx);
            for (Object row : dataVector) {
                ((Vector<?>) row).remove(idx);
            }
            fireTableStructureChanged();
        }
    }

    private static class HeaderMenu extends JPopupMenu {

        private int columnViewIndex;

        private HeaderMenu(final TableDemo table) {
            JMenuItem item = new JMenuItem("Delete column");
            item.addActionListener(e -> table.deleteColumn(columnViewIndex));
            add(item);

            final MouseAdapter ma = new MouseAdapter() {

                boolean dragged = false;

                @Override
                public void mouseReleased(MouseEvent e) {
                    if (!dragged && e.getButton() == MouseEvent.BUTTON3) {
                        final Point p = e.getPoint();
                        SwingUtilities.invokeLater(() -> {
                            columnViewIndex = table.columnAtPoint(p);
                            show(e.getComponent(), p.x, p.y);
                        });
                    }
                    dragged = false;
                }

                @Override
                public void mouseDragged(MouseEvent e) {
                    dragged = true;
                }
            };

            table.getTableHeader().addMouseListener(ma);
            table.getTableHeader().addMouseMotionListener(ma);
        }
    }

    private MyTableModel tableModel = new MyTableModel();

    private TableDemo() {
        new HeaderMenu(this);
        setModel(tableModel);
        setAutoCreateColumnsFromModel(false);
        setDefaultEditor(Object.class, null);
    }

    private void addColumn() {
        TableColumn tc = tableModel.addColumn();
        addColumn(tc);
    }

    void deleteColumn(int idxView) {
        TableColumn tc = getColumnModel().getColumn(idxView);
        tableModel.removeColumn(tc.getModelIndex());
        removeColumn(tc);
    }

    private static void buildAndShowGui() {
        TableDemo table = new TableDemo();
        table.setPreferredScrollableViewportSize(new Dimension(800, 300));
        table.setFillsViewportHeight(true);
        JScrollPane tableScrollPane = new JScrollPane(table);
        JButton buttonAdd = new JButton("Add column");
        buttonAdd.addActionListener(e -> table.addColumn());
        int gaps = 10;
        JPanel panel = new JPanel(new BorderLayout(gaps, gaps));
        panel.setBorder(BorderFactory.createEmptyBorder(gaps, gaps, gaps, gaps));
        panel.add(buttonAdd, BorderLayout.NORTH);
        panel.add(tableScrollPane, BorderLayout.CENTER);
        JFrame frame = new JFrame(table.getClass().getSimpleName());
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setContentPane(panel);
        frame.pack();
        frame.setVisible(true);

        new Timer().schedule(new TimerTask() {

            @Override
            public void run() {
                SwingUtilities.invokeLater(() -> table.tableModel.addRow());
            }
        }, 500, 100);
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(() -> buildAndShowGui());
    }
}
Serg :

The problem is that after removal of a column from some modelIndex in the table/data model, the TableColumn#getModelIndex() of some of the columns in the columns model may become shifted by 1. Here is an example, suppose the table has 3 columns and the code below produces 0 1 2:

for (int i = 0; i < getColumnModel().getColumnCount(); i++) {
    System.out.print(getColumnModel().getColumn(i).getModelIndex() + " ");
}

Then after removal of the column 1 from both data model and column model, the output of this code becomes: 0 2. Therefore JTable produces ArrayIndexOutOfBoundsException when accessing non-existing column 2 in the data model. This is why removal of the last column didn't generate an error.

The solution is to shift the modelIndex of the columns on the right side of the removed column. This can be done with a custom TableColumnModel:

private static class MyColumnsModel extends DefaultTableColumnModel {

    private TableColumn deleteColumn(int idxView) {
        if (selectionModel != null) {
            selectionModel.removeIndexInterval(idxView, idxView);
        }
        TableColumn tc = tableColumns.remove(idxView);
        tc.removePropertyChangeListener(this);
        for (TableColumn tableColumn : tableColumns) {
            if (tableColumn.getModelIndex() > tc.getModelIndex()) {
                tableColumn.setModelIndex(tableColumn.getModelIndex() - 1);
            }
        }
        return tc;
    }
}

And with add / remove methods of the table as following:

private void addColumn() {
    TableColumn tc = tableModel.addColumn();
    addColumn(tc); // equal to columnsModel.addColumn(tc);
}

private void deleteColumn(int idxView) {
    TableColumn tc = columnsModel.deleteColumn(idxView);
    tableModel.removeColumn(tc.getModelIndex());
}

Here is the entire fixed code:

import java.awt.BorderLayout;
import java.awt.Dimension;
import java.awt.Point;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.List;
import java.util.Timer;
import java.util.TimerTask;
import java.util.Vector;

import javax.swing.BorderFactory;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JMenuItem;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.SwingUtilities;
import javax.swing.table.DefaultTableColumnModel;
import javax.swing.table.DefaultTableModel;
import javax.swing.table.TableColumn;

@SuppressWarnings("serial")
public class TableDemo extends JTable {

    private static class Column {

        private int rowsCounter = 0;
        private final String identifier;

        public Column(String identifier) {
            this.identifier = identifier;
        }

        private String nextCellValue() {
            return (rowsCounter++) + ", id: " + identifier;
        }
    }

    private static class MyTableModel extends DefaultTableModel {

        private final List<Column> columns = new ArrayList<>();
        private int nextColumnIdentifier = 0;

        private void addRow() {
            Object[] row = columns.stream().map(Column::nextCellValue).toArray();
            insertRow(0, row);
        }

        private TableColumn addColumn() {
            String identifier = String.valueOf(nextColumnIdentifier++);
            columns.add(new Column(identifier));
            addColumn(identifier);
            TableColumn tc = new TableColumn();
            tc.setIdentifier(identifier);
            tc.setHeaderValue(identifier);
            tc.setModelIndex(columns.size() - 1);
            return tc;
        }

        private void removeColumn(int idx) {
            columns.remove(idx);
            columnIdentifiers.remove(idx);
            for (Object row : dataVector) {
                ((Vector<?>) row).remove(idx);
            }
            fireTableStructureChanged();
        }
    }

    private static class MyColumnsModel extends DefaultTableColumnModel {

        private TableColumn deleteColumn(int idxView) {
            if (selectionModel != null) {
                selectionModel.removeIndexInterval(idxView, idxView);
            }
            TableColumn tc = tableColumns.remove(idxView);
            tc.removePropertyChangeListener(this);
            for (TableColumn tableColumn : tableColumns) {
                if (tableColumn.getModelIndex() > tc.getModelIndex()) {
                    tableColumn.setModelIndex(tableColumn.getModelIndex() - 1);
                }
            }
            return tc;
        }
    }

    private static class HeaderMenu extends JPopupMenu {

        private int columnViewIndex;

        private HeaderMenu(final TableDemo table) {
            JMenuItem item = new JMenuItem("Delete column");
            item.addActionListener(e -> table.deleteColumn(columnViewIndex));
            add(item);

            final MouseAdapter ma = new MouseAdapter() {

                boolean dragged = false;

                @Override
                public void mouseReleased(MouseEvent e) {
                    if (!dragged && e.getButton() == MouseEvent.BUTTON3) {
                        final Point p = e.getPoint();
                        SwingUtilities.invokeLater(() -> {
                            columnViewIndex = table.columnAtPoint(p);
                            show(e.getComponent(), p.x, p.y);
                        });
                    }
                    dragged = false;
                }

                @Override
                public void mouseDragged(MouseEvent e) {
                    dragged = true;
                }
            };

            table.getTableHeader().addMouseListener(ma);
            table.getTableHeader().addMouseMotionListener(ma);
        }
    }

    private MyTableModel tableModel = new MyTableModel();
    private MyColumnsModel columnsModel = new MyColumnsModel();

    private TableDemo() {
        new HeaderMenu(this);
        setModel(tableModel);
        setColumnModel(columnsModel);
        setAutoCreateColumnsFromModel(false);
        setDefaultEditor(Object.class, null);
    }

    private void addColumn() {
        TableColumn tc = tableModel.addColumn();
        addColumn(tc); // equal to columnsModel.addColumn(tc);
    }

    private void deleteColumn(int idxView) {
        TableColumn tc = columnsModel.deleteColumn(idxView);
        tableModel.removeColumn(tc.getModelIndex());
    }

    private static void buildAndShowGui() {
        TableDemo table = new TableDemo();
        table.setPreferredScrollableViewportSize(new Dimension(800, 300));
        table.setFillsViewportHeight(true);
        JScrollPane tableScrollPane = new JScrollPane(table);
        JButton buttonAdd = new JButton("Add column");
        buttonAdd.addActionListener(e -> table.addColumn());
        int gaps = 10;
        JPanel panel = new JPanel(new BorderLayout(gaps, gaps));
        panel.setBorder(BorderFactory.createEmptyBorder(gaps, gaps, gaps, gaps));
        panel.add(buttonAdd, BorderLayout.NORTH);
        panel.add(tableScrollPane, BorderLayout.CENTER);
        JFrame frame = new JFrame(table.getClass().getSimpleName());
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setContentPane(panel);
        frame.pack();
        frame.setVisible(true);

        new Timer().schedule(new TimerTask() {

            @Override
            public void run() {
                SwingUtilities.invokeLater(() -> table.tableModel.addRow());
            }
        }, 500, 100);
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(() -> buildAndShowGui());
    }
}

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=141202&siteId=1