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).
![]() |
![]() |
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.
October 6, 2008 at 2:50 pm
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.
October 6, 2008 at 3:05 pm
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
October 6, 2008 at 4:37 pm
[…] 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 […]
October 6, 2008 at 6:49 pm
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/
October 7, 2008 at 2:57 am
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.
October 7, 2008 at 9:04 am
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…
October 7, 2008 at 12:15 pm
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();
}
October 7, 2008 at 12:17 pm
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
October 12, 2008 at 9:05 am
Unfortunately, the suggestion to overwrite getScrollableTracksViewportHeight() does not work with Quaqua 5. :(