Search code examples
javaswinguser-interfacejtablejscrollpane

JTable which is scrollable horizontally AND grows to fill parent container


I want to put a JTable in a JScrollPane and want it to:

  • be scrollable horizontally and vertically (only if it has content that makes it larger than parent container)
  • automatically grow to fill parent container if needed (in both directions)
  • only have either/both scroll bar(s) when needed

The JList behaves exactly like this, btw. But I have found no easy way to accomplish all goals simultaneously with a JTable. The easy way to accomplish the first is to disable auto-resizing, but that then prevents the second from occurring. I've come up with a very hacky solution which adds a component listener to the parent container of the JScrollPane and resizes the table when that container is resized. However, I've had to add some "fudge factors" (see 4x comments) to the resize method, altering my height/width checks by small pixel amounts because of some inherent inaccuracy I guess. Without these fudge factors, the scrollbars either appear too soon (before they are needed), or the very end of long table cells gets cut off even with the scrollbar present. The worst part about these hacky fudge factors is that if you change up the look and feel of the GUI, then the entire solution stops working so cleanly (i.e. the fudge factors would need to change). Which is obviously very far from ideal.

There has got to be a cleaner way to accomplish this. Can anybody help? Run the attached code and drag the size of the entire frame around to see the table update. This code uses the metal look and feel which seems to work on my Windows 7 machine with the fudge factors chose, but I wouldn't be surprised if it didn't work as cleanly on another OS.

import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.EventQueue;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;

import javax.swing.BoxLayout;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.ScrollPaneConstants;
import javax.swing.UIManager;
import javax.swing.table.DefaultTableModel;
import javax.swing.table.TableCellRenderer;

public class JTableScrollTest {

    private JFrame frame;
    private JPanel panel;
    private DefaultTableModel tableModel;
    private JTable table;
    private JScrollPane scrollPane;

    /**
     * Launch the application.
     */
    public static void main(String[] args) {
        try {
            UIManager.setLookAndFeel("javax.swing.plaf.metal.MetalLookAndFeel");
        } catch (Throwable e) {
            e.printStackTrace();
        }

        EventQueue.invokeLater(new Runnable() {
            public void run() {
                try {
                    JTableScrollTest window = new JTableScrollTest();
                    window.frame.setVisible(true);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        });
    }

    /**
     * Create the application.
     */
    public JTableScrollTest() {
        initialize();
    }

    /**
     * Initialize the contents of the frame.
     */
    private void initialize() {
        frame = new JFrame();
        frame.setBounds(100, 100, 450, 300);
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

        panel = new JPanel();
        frame.getContentPane().add(panel, BorderLayout.CENTER);
        panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));

        scrollPane = new JScrollPane();
        panel.add(scrollPane);

        tableModel = new DefaultTableModel(new Object[]{"Stuff"},0);
        tableModel.addRow(new Object[]{"reeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeally long string"});
        tableModel.addRow(new Object[]{"a"});
        tableModel.addRow(new Object[]{"a"});
        tableModel.addRow(new Object[]{"a"});
        tableModel.addRow(new Object[]{"a"});
        tableModel.addRow(new Object[]{"a"});
        tableModel.addRow(new Object[]{"a"});
        tableModel.addRow(new Object[]{"a"});
        tableModel.addRow(new Object[]{"a"});
        tableModel.addRow(new Object[]{"a"});
        tableModel.addRow(new Object[]{"a"});
        tableModel.addRow(new Object[]{"a"});
        tableModel.addRow(new Object[]{"LAST ITEM"});
        table = new JTable(tableModel);
        table.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
        scrollPane.setViewportView(table);

        resizeTable();
        panel.addComponentListener(new ComponentAdapter() {
            @Override
            public void componentResized(ComponentEvent evt) {
                resizeTable();
            }
        });
    }

    public void resizeTable() {
        int width = 0;
        int height = 0;
        for (int row = 0; row < table.getRowCount(); row++) {
            TableCellRenderer renderer = table.getCellRenderer(row, 0);
            Component comp = table.prepareRenderer(renderer, row, 0);
            width = Math.max (comp.getPreferredSize().width, width);
            height += comp.getPreferredSize().height;
        }
        if (width > panel.getWidth() - 2) { // fudge factor
            width += 4; // fudge factor
            scrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_ALWAYS);
        } else {
            width = panel.getWidth();
            scrollPane.setHorizontalScrollBarPolicy(ScrollPaneConstants.HORIZONTAL_SCROLLBAR_NEVER);
        }
        if (height > panel.getHeight() + 4) { // fudge factor
            height -= 26; // fudge factor
            scrollPane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS);
        } else {
            height = panel.getHeight();
            scrollPane.setVerticalScrollBarPolicy(ScrollPaneConstants.VERTICAL_SCROLLBAR_NEVER);
        }
        table.setPreferredSize(new Dimension(width, height));
    }
}

Solution

  • I had the same issue. The table just makes the columns larger or smaller rather that show a scrollbar. Through trial and error, the hack I came up with is a ComponentListener that you attach to the ScrollPane.

    The trick is to turn off the table's auto resize once the scroll pane is smaller than the table's preferred size. Note that you shouldn't set the table's preferred size directly. Leave that alone to be automatically calculated. If you need certain columns larger or smaller, set their width via the column model (e.g. table.getColumnModel().getColumn(0).setMaxWidth(25)).

    This class also includes another TableModelListener that performs the same logic when columns are added to the table.

    public final class ScrollingTableFix implements ComponentListener {
      private final JTable table;
    
      public ScrollingTableFix(JTable table, JScrollPane scrollPane) {
        assert table != null;
    
        this.table = table;
    
        table.getModel().addTableModelListener(new ColumnAddFix(table, scrollPane));
      }
    
      public void componentHidden(final ComponentEvent event) {}
    
      public void componentMoved(final ComponentEvent event) {}
    
      public void componentResized(final ComponentEvent event) {
        // turn off resize and let the scroll bar appear once the component is smaller than the table
        if (event.getComponent().getWidth() < table.getPreferredSize().getWidth()) {
            table.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
        }
        // otherwise resize new columns in the table
        else {
            table.setAutoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS);
        }
      }
    
      public void componentShown(final ComponentEvent event) {}
    
      // similar behavior is needed when columns are added to the table
      private static final class ColumnAddFix implements TableModelListener {
        private final JTable table;
        private final JScrollPane scrollPane;
    
        ColumnAddFix(JTable table, JScrollPane scrollPane) {
          this.table = table;
          this.scrollPane = scrollPane;
         }
    
        @Override
        public void tableChanged(TableModelEvent e) {
          if (e.getFirstRow() == TableModelEvent.HEADER_ROW) {
            if (scrollPane.getViewport().getWidth() < table.getPreferredSize().getWidth()) {
              table.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
            }
            else {
              table.setAutoResizeMode(JTable.AUTO_RESIZE_ALL_COLUMNS);
            }
          }
         }
       }
    }
    

    To use this, simply wire everything up like so:

    JTable table = new JTable();
    JScrollPane scroller = new JScrollPane(table);
    scroller.addComponentListener(new ScrollingTableFix(table, scroller));