Search code examples
javaswingjtablegridbaglayoutjtableheader

Empty JTables inside a scrollable (GridBagLayout) panel - header collapses on column resize


I'm attempting to create a "sectioned" table, which are actually multiple tables laid out within a "scrollable" JPanel via GridBagLayout. The tables share the same table model (class), table header and column model, the JTableHeader is set as column header view of a JScrollPane that contains everything. There is only one JScrollPane.

frame (BorderLayout)
    |- JScrollPane
        |- JPanel (GridBagLayout)
            |- Section title panel
            |- JTable 1
            |- Section title panel
            |- JTable 2
            |- JTable (fake)
            |- vertical filler

Everything seemed to work as expected, until I tried to resize an arbitrary column with no values in either of the tables - if the tables had at least one row, it worked as expected. I considered having "null" rows in the table, but that interferes with filtering, sorting, etc. So I changed the code to contain an "obscured" fake table, that is meant to keep the header behaving nicely by always having one row.

This does not work however. As soon as one of the tables is empty and a column resize is attempted, the table header is corrupted (one of the columns shrinks).

enter image description here

Why is this happening and what can I do about it?

import java.awt.BorderLayout;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.LayoutManager;
import java.awt.Rectangle;
import javax.swing.BorderFactory;
import javax.swing.Box;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.Scrollable;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.table.DefaultTableModel;
import javax.swing.table.JTableHeader;
import javax.swing.table.TableColumnModel;
import javax.swing.table.TableModel;

public class SectionTables extends JFrame {

    public SectionTables() {
        setDefaultCloseOperation(EXIT_ON_CLOSE);
        setLayout(new BorderLayout());

        GridBagConstraints gbc;

        JPanel tables = new ScrollableJPanel(new GridBagLayout());
        JScrollPane scrollPane = new JScrollPane(tables);

        JPanel section1Title = new JPanel(new BorderLayout());     
        section1Title.setBorder(BorderFactory.createMatteBorder(1, 1, 1, 1, UIManager.getColor("controlShadow")));
        JLabel section1 = new JLabel("Section One", null, JLabel.CENTER);
        section1Title.add(section1);
        gbc = new GridBagConstraints();
        gbc.gridx = 0;
        gbc.gridy = 0;
        gbc.weightx = 1.0d;
        gbc.fill = GridBagConstraints.HORIZONTAL;
        tables.add(section1Title, gbc);

        MyTableModel model1 = new MyTableModel();
        JTable table1 = new MyTable(model1);
        gbc = new GridBagConstraints();
        gbc.gridx = 0;
        gbc.gridy = 1;
        gbc.weightx = 1.0d;
        gbc.fill = GridBagConstraints.HORIZONTAL;
        tables.add(table1, gbc);

        JPanel section2Title = new JPanel(new BorderLayout());     
        section2Title.setBorder(BorderFactory.createMatteBorder(1, 1, 1, 1, UIManager.getColor("controlShadow")));
        JLabel section2 = new JLabel("Section Two", null, JLabel.CENTER);
        section2Title.add(section2);
        gbc = new GridBagConstraints();
        gbc.gridx = 0;
        gbc.gridy = 2;
        gbc.weightx = 1.0d;
        gbc.fill = GridBagConstraints.HORIZONTAL;
        tables.add(section2Title, gbc);

        MyTableModel model2 = new MyTableModel();
        JTable table2 = new MyTable(model2);   
        gbc = new GridBagConstraints();
        gbc.gridx = 0;
        gbc.gridy = 3;
        gbc.weightx = 1.0d;
        gbc.fill = GridBagConstraints.HORIZONTAL;
        tables.add(table2, gbc);

        MyTableModel modelFake = new MyTableModel();
        modelFake.addRow(new String[] {"", "", ""});
        JTable tableFake = new MyObscuredTable();   
        tableFake.setModel(modelFake);
        gbc = new GridBagConstraints();
        gbc.gridx = 0;
        gbc.gridy = 4;
        gbc.weightx = 1.0d;
        gbc.fill = GridBagConstraints.HORIZONTAL;
        tables.add(tableFake, gbc);

        Box.Filler filler1 = new Box.Filler(new Dimension(0, 0), new Dimension(0, 0), new Dimension(0, 32767));
        gbc = new GridBagConstraints();
        gbc.gridx = 0;
        gbc.gridy = 5;
        gbc.weighty = 1.0d;
        gbc.fill = GridBagConstraints.VERTICAL;
        tables.add(filler1, gbc);

        add(scrollPane);

        TableColumnModel columnModel = table1.getColumnModel();
        table2.setColumnModel(columnModel);
        tableFake.setColumnModel(columnModel);

        JTableHeader tableHeader = new JTableHeader(columnModel);
        scrollPane.setColumnHeaderView(tableHeader);
        table1.setTableHeader(tableHeader);
        table2.setTableHeader(tableHeader);
        tableFake.setTableHeader(tableHeader);


        // if tables are filled, the issue does not occur
//        model1.addRow(new String[] {"", "", ""});
//        model2.addRow(new String[] {"", "", ""});

        pack();
        setLocationRelativeTo(null);
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {
            @Override
            public void run() {
                new SectionTables().setVisible(true);
            }
        });
    }

    private class MyTableModel extends DefaultTableModel {
        private String[] columnNames = new String[] {"First", "Second", "Third"};
        private Class[] columnClasses = new Class[] {String.class, String.class, String.class};

        @Override
        public int getColumnCount() {
            return columnNames.length;
        }

        @Override
        public String getColumnName(int column) {
            return columnNames[column];
        }

        @Override
        public Class<?> getColumnClass(int columnIndex) {
            return columnClasses[columnIndex];
        }

        @Override
        public boolean isCellEditable(int row, int column) {
            return false;
        }        

    }

    private class ScrollableJPanel extends JPanel implements Scrollable {

        public ScrollableJPanel(LayoutManager layout) {
            super(layout);
        }

        public ScrollableJPanel() {
            super();
        }

        @Override
        public Dimension getPreferredScrollableViewportSize() {
            return getPreferredSize();
        }

        @Override
        public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) {
            return 16;
        }

        @Override
        public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction) {
            return 16;
        }

        @Override
        public boolean getScrollableTracksViewportWidth() {
            return true;
        }

        @Override
        public boolean getScrollableTracksViewportHeight() {
            return false;
        }

    }

    private class MyObscuredTable extends JTable {

        @Override
        public Dimension getPreferredSize() {
            int height; 
            height = 1; // obscure null row           
            Container ancestorOfClass = SwingUtilities.getAncestorOfClass(JPanel.class, this);
            int width = ancestorOfClass.getWidth();
            return new Dimension(width, height);
        }

    }

    private class MyTable extends JTable {

        public MyTable(TableModel model) {
            super(model);
        }

        @Override
        public Dimension getPreferredSize() {
            int height; 
            height = getRowHeight() * getRowCount();
            Container ancestorOfClass = SwingUtilities.getAncestorOfClass(JPanel.class, this);
            int width = ancestorOfClass.getWidth();
            return new Dimension(width, height);
        }

    }
}

