Robin
Robin

Reputation: 36611

Preferred height of JPanel is lower then combined height of its children in table renderer

I have a JTable for which the renderer returns a JPanel composed of multiple JLabel instances. One of those JLabels can contain HTML used among other things to split the output over multiple lines using <br/> tags.

To show the multiple lines in the table, the renderer calls in the getTableCellRendererComponent method

table.setRowHeight(row, componentToReturn.getPreferredSize().height);

to dynamically update the row height, based on the contents. This only works correctly if componentToReturn indicates a correct preferred size.

It looks however that the getPreferredSize returns bogus values. The preferred height of the returned component is smaller than the sum of the heights of the labels inside the component.

Here is a little program illustrating this behaviour (without using a JTable)

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

public class SwingLabelTest {
  public static void main(String[] args) {
    EventQueue.invokeLater(new Runnable() {
      @Override
      public void run() {
        LabelPanel renderer = new LabelPanel();
        Component component = renderer.getComponent(false);
        //asking for a bigger component will not
        //update the preferred size of the returned component
        component = renderer.getComponent(true);
      }
    });
  }

  private static class LabelPanel {
    private final JPanel compositePanel;
    private final JLabel titleLabel = new JLabel();
    private final JLabel propertyLabel = new JLabel();

    public LabelPanel() {
      JPanel labelPanel = new JPanel();
      labelPanel.setLayout(new BoxLayout(labelPanel, BoxLayout.PAGE_AXIS));

      labelPanel.add(titleLabel);
      labelPanel.add(propertyLabel);

      compositePanel = new JPanel(new BorderLayout());
      //normally it contains more components,
      //but that is not needed to illustrate the problem
      compositePanel.add(labelPanel, BorderLayout.CENTER);
    }

    public Component getComponent( boolean aMultiLineProperty ) {
      titleLabel.setText("Title");
      if ( aMultiLineProperty ){
        propertyLabel.setText("<html>First line<br/>Property: value</html>");
      } else {
        propertyLabel.setText("Property: value");
      }

      int titleLabelHeight = titleLabel.getPreferredSize().height;
      int propertyLabelHeight = propertyLabel.getPreferredSize().height;
      int compositePanelHeight = compositePanel.getPreferredSize().height;
      if ( compositePanelHeight < titleLabelHeight + propertyLabelHeight){
        throw new RuntimeException("Preferred size of the component returned "
                                   + "by the renderer is incorrect");
      }

      return compositePanel;
    }
  }
}

As I am aware that the previous example is a bit far-fetched, here an example which includes a JTable

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

public class SwingTableTest {

  public static void main(String[] args) {
    EventQueue.invokeLater(new Runnable() {
      @Override
      public void run() {
        DefaultTableModel tableModel = new DefaultTableModel(0, 1);
        JTable table = new JTable(tableModel);
        table.setDefaultRenderer(Object.class, new DataResultRenderer());
        tableModel.addRow(new Object[]{new Object()});
        tableModel.addRow(new Object[]{new Object()});
        tableModel.addRow(new Object[]{new Object()});

        JFrame testFrame = new JFrame("TestFrame");
        testFrame.getContentPane().add(new JScrollPane(table));
        testFrame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
        testFrame.setSize(new Dimension(300, testFrame.getPreferredSize().height));
        testFrame.setVisible(true);
      }
    });
  }

  private static class DataResultRenderer implements TableCellRenderer {
    private final JPanel compositePanel;
    private final JLabel titleLabel = new JLabel();
    private final JLabel propertyLabel = new JLabel();

    public DataResultRenderer() {

      JPanel labelPanel = new JPanel();
      labelPanel.setOpaque(false);
      labelPanel.setLayout(new BoxLayout(labelPanel, BoxLayout.PAGE_AXIS));

      labelPanel.add(titleLabel);
      labelPanel.add(propertyLabel);

      compositePanel = new JPanel(new BorderLayout());
      //normally it contains more components,
      //but that is not needed to illustrate the problem
      compositePanel.add(labelPanel, BorderLayout.CENTER);
    }

    @Override
    public Component getTableCellRendererComponent(
        JTable table, Object value, boolean isSelected, 
        boolean hasFocus, int row, int column) {
      titleLabel.setText("Title");

      if ( row == 2 ){
        propertyLabel.setText("<html>Single property: value</html>");
      } else {
        String text = "<html>";
        text += "First property<br/>";
        text += "Second property<br/>";
        text += "Third property:value";
        text += "</html>";
        propertyLabel.setText(text);
      }

      int titleLabelHeight = titleLabel.getPreferredSize().height;
      int propertyLabelHeight = propertyLabel.getPreferredSize().height;
      int compositePanelHeight = compositePanel.getPreferredSize().height;
      if ( compositePanelHeight < titleLabelHeight + propertyLabelHeight){
        throw new RuntimeException("Preferred size of the component returned "
                                   + "by the renderer is incorrect");
      }
      table.setRowHeight(row, compositePanel.getPreferredSize().height);
      return compositePanel;
    }

  }
}

I am looking for a way to update the row height of the table to ensure that the multi-line content is completely visible, without knowing up front how many lines each row will contain.

So either I need a solution to retrieve the correct preferred size, or my approach is completely wrong and then I need a better one.

Note that the above examples are simplified. In the real code, the "renderer" (the code responsible for creating the component) is decorated a few times. This means that the outer renderer is the only with access to the JTable, and it has no knowledge about what kind of Component the inner code returns.

Upvotes: 1

Views: 73

Answers (1)

trashgod
trashgod

Reputation: 205765

Because setRowHeight() "Sets the height, in pixels, of all cells to rowHeight, revalidates, and repaints," the approach is unsound. Absent throwing an exception, profiling shows 100% CPU usage as an endless cascade of repaints tries to change the row height repeatedly. Moreover, row selection becomes unreliable.

Some alternatives include these:

  • Use TablePopupEditor to display multi-line content on request from a TableCellEditor.

  • Update an adjacent multi-line panel from a TableModelListener, as shown here.

Upvotes: 2

Related Questions