JavaSwingJTable and TableModelRow headers: JTable


JTable

The JTable needs some configuration to act properly as row header.

For the more sophisticated stuff, the main table needs to know about its header, and the header about the main table (like it is done for the column header). Subclassing one or both can hardly be avoided anyway unless you do only the most basic things.

Widths and heights

JTable by default returns a huge width from getPreferredScrollableViewportSize, even if it only contains a few columns. As the row header

probably contains only one column, just adjust the width:

Dimension d = header.getPreferredScrollableViewportSize();

d.width = header.getPreferredSize().width;

header.setPreferredScrollableViewportSize(d);

Note that unless you set a preferred width on the column in the TableColumnModel, it will default to 100 pixels. It is never automatically calculated based on contents.

If you use the default table header border (which the suggested renderer below does), it looks strange unless you turn the header's intercell spacing off:

header.setIntercellSpacing(new Dimension(0, 0));

If you don't use variable-height rows, the most simple approach would be to adjust the the row heights by calling header.setRowHeight(table.getRowHeight()); This isn't needed for freshly created JTables, since they have the same height anyway. But more importantly, it doesn't work properly if either of these change later, since the height isn't automatically synchronized. You could of course add PropertyChangeListeners to header and table and adjust the other height, if it changed; but this is actually easier with subclasses that know about each other, and also can be used for variable height rows with minimum extra code.

Variable row heights

Note: Variable row heights are partly buggy with JTable itself. Model events "tableStructureChanged" and "tableDataChanged" and setting a new model led to invisible rows and continously increasing memory usage before JDK1.4beta2.

JTable doesn't fire any specific events if one uses variable-sized row heights and changes the height of a row. In order to be able to adjust the row header to the same size, one must subclass JTable and notify the header (or other handling code) of changes.

Appearance

To make the row header look more like a header, adjust colors and borders.

The problem with just header.setForeground(table.getTableHeader().getForeground()); and so on is that this is a case where the LAF-specific property adjustment mechanism won't work properly. The properties installed on the JTable header are UIResources (unless explicitly changed), and thus will be unset on the header component once the LAF is changed/updated. On the other hand, if you convert them into non-UIResources, they will not adjust to the colors of the new LAF, which is also not desired.

The cleanest way to solve this problem is to subclass the row header component and set the TableHeader's colors (and font) in updateUI():

public void updateUI()
{
    super.updateUI();

    LookAndFeel.installColorsAndFont
        (this, "TableHeader.background",
         "TableHeader.foreground", "TableHeader.font");
}

or by a PropertyChangeListenr for "UI".

That lookup could also be done in the renderer, but then colors set on the header component would be completely ignored and could not be configured at all.

Note that the main JTable doesn't have a border set, so we don't set a possible 'TableHeader.border' since that would lead to unadjusted row positions. As JTable almost always lives in a scrolling container (like JScrollPane), it usually doesn't have a border. For completeness' sake, the borders (or insets) could be synchronized as well to avoid misplaced row positions, but this doesn't happen anyway.

You also need a custom renderer to get the same cell border as the TableHeader (dynamically adjusting). The following example can be used both as Table- and ListCellRenderer.

It may be better to ignore the selected and/or focused argument altogether (see below); then remove those code lines, but this code shows them both. Note that the border looks strange on the right side with MetalLookAndFeel because its 'TableHeader.cellBorder' lies about its insets (#4422795).

Source code: RowHeaderRenderer.java

Example

A simple example before discussing the more complicated things. Selection isn't synchronized. To avoid having to handle the models, it only contains static data. I don't use subclasses or PropertyChangeListeners, so appearance will not work with the LAF changing.

You will need the additional source files: RowHeaderRenderer.java JScrollPaneAdjuster.java JTableRowHeaderResizer.java (see below)

RowHeaderTable.java

Selection

Sharing row selection models between JTable and row selection model does not work because when rows are added/removed, it would be modified twice to insert/remove indices. If you want selection, a specialized bridge selection model has to be written (That isn't difficult, it just delegates every method to the original model except insertIndexInterval and removeIndexInterval; it listens to events of the original and forwards them to its listeners).

Right now I prefer not having selection and focus for the row header at all, similar to the column header (and it is also easier). For JTable there is setRowSelectionAllowed(false); and/or just ignore the selected and focused arguments in the renderer (The first is cleaner, since even logically the indices aren't selected). To avoid the row header getting focus, override isFocusTraversable() to return false and call requestFocusEnabled(false);

Unfortunately, the default mouse selection methods cannot be totally disabled, even if nothing appears or is selected: pressing the mouse on the header and dragging will scroll both header and main table (It shouldn't if the header doesn't have focus, should it?). This may also be useful, though.

To select whole columns, a MouseListener can be added, similar to the one for the column header found in the selection document.

Model

The row header must have a model that is always in sync with the main JTable's model.

If your TableModel is only modified externally (like DefaultTableModel), you may use separate models for header and table and just modify header and main model in parallel. That doesn't work if the model itself changes if it doesn't either know about the header or doesn't notify the header. It may also be cleaner to have the row header data contained in the TableModel (like the column header data), and not in another, unrelated structure.

TableColumnModel allow to have only some columns of the TableModel in the JTable, so it is possible to use the same TableModel for header and main view and have a column (probably the first) contain the data for the row header. By properly adapting the TableColumnModels, it is only shown in the header and not in the main table. This requires that you manage the TableColumns of the main table on your own, otherwise JTable will re-introduce the header column into the main table. That is done for many non-trivial JTables anyway.

TableColumnModel columns = new DefaultTableColumnModel();

for (int count = data.getColumnCount(), i = 1; i < count; i++)
{
    TableColumn c = new TableColumn(i);
    c.setHeaderValue(data.getColumnName(i));
    columns.addColumn(c);
}

JTable table = new JTable(data, columns);

TableColumnModel headerColumns = new DefaultTableColumnModel();

TableColumn h = new TableColumn(0);
h.setHeaderValue(data.getColumnName(0));
headerColumns.add(h);

JTable header = new JTable(data, headerColumns);

For reasons mentioned above, JTable will probably be replaced by subclasses in real code.

Corner component and row header resizing

scrollPane.setCorner(JScrollPane.UPPER_LEFT_CORNER, cornerHeader);

The row header's JTableHeader can be used for this, but resizing (and reordering) should be disabled as they doesn't work anyway.

This is more flexible than using an unrelated component because the contents of the corner can be any object, not just, typically, a String, and will update automatically when you modify the corresponding headerValue in the TableColumn for the row header, so they are (sort of) stored in a model and not only in the GUI.

The resizing code below will probably not show the proper cursor, since the default LAF classes modify the cursor even when resizing is not enabled at all. (fixed in 1.4).

A somewhat sensible version of a resizer Mouse(Motion)Listener: RowHeaderResizer.java

It is quite generic, the only thing it needs is a JTable as main component (since JTable seems to want a sizeColumnsToFit() call in some setups).

Install with new RowHeaderResizer(scrollPane).setEnabled(true);

The main disadvantage is that it can only modify the preferred size of the JViewport and not that of the TableColumn shown in the header. Something more specialized can handle that: JTableRowHeaderResizer.java

Note: it does not handle right-to-left layout. As JTable only recently honours that at all, I haven't found time you do fix this. Before it was not doable.

Accessibility

For proper accessibility, the main JTable's Accessible implementation should be subclassed to also give information about the row header; and possibly the row header for some adjustments. This is another reason for subclassing JTable.


(C) 2001-2009 Christian Kaufhold (swing@chka.de)