Making a JTable fill the view without extension

October 5, 2008


Ahhh, the old table-doesn’t-fill-the-view problem. Nothing says “this is a Swing app” more than a JTable that only fills up part of it’s parent JViewport. Apparently this behavior was not the original behavior (as I had once thought), but was introduced, presumably as a bug, back in Java 1.2 (see this bug report). This behavior has been fixed in Java 6, though not by default – you have to call JTable.setFillsViewportHeight(true).

JTable not filling view JTable filling view with custom Viewport border
Default fill behavior Fixed fill behavior

Most work-arounds to this problem involve sub-classing JTable, and overriding getScrollableTracksViewportHeight() like this:

    @Override
    public boolean getScrollableTracksViewportHeight() {
        return getParent() instanceof JViewport
                && getPreferredSize().height < getParent().getHeight();
    }

This works great if you have control over the actual JTable. But if you’re crafting a UI delegate to be used with a table, your out of luck – at least with this technique.

There is, however, another way to make a table, or any other component that sits inside a JViewport, fill the view bounds. To do this, you can create an extension of ViewportLayout where you can add the corrected fill logic. You’ll also need to add a property listener to the ancestor property of the component sitting inside the JViewport. Listening to this property will give you a hook from which to install the custom layout on the containing JScrollPane‘s JViewport. The ancestor property is fired whenever the component is added as a child to another component.

Here’s a simple table UI delegate that paints row striping:

public class CustomTableUI extends BasicTableUI {

    private static final Color EVEN_ROW_COLOR = new Color(241, 245, 250);
    private PropertyChangeListener fAncestorPropertyChangeListener =
            createAncestorPropertyChangeListener();

    @Override
    public void installUI(JComponent c) {
        super.installUI(c);

        // TODO save defaults.

        table.setFont(MacFontUtils.ITUNES_FONT);
        table.setShowVerticalLines(true);
        table.setShowHorizontalLines(false);
        table.setShowGrid(false);
        table.setIntercellSpacing(new Dimension(0, 0));

        table.addPropertyChangeListener("ancestor", fAncestorPropertyChangeListener);
    }

    private PropertyChangeListener createAncestorPropertyChangeListener() {
        return new PropertyChangeListener() {
            public void propertyChange(PropertyChangeEvent evt) {
                // indicate that the parent of the JTable has changed.
                parentDidChange();
            }
        };
    }

    private void parentDidChange() {
        // if the parent of the table is an instance of JViewport, and that JViewport's parent is
        // a JScrollpane, then install the custom BugFixedViewportLayout.
        if (table.getParent() instanceof JViewport
                && table.getParent().getParent() instanceof JScrollPane) {
            JScrollPane scrollPane = (JScrollPane) table.getParent().getParent();
            scrollPane.getViewport().setLayout(new BugFixedViewportLayout());
        }
    }

    @Override
    public void paint(Graphics g, JComponent c) {
        // get the row index at the top of the clip bounds (the first row to paint).
        int rowAtPoint = table.rowAtPoint(g.getClipBounds().getLocation());
        // get the y coordinate of the first row to paint. if there are no rows in the table, start
        // painting at the top of the supplied clipping bounds.
        int topY = rowAtPoint < 0 ? g.getClipBounds().y : table.getCellRect(rowAtPoint,0,true).y;

        // create a counter variable to hold the current row. if there are no rows in the table,
        // start the counter at 0.
        int currentRow = rowAtPoint < 0 ? 0 : rowAtPoint;
        while (topY < g.getClipBounds().y + g.getClipBounds().height) {
            int bottomY = topY + table.getRowHeight();
            g.setColor(getRowColor(currentRow));
            g.fillRect(g.getClipBounds().x, topY, g.getClipBounds().width, bottomY);
            topY = bottomY;
            currentRow ++;
        }

        super.paint(g, c);
    }

    private Color getRowColor(int row) {
        return row % 2 == 0 ? EVEN_ROW_COLOR : table.getBackground();
    }

    // BugFixedViewportLayout implementation. /////////////////////////////////////////////////////