Solution

  • Maybe you can modify this approach which uses a TableColumnModelListener to keep the column widths in sync when using multiple tables:

    import java.awt.*;
    import java.util.*;
    import javax.swing.*;
    import javax.swing.event.*;
    import javax.swing.table.*;
    
    public class TableColumnsShared implements Runnable
    {
      JTable table1, table2;
      TableColumnModelListener columnListener1, columnListener2;
      Map<JTable, TableColumnModelListener> map;
    
      public static void main(String[] args)
      {
        SwingUtilities.invokeLater(new TableColumnsShared());
      }
    
      public void run()
      {
        Vector<String> names = new Vector<String>();
        names.add("One");
        names.add("Two");
        names.add("Three");
    
        table1 = new JTable(null, names);
        table2 = new JTable(null, names);
    
        columnListener1 = new ColumnChangeListener(table1, table2);
        columnListener2 = new ColumnChangeListener(table2, table1);
    
        table1.getColumnModel().addColumnModelListener(columnListener1);
        table2.getColumnModel().addColumnModelListener(columnListener2);
    
        map = new HashMap<JTable, TableColumnModelListener>();
        map.put(table1, columnListener1);
        map.put(table2, columnListener2);
    
        JPanel p = new JPanel(new GridLayout(2,1));
        p.add(new JScrollPane(table1));
        p.add(new JScrollPane(table2));
    
        JFrame frame = new JFrame();
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.getContentPane().add(p);
        frame.setSize(300, 200);
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
      }
    
      class ColumnChangeListener implements TableColumnModelListener
      {
        JTable sourceTable;
        JTable targetTable;
    
        public ColumnChangeListener(JTable source, JTable target)
        {
          this.sourceTable = source;
          this.targetTable = target;
        }
    
        public void columnAdded(TableColumnModelEvent e) {}
        public void columnSelectionChanged(ListSelectionEvent e) {}
        public void columnRemoved(TableColumnModelEvent e) {}
        public void columnMoved(TableColumnModelEvent e) {}
    
        public void columnMarginChanged(ChangeEvent e)
        {
          TableColumnModel sourceModel = sourceTable.getColumnModel();
          TableColumnModel targetModel = targetTable.getColumnModel();
          TableColumnModelListener listener = map.get(targetTable);
    
          targetModel.removeColumnModelListener(listener);
    
          for (int i = 0; i < sourceModel.getColumnCount(); i++)
          {
            targetModel.getColumn(i).setPreferredWidth(sourceModel.getColumn(i).getWidth());
          }
    
          targetModel.addColumnModelListener(listener);
        }
      }
    }