10

I want to put a String in a JTable that is longer than the given cell-width. How can I set the rowHeight dynamically so that I can read the whole String? Here is an example:

import javax.swing.*;

public class ExampleTable {

public JPanel createTable() {               
    JPanel totalGUI = new JPanel();

    //define titles for table
    String[] title = {"TITLE1", "TITLE2", "TITLE3"};

    //table data
    Object[][] playerdata = {       
    {new Integer(34), "Steve", "test test test"},
    {new Integer(32), "Patrick", "dumdi dumdi dummdi dumm di di didumm"},
    {new Integer(10), "Sarah", "blabla bla bla blabla bla bla blabla"},};

    //create object 'textTable'
    JTable textTable = new JTable(playerdata,title);

    //set column width
    textTable.setAutoResizeMode(JTable.AUTO_RESIZE_OFF); 
    textTable.getColumnModel().getColumn(0).setPreferredWidth(60);
    textTable.getColumnModel().getColumn(1).setPreferredWidth(60);
    textTable.setDefaultRenderer(String.class, new RowHeightCellRenderer());

    //scrollbar
    JScrollPane scrollPane = new JScrollPane(textTable);

    totalGUI.add(scrollPane);               
    return totalGUI;
}

private static void createAndShowGUI() {

    //create main frame
    JFrame mainFrame = new JFrame("");
    ExampleTable test = new ExampleTable();

    JPanel totalGUI = new JPanel();
    totalGUI = test.createTable();

    //visible mode
    mainFrame.add(totalGUI); //integrate main panel to main frame
    mainFrame.pack();
    mainFrame.setVisible(true);     
}


public static void main (String[] args) {               

    createAndShowGUI();     

}//main
}

And here you'll see the code which line-breaks each text that is to long for the given cell

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


    public class RowHeightCellRenderer extends JTextArea implements TableCellRenderer
    {
      /**
         * 
         */
        private static final long serialVersionUID = 1L;

    public Component getTableCellRendererComponent (JTable table, 
                                                    Object value, 
                                                    boolean isSelected, 
                                                    boolean hasFocus, 
                                                    int row, 
                                                    int column )  {
        setText( value.toString() );    
        return this;
      }
    }

thank you but I want to implement the RowHeight dynamically, depending on String length... I want to read the whole String/text in the cell. any suggestions?

I'm java beginner and this is my first question. I would be delighted I get an answer.

Ramses
  • 652
  • 2
  • 8
  • 30
  • I see a `RowCellRender`. Can we see that class. I think you may be on the right path with this. – Paul Samsotha Feb 12 '14 at 09:04
  • thank you, my code is just an example and the column width has a fixed size. Therefore Rowheight has to adjust itself autmatically – Ramses Feb 12 '14 at 09:20
  • beware: @peeskillet's solution is **wrong** in setting table's rowHeight in the renderer! Instead, listen to all changes that might require an adjustment and change it there. – kleopatra Feb 14 '14 at 11:36
  • @kleopatra I wasn't able to correct my answer to your standards. Please consider ansering this question for a bounty (not that points are of major importance to you) ad I would love to see your approach to this. Or for anyone else who wants to take on this challenge, feel free! – Paul Samsotha Feb 14 '14 at 11:50
  • @user3300710 please un-accept this answer and wait for a more proper answer to accept. Also so I can delete my answer, after\if the bounty is rewarded. – Paul Samsotha Feb 14 '14 at 11:51
  • @peeskillet: ok i did it. – Ramses Feb 14 '14 at 21:04
  • @kleopatra:I'm very interested for solution from you! – Ramses Feb 14 '14 at 21:06

5 Answers5

15

