JavaSwingJTable and TableModel: TableColumnModel


TableColumnModel

A TableColumnModel is a list of TableColumns, ordered like they are/can be shown in a JTable. I'll discuss the properties of TableColumns first.

TableColumn

An instance of this class represents most information about a single column in a JTable (except its visual position, which is only known if you know the TableColumnModel it is part of).

Nearly all methods are accessors and modifiers for TableColumn's properties:

There is a problem with TableColumn: Just with a TableColumn, you can do almost nothing useful, you need the TableColumnModel and possibly the JTable itself to have all the information around (A TableColumn doesn't even know its index in the TableColumnModel, so you cannot pass the renderers a valid column argument).

The incompleteness shows in the following method, which should not be used:

void sizeWidthToFit()

This method does not have enough information to properly query the header renderer for its preferred width. It doesn't know about the JTable, so it passes null, it doesn't know about the proper column index of the TableColumn, so it passes 0. It also passes 0 as row index, which is a bug, it has to pass -1. It doesn't know about the column margin, which is now inclusive, so the calculated width is off by that. As it is highly probably that the header renderer's preferred width will depend on the JTableHeader's font (size), this method can hardly be expected to produce a reasonable width.

(Finally it has been accepted: (#4765245).)

TableColumnModel

In some ways, TableColumnModel is incomplete as a modifiable list, in some ways augmented to serve the needs of JTable.

Something that very annoying is that each method name contains the redundant word "column".

Besides the usual listener adding/removal methods, the TableColumnModel methods can be separated in three parts:

List methods

No TableColumn may be contained twice in a TableColumnModel, or generally be contained in multiple TableColumnModels. This is because the TableColumn stores the width of the column in the JTable, and that must not be implicitly changed by another JTable, and cannot be properly calculated if the column is contained multiple times. It is possible to store a TableColumn in multiple TableColumnModels if only one of the models is active in a JTable, or if a subclass of JTable doesn't layout the TableColumns anymore (for example a total row that uses the same TableColumnModel as the main table, and relies on it to do the layout). That makes it possible to completely exchange the column structure by exchanging the TableColumnModel.

While it seems to work the have multiple TableColumns for the same model index in a JTable, I have found no code that explicitly handles this (so possibly it only works by accident), and JTable.convertColumnIndexToView() isn't well defined in such a case. It is best avoided.

int getColumnCount()
TableColumn getColumn(int column)
Enumeration getColumns()
int getColumnIndex(Object identifier)

void addColumn(TableColumn c)
void moveColumn(int column, int to)
void removeColumn(TableColumn c)

(#4469061)

What is missing? A method to add a column not at the end of the list (the event mechanism is capable of handling that), methods to add multiple columns, a method to remove the column at a certain index, and methods to remove multiple (or all) columns; a method to get the index of a column (or whether it is contained at all).

getColumnIndex() requires a column with the identifier to be contained - which is a stupid precondition, since in general, you would have to loop yourself before calling this method to ensure it is contained, and then you already know the index. The method can be implemented on top of getColumns(), for example, if you know getColumns() uses the proper order.

Pixel position methods

int getColumnMargin()
void setColumnMargin()
int getColumnIndexAtX(int x)
int getTotalColumnWidth()

(#4469061)

As of JDK1.3, the column margin in inclusive in the widths stored in the columns. That means that now the column margin wouldn't need to be stored in the column model at all. Before, the latter two methods needed the margin for their calculations.

getTotalColumnWidth() just sums all the columns' widths. getColumnIndexAtX() iterates over the columns until the total sum of the widths is larger than x (plus handling of cases when it is smaller then zero or larger then the column width). These methods can be implemented on top of the list methods.

There is no method to perform the inverse mapping (column -> starting x), or methods to calculate the total minimum, preferred, or maximum width.

Selection methods

These probably will be discussed in a general JTable selection mode document elsewhere and not here.

From a design standpoint, TableColumnModel and TableColumn are both unsatisfying because they mix potentially sharable state (like cell renderers, preferred widths, header values) that could be used in multiple JTables at the same time, with state that depends on exactly one JTable (current width, current order, probably column margin). TableColumnModel is also passive when the JTable itself resizes the columns, that logic could be just as well (and probably in a more flexible way) be in the TableColumnModel. That would also make sharing TableColumnModels easier (or possible without subclasses).

TableColumnModel events

columnAdded / columnRemoved / columnMoved

There is nothing special to be said about these events. Only single columns can be added/removed/moved. For multiple, the modifications and the firing of events must be done one-by-one (i.e. not all the modifications and then sending all the events). An event must be send from columnMoved even if the column is moved on itself. This is (mis)used for notification that the visual position of the dragged column changed (but these events (move from a column on itself) may be trivially ignored).

columnMarginChanged

In JDK1.2, this event was sent iff the column margin changed (as suggested by the name). In JDK1.3, it is (must be) also sent whenever a column's width or preferred width changes (relevant changes to the minimum/maximum width will automatically affect these widths, does that really work, since there are possibly two events?) so that the JTable can invalidate itself automatically and explicitly calling sizeColumnsToFit() or revalidate() isn't necessary any more. That way you can (like, the most important case, the JTableHeader) modify the TableColumnModel without explicitly notifying the JTable(s) that contain it afterwards. Still, there is no way for listeners to know whichcolumn has changed without caching all the TableColumns and all the widths. JTable only does column-specific stuff if the column in question is the JTableHeaders' resizing column.

columnSelectionChanged

Usually such events will a) be forwarded from the events the TableColumnModel itself receives from its ListSelectionModel and b) a special event must be constructed when the ListSelectionModel is changed (DefaultTableColumnModel doesn't do that (#4469061)); c) maybe if column selection is turned on/off an event notifies of the changes. It is questionable whether these events should be sent at all if column selection is disallowed (But that only has effect on JTable.isCellSelected() and the appearance, not on the logical selection.), except that as usual, selection is mixed up with focused cell handling.

During adding/removal of columns, forwarding may be temporarily disabled, and the event sent only afterwards. Otherwise the index-column mapping will be in an inconsistent state while the events are forwarded.


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