Java: Swing: JTable and TableModel: FAQ
JTable, as any swing component, doesn't display scroll bars etc. on its own. Instead these are handled by a JScrollPane (or a similar component).
The column headers are displayed by a separate component, a JTableHeader. If the JTable is put inside a JScrollPane, it adds the header automatically as column header of the JScrollPane.
If you don't put it into a JScrollPane, you have to add the header by hand (it's probably the best to use a BorderLayout).
pane.setLayout(new BorderLayout()); // unless already there pane.add(table, BorderLayout.CENTER); pane.add(table.getTableHeader(), BorderLayout.NORTH);
This will not work with dragging in JDK1.4beta3 (if you care).
If you don't use a JScrollPane, you won't be able to scroll at all. Unless the JTable is guaranteed to have only few columns and rows, that is a bad idea.
table.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
If autoResizeMode is set differently, the JTable automatically resizes itself to the width of the JScrollPane.
The default size for components in JScrollPane is the size returned by getPreferredScrollableViewportSize(), which for JTable is set to a (somewhat reasonable) default value independent from the actual widths of the columns (= the preferred size of the JTable). The most simple solution might be to call
table.setPreferredScrollableViewportSize(table.getPreferredSize());
but that also effects the viewport height, which leads to huge heights unless the TableModel has few rows; and horizontal scroll bars will never appear, even if the JTable is much too wide. It is better to only adjust the width to lower values by overriding getPreferredScrollableViewportSize() as follows:
public Dimension getPreferredScrollableViewportSize()
{
Dimension size = super.getPreferredScrollableViewportSize();
return new Dimension(Math.min(getPreferredSize().width, size.width), size.height);
}
or to call:
Dimension size = table.getPreferredScrollableViewportSize();
table.setPreferredScrollableViewportSize
(new Dimension(Math.min(getPreferredSize().width, size.width), size.height);
The latter will not adjust dynamically.
For cells, it can be done easily with: table.scrollRectToVisible(table.getCellRect(row, column, true));
See JTable scrolling article for row / column scrolling.
After changing the table model (adding rows etc.), these method calls must be wrapped in SwingUtilities.invokeLater() to give the JTable a chance to relayout, at least if there are any changes in the columns / column widths.
See JTable scrolling.
See discussion of Row headers.
This is not possible with a single standard JScrollPane. It must be (non-easily) extended to support it. It may be possible to use two JScrollPanes, both with a JTable, and synchronize them, but that is difficult.
See introduction to TableColumnModel.
As soon as you do any modifications to the TableColumnModel, I would do everything explicitly and not rely on JTable's implicit changes that happen when its property autoCreateColumnsFromModel is true. Creating default columns is done like this:
TableModel data = ...
TableColumnModel columns = new DefaultTableColumnModel();
for (int count = data.getColumnCount(), i = 0; i < count; i++)
{
TableColumn c = new TableColumn(i);
c.setHeaderValue(data.getColumnName(i));
// insert column-specific adjustments here
// for example a column-wise renderer
// c.setCellRenderer(...);
columns.add(c);
}
JTable table = new JTable(data, columns);
You can set minimum/maximum widths and a preferred width on TableColumns. Explicitly setting the width property is useless, as it is almost always soon overridden again by JTable. Insert the calls at the place marked above.
With autoResizeMode == AUTO_RESIZE_OFF, the preferred width is taken directly as width, with other modes, the available space is distributed on the columns relative to their preferred widths.
The arguments to TableColumns' setWidth methods snap to currently allowed range. This means the proper order is:
setMinWidth
setMaxWidth
setPreferredWidth
There is a sizeWidthsToFit() method in TableColumn, but it only uses the header value and doesn't work properly because it does not have all the needed information. See Cell sizes article for example code that sets the (preferred) widths based on the renderer's preferred widths.
This also happens with DefaultTableModel.setDataVector(), even if the column structure remains the same..
TableModel doesn't support adding of columns directly. Instead it has to notify its listeners that it completely changed. Depending on the property "autoCreateColumnsFromModel" JTable throws aways the old TableColumns (which store all the information) and creates new ones. You can turn this behaviour off by calling table.setAutoCreateColumnsFromModel(false); but then you will have to modify the TableColumnModel by hand to actually add/remove columns.
TableColumnModel.removeColumn()
This isn't really supported by TableModel. The only way for the TableModel to message a row name change is to tell that the whole table structure changed, which either will destroy all column settings (autoCreateColumnsFromModel == false) or do nothing at all.
Instead modify the value directly in the TableColumnModel.
int viewColumn = table.convertColumnIndexToView(column); TableColumn column = table.getColumnModel().getColumn(viewColumn); column.setHeaderValue(newName); table.getTableHeader().repaint();
These are handled by the JTable's JTableHeader. Use:
table.getTableHeader().setReorderingAllowed(false);
or
table.getTableHeader().setResizingAllowed(false);
TableColumn c = table.getColumnModel().getColumn(table.convertColumnIndexToView(column)); c.setResizable(false);
Watch out that the JTable doesn't implicitly create new TableColumns.
You must keep in mind whether column indices are in the view (order of the TableColumnModel) or in the model. Only the latter are reordered by the user. Usually column arguments are those of the view, except (of course) those given to model methods. To convert between these types of column indices, use the convertColumnIndexToXXX() methods in JTable. Note that the renderer also get the 'view' column as argument.
See selection article.
The following methods actually modify the ListSelectionModel of the JTable, which contains the selected row indices:
table.addRowSelectionInterval
table.setRowSelectionInterval
table.removeRowSelectionInterval
To modify column selections there are equivalent methods which modify the ListSelectionModel of the TableColumnModel:
table.addColumnSelectionInterval
etc.
table.clearSelection affects both column and row selection.
This is a bug in DefaultTableColumnModel. The method moveColumn() does not properly call insertIndexInterval/removeIndexInterval on the column selection model. This is fixed in 1.4. Before, use a fixed subclass:
public class FixedDefaultTableColumnModel
extends DefaultTableColumnModel
{
public void moveColumn(int column, int to)
{
if (to < 0 || to >= getColumnCount())
throw new IllegalArgumentException(String.valueOf(to));
if (to == column)
{
fireColumnMoved(new TableColumnModelEvent(this, column, to));
return;
}
TableColumn c = (TableColumn)tableColumns.remove(column);
boolean selected = selectionModel.isSelectedIndex(column);
boolean oldAdjusting = selectionModel.getValueIsAdjusting();
selectionModel.setValueIsAdjusting(true);
selectionModel.removeIndexInterval(column, column);
tableColumns.add(to, c);
selectionModel.insertIndexInterval(to, 1, true);
if (selected)
selectionModel.addSelectionInterval(to, to);
else
selectionModel.removeSelectionInterval(to, to);
fireColumnMoved(new TableColumnModelEvent(this, column, to));
selectionModel.setValueIsAdjusting(oldAdjusting);
}
}
table.getSelectionModel().addListSelectionListener()
table.getColumnModel().addColumnModelListener()
getValueIsAdjusting() is probably once true, one false. Just ignore events with it being true.
Each cell's appearance is governed by a TableCellRenderer. TableCellRenderers are assigned column-wise on TableColumn in the TableColumnModel, or (only used when there is none set on the table column) based on column class, by JTable.setDefaultRenderer(). See also Columns section above for configuring the former.
The default implementation, DefaultTableCellRenderer, has some strange logic with foreground/background. Calls to setBackground/setForeground will also change the default color for all subsequent calls to getTableCellRendererComponent (instead of using the JTable colors). This is probably meant to change easily the color of a column, but the colors set that way are lost once the LAF is changed, so it isn't really useful. To avoid confusion, subclasses that call the DefaultTableCellRenderer implementation of getTableCellRenderer() must either always or never (not sometimes, depending on a condition) set the color (back to null if the implementation should take the JTable's value). It is usually better to set the colors before the call to super.getTableCellRendererComponent (although that code will then read and set them again), because a) that will set proper colors on the currently focused, editable cell, and b) it will handle selection automatically (take the selection colors of JTable, the way of least confusion), c) changing the background colors doesn't require an additional call setOpaque(true). (which is not needed for 1.4 anyway).
The default renderers don't react to the JTable's enabled state. Since the renderers don't react to user input anyway, it is only a matter of apperarance. It is not possible to adjust the default renderers (those for Numbers, Booleans, Date, the header renderer) in any way. Custom renderers should always adjust the renderer components like this:
class FixedTableCellRenderer
extends DefaultTableCellRenderer
{
public Component getTableCellRendererComponent
(JTable table, Object value, boolean selected, boolean focused, int row, int column)
{
setEnabled(table == null || table.isEnabled());
super.getTableCellRendererComponent(table, value, selected, focused, row, column);
return this;
}
}
In theory, DefaultTableCellRenderer allows customizing the default background/foreground, but that doesn't work properly with LAF changes (yet?), so you always need a subclass.
Here is an example subclass that paints every fifth row with a green background.
class ColoredTableCellRenderer
extends DefaultTableCellRenderer
{
public Component getTableCellRendererComponent
(JTable table, Object value, boolean selected, boolean focused, int row, int column)
{
setEnabled(table == null || table.isEnabled()); // see question above
if ((row % 5) == 0)
setBackground(Color.green);
else
setBackground(null);
super.getTableCellRendererComponent(table, value, selected, focused, row, column);
return this;
}
}
You need to call setHorizontalAlignment on the renderer component. If you use a custom renderer, simply put in the constructor. If you are using DefaultTableCellRenderer, create one of them, set the alignment and set it on the JTable (setDefaultRenderer) or on the TableColumns you want to have it it.
You probably lack a call to setOpaque(true);There is a catch with DefaultTableCellRenderer in JDK1.3: It implicitly sets its opacity back to false when it believes it has the same background color as the JTable. If you change the background in your subclass, it is easiest to explicit call setOpaque(true); always afterwards.
1.4: This is fixed. With DefaultTableCellRenderer, you don't even need to call setOpaque() anymore, the background color will always be painted if it is different from the JTable's.
The TableCellRenderer method is passed a row argument. Based on that (or other ones) you can either return different Components or b) delegate to different renderers.
The default renderer for header cells takes its font from the JTableHeader. Set the font on the header.
The default header renderer can not be customized at all. The following is an general purpose implementation that is even better than the default renderer.
class HeaderRenderer
extends DefaultTableCellRenderer
{
public HeaderRenderer()
{
setHorizontalAlignment(SwingConstants.CENTER);
setOpaque(true);
// This call is needed because DefaultTableCellRenderer calls setBorder()
// in its constructor, which is executed after updateUI()
setBorder(UIManager.getBorder("TableHeader.cellBorder"));
}
public void updateUI()
{
super.updateUI();
setBorder(UIManager.getBorder("TableHeader.cellBorder"));
}
public Component getTableCellRendererComponent(JTable table, Object value,
boolean selected, boolean focused, int row, int column)
{
JTableHeader h = table != null ? table.getTableHeader() : null;
if (h != null)
{
setEnabled(h.isEnabled());
setComponentOrientation(h.getComponentOrientation());
setForeground(h.getForeground());
setBackground(h.getBackground());
setFont(h.getFont());
}
else
{
/* Use sensible values instead of random leftover values from the last call */
setEnabled(true);
setComponentOrientation(ComponentOrientation.UNKNOWN);
setForeground(UIManager.getColor("TableHeader.foreground"));
setBackground(UIManager.getColor("TableHeader.background"));
setFont(UIManager.getFont("TableHeader.font"));
}
setValue(value);
return this;
}
}
and install it.
By default, JTable's tooltips are enabled, and it (and its header) prepares the renderer component to ask it for it tooltip. This can be turned off by:
ToolTipManager.sharedInstance().unregisterComponent(table); ToolTipManager.sharedInstance().unregisterComponent(table.getTableHeader());
By default, JTable asks the renderer for its tooltip, so just set the tooltip on the renderer.
For speed reasons, it might be preferable to override JTable.getToolTipText(MouseEvent) (and maybe also for the JTableHeader) and look up the value there directly.
These areas don't belong to JTable by default. Instead its JViewport parent shows through there. You may either set a background color on the JViewport:
table.getParent().setBackground(...); // table.getParent().setOpaque(true); if possibly turned off somewhere else
or use something like described in the Drag-and-Drop section to make the JTable higher/wider.
The components returned by TableCellRenderers are only used to paint the cell, not to react to user interaction. Add a MouseListener to the JTable instead. Note that single clicks already have a LAF- and TableCellEditor-specific meaning, so it is better only to react to double clicks. For editing of values, use a TableCellEditor.
Add a MouseListener to the JTable. Renderers do not respond to user interaction. Editors would.
table.addMouseListener(new MouseAdapter()
{
public void mouseClicked(MouseEvent e)
{
if (e.getComponent().isEnabled() && e.getButton() == MouseEvent.BUTTON1 && e.getClickCount() == 2)
{
Point p = e.getPoint();
int row = table.rowAtPoint(p);
int column = table.columnAtPoint(p);
// Use table.convertRowIndexToModel / table.convertColumnIndexToModle to convert to view indices
// ...
}
}
}
JTable (or its TableUI) may handle KeyStrokes directly that should be handled on a higher level (accelerators, mnemonics).
One has to install a disabled action for the KeyStroke in question. There is no way to remove KeyStrokes directly that a parent input map handles.
disabled = new AbstractAction()
{
public boolean isEnabled() { return false; }
public void actionPerformed(ActionEvent e) { }
};
// ....
// could use any unique-enough string
table.getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT).put(KeyStroke.getKeyStroke(...), "none");
table.getActionMap().put("none", disabled);
Strictly speaking, one should also install this in the WHEN_FOCUSED input map.
Note that if an editor component has focus, it will also possibly handle the key stroke.
Your TableModel must return false from isCellEditable(row, column).
DefaultTableModel always returns true from this method. You need to write a subclass to change this, for example
DefaultTableModel data = new DefaultTableModel(....)
{
public boolean isCellEditable(int row, int column)
{
return <insert condition>;
}
};
Use DefaultTableModel.
DefaultTableModel data = new DefaultTableModel(...); JTable table = new JTable(data); // somewhen later data.addRow(...);
That area isn't part of the JTable anymore, instead its parent component, the JViewport, shows, and it cannot know what to do with any data to be dropped. To make JTable, like JList, always fill the whole viewport, even its cells aren't enough, use:
JTable table = new JTable(data)
{
public boolean getScrollableTracksViewportHeight()
{
Component parent = getParent();
if (parent instanceof JViewport)
return parent.getHeight() > getPreferredSize().height;
return false;
}
};
This has (vaguely suggested as concerning JViewport and not JTable) been reported as a bug. (#4310721)
The horizontal equivalent cannot be used for JTables without autoresizing whose columns may be not wide enough because then JTable will start resizing the columns again to fill all space; this also leads to weird effects during resizing. JTable cannot be easily changed to span the whole viewport while the columns have lesser widths.
(C) 2001-2009 Christian Kaufhold (swing@chka.de)