There are several issues when using a JTextArea as rendering component (and most if not all of them already explained in several QA's on this site). Trying to sum them up:

Adjust individual row height to the size requirements of the rendering component

Basically, the way to go is to loop through the cells as needed, then

  • configure its renderer with the data
  • ask the rendering component for its preferred size
  • set the table row height to the pref height

The updateRowHeight method in the OP's edited question is just fine.

JTextArea's calculation of its preferredSize

to get a reasonable sizing hint for one dimension, it needs to be "seeded" with some reasonable size in the other dimension. That is if we want the height it needs a width, and that must be done in each call. In the context of a table, a reasonable width is the current column width:

public Component getTableCellRendererComponent(JTable table,
        Object value, boolean isSelected, boolean hasFocus, int row,
        int column) {
    ... // configure visuals
    setText((String) value);
    setSize(table.getColumnModel().getColumn(column).getWidth(),
            Short.MAX_VALUE);
    return this;
}// getTableCellRendererComponent

Dynamic adjustment of the height

The row height it fully determined in some steady state of the table/column/model. So you set it (call updateRowHeight) once after the initialization is completed and whenever any of the state it depends on is changed.

// TableModelListener
@Override
public void tableChanged(TableModelEvent e) {
    updateRowHeights();
}

// TableColumnModelListener
@Override
public void columnMarginChanged(ChangeEvent e) {
    updateRowHeights();
}

Note

As a general rule, all parameters in the getXXRendererComponent are strictly read-only, implementations must not change any state of the caller. Updating the rowHeight from within the renderer is wrong.

kleopatra
  • 51,061
  • 28
  • 99
  • 211
  • Is there a nice way to: "So you set it (call updateRowHeight) once after the initialization is completed" Now I am using a componentListener, so after the table is renderered for the first time, the table has a map with all preferred heights for each cell, then I want to call the updateRowHeights method. The only way I found to have this invoked after the rendering is using a component listener on the table... – Nieke Aerts Dec 22 '16 at 13:16
5

Many good ideas are linked in other answers and also in the related questions.

So here is how I modified you classes to get a table with fully working auto-line-heights:

import javax.swing.*;
import javax.swing.event.*;
import javax.swing.table.DefaultTableModel;
import javax.swing.table.TableModel;
import java.awt.*;

public class ExampleTable implements TableModelListener {
    JTable textTable;

    public JPanel createTable() {
        JPanel totalGUI = new JPanel();

        //define titles for table
        String[] title = {"TITLE1", "TITLE2", "TITLE3"};

        //table data
        Object[][] playerdata = {
                {new Integer(3), "Steve", "test test test"},
                {new Integer(32), "Patrick", "du hu hu hu hu hu hu hu uh u kkkkkk oooo pppp"},
                {new Integer(10), "Sarah", "xxxxxxxxxxxxx aaaaaaaaaa bbbbbbbbbbbb dddddddddddd xxxxxxx gggewr  eeeeeeeeee22 ddd g fffffff zzzzzzz"},};

        //define tablemodel
        TableModel model = new DefaultTableModel(playerdata, title);

        //create object 'textTable'
        textTable = new JTable(model);

        //set column width
        textTable.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);
        textTable.getColumnModel().getColumn(0).setPreferredWidth(17);
        textTable.getColumnModel().getColumn(1).setPreferredWidth(45);
        textTable.getColumnModel().getColumn(2).setPreferredWidth(200);

        //put line breaks if string is longer than cell-width
        RowHeightCellRenderer dynRow = new RowHeightCellRenderer();
        textTable.getColumnModel().getColumn(2).setCellRenderer(dynRow);

        // No more data changes; install listeners
        textTable.getModel().addTableModelListener(this);
        textTable.getColumnModel().addColumnModelListener(new TableColumnModelListener() {
            /**
             * We only need to recalculate once; so track if we are already going to do it.
             */
            boolean columnHeightWillBeCalculated = false;

            @Override
            public void columnAdded(TableColumnModelEvent e) {
            }

            @Override
            public void columnRemoved(TableColumnModelEvent e) {
            }

            @Override
            public void columnMoved(TableColumnModelEvent e) {
            }

            @Override
            public void columnMarginChanged(ChangeEvent e) {
                if (!columnHeightWillBeCalculated && textTable.getTableHeader().getResizingColumn() != null) {
                    columnHeightWillBeCalculated = true;
                    SwingUtilities.invokeLater(new Runnable() {
                        @Override
                        public void run() {
                            // textTable.getTableHeader().getResizingColumn() is != null as long as the user still is holding the mouse down
                            // To avoid going over all data every few milliseconds wait for user to release
                            if (textTable.getTableHeader().getResizingColumn() != null) {
                                SwingUtilities.invokeLater(this);
                            } else {
                                tableChanged(null);
                                columnHeightWillBeCalculated = false;
                            }
                        }
                    });
                }
            }

            @Override
            public void columnSelectionChanged(ListSelectionEvent e) {
            }
        });

        //scrollbar
        JScrollPane scrollPane = new JScrollPane(textTable);
        totalGUI.add(scrollPane);
        return totalGUI;
    }

    public void tableChanged(TableModelEvent e) {
        final int first;
        final int last;
        if (e == null || e.getFirstRow() == TableModelEvent.HEADER_ROW) {
            // assume everything changed
            first = 0;
            last = textTable.getModel().getRowCount();
        } else {
            first = e.getFirstRow();
            last = e.getLastRow() + 1;
        }
        // GUI-Changes should be done through the EventDispatchThread which ensures all pending events were processed
        // Also this way nobody will change the text of our RowHeightCellRenderer because a cell is to be rendered
        if(SwingUtilities.isEventDispatchThread()) {
            updateRowHeights(first, last);
        } else {
            SwingUtilities.invokeLater(new Runnable() {
                public void run() {
                    updateRowHeights(first, last);
                }
            });
        }
    }

    private void updateRowHeights(final int first, final int last) {
    /*
     * Auto adjust the height of rows in a JTable.
     * The only way to know the row height for sure is to render each cell
     * to determine the rendered height. After your table is populated with
     * data you can do:
     *
     */
        for (int row = first; row < last; row++) {
            int rowHeight = 20;
            for (int column = 0; column < textTable.getColumnCount(); column++) {
                Component comp = textTable.prepareRenderer(textTable.getCellRenderer(row, column), row, column);
                rowHeight = Math.max(rowHeight, comp.getPreferredSize().height);
            }
            if(rowHeight != textTable.getRowHeight(row)) {
                textTable.setRowHeight(row, rowHeight);
                System.out.println("neue Zeilenhöhe: "+rowHeight+" bei Zeile: "+row);
            }
        }
    }

    private static void createAndShowGUI() {

        //create main frame
        JFrame mainFrame = new JFrame("");
        mainFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        ExampleTable test = new ExampleTable();
        JPanel totalGUI = new JPanel();
        totalGUI = test.createTable();

        //visible mode
        mainFrame.add(totalGUI); //integrate main panel to main frame
        mainFrame.pack();
        mainFrame.setVisible(true);

        //adjust dynamically the Row height of each cell
        test.tableChanged(null);
    }


    public static void main (String[] args) {
        createAndShowGUI();
    }//main
}

