JTable header text wrapping for multiline header (custom TableCellRenderer)

7.9k views Asked by At

How can I obtain a multiline JTable header where the header column correctly enlarges to fit some text and then wraps to a new line?

Something like shown below:

wrapping column header

Currently searching for the above requirements returns a lot of solutions of which none really solves the problem:

http://www.javarichclient.com/multiline-column-header/

Creating multi-line header for JTable

Java JTable header word wrap

The above solutions all propose using HTML code, for instance:

String[] columnNames = {
    "<html><center>Closing<br>Date</html>",
    "<html><center>Open<br>Price</html>",
    "<html>Third<br>column</html>"
};

That solution is not elegant for a couple of reasons, mainly because in the case of variable columns names I need to pass the string to a function which strips spaces and subtitutes them with <br> symbols, however if the column text contains very short text that appears in a line of its own.

I would need to decide a minimum and a maximum length of a column and then be able to make text centering possible, the above solution quickly becomes overengineered and unmanageable.

http://www.java2s.com/Code/Java/Swing-Components/MultiLineHeaderTable.htm

http://www.java2s.com/Code/Java/Swing-Components/MultiLineHeaderExample.htm

multilineheader2

Above solutions require manually creating a header array with words already correctly split up as in:

  public static Object[][] tableHeaders = new Object[][] {
      new String[] { "Currency" },
      new String[] { "Yesterday's", "Rate" },
      new String[] { "Today's", "Rate" },
      new String[] { "Rate", "Change" } };

-or-

DefaultTableModel dm = new DefaultTableModel();
    dm.setDataVector(
        new Object[][] { { "a", "b", "c" }, { "A", "B", "C" } },
        new Object[] { "1st\nalpha", "2nd\nbeta", "3rd\ngamma" });

Still not elegant because variable text in the column names would not be feasible.

How to change JTable header height?

Manually setting the header height as in the above solutions is only half of what I want to do, because then text would still not correctly wrap and deciding the height is still not feasible.

Currently all I was able was to create a custom TableCellRenderer but yet no solution:

import java.awt.Component;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.table.TableCellRenderer;
import javax.swing.table.TableColumn;

import java.util.*;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.StringReader;
import javax.swing.*;
import javax.swing.table.*;

/**
 * @version 1.0 11/09/98
 */
public class MultiLineHeaderExample extends JFrame
{

    MultiLineHeaderExample()
    {
        super("Multi-Line Header Example");

        DefaultTableModel dm = new DefaultTableModel();
        dm.setDataVector(new Object[][]
        {
            {
                "a", "b", "c"
            },
            {
                "A", "B", "C"
            }
        },
        new Object[]
                {
                    "My First Column, Very Long But Space Separated", "short col", "VeryLongNoSpaceSoShouldSomeHowWrap"
        });

        JTable table = new JTable(dm);
        MultiLineHeaderRenderer renderer = new MultiLineHeaderRenderer();
        Enumeration enumK = table.getColumnModel().getColumns();
        while (enumK.hasMoreElements())
        {
            ((TableColumn) enumK.nextElement()).setHeaderRenderer(renderer);
        }
        JScrollPane scroll = new JScrollPane(table);
        getContentPane().add(scroll);
        setSize(400, 110);
        setVisible(true);
    }

    public static void main(String[] args)
    {
        MultiLineHeaderExample frame = new MultiLineHeaderExample();
        frame.addWindowListener(new WindowAdapter()
        {
            public void windowClosing(WindowEvent e)
            {
                System.exit(0);
            }
        });
    }
}

class MultiLineHeaderRenderer extends JList implements TableCellRenderer
{

    public MultiLineHeaderRenderer()
    {
        ListCellRenderer renderer = getCellRenderer();
        ((JLabel) renderer).setHorizontalAlignment(JLabel.CENTER);
        setCellRenderer(renderer);
    }

    @Override
    public Component getTableCellRendererComponent(JTable table, Object value,
            boolean isSelected, boolean hasFocus, int row, int column)
    {
        setFont(table.getFont());
        String str = (value == null) ? "" : value.toString();
        BufferedReader br = new BufferedReader(new StringReader(str));
        String line;
        Vector v = new Vector();
        try
        {
            while ((line = br.readLine()) != null)
            {
                v.addElement(line);
            }
        }
        catch (IOException ex)
        {
            ex.printStackTrace();
        }
        setListData(v);
        return this;
    }
}
3

There are 3 answers

4
Stefan On BEST ANSWER

This here also uses JTextArea and also resizes the header height when the table is resized. The key to the correct calculation of the table header height is setSize(width, getPreferredSize().height);

