SHORTCOMINGS OF OTHER SOLUTIONS
http://www.codeguru.com/java/articles/305.shtml
Wernitz' code almost worked for me. First I fixed the getFont() = null
problem by using UIManager.getDefaults().getFont("Table.font"). Then
when I added a row to my table the "row added" event fired and then it
tried to use getRowHeight(void), which is undefined in Wernitz' code
because it assumes all rows are the same height, which they are not.
The result is that the cells painted correctly but the entire table
height was based (I suppose) on the default row height and ended up
clipping most of the bottom rows. His MultiLineTable.java code was so
confusing for me that I just didn't want to have to override yet
another method in another class to resolve the calls to getRowHeight().
http://archive.devx.com/java/free/articles/Kabutz07/Kabutz07-1.asp
Kabutz' solution really became my solution, although with a slight
modification (of course!)
MY SOLUTION
Below is the *only* class I needed to create. I keep the original
author's comments for credit, but note that I changed it a bit by (1)
cleaning up some empty methods and unnecessary imports and deleting the
"implements Serializable", which JTable already implements, (2) by
modifying the constructor to get the JTextArea to behave as I wanted
(only break at newlines) and (3) modifying
getTableCellRendererComponent() by inserting a variation of Kabutz'
solution (after "setValue(value);").
The magic is to simply call
setDefaultRenderer(Object.class, new MultiLineCellRenderer());
against the JTable or JTable descendant. I called it in the
constructor. I don't know what Kabutz was talking about coordinating
cell renderers for various columns. The default renderer applies to
all columns, and I figured a default renderer that handled Objects (and
Object descendants) should be general enough to fit most people's
needs. I don't know what happens with primitive types.
Setting the preferredHeight in the cell renderer is so much easier than
all the math that Wernitz was doing, and it WORKED for me!
/**
* MultiLineCellRenderer.java
*
* Created: Mon May 17 09:41:53 1999
*
* @author Thomas Wernitz, Da Vinci Communications Ltd
<thomas_...@clear.net.nz>
*
* credit to Zafir Anjum for JTableEx and thanks to SUN for their
source code ;)
*/
package multiLineTable;
import javax.swing.*;
import javax.swing.table.TableCellRenderer;
import javax.swing.border.*;
import java.awt.Component;
import java.awt.Color;
public class MultiLineCellRenderer extends JTextArea implements
TableCellRenderer {
protected static Border noFocusBorder;
private Color unselectedForeground;
private Color unselectedBackground;
public MultiLineCellRenderer() {
super();
noFocusBorder = new EmptyBorder(1, 2, 1, 2);
setLineWrap(false);
setWrapStyleWord(false);
setOpaque(true);
setBorder(noFocusBorder);
}
public void setForeground(Color c) {
super.setForeground(c);
unselectedForeground = c;
}
public void setBackground(Color c) {
super.setBackground(c);
unselectedBackground = c;
}
public void updateUI() {
super.updateUI();
setForeground(null);
setBackground(null);
}
public Component getTableCellRendererComponent(JTable table, Object
value,
boolean isSelected, boolean hasFocus,
int row, int column) {
if (isSelected) {
super.setForeground(table.getSelectionForeground());
super.setBackground(table.getSelectionBackground());
}
else {
super.setForeground((unselectedForeground != null) ?
unselectedForeground
: table.getForeground());
super.setBackground((unselectedBackground != null) ?
unselectedBackground
: table.getBackground());
}
setFont(table.getFont());
if (hasFocus) {
setBorder( UIManager.getBorder("Table.focusCellHighlightBorder")
);
if (table.isCellEditable(row, column)) {
super.setForeground( UIManager.getColor("Table.focusCellForeground")
);
super.setBackground( UIManager.getColor("Table.focusCellBackground")
);
}
} else {
setBorder(noFocusBorder);
}
setValue(value);
int rowHeight = (int)getPreferredSize().getHeight();
if (table.getRowHeight() != rowHeight) table.setRowHeight(rowHeight);
return this;
}
protected void setValue(Object value) {
setText((value == null) ? "" : value.toString());
}
}
Cargo-cult code.
> int rowHeight = (int)getPreferredSize().getHeight();
> if (table.getRowHeight() != rowHeight) table.setRowHeight(rowHeight);
Have you actually tried this? With contents of different length?
Christian
cargo cult programming: n.
A style of (incompetent) programming dominated by ritual inclusion
of code or program structures that serve no real purpose. A cargo cult
programmer will usually explain the extra code as a way of working
around some bug encountered in the past, but usually neither the bug
nor the reason the code apparently avoided the bug was ever fully
understood.
Dr. Heinz Kabutz:
"On first glimpse the program seemed to work correctly-except that my
poor CPU was running at 100 percent. The problem was that when I set
the row height I invalidated the table, which caused the program to
call getTableCellRendererComponent() in order to render all the cells
again. This, in turn, set the row height, which invalidated the table
again. In order to put a stop to this cycle of invalidation, I needed
to check whether the row was already the correct height before setting
it."
Are you saying this is not/no longer the case?
> Have you actually tried this? With contents of different length?
Yes, in fact my interface has two tables in side-by-side scoll panes.
This allows me to synchronize row selections between two tables with
rows of varying heights. The application generates the table data,
which varies in length.
int rowHeight = (int)getPreferredSize().getHeight();
if (table.getRowHeight() != rowHeight) table.setRowHeight(rowHeight);
should be
int rowHeight = (int)getPreferredSize().getHeight();
if (table.getRowHeight(row) != rowHeight) table.setRowHeight(row,
rowHeight);
Note that getRowHeight/setRowHeight is overloaded and I mistakenly used
the wrong one the first time (should be row-specific).
It does not matter, it is still wrong. If the preferred heights of two
different columns are different, they will still fight forever setting
their row height (whether the global one or just the row itself does not
matter)..
import javax.swing.JTextArea;
import javax.swing.JTable;
import javax.swing.table.TableCellRenderer;
import javax.swing.UIManager;
import javax.swing.BorderFactory;
import javax.swing.border.Border;
import java.awt.Color;
import java.awt.Insets;
import java.awt.Component;
import javax.swing.JFrame;
import javax.swing.JScrollPane;
public class MultiLineCellRenderer
extends JTextArea
implements TableCellRenderer
{
private Border focusBorder, noFocusBorder;
public MultiLineCellRenderer()
{
setOpaque(true);
setLineWrap(false);
setWrapStyleWord(false);
}
public void updateUI()
{
super.updateUI();
focusBorder = UIManager.getBorder("Table.focusCellHighlightBorder");
noFocusBorder = focusBorder == null ? null : newEmptyBorder(focusBorder.getBorderInsets(this));
}
private static Border newEmptyBorder(Insets n)
{
return BorderFactory.createEmptyBorder(n.top, n.left, n.bottom, n.right);
}
private void setForeground_maybe(Color value)
{
if (value != null)
setForeground(value);
}
private void setBackground_maybe(Color value)
{
if (value != null)
setBackground(value);
}
public Component getTableCellRendererComponent
(JTable table, Object value, boolean selected, boolean focused, int row, int column)
{
setEnabled(table.isEnabled());
setComponentOrientation(table.getComponentOrientation());
if (selected)
{
setForeground(table.getSelectionForeground());
setBackground(table.getSelectionBackground());
}
else
{
setForeground(table.getForeground());
setBackground(table.getBackground());
}
setFont(table.getFont());
if (focused)
{
setBorder(focusBorder);
if (table.isCellEditable(row, column))
{
setForeground_maybe(UIManager.getColor("Table.focusCellForeground"));
setBackground_maybe(UIManager.getColor("Table.focusCellBackground"));
}
}
else
{
setBorder(noFocusBorder);
}
setValue(value);
int rowHeight = getPreferredSize().height;
if (table.getRowHeight(row) != rowHeight) table.setRowHeight(row, rowHeight);
return this;
}
protected void setValue(Object value)
{
setText(value == null ? "" : value.toString());
}
public static void main(String[] args)
{
JTable t = new JTable(1, 2);
t.setValueAt("line", 0, 0);
t.setValueAt("two\nlines", 0, 1);
t.setDefaultRenderer(Object.class, new MultiLineCellRenderer());
JFrame f = new JFrame();
f.getContentPane().add(new JScrollPane(t));
f.pack(); f.setVisible(true);
}
}
Christian
Do you understand why this stuff with overriding setBackground/set-
Foreground and invoking super.setBackground/Foreground was done? Is it
useful that way?
> Dr. Heinz Kabutz:
>
> "On first glimpse the program seemed to work correctly-except that my
> poor CPU was running at 100 percent. The problem was that when I set
> the row height I invalidated the table, which caused the program to
> call getTableCellRendererComponent() in order to render all the cells
> again. This, in turn, set the row height, which invalidated the table
> again. In order to put a stop to this cycle of invalidation, I needed
> to check whether the row was already the correct height before setting
> it."
That's not the point. The JTable *could* do this check itself and avoid
revalidation (but it does not).
The problems with setting the row height from the renderer are:
* It fails unless all renderers agree on a row height. Otherwise the row
height will switch randomly, or even pseudo-recursive as each layout/
painting will cause relayout/repainting.
* It destroys the previously set row height. If the contents of the cell
are later modified (or the cell/column is removed), it cannot be restored.
See also previous postings by me (e.g. search for "setRowHeight" and my
name).
> Yes, in fact my interface has two tables in side-by-side scoll panes.
> This allows me to synchronize row selections between two tables with
> rows of varying heights. The application generates the table data,
> which varies in length.
See the example code in the other posting.
Christian
I don't know why Wernitz overrode setBackground/Foreground; I did not
focus on that. I commented it out, and the table stills acts as I
expect in my test. As I understand, super.setBackground/Foreground is
used to reproduce highlighting/borders for selected rows/cells.
Without it, there is no indication of selection. Is there a different,
standard way this should be done?
> * It fails unless all renderers agree on a row height. Otherwise the
row
> height will switch randomly, or even pseudo-recursive as each layout/
> painting will cause relayout/repainting.
I see your point. Although the JTextArea's getPreferredSize()
calculates the size of the component based on its contents (and so
implies the cell's height), it is unnecessary to calculate the height
each time the cell is rendered. I should use the renderer's
getPreferredSize() method at a more appropriate time.
The row height shall be equal to the maximum of its cells' heights.
Wernitz iterated through all columns in a row to determine the maximum
height. In a dynamic table, it seems necessary to (re)calculate the
row height whenever a cell is initialized or edited. In my case, my
tables are static, so I only need to worry about calculating the row
height when a row is added. I guess I will use a TableModelListener to
listen for an INSERT. If I had a dynamic table, would I simply need to
also listen for an UPDATE? That is, is the TableModelEvent guaranteed
to be fired when the contents of a cell are changed in a way that
affects cell height? Does this depend on the type of editor?
Suggestions?
> * It destroys the previously set row height. If the contents of the
cell
> are later modified (or the cell/column is removed), it cannot be
restored.
I guess you mean if we, say, scrolled the table and the row height
decreased because the cell with greatest height was not rendered. I
agree that render-time is not the right time to set row height.
Of course you need to call setBackground/Foreground to set the colors
(taken from the table if you like). But overridding them etc. is confusing
(especially in subclasses) and, in this implemented, does not really work.
Christian
>> * It fails unless all renderers agree on a row height. Otherwise the
> row
>> height will switch randomly, or even pseudo-recursive as each layout/
>> painting will cause relayout/repainting.
>
> I see your point. Although the JTextArea's getPreferredSize()
> calculates the size of the component based on its contents (and so
> implies the cell's height), it is unnecessary to calculate the height
> each time the cell is rendered. I should use the renderer's
> getPreferredSize() method at a more appropriate time.
>
> The row height shall be equal to the maximum of its cells' heights.
This is not what your code does. That's why it may still cause infinite
relayout.
> Wernitz iterated through all columns in a row to determine the maximum
> height. In a dynamic table, it seems necessary to (re)calculate the
> row height whenever a cell is initialized or edited. In my case, my
> tables are static, so I only need to worry about calculating the row
> height when a row is added. I guess I will use a TableModelListener to
> listen for an INSERT. If I had a dynamic table, would I simply need to
> also listen for an UPDATE? That is, is the TableModelEvent guaranteed
> to be fired when the contents of a cell are changed in a way that
> affects cell height? Does this depend on the type of editor?
Yes. Yes. Yes. No. There is a catch that you need to make sure to know
whether the table has already updated itself (i.e. received the Table-
ModelEvent) or not - the easiest is to override tableChanged and do it
afterwards or in advance depending on what you want.
>> * It destroys the previously set row height. If the contents of the
> cell
>> are later modified (or the cell/column is removed), it cannot be
> restored.
>
> I guess you mean if we, say, scrolled the table and the row height
> decreased because the cell with greatest height was not rendered. I
I don't mean that. I mean if, e.g. if contents of the cell with the largest
height are modified so that the height would be smaller, then this
will not cause a decrease of the height.
> agree that render-time is not the right time to set row height.
True.
Christian