Important changes:

  • calculate height only after JFrame is visible because we may not have the correct font before then
  • change rowHeight only from event-dispatch-thread (SwingUtilities.isEventDispatchThread()/SwingUtilities.invokeLater()); otherwise the table might assign different values to our RowHeightCellRenderer while we are in the middle of calculating height
  • initalize rowHeight to 20, so we can shrink again
  • TableColumnModelListener so we know when the user resizes a column and shrink or grow rowHeights (you can safely remove the Listener if this is undesired)

Also now I only evaluate changed rows for TableModel changes

import java.awt.*;
import javax.swing.*;
import javax.swing.table.*;
import javax.swing.text.BadLocationException;

public class RowHeightCellRenderer extends JTextArea implements TableCellRenderer {

    private static final long serialVersionUID = 1L;


    public RowHeightCellRenderer() {
        setLineWrap(true);
        setWrapStyleWord(true);
    }//constructor

    public Component getTableCellRendererComponent (JTable table,
                                                    Object value,
                                                    boolean isSelected,
                                                    boolean hasFocus,
                                                    int row,
                                                    int column ) {
        this.setText((String) value);

        if(isSelected) {
            this.setBackground(table.getSelectionBackground());
            this.setForeground(table.getSelectionForeground());
        } else {
            this.setBackground(table.getBackground());
            this.setForeground(table.getForeground());
        }

        final int columnWidth = table.getColumnModel().getColumn(column).getWidth();
        final int rowHeight = table.getRowHeight(row);
        this.setSize(columnWidth, rowHeight);

        this.validate();
        return this;
    }//getTableCellRendererComponent

    @Override
    public Dimension getPreferredSize() {
        try {
            // Get Rectangle for position after last text-character
            final Rectangle rectangle = this.modelToView(getDocument().getLength());
            if(rectangle != null) {
                return new Dimension(this.getWidth(),
                                     this.getInsets().top + rectangle.y + rectangle.height +
                                                                  this.getInsets().bottom);
            }
        } catch (BadLocationException e) {
            e.printStackTrace();  // TODO: implement catch
        }

        return super.getPreferredSize();
    }
}//RowHeightCellRenderer

Changes:

  • setSize() in getTableCellRendererComponent() as otherwise the object is unable to wrap correctly
  • calculate preferedSize according to position of last character
TheConstructor
  • 4,285
  • 1
  • 31
  • 52
3

Actually JTextArea already implements all the features required to dynamically change its height based on its width. One can see this functionality in action, when JTextArea is used inside a ScrollPane, where its height is automatically adjusted to fit the width of the ScrollPane. To use this feature, one has to first set the size of the JTextArea to some width and then JTextArea#getPreferredSize() will return the required height to display the text (if line wrap is set to true).

So to dynamically change the row height of a JTable based on its width, one can do the following:

  1. Add a custom TableCellRenderer to the table that returns a JTextArea in TableCellRenderer#getTableCellRendererComponent()
  2. Listen to the resizing of the columns as explained here: Java JTable detect Column re-sized by user
  3. Update the row heights of the columns as explained here: Auto adjust the height of rows in a JTable
  4. During the update of the row heights, one calculates the required size of the JTextArea by first setting the new width and then getting the preferred height as shown in the following code snippet

Update function of the row heights:

public static void updateRowHeights(int column, int width, JTable table){
    for (int row = 0; row < table.getRowCount(); row++) {
        int rowHeight = table.getRowHeight();
        Component comp = table.prepareRenderer(table.getCellRenderer(row, column), row, column);
        Dimension d = comp.getPreferredSize();
            // first set the size to the new width
        comp.setSize(new Dimension(width, d.height));
            // then get the preferred size
        d = comp.getPreferredSize();
        rowHeight = Math.max(rowHeight, d.height);
            // finally set the height of the table
        table.setRowHeight(row, rowHeight);
    }
}

With this approach, no further calculations of colums or rows are necessary.

Here is the complete code of the OP's ExampleTable, adjusted to this implementation.

import java.awt.Component;
import java.awt.Dimension;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;

import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.JTextArea;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.TableColumnModelEvent;
import javax.swing.event.TableColumnModelListener;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.JTableHeader;
import javax.swing.table.TableCellRenderer;
import javax.swing.table.TableColumn;

public class ExampleTable {

    public class RowHeightCellRenderer extends JTextArea implements TableCellRenderer {

        public Component getTableCellRendererComponent(
                JTable table, Object value,
                boolean isSelected, boolean hasFocus,
                int row, int column) {

            setEditable(false);
            setLineWrap(true);
            setWrapStyleWord(true);

            if (isSelected) {
                setBackground(table.getSelectionBackground());
                setForeground(table.getSelectionForeground());
            } else {
                setBackground(table.getBackground());
                setForeground(table.getForeground());
            }

            setText(value.toString());
            return this;
        }
    }

    public static void updateRowHeights(int column, int width, JTable table){
        for (int row = 0; row < table.getRowCount(); row++) {
            int rowHeight = table.getRowHeight();
            Component comp = table.prepareRenderer(table.getCellRenderer(row, column), row, column);
            Dimension d = comp.getPreferredSize();
            comp.setSize(new Dimension(width, d.height));
            d = comp.getPreferredSize();
            rowHeight = Math.max(rowHeight, d.height);
            table.setRowHeight(row, rowHeight);
        }
    }

    public JPanel createTable() {

        JPanel totalGUI = new JPanel();

        //define titles for table
        final String[] columnNames = {"TITLE1", "TITLE2", "TITLE3"};

        //table data
        final Object[][] rowData = {       
                {new Integer(34), "Steve", "test test test"},
                {new Integer(32), "Patrick", "dumdi dumdi dummdi dumm di di didumm"},
                {new Integer(10), "Sarah", "blabla bla bla blabla bla bla blabla"},};

        AbstractTableModel model = new AbstractTableModel() {
            @Override
            public Class<?> getColumnClass(int columnIndex) {
                return String.class;
            }

            public String getColumnName(int column) { return columnNames[column].toString(); }
            public int getRowCount() { return rowData.length; }
            public int getColumnCount() { return columnNames.length; }
            public Object getValueAt(int row, int col) { return rowData[row][col]; }
            public boolean isCellEditable(int row, int column) { return true; }
            public void setValueAt(Object value, int row, int col) {
                rowData[row][col] = value;
                fireTableCellUpdated(row, col);
            }
        };

        //create object 'textTable'
        final JTable textTable = new JTable();

        textTable.setDefaultRenderer(String.class, new RowHeightCellRenderer());
        textTable.setModel(model);

        //set column width
        textTable.setAutoResizeMode(JTable.AUTO_RESIZE_OFF); 
        textTable.getColumnModel().getColumn(0).setPreferredWidth(60);
        textTable.getColumnModel().getColumn(1).setPreferredWidth(60);

        ColumnListener cl = new ColumnListener(){

            @Override
            public void columnMoved(int oldLocation, int newLocation) {
            }

            @Override
            public void columnResized(int column, int newWidth) {
                updateRowHeights(column, newWidth, textTable);
            }

        };

        textTable.getColumnModel().addColumnModelListener(cl);
        textTable.getTableHeader().addMouseListener(cl);

        // initial update of row heights
        TableColumn c = textTable.getColumnModel().getColumn(2);
        updateRowHeights(2, c.getWidth(), textTable);

        //scrollbar
        JScrollPane scrollPane = new JScrollPane(textTable);

        totalGUI.add(scrollPane);               
        return totalGUI;
    }

    private static void createAndShowGUI() {

        //create main frame
        JFrame mainFrame = new JFrame("");
        mainFrame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        ExampleTable test = new ExampleTable();

        JPanel totalGUI = new JPanel();
        totalGUI = test.createTable();

        //visible mode
        mainFrame.add(totalGUI); //integrate main panel to main frame
        mainFrame.pack();
        mainFrame.setVisible(true);     
    }


    public static void main (String[] args) {               

        createAndShowGUI();     

    }//main


    abstract class ColumnListener extends MouseAdapter implements TableColumnModelListener {

        private int oldIndex = -1;
        private int newIndex = -1;
        private boolean dragging = false;

        private boolean resizing = false;
        private int resizingColumn = -1;
        private int oldWidth = -1;

        @Override
        public void mousePressed(MouseEvent e) {
            // capture start of resize
            if(e.getSource() instanceof JTableHeader) {
                JTableHeader header = (JTableHeader)e.getSource();
                TableColumn tc = header.getResizingColumn();
                if(tc != null) {
                    resizing = true;
                    JTable table = header.getTable();
                    resizingColumn = table.convertColumnIndexToView( tc.getModelIndex());
                    oldWidth = tc.getPreferredWidth();
                } else {
                    resizingColumn = -1;
                    oldWidth = -1;
                }
            }   
        }