class MultiLineTableHeaderRenderer extends JTextArea implements TableCellRenderer
{
  public MultiLineTableHeaderRenderer() {
    setEditable(false);
    setLineWrap(true);
    setOpaque(false);
    setFocusable(false);
    setWrapStyleWord(true);
    LookAndFeel.installBorder(this, "TableHeader.cellBorder");
  }

  @Override
  public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
    int width = table.getColumnModel().getColumn(column).getWidth();
    setText((String)value);
    setSize(width, getPreferredSize().height);
    return this;
  }
}
0
David On

Here is another approach. This solution has the following advantages:

  1. You need not manually break the column names.
  2. The columns dynamically word-wrap as you resize the columns and/or window.
  3. The header appearance will automatically be consistent with the installed look-and-feel.
  4. Unlike other solutions I have seen, this works even if the first column doesn't wrap (as in the example below).

It has the following disadvantage, however: It creates an unused JTableHeader object for each column, so it's a bit inelegant and probably not suitable if you have many columns.

The basic idea is that you wrap the column names in an <html> tags, and, crucially, every TableColumn gets its own TableCellRenderer object.

I came to this solution after debugging deep into the guts of the Swing table header layout plumbing. Without getting too much into the weeds, the problem is that if the TableColumns don't have a headerRenderer defined, the same default renderer is used for every column header cell. The layout code used for JTableHeader only bothers to ask the renderer of the first column header for its preferred size (see feature 4. above), and because the renderer is re-used, the call to its setText() method triggers the creation of a new View for the label, which, for reasons I'm too tired to even think about explaining, causes the header renderer to always report its preferred unwrapped height.

Here is a quick-and-dirty proof-of-concept:

package scratch;

import java.util.*;
import javax.swing.*;
import javax.swing.table.*;

@SuppressWarnings("serial")
public class WordWrappingTableHeaderDemo extends JFrame {

    class DemoTableModel extends AbstractTableModel {

        private ArrayList<String> wrappedColumnNames = new ArrayList<String>(); 
        private int numRows;

        DemoTableModel(List<String> columnNames, int numRows) {
            for (String name: columnNames)
                wrappedColumnNames.add("<html>" + name + "</html>");
            this.numRows = numRows;
        }

        public int getRowCount() {
            return numRows;
        }

        public int getColumnCount() {
            return wrappedColumnNames.size();
        }

        public Object getValueAt(int rowIndex, int columnIndex) {
            return Integer.valueOf(10000 + (rowIndex + 1)*(columnIndex + 1));
        }

        public String getColumnName(int column) {
            return wrappedColumnNames.get(column);
        }

        public Class<?> getColumnClass(int columnIndex) {
            return Integer.class;
        }
    }

    public WordWrappingTableHeaderDemo() {

        DefaultTableColumnModel tableColumnModel = new DefaultTableColumnModel() {
            public void addColumn(TableColumn column) {
                // This works, but is a bit kludgey as it creates an unused JTableHeader object for each column:
                column.setHeaderRenderer(new JTableHeader().getDefaultRenderer());
                super.addColumn(column);
            }
        };

        JTable table = new JTable();
        table.setFillsViewportHeight(true);;
        table.setColumnModel(tableColumnModel);
        table.setModel(
                new DemoTableModel(Arrays.asList("Name", "The Second Column Name is Very Long", "Column Three"), 20));
        getContentPane().add(new JScrollPane(table));
    }

    public static void createAndShowGUI() {
        WordWrappingTableHeaderDemo app = new WordWrappingTableHeaderDemo();
        app.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        app.setLocationByPlatform(true);
        app.pack();
        app.setVisible(true);
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(() -> {createAndShowGUI();});
    }
}
0
Timothy Truckle On

you need a Conponent that is able to wordwrap its content like JTextArea. I changed the cell renderer from your SSCCE so that is works initially, but it has a nasty resize behavior.

 class MultiLineHeaderRenderer extends JTextArea implements TableCellRenderer {
    public MultiLineHeaderRenderer()
    {
        setAlignmentY(JLabel.CENTER);
        setLineWrap(true);
        setWrapStyleWord(true);
        setBorder(BorderFactory.createCompoundBorder(
                BorderFactory.createLineBorder(Color.BLACK),
                BorderFactory.createEmptyBorder(3,3,3,3)
                ));

    }

    @Override
    public Component getTableCellRendererComponent(JTable table,
            Object value,
            boolean isSelected,
            boolean hasFocus,
            int row,
            int column) {
        setFont(table.getFont());
        String str = (value == null) ? "" : value.toString();
        setText(str);
        int columnWidth= getColumnWidth();
        setRows(str.length()/columnWidth);
        return this;
    }
}