George Z.
George Z.

Reputation: 6808

JTextArea (line wrapping) as TableCellRender to multiple colums

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)

good

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.

bad

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:

enter image description here

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 JLabels 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

Answers (1)

George Z.
George Z.

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.

  1. When a column's width is changed manually by the user.
  2. When table for some reason is being resized.

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:

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

Related Questions