        @Override
        public void mouseReleased(MouseEvent e) {
            // column moved
            if(dragging && oldIndex != newIndex) {
                columnMoved(oldIndex, newIndex);
            }
            dragging = false;
            oldIndex = -1;
            newIndex = -1;

            // column resized
            if(resizing) {
                if(e.getSource() instanceof JTableHeader) {
                    JTableHeader header = (JTableHeader)e.getSource();
                    TableColumn tc = header.getColumnModel().getColumn(resizingColumn);
                    if(tc != null) {
                        int newWidth = tc.getPreferredWidth();
                        if(newWidth != oldWidth) {
                            columnResized(resizingColumn, newWidth);
                        }
                    }
                }   
            }
            resizing = false;
            resizingColumn = -1;
            oldWidth = -1;
        }

        @Override
        public void columnAdded(TableColumnModelEvent e) {      
        }

        @Override
        public void columnRemoved(TableColumnModelEvent e) {        
        }

        @Override
        public void columnMoved(TableColumnModelEvent e) {
            // capture dragging
            dragging = true;
            if(oldIndex == -1){
                oldIndex = e.getFromIndex();
            }

            newIndex = e.getToIndex();  
        }

        @Override
        public void columnMarginChanged(ChangeEvent e) {
        }

        @Override
        public void columnSelectionChanged(ListSelectionEvent e) {
        }

        public abstract void columnMoved(int oldLocation, int newLocation);
        public abstract void columnResized(int column, int newWidth);
    }
}

Note, that I reused and changed some of the code from Java JTable detect Column re-sized by user and Auto adjust the height of rows in a JTable.

Community
  • 1
  • 1
Balder
  • 8,623
  • 4
  • 39
  • 61
  • 1
    @peeskillet Yes, of course you are right. I changed the OP's code to recreate the rendering component for testing purposes and forgot to change it back - thanks for the heads-up. – Balder Feb 15 '14 at 14:37
  • A little correction: in updateRowHeights() line 3 should be "int rowHeight = table.getRowHeight(row);" – sers Oct 20 '16 at 13:02
1

I find it simplest to adjust the row height from inside the component renderer, like this:

public class RowHeightCellRenderer extends JTextArea implements TableCellRenderer
{
    public Component getTableCellRendererComponent(JTable table, Object 
            value, boolean isSelected, boolean hasFocus,
            int row, int column) {

        setText(value.toString());

        // Set the component width to match the width of its table cell
        // and make the height arbitrarily large to accomodate all the contents
        setSize(table.getColumnModel().getColumn(column).getWidth(), Short.MAX_VALUE);

        // Now get the fitted height for the given width
        int rowHeight = this.getPreferredSize().height;

        // Get the current table row height
        int actualRowHeight = table.getRowHeight(row);

        // Set table row height to fitted height.
        // Important to check if this has been done already
        // to prevent a never-ending loop.
        if (rowHeight != actualRowHeight) {
           table.setRowHeight(row, rowHeight);
        }

        return this;
    }
}

This will also work for a renderer which returns a JTextPane.

Ned Howley
  • 828
  • 11
  • 19
0

Normally we are setting the RowHeight when the text in the row goes beyond the height of a row, to set the height of a row we do this:

table.setRowHeight(40);

I want to put a String in a JTable that is longer than the given cell-width. How can I set the rowHeight dynamically so that I can read the whole String?

To read complete string from the jtable row you don't need to set RowHeight you need to set the PreferredWidth of a column as you are already doing so. Just add a line like:

textTable.getColumnModel().getColumn(2).setPreferredWidth(300);

And you will be able to read the whole string.


Output

output

ravibagul91
  • 20,072
  • 5
  • 36
  • 59
  • thank you, my code is just an example and the column width has a fixed size. Therefore Rowheight has to adjust itself autmatically – Ramses Feb 12 '14 at 09:20