    /**
     * A modified ViewportLayout to fix the JFC bug where components that implement Scrollable do
     * not resize correctly, if their size is less than the viewport size.
     * This is a JDK1.2.2 bug (id 4310721). This used to work in Swing 1.0.3 and the fix is putting
     * the old logic back.
     * Copied from: http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4310721
     */
    private class BugFixedViewportLayout extends ViewportLayout {
        public void layoutContainer(Container parent) {
            // note that the original code (at the link supplied in the comment above) contained the
            // following call to super.layoutContainer(parent). this call caused the table to
            // continuously paint itself when the table did not fill the view. thus, i've commented
            // it out for now, as doing so seems to have no ill effects.
            // super.layoutContainer(parent);

            JViewport vp = (JViewport)parent;
            Component view = vp.getView();

            if(view == null) {
                return;
            }

            Point viewPosition = vp.getViewPosition();
            Dimension viewPrefSize = view.getPreferredSize();
            Dimension vpSize = vp.getSize();
            Dimension viewSize = new Dimension(viewPrefSize);

            if ((viewPosition.x == 0) && (vpSize.width > viewPrefSize.width)) {
                viewSize.width = vpSize.width;
            }

            if ((viewPosition.y == 0) && (vpSize.height > viewPrefSize.height)) {
                viewSize.height = vpSize.height;
            }

            if (!viewSize.equals(viewPrefSize)) {
                vp.setViewSize(viewSize);
            }
        }
    }
}

Note that this is a simplistic example in that BugFixedLayout ignores JTables autoResize property – that is, if the JTable is set to use AUTO_RESIZE_OFF, the table will still fill the width of the view. Ultimately, the table’s background would still fill the width of the view, but the columns would not, as is the case with native tables on both Windows and Mac.

Advertisement

9 Responses to “Making a JTable fill the view without extension”

  1. bitguru Says:

    I had advocated simply calling yourScrollpane.getViewport().setBackground(matchingColor).

    It doesn’t stretch the table to be any larger, but it makes it harder to tell.

  2. Ken Says:

    Hi bitguru,

    The technique you suggest works for the simple case, where you only want to change the background color. This wouldn’t work, though, if you wanted to draw row striping. I suppose you could install a custom JViewport on the JScrollpane, but it would have to have access to the JTable in order to draw the row striping.

    Also note that if the JTable doesn’t fill the view, you can only right click directly over the table to bring up a context menu and only drop items (for a D&D operation) directly over the table.

    -Ken


  3. […] Orr attacks the problem of JTable-in-a-JViewport and not filling the entire viewport. His solution employs a UI delegate that installs a custom layout manager on the enclosing […]

  4. bitguru Says:

    hey Ken,

    No, I understand those limitations. I mentioned setting the viewport’s background color only because it’s a quick-and-easy solution for some people’s needs. When I had needed to support DnD drops in the past, I had to use better fixes.

    btw, if you want to see a different JViewPort hack, see http://bitguru.wordpress.com/2008/01/07/lazyviewport/

  5. rbs Says:

    Ken,

    Although this looks like an elegant solution to the “empty rows problem”, one area where it seems to fail is painting of any gridlines which might have been specified.

  6. Aekold Says:

    http://java.sun.com/javase/6/docs/api/javax/swing/JTable.html#setFillsViewportHeight(boolean)
    This method does all the job for Java 6. Internal it may be the same way, but it is much better, than custom UI delegates…

  7. Ken Says:

    Hi rbs,

    Your absolutely right – the grid lines don’t paint the entire viewport (one of the reasons I hid them!). Unfortunately, the BasicTableUI.paintGrid method is private, which makes fixing this issue just a little bit harder. Here is a method that you could stick in the table UI delegate I provided that would paint the missing grid lines:

    private void paintEmptyRowGridLines(Graphics g) {
    Graphics newGraphics = g.create();

    // grab the y coordinate of the top of the first non-existent row (also
    // can be thought of as the bottom of the last row).
    int firstNonExistentRowY = getRowCount() * getRowHeight();

    // only paint the region within the clipp bounds.
    Rectangle clip = newGraphics.getClipBounds();

    // paint the column grid dividers for the non-existent rows.
    int x = 0;
    for (int i = 0; i < getColumnCount(); i++) {
    TableColumn column = getColumnModel().getColumn(i);
    // increase the x position by the width of the current column.
    x += column.getWidth();
    newGraphics.setColor(table.getGridColor());
    // draw the grid line (not sure what the -1 is for, but BasicTableUI
    // also does it.
    newGraphics.drawLine(x – 1, firstNonExistentRowY, x – 1, getHeight());
    }

    newGraphics.dispose();
    }

  8. Ken Says:

    Hi Aekold,

    I mentioned in the opening paragraph that this problem is a non-issue in JDK 6. Those of us still using JDK 5, though, need a solution!

    -Ken


  9. Unfortunately, the suggestion to overwrite getScrollableTracksViewportHeight() does not work with Quaqua 5. :(


Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: