Reputation: 6808
Title says it all. I have a table where in each cell there is the possibility of having long text. What TableCellRenderer
should i use in order to display the data - text of each cell with proper line wrap? I have tried a lot of variations messing with renderer's size and setRowHeight
but nothing seem to be optimal. Every thing I tried has some kind of instability.
First thing I tried is to use a JTextArea
as my TableCellRenderer
in order to take the advantage of setLineWrap
. This answer describes exactly what i did. It works exactly as i want but there is a problem with it. You can have only one column with this renderer. If you add the renderer to a second column, the column with the bigger "id" (column index in table) will "dominate" (and give the height to table's row) ignoring the case where the text in the column with the smaller "id" needs more line to be shown a.k.a bigger height.
Check this gif. It is exactly the behavior i want to achieve, with the column with "highest" text to dominate in row's height. It works because the column has the biggest index. (It renders it last)
You see? The column's width is decreased, so the text need more lines to be represented completely, and the text in column 1 is ok, since all its text is visible.
Now lets see the case where the text in column 1 needs more lines than the last column.
It is obvious that we are loosing the text in the first column. The last column (which is rendered last) has the proper height, so column 1 does not "dominate" and fill with space the last column.
The SSCCE that produces this behavior:
public class TableTest extends JTable {
private static final long serialVersionUID = 7180027425789244942L;
private static final String[] COLUMNS = { "SomeColumn", "OtherColumn", "OtherOtherColumn" };
public TableTest() {
super();
Object[][] data = new Object[5][3];
for (int i = 0; i < data.length; i++) {
data[i][0] = "Row: " + i + " - " + loremIpsum();
data[i][1] = "Row: " + i + " Maybe something small?";
data[i][2] = "Row: " + i + "___" + new StringBuilder(loremIpsum()).reverse().toString();
}
setModel(new DefaultTableModel(data, COLUMNS) {
@Override
public Class<?> getColumnClass(int columnIndex) {
return String.class;
}
});
setDefaultRenderer(String.class, new WordWrapCellRenderer());
getTableHeader().setReorderingAllowed(false);
}
public static class WordWrapCellRenderer extends JTextArea implements TableCellRenderer {
private WordWrapCellRenderer() {
setLineWrap(true);
setWrapStyleWord(true);
}
@Override
public WordWrapCellRenderer getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
setText(value.toString());
setSize(table.getColumnModel().getColumn(column).getWidth(), getPreferredSize().height);
if (table.getRowHeight(row) != getPreferredSize().height) {
table.setRowHeight(row, getPreferredSize().height);
}
return this;
}
}
private String loremIpsum() {
return "Lorem Ipsum is simply dummy text of the printing and typesetting industry."
+ " Lorem Ipsum has been the industry's standard dummy text ever since the 1500s,"
+ " when an unknown printer took a galley of type and scrambled it to make a type specimen book."
+ " It has survived not only five centuries, but also the leap into electronic typesetting, "
+ "remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset"
+ " sheets containing Lorem Ipsum passages, and more recently with desktop publishing software"
+ " like Aldus PageMaker including versions of Lorem Ipsum";
}
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> {
JFrame frame = new JFrame("Test");
frame.setLayout(new BorderLayout());
TableTest table = new TableTest();
JScrollPane sp = new JScrollPane(table);
frame.add(sp);
frame.setSize(500, 500);
frame.setLocationRelativeTo(null);
frame.setVisible(true);
});
}
}
First attempt to solve this issue is to change table.getRowHeight(row) != getPreferredSize().height
to table.getRowHeight(row) <= getPreferredSize().height
in order to make the height of the row change only when a "taller" component must be rendered. This will not work either because after a "tall" render row height will not be restored (wrapped). Related gif image:
After seeing this, i tried to create some kind of listener (ComponentListener#componentResized
- MouseListener#mouseReleased
to header?) that restores each row's height like the following:
private void restoreRowHeight() {
if (getModel() == null) //causing NPE
return;
for (int row = 0; row < getRowCount(); row++) {
int heightOfTheTallestComponent = -1;
for (int column = 0; column < getColumnCount(); column++) {
Component c = prepareRenderer(getDefaultRenderer(String.class), row, column);
if (c.getPreferredSize().height > heightOfTheTallestComponent)
heightOfTheTallestComponent = c.getPreferredSize().height;
}
setRowHeight(row, heightOfTheTallestComponent);
}
}
None of the listeners i could think of seem to fit. However, even if i find the proper listener that will call this method, a small but very annoying glitch takes place. (Any alternatives that prevent it you are welcome).
Finally i took some hopes (i regret it after 10 minutes) that maybe JTable renders properly JLabel
s with <html>
text (a JLabel
wraps lines when it has an HTML text) and used the following (extending DefaultTableCellRenderer
)
int width = table.getColumnModel().getColumn(column).getWidth();
setText("<html><p style='width: " + width + "px'>" + String.valueOf(value));
But of course, no chance.
Another approach i tried is again with a JLabel which is described in this answer but again, there is no height restoration in case it needs to be restored.
Is there any solution out there that will wrap and show the text of all columns properly and does not cause a glitch?
Upvotes: 2
Views: 716
Reputation: 6808
Maybe the solution i mention in my question, about using listeners to restore extra row space is not bad at all. Even if it is not the 100% optimal solution to this problem (that's why i will not accept my answer) it is stable and it doesn't cause any performance issues. Plus it is kind of simple to understand (it is a plus, right?).
The thing is that restoring extra row height and wrap the cell must take place at the right time a.k.a use the right listeners. Extra space can be caused by 2 events.
In order to cover the first one, the best listener i found is to use a MouseListener
to table's header and more particular to catch mouseReleased
event since the resizing of the column ends when the mouse click to the header is released.
About the second one a ComponentListener#componentResized
is enough to cover it. Note that this listener is being called even when the data of the table change, that's why a "dataChanged
" kind of listener is not required (probably override model's fireTableDataChanged
method ?)
It is not best, but it is something.
Preview:
Code:
public class TableTest extends JTable {
private static final String[] COLUMNS = { "SomeColumn", "OtherColumn", "OtherOtherColumn" };
public TableTest() {
super();
Object[][] data = new Object[5][3];
for (int i = 0; i < data.length; i++) {
data[i][0] = "Row: " + i + " - " + loremIpsum();
data[i][1] = "Row: " + i + " Maybe something small?";
data[i][2] = "Row: " + i + "___" + new StringBuilder(loremIpsum()).reverse().toString();
}
setModel(new DefaultTableModel(data, COLUMNS) {
@Override
public Class<?> getColumnClass(int columnIndex) {
return String.class;
}
});
setDefaultRenderer(String.class, new WordWrapCellRenderer());
getTableHeader().setReorderingAllowed(false);
getTableHeader().setReorderingAllowed(true);
getTableHeader().addMouseListener(new MouseAdapter() {
@Override
public void mouseReleased(MouseEvent e) {
restoreRowHeight();
}
});
addComponentListener(new ComponentAdapter() {
@Override
public void componentResized(ComponentEvent e) {
restoreRowHeight();
}
});
}
private void restoreRowHeight() {
if (getModel() == null) // causing NPE
return;
for (int row = 0; row < getRowCount(); row++) {
int heightOfTheTallestComponent = -1;
for (int column = 0; column < getColumnCount(); column++) {
Component c = prepareRenderer(getDefaultRenderer(String.class), row, column);
if (c.getPreferredSize().height > heightOfTheTallestComponent)
heightOfTheTallestComponent = c.getPreferredSize().height;
}
setRowHeight(row, heightOfTheTallestComponent);
}
}
public static class WordWrapCellRenderer extends JTextArea implements TableCellRenderer {
private WordWrapCellRenderer() {
setLineWrap(true);
setWrapStyleWord(true);
}
@Override
public WordWrapCellRenderer getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
setText(value.toString());
setSize(table.getColumnModel().getColumn(column).getWidth(), getPreferredSize().height);
if (table.getRowHeight(row) < getPreferredSize().height) {
table.setRowHeight(row, getPreferredSize().height);
}
return this;
}
}
private String loremIpsum() {
return "Lorem Ipsum is simply dummy text of the printing and typesetting industry."
+ " Lorem Ipsum has been the industry's standard dummy text ever since the 1500s,"
+ " when an unknown printer took a galley of type and scrambled it to make a type specimen book."
+ " It has survived not only five centuries, but also the leap into electronic typesetting, "
+ "remaining essentially unchanged. It was popularised in the 1960s with the release of Letraset"
+ " sheets containing Lorem Ipsum passages, and more recently with desktop publishing software"
+ " like Aldus PageMaker including versions of Lorem Ipsum";
}
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> {
JFrame frame = new JFrame("Test");
frame.setLayout(new BorderLayout());
TableTest table = new TableTest();
JScrollPane sp = new JScrollPane(table);
frame.add(sp);
frame.setSize(500, 500);
frame.setLocationRelativeTo(null);
frame.setVisible(true);
});
}
}
Upvotes: 1