Java: Swing: JTable and TableModel: Layout
This discussion is based on JDK1.3. A lot of (internal) things have been changed, the most important possibly the new interpretation of columnMarginChanged() in TableColumnModel, which makes it possible to change TableColumn (preferred) widths and have the JTable react automatically (later); and which makes the user-resizing code (in the TableHeaderUI) cleaner.
Note: In the JDK1.2, the widths set on the TableColumns excluded the column margin, with the JDK1.3, they include it (see Changes). (This with maybe done with consistency to different row heights, which are now also inclusive, and which seems to make the row height model management easier). It is not really good because if the width is really calculated based on renderer component size, it doesn't include the column margin at first, and thus must be adjusted every time the margin changes.
I have included some 1.4 updates, but that has made the text somewhat incoherent. Many things are true only for 1.3. This needs a major rewrite.
widths - by BasicTableUI: sum of the corresponding TableColumns' values; watches out for overflow (maximum width of a TableColumn is Integer.MAX_VALUE most of the time).
heights - by BasicTableUI: getCellRect() for the last row (y + height); 0 if no rows or columns
The height is thus completely fixed.
preferred viewport size - by JTable: (450, 400), even if actual preferred (or maximum) width or height are smaller
JTable tracks the viewport width iff the auto resize mode is not OFF.
widths - by BasicTableHeaderUI: sum of the corresponding TableColumn's values; watches out for overflow
heights - by BasicTableHeaderUI: maximum preferred height of all renderers (buggy optimization with default renderer)
There is no (sensible) policy at all for the LAF to return something different (except for the header height), since getCellRect() and column layouting are handled in the JTable itself, so the LAF cannot place the cells differently. The code could just as well be placed in JTable/JTableHeader itself.
In early versions of Swing, at least getHeaderRect() was delegated to the UI (#4106449), but this way, which would be cleaner in principle (but wasn't in the implementation) was removed.
The JTableHeader is usually (by ViewportLayout) as wide as its Viewport. If the main viewport of the JScrollPane is larger than the JTable, the JTableHeader fills it, while the JTable is only as large as the column model indicates.
JTable and JTableHeader ignore their Insets (#4222732), so setting a Border causes it to be painted over the cells:
JTable widths are off by left and right (BasicTableUI)
JTable heights are off by bottom (BasicTableUI) and top (indirectly by getCellRect)
JTable.getCellRect, rowAtPoint, columnAtPoint are off by top and left
JTable column layout methods (no matter what autoResizeMode!) are off by left and right (using getWidth())
JTableHeader.getHeaderRect is off by left and top
JTableHeader.columnAtPoint off by left
Painting code (in UI classes) is based on JTable methods (getCellRect, row/columnAtPoint), so probably isn't off itself.
There are two different kinds of layout:
"Normal" layout, caused by revalidate() / validate(). JTable calls sizeColumnsToFit(-1) from doLayout() to calculate the column widths.
Internal column layout, done by explicitly calling sizeColumnsToFit() or, internally, if the user is resizing a column (and the auto resize mode is not OFF)
This kind of layout can only be done when the JTable's width has remained the same. It has been obsoleted in 1.4.
1.4: real layout has moved from sizeColumnsToFit() into doLayout() directly. The code is also a little more robust, since it tries twice to adjust the column widths to the JTable's size (The problem was when columns hit minimum/maximum widths during resizing).
sizeColumnsToFit() should not be called anymore. That actually means a loss of flexibility (the resizing column is taken by a private method from the JTableHeader, so it cannot be replaced as easily as in 1.3 by a custom one).
pre-1.3: if the JTable didn't change width, no layout was performed at all. But unless the autoResizeMode is AUTO_RESIZE_OFF, JTables never change width (at least in a JScrollPane), so sizeColumnsToFit() had to be called manually after each column model (or column) change.
Both JTable and JTableHeader have resizeAndRepaint(), which calls revalidate() and repaint() when normal revalidation. Most of the time, it is called instead of both methods directly. It is called from:
updateUI
setRowHeight (both)
setRowMargin (useless), setIntercellSpacing (useless)
setAutoResizeMode
Column model changes (setColumnModel, columnAdded, columnRemoved, columnMarginChanged; not columnMoved) (unless it was user-caused column resizing and the auto resize mode is not OFF).
columnMarginChanged has special logic to avoid recursion, since it will be called again from sizeColumnsToFit() whenever a TableColumn width or preferred width is set. In 1.4 the actual column adjustments (except for AUTO_RESIZE_OFF, where recursion takes place) are postponed until doLayout, so recursion needn't be handled anymore (That means the JTable is always revalidated during column resizing, which may solve some size calculation problems but introduce different behaviour and/or have more overhead).
Model changes (setModel, maybe tableChanged)
updateUI
Column model changes: setColumnModel, columnAdded, columnRemoved, columnMarginChanged
setAutoResizeMode (by JTable)
When the JTable is actually validated (doLayout()), it calls sizeColumnsToFit(-1); (1.4: does it directly there). The header relies completely on its JTable to layout the columns.
A situation where the columns widths may be reconsidered without actually calling revalidate() was pre-1.4-columnMarginChanged (for example when a column is resized).
For the row, the explicit sizes set for all rows or for each row are taken.
sizeColumnsToFit() does in fact two (or three) unrelated things:
If the argument is -1, it does a standard layout (relative to the preferred widths).
If the argument is a column, it assumes the columns' width has been changed. The difference between the JTable width and the new total column width is distributed on the columns based on the auto resize mode (unless that is OFF).
This kind of layout is never used internally (it was in the JDK1.2). Now a very similar logic is contained in columnMarginChanged(), if the changed column is the column resized by the user (or, more generally, if it is the resizing column of the JTableHeader).
no tracking of viewport width: width is usually the sum of the preferred widths of the TableColumns (dependent on the layout manager)
sizeColumnsToFit(-1) usually ends up setting the widths exactly to the preferred widths (unless the JTable width is not equal to its preferred width for some reason, maybe it should do it even then, or automatic resizing is not really off?).
sizeColumnsToFit(column) / columnMarginChanged(resizing column): preferred width of column is set to width
Basically, widths and preferred widths are regarded as the same and only need synchronizing. Programmatically, setting the width is useless, since it will be overridden again.
tracking of viewport width: columns are fit into the viewport
sizeColumnsToFit(-1): column widths are calculated from preferred widths (usually unequal because the width is 450, unless explicit changed)
sizeColumnsToFit(column) / columnMarginChanged (resizing column): difference between (width and total column width) is somehow distributed on the columns, preferred widths are adjusted relative to the widths so that the change is permanent (and not lost the next time a complete layout takes place).
Maximum and minimum width are never set. Preferred width and width are set by TableColumn to fit into the range by minimum/maximum width if they are changed.
Preferred width: accommodateDelta
Width: standard layout, accommodateDelta, BasicTableHeaderUI (resizing)
BasicTableHeaderUI sets the width of the resizing column, and that's what makes sense for a controller, since it cannot assume that the preferred width has a one-to-one correspondence with the width, and resizing would be very confusing for the user if the visible width didn't adjust by the "right" amount.
The change is noticed in JTable.columnMarginChanged(), where:
if autoResizeMode == OFF: The preferred width of resizing column is set to the new width; and resizeAndRepaint() is called, because now the total preferred width has changed.
otherwise: accomodateDelta (and then repaint) are called; resizeAndRepaint() is not necessary, since the width has remained the same.
It is assumed that the resizing column is sized on the right size; since JTableHeader doesn't carry any information about it, that is a necessary assumption. It makes resizing on the left of the first column impossible, unless one accepts incompability with all resize modes but AUTO_RESIZE_OFF and AUTO_RESIZE_ALL_COLUMNS. It would have been better to have both a column and a bias (left or right size) because a) this leads to clearer code that handles the resizing and b) it is more powerful.
Only the painting code paints the column (possibly) at a different position. No actual relayout takes place; if it is moved (i.e. dragged far enough to change place with another column), it just appears with the same width at the new position.
One can change JTable to use the widths of the TableColumns without adjustments (column layout).
(C) 2001-2009 Christian Kaufhold (swing@chka.de)