Search code examples
javaswingjtablejscrollpanepreferredsize

Properly overriding getPreferredScrollableViewportSize


I have a JTable inside a JScrollPane, and I'm overriding getPreferredScrollableViewportSize in order to fit the JScrollPane to a fixed number of table rows, allowing scrollbars to appear when needed.

I also used parts of code found on SO to resize the table columns to fit their content.

The issue I'm having is that when I dynamically add a wider row to the table, the horizontal JScrollBar doesn't show.

I tried different ways to force the JTable, JViewport and JScrollPane to correctly update, calling revalidate (), or repaint (), doLayout(), etc., but i didn’t succeed.

Of course I could recall pack () method on JFrame, but it would cause the entire scrollpane to grow in width, and the horizontal scrollbar would still not appear.

What am I missing?

Here there is a MVCE, it is just an ugly toy example, but it should be enough to replicate the unwanted behaviour.

Thanks for your help !

Code :

import java.awt.*;
import java.awt.event.ActionEvent;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import javax.swing.table.*;
public class Main
{
    public static void main (String [] a) {
        SwingUtilities.invokeLater (new Runnable () {
            public void run () {
                initGUI ();
            }
        });
    }
    private static void initGUI () {
        final DataTable table = new DataTable ();
        JPanel container = new JPanel (new BorderLayout (0, 20));
        container.add (new JScrollPane (table));
        container.add (new JButton (new AbstractAction ("Add wider row") {
            public void actionPerformed (ActionEvent e) {
                table.addRow ();
                // some of many useless attempts ...
                // table.resizeColumnWidth ();
                // table.revalidate ();
            }
        }), BorderLayout.SOUTH);
        container.setBorder (new EmptyBorder (10, 10, 10, 10));
        JFrame frame = new JFrame ("Test");
        frame.setDefaultCloseOperation (JFrame.EXIT_ON_CLOSE);
        frame.setResizable (false);
        frame.setContentPane (container);
        frame.setLocationRelativeTo (null);
        frame.pack ();
        frame.setVisible (true);
    }
}
class DataTable extends JTable
{
    private static final int VISIBLE_ROWS = 5;
    
    DataTable () {
        String [][] data = new String [][] {
            {"SomeText", "SomeText"},
            {"SomeText", "SomeText"},
            {"SomeText", "SomeText"},
            {"SomeText", "SomeText"},
            {"SomeText", "SomeText"},
            {"SomeText", "SomeText"}
        };
        setModel (new DefaultTableModel (data, new String [] {"", ""}) {
            @Override public boolean isCellEditable (int row, int column) {
               return false;
            }
        });
        columnModel.setColumnMargin (20);
        setBorder (null);
        setFocusable (false);
        setRowSelectionAllowed (false);
        setShowGrid (false);
        setTableHeader (null);
        resizeColumnWidth ();
    }
    public void addRow () {
        ((DefaultTableModel) getModel ()).addRow (new String [] {"SomeLongerText", "SomeLongerText"});
    }
    private int calculateColumnWidth (int column) {
        TableColumn tableColumn = columnModel.getColumn (column);
        int width = 0;
        for (int row=0; row<getRowCount(); row++) width = Math.max (prepareRenderer (getCellRenderer (row, column), row, column).getPreferredSize ().width, width);
        return width + getIntercellSpacing ().width;
    }
    @Override public Dimension getPreferredScrollableViewportSize () {
        int columnsWidth = 0;
        for (int column=0; column<getColumnCount (); column++) columnsWidth += calculateColumnWidth (column);
        return new Dimension (columnsWidth, VISIBLE_ROWS * getRowHeight ());
    }
    protected void resizeColumnWidth () {
        for (int column=0; column<getColumnCount (); column++) columnModel.getColumn (column).setPreferredWidth (calculateColumnWidth (column));
    }
}

EDIT :

Thanks to @camickr, i solved the issue, setting the desired auto resize mode and recalling resizeColumnWidth method every time a row is added. I had already tried them separately, i can't believe i didn't use both :)

Hovewer, i post below the updated code, now it works well :

import java.awt.*;
import java.awt.event.ActionEvent;
import javax.swing.*;
import javax.swing.border.EmptyBorder;
import javax.swing.table.*;
public class Main
{
    public static void main (String [] a) {
        SwingUtilities.invokeLater (new Runnable () {
            public void run () {
                initGUI ();
            }
        });
    }
    private static void initGUI () {
        final DataTable table = new DataTable ();
        JPanel container = new JPanel (new BorderLayout (0, 20));
        container.add (new JScrollPane (table));
        container.add (new JButton (new AbstractAction ("Add wider row") {
            public void actionPerformed (ActionEvent e) {
                table.addRow ();
            }
        }), BorderLayout.SOUTH);
        container.setBorder (new EmptyBorder (10, 10, 10, 10));
        JFrame frame = new JFrame ("Test");
        frame.setDefaultCloseOperation (JFrame.EXIT_ON_CLOSE);
        frame.setResizable (false);
        frame.setContentPane (container);
        frame.setLocationRelativeTo (null);
        frame.pack ();
        frame.setVisible (true);
    }
}
class DataTable extends JTable
{
    private static final int VISIBLE_ROWS = 5;
    
    DataTable () {
        String [][] data = new String [][] {
            {"SomeText", "SomeText"},
            {"SomeText", "SomeText"},
            {"SomeText", "SomeText"},
            {"SomeText", "SomeText"},
            {"SomeText", "SomeText"},
            {"SomeText", "SomeText"}
        };
        setModel (new DefaultTableModel (data, new String [] {"", ""}) {
            @Override public boolean isCellEditable (int row, int column) {
               return false;
            }
        });
        columnModel.setColumnMargin (20);
        setAutoResizeMode (AUTO_RESIZE_OFF);
        setBorder (null);
        setFocusable (false);
        setRowSelectionAllowed (false);
        setShowGrid (false);
        setTableHeader (null);
        resizeColumnWidth ();
    }
    public void addRow () {
        ((DefaultTableModel) getModel ()).addRow (new String [] {"SomeLongerText", "SomeLongerText"});
        resizeColumnWidth ();
    }
    private int calculateColumnWidth (int column) {
        TableColumn tableColumn = columnModel.getColumn (column);
        int width = 0;
        for (int row=0; row<getRowCount(); row++) width = Math.max (prepareRenderer (getCellRenderer (row, column), row, column).getPreferredSize ().width, width);
        return width + getIntercellSpacing ().width;
    }
    @Override public Dimension getPreferredScrollableViewportSize () {
        int columnsWidth = 0;
        for (int column=0; column<getColumnCount (); column++) columnsWidth += calculateColumnWidth (column);
        return new Dimension (columnsWidth, VISIBLE_ROWS * getRowHeight ());
    }
    protected void resizeColumnWidth () {
        for (int column=0; column<getColumnCount (); column++) columnModel.getColumn (column).setPreferredWidth (calculateColumnWidth (column));
    }
}

Solution

  • when I dynamically add a wider row to the table, the horizontal JScrollBar doesn't show.

    A horizontal scrollbar will only occur when you use:

    setAutoResizeMode( JTable.AUTO_RESIZE_OFF );
    

    All columns will be displayed at their preferred size. They could be smaller than the viewport in which case you will see empty space or they could be larger in which case you will see the scrollbar.

    You will need to invoke your resizeColumnWidth() method in your ActionListener in order for the scrollbars to appear.

    You can also check out Table Column Adjuster which support dynamic column size calculation as data is added/changed in the TableModel.