2004-09-27

Alternating row colors in JTable

[An updated post Making JTable look more at home on Mac OS and GNOME is available, describing fixed bugs and added features.]

If you look at iTunes, the rows in the table are striped, and the stripes fill the whole table area even when there isn't enough data to fill the table.

If you look at a Java application on Mac OS, there are no row stripes in its tables.

You can add your own cell renderer to a JTable to get stripes. The cell renderer sets the row background based on the row number modulo 2. This is a well-known technique. The trouble with this implementation is that if the model doesn't contain enough rows to fill the table's area, there's no reason for the cell renderer to be called on the implied empty rows.

This means you get a big empty space at the bottom of the table, instead of having the alternating row pattern go all the way to the bottom. In fact, JTable doesn't even track the viewport height (though JList does), so there's no table there. That gap isn't the table painting background; it's the component behind painting it's background.

As usual, the UI delegate system makes it inconvenient to mess with the details of JTable painting, but overriding JTable.paint to paint the empty rows afterwards works out fine, and means we work whatever LAF is installed.

The only other thing we need to do is behave more like JList in tracking the viewport height (so we've actually got some space to paint into).

package e.gui;

import java.awt.*;
import javax.swing.*;
import javax.swing.table.*;
import e.util.*;

/**
* A better-looking table than JTable. In particular, on Mac OS this looks
* more like a Cocoa table than the default Aqua LAF manages.
*
* @author Elliott Hughes
*/
public class ETable extends JTable {
public ETable() {
}

/**
* Paints empty rows too, after letting the UI delegate do
* its painting.
*/
public void paint(Graphics g) {
super.paint(g);
paintEmptyRows(g);
}

/**
* Paints the backgrounds of the implied empty rows when the
* table model is insufficient to fill all the visible area
* available to us. We don't involve cell renderers, because
* we have no data.
*/
protected void paintEmptyRows(Graphics g) {
final int rowCount = getRowCount();
final Rectangle clip = g.getClipBounds();
if (rowCount * rowHeight < clip.height) {
for (int i = rowCount; i <= clip.height/rowHeight; ++i) {
g.setColor(colorForRow(i));
g.fillRect(clip.x, i * rowHeight, clip.width, rowHeight);
}
}
}

/**
* Changes the behavior of a table in a JScrollPane to be more like
* the behavior of JList, which expands to fill the available space.
* JTable normally restricts its size to just what's needed by its
* model.
*/
public boolean getScrollableTracksViewportHeight() {
if (getParent() instanceof JViewport) {
JViewport parent = (JViewport) getParent();
return (parent.getHeight() > getPreferredSize().height);
}
return false;
}

/**
* Returns the appropriate background color for the given row.
*/
protected Color colorForRow(int row) {
return (row % 2 == 0) ? GuiUtilities.ALTERNATE_ROW_COLOR : getBackground();
}

/**
* Shades alternate rows in different colors.
*/
public Component prepareRenderer(TableCellRenderer renderer, int row, int column) {
Component c = super.prepareRenderer(renderer, row, column);
if (isCellSelected(row, column) == false) {
c.setBackground(colorForRow(row));
c.setForeground(UIManager.getColor("Table.foreground"));
} else {
c.setBackground(UIManager.getColor("Table.selectionBackground"));
c.setForeground(UIManager.getColor("Table.selectionForeground"));
}
return c;
}
}

I wasn't looking for a solution that requires you to use a subclass of JTable; I'd have liked to be able to fix all JTables, but this will have to do for now.

I have an implementation of the equivalent feature for JList, too, but it's not as nice because of the support for variable-height rows in JList. I'm still trying to think if there's a better implementation.

Funnily enough, I thought JList would be the easy one!