Table cells with HTML strings inconsistently rendered as multiline
Asked Answered
L

2

7

Cells of one of columns in my table are HTML strings. HTML is used to provide some color indication. Usually the width of the column is enough to contain the whole string. But when it is not enough then the string is nicely cut on a word boundary. This is the desired behavior. The default cell renderer is used.

I noticed that occasionally, some interaction with the table triggers the rendererer to wrap the string. As I understand, wrapping the HTML string is a normal behavior of JLabel from which DefaultTableCellRenderer derives. What is not clear is why is this behavior so inconsistent and what triggers its deviation. What is the reason for JLabel to jump back and forth, as if its being constantly re-measured? See attached image for an example.

To solve the problem I can either add <nobr> to the HTML string to prevent wrapping, or use a more sophisticated renderer to render colored strings. But I wonder if there is a way to make JLabel play nice.

I managed to reduce the whole case to a simple example. What I do to reproduce the issue is click various rows to change selection.

enter image description here

import java.awt.BorderLayout;
import java.awt.Dimension;
import java.util.Locale;

import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.SwingUtilities;
import javax.swing.table.DefaultTableModel;

public class TestTable extends JPanel{
    public TestTable() {
        setLayout(new BorderLayout());

        Object[][] rows = { 
                { "<html><font color=red>1 Lorem ipsum</font> dolor sit amet, " +
                        "consectetur adipiscing elit. In lectus dolor</html>"},
                { "<html><font color=green>2 Lorem ipsum</font> dolor sit amet, " +
                        "consectetur adipiscing elit. In lectus dolor</html>"},
                { "<html><font color=blue>3 Lorem ipsum</font> dolor sit amet, " +
                        "consectetur adipiscing elit. In lectus dolor</html>"},
                { "<html><font color=red>4 Lorem ipsum</font> dolor sit amet, " +
                        "consectetur adipiscing elit. In lectus dolor</html>"},
                { "<html><font color=green>5 Lorem ipsum</font> dolor sit amet, " +
                        "consectetur adipiscing elit. In lectus dolor</html>"},

                };
        Object[] columns = {"Column"};

        DefaultTableModel model = new DefaultTableModel(rows, columns) {
            @Override
            public boolean isCellEditable(int row, int column) {
                return false;
            }
        };
        JTable table = new JTable(model);
        table.setRowHeight(table.getFont().getSize() * 2);

        add(new JScrollPane(table));

        add(new JLabel(String.format("%s, %s, JRE %s (%s)", 
                System.getProperty("os.name"), System.getProperty("os.arch"), 
                System.getProperty("java.version"), Locale.getDefault().toString())), 
                BorderLayout.SOUTH);
    }

    public Dimension getPreferredSize() {
        return new Dimension(300, 200);
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {   
            public void run() {   
                JFrame frame = new JFrame("Test");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.setLocationByPlatform(true);

                TestTable panel = new TestTable();
                frame.add(panel);
                frame.pack();

                frame.setVisible(true);
            }
        });
    }
}

My environment is Java 7 Win 7 x64, also tested with Java 6 and 8 and it looks the same.

Lactone answered 30/7, 2014 at 17:26 Comment(13)
Is the behavior different if you select rows that aren't adjacent to the currently-selected one? Is the behavior related to the order in which rows are selected, or is it simply that the first time a row is unselected, it displays the full text, and the second time it displays the same text as when it's selected? (And what happens the third time it's unselected?)Corell
This is just an educated guess: table.setRowHeight(table.getFont().getSize() * 2); will just permit two lines if there's no space left from the top border. What I'm guessing is that positioning of the baseline of the first row depends on the way the focus enters the cell. If some space is left from the top, only one row is placed, otherwise two rows are squeezed in. - Suggestion: reduce the factor 2 to 1.5: table.setRowHeight(table.getFont().getSize() * 1.5);Derbent
@Corell maybe there is a pattern, but I can't catch it. Everything seems to have an effect, but almost in any case I am able to mess up some cells.Lactone
@laune's suggestion is a good one, though if you want 2 lines to show in the table, you might need to use a number like 2.1 instead of 1.5. Or to be more precise, you might need to include the cell padding, etc. in your computation; remember that the text itself may not be the only space being used in a table cell.Corell
@Derbent I just wonder why the string wrapped only sometimes. Playing with row height still results in the same effect. Though, if row is high enough, then wrapping happens always.Lactone
Probably an indication that the implementation didn't reckon with truncation in the first place, and thus didn't guarantee a uniform way of squeezing the text into the too narrow space.Derbent
@Derbent yes, indeed, I just wonder why under same conditions (measurements) JLabel presents different versions of a string.Lactone
Pure HTML is a poor vehicle for precision formatting. I can see that you'd like to have the color convenience. Leaving ample room for the full text might be the best way.Derbent
@Derbent yes, I agree, HTML is tricky. Unfortunately the columns are resizable so I may go for another renderer or <nobr> which seems to work OK. Thanks for you input!Lactone
1. override getPreferredSize and 2. construct Html inside Renderer (is only painting illusion with unwanted color and formatting memmory)Bourguiba
@Bourguiba thanks, but getPreferredSize of what?Lactone
inside Renderer, I'm sure that mentioned by camickr or kleopatra (clear and in English language, for JTable/JComboBox/JList), eventually you can to try to do by using doLayout(:-)Bourguiba
@Bourguiba good one! In my case though I wanted to force the label to stay single line and just cut on cell boundary. I understand that a bit more sophisticated renderer will give much better results. Also, as I mentioned in the question, I can simply add <nobr> in the HTML which is respected by JLabel. I noticed that JLabel's view.getPreferredSpan(View.Y_AXIS) doubles because the numbers of ParagraphView children is 2 instead of 1. But inside FlowView and FlowStrategy it is hard to pinpoint what makes the change. I should just let it go ;)Lactone
M
4

The core problem is the way that the JLabel (which the DefaultTableCellRenderer is using) is trying to format the HTML, it's allowing the HTML to wrap when it's available width is to short to accommodate the text. This is the default behaviour for JLabel

Why this seems to only happen after the cell is selected is one of those wonderful mysterious of Swing...cause it "should" be happening all the time...

One solution might be to use a layout manager which will prevent (or discourage) the JLabel from wrapping at the "available" width point...This, however, would require to provide your own TableCellRenderer, for example...

Table

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.util.Locale;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.border.Border;
import javax.swing.border.EmptyBorder;
import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.table.DefaultTableModel;
import javax.swing.table.TableCellRenderer;
import sun.swing.DefaultLookup;

public class TestTable extends JPanel {

    public TestTable() {
        setLayout(new BorderLayout());

        Object[][] rows = {
            {"<html><font color=red>1 Lorem ipsum</font> dolor sit amet, "
                + "consectetur adipiscing elit. In lectus dolor</html>"},
            {"<html><font color=green>2 Lorem ipsum</font> dolor sit amet, "
                + "consectetur adipiscing elit. In lectus dolor</html>"},
            {"<html><font color=blue>3 Lorem ipsum</font> dolor sit amet, "
                + "consectetur adipiscing elit. In lectus dolor</html>"},
            {"<html><font color=red>4 Lorem ipsum</font> dolor sit amet, "
                + "consectetur adipiscing elit. In lectus dolor</html>"},
            {"<html><font color=green>5 Lorem ipsum</font> dolor sit amet, "
                + "consectetur adipiscing elit. In lectus dolor</html>"},};
        Object[] columns = {"Column"};

        DefaultTableModel model = new DefaultTableModel(rows, columns) {
            @Override
            public boolean isCellEditable(int row, int column) {
                return false;
            }
        };
        JTable table = new JTable(model);
        table.setDefaultRenderer(Object.class, new HTMLRenderer());
        table.setRowHeight(table.getFont().getSize() * 2);

        add(new JScrollPane(table));

        add(new JLabel(String.format("%s, %s, JRE %s (%s)",
                System.getProperty("os.name"), System.getProperty("os.arch"),
                System.getProperty("java.version"), Locale.getDefault().toString())),
                BorderLayout.SOUTH);
    }

    public Dimension getPreferredSize() {
        return new Dimension(300, 200);
    }

    public static class HTMLRenderer extends JPanel implements TableCellRenderer {

        private JLabel label;
    private static final Border SAFE_NO_FOCUS_BORDER = new EmptyBorder(1, 1, 1, 1);
    private static final Border DEFAULT_NO_FOCUS_BORDER = new EmptyBorder(1, 1, 1, 1);
    protected static Border noFocusBorder = DEFAULT_NO_FOCUS_BORDER;

        public HTMLRenderer() {
            label = new DefaultTableCellRenderer();
//            setOpaque(false);
            setLayout(new FlowLayout(FlowLayout.LEFT, 0, 0));
            add(label);
        }

        @Override
        public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
            if (table == null) {
                return this;
            }

            Color fg = null;
            Color bg = null;

            JTable.DropLocation dropLocation = table.getDropLocation();
            if (dropLocation != null
                    && !dropLocation.isInsertRow()
                    && !dropLocation.isInsertColumn()
                    && dropLocation.getRow() == row
                    && dropLocation.getColumn() == column) {

                fg = UIManager.getColor("Table.dropCellForeground");
                bg = UIManager.getColor("Table.dropCellBackground");

                isSelected = true;
            }

            if (isSelected) {
                super.setForeground(fg == null ? table.getSelectionForeground()
                        : fg);
                super.setBackground(bg == null ? table.getSelectionBackground()
                        : bg);
            } else {
                Color background = table.getBackground();
                if (background == null || background instanceof javax.swing.plaf.UIResource) {
                    Color alternateColor = UIManager.getColor("Table.alternateRowColor");
                    if (alternateColor != null && row % 2 != 0) {
                        background = alternateColor;
                    }
                }
                super.setForeground(table.getForeground());
                super.setBackground(background);
            }

            setFont(table.getFont());

            if (hasFocus) {
                Border border = null;
                if (isSelected) {
                    border = UIManager.getBorder("Table.focusSelectedCellHighlightBorder");
                }
                if (border == null) {
                    border = UIManager.getBorder("Table.focusCellHighlightBorder");
                }
                setBorder(border);

                if (!isSelected && table.isCellEditable(row, column)) {
                    Color col;
                    col = UIManager.getColor("Table.focusCellForeground");
                    if (col != null) {
                        super.setForeground(col);
                    }
                    col = UIManager.getColor("Table.focusCellBackground");
                    if (col != null) {
                        super.setBackground(col);
                    }
                }
            } else {
                setBorder(getNoFocusBorder());
            }

            label.setText(value == null ? "" : value.toString());
            return this;
        }
    protected  Border getNoFocusBorder() {
        Border border = UIManager.getBorder("Table.cellNoFocusBorder");
        if (System.getSecurityManager() != null) {
            if (border != null) return border;
            return SAFE_NO_FOCUS_BORDER;
        } else if (border != null) {
            if (noFocusBorder == null || noFocusBorder == DEFAULT_NO_FOCUS_BORDER) {
                return border;
            }
        }
        return noFocusBorder;
    }

    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                JFrame frame = new JFrame("Test");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.setLocationByPlatform(true);

                TestTable panel = new TestTable();
                frame.add(panel);
                frame.pack();

                frame.setVisible(true);
            }
        });
    }
}

Updated...

I had a nice dig through the JTable and BasicTableUI code and the TableCellRenderer component is been "sized" to the requirements of the individual cell, meaning that when the JLabel is rendered, it is automatically wrapping the text without consideration, why this then causes issues with the layout may have to do with the fact that the default verticalAlignment...

Updated with alternative...

Another alternative might be to set the verticalAlignment to JLabel.TOP of a DefaultTableCellRenderer, which is backed by a JLabel, for example...

Example

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.Dimension;
import java.awt.FlowLayout;
import java.util.Locale;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.border.Border;
import javax.swing.border.EmptyBorder;
import javax.swing.table.DefaultTableCellRenderer;
import javax.swing.table.DefaultTableModel;
import javax.swing.table.TableCellRenderer;

public class TestTable extends JPanel {

    public TestTable() {
        setLayout(new BorderLayout());

        Object[][] rows = {
            {"<html><font color=red>1 Lorem ipsum</font> dolor sit amet, "
                + "consectetur adipiscing elit. In lectus dolor</html>"},
            {"<html><font color=green>2 Lorem ipsum</font> dolor sit amet, "
                + "consectetur adipiscing elit. In lectus dolor</html>"},
            {"<html><font color=blue>3 Lorem ipsum</font> dolor sit amet, "
                + "consectetur adipiscing elit. In lectus dolor</html>"},
            {"<html><font color=red>4 Lorem ipsum</font> dolor sit amet, "
                + "consectetur adipiscing elit. In lectus dolor</html>"},
            {"<html><font color=green>5 Lorem ipsum</font> dolor sit amet, "
                + "consectetur adipiscing elit. In lectus dolor</html>"},};
        Object[] columns = {"Column"};

        DefaultTableModel model = new DefaultTableModel(rows, columns) {
            @Override
            public boolean isCellEditable(int row, int column) {
                return false;
            }
        };
        JTable table = new JTable(model);
        table.setDefaultRenderer(Object.class, new HTMLRenderer());
        table.setRowHeight(table.getFont().getSize() * 2);

        add(new JScrollPane(table));

        add(new JLabel(String.format("%s, %s, JRE %s (%s)",
                System.getProperty("os.name"), System.getProperty("os.arch"),
                System.getProperty("java.version"), Locale.getDefault().toString())),
                BorderLayout.SOUTH);
    }

    public Dimension getPreferredSize() {
        return new Dimension(300, 200);
    }

    public static class HTMLRenderer extends DefaultTableCellRenderer {

        @Override
        public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
            Component comp = super.getTableCellRendererComponent(table, value, isSelected, hasFocus, row, column);
            setVerticalAlignment(JLabel.TOP);
            return comp;
        }

    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {
            public void run() {
                JFrame frame = new JFrame("Test");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.setLocationByPlatform(true);

                TestTable panel = new TestTable();
                frame.add(panel);
                frame.pack();

                frame.setVisible(true);
            }
        });
    }
}

But this will come down to your individual needs...

Mangonel answered 31/7, 2014 at 1:21 Comment(1)
+1 The examples are very interesting. I was hoping to catch that mystery, but I guess I can simply prevent it. Thank you!Lactone
T
2

That may have been caused by the JLabel vertical alignment:

// Works for me (Java 1.7.0_65, Windows 7)
((JLabel) table.getDefaultRenderer(Object.class)).setVerticalAlignment(JLabel.TOP);

EDIT

Here is my testing code:

import java.awt.*;
import java.awt.event.*;
import java.util.Arrays;
import javax.swing.*;
import javax.swing.table.*;

public class TestTable2 extends JPanel {
  public TestTable2() {
    super(new BorderLayout());

    Object[][] rows = {
      {
        "<html><font color=red>1 Lorem ipsum</font> dolor sit amet, " +
        "consectetur adipiscing elit. In lectus dolor</html>"
      },
      {
        "<html><font color=green>2 Lorem ipsum</font> dolor sit amet, " +
        "consectetur adipiscing elit. In lectus dolor</html>"
      },
      {
        "<html><font color=blue>3 Lorem ipsum</font> dolor sit amet, " +
        "consectetur adipiscing elit. In lectus dolor</html>"
      },
      {
        "<html><font color=red>4 Lorem ipsum</font> dolor sit amet, " +
        "consectetur adipiscing elit. In lectus dolor</html>"
      },
      {
        "<html><font color=green>5 Lorem ipsum</font> dolor sit amet, " +
        "consectetur adipiscing elit. In lectus dolor</html>"
      },
    };
    Object[] columns = {"Column"};

    DefaultTableModel model = new DefaultTableModel(rows, columns) {
      @Override
      public Class<?> getColumnClass(int column) {
        return String.class;
      }
      @Override
      public boolean isCellEditable(int row, int column) {
        return false;
      }
    };
    final JTable table = new JTable(model);
    //table.setRowHeight(table.getFont().getSize() * 2);
    table.setRowHeight(20);

    add(new JScrollPane(table));

    final JRadioButton centerRadio = new JRadioButton("CENTER");
    final JRadioButton topRadio = new JRadioButton("TOP");
    final JRadioButton bottomRadio = new JRadioButton("BOTTOM");
    ActionListener al = new ActionListener() {
      @Override public void actionPerformed(ActionEvent e) {
        TableCellRenderer r = table.getDefaultRenderer(String.class);
        if (r instanceof JLabel) {
          JLabel label = (JLabel) r;
          if (topRadio.isSelected()) {
            label.setVerticalAlignment(SwingConstants.TOP);
          } else if (bottomRadio.isSelected()) {
            label.setVerticalAlignment(SwingConstants.BOTTOM);
          } else {
            label.setVerticalAlignment(SwingConstants.CENTER);
          }
          table.repaint();
        }
      }
    };
    ButtonGroup bg = new ButtonGroup();
    JPanel p = new JPanel();
    for (JRadioButton b : Arrays.asList(centerRadio, topRadio, bottomRadio)) {
      b.addActionListener(al);
      bg.add(b);
      p.add(b);
    }
    centerRadio.setSelected(true);
    add(p, BorderLayout.SOUTH);
  }

  public static void main(String[] args) {
    SwingUtilities.invokeLater(new Runnable() {
      @Override public void run() {
        JFrame frame = new JFrame("Test");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setLocationByPlatform(true);
        frame.add(new TestTable2());
        frame.setSize(320, 240);
        frame.setVisible(true);
      }
    });
  }
}
Tempura answered 30/7, 2014 at 19:14 Comment(2)
The "default" renderer is not a JLabel, it's a TableCellRenderer, even if you could get access to the rendering component (which you can), there's no guarantee that the property won't be reset by the renderer the next time it renders a cell...Mangonel
Thank you for you answer! The alignment appears to have an effect, now there is bit more space and the string are always wrapping with current line height. +1. Of course, it depends on the available row height.Lactone

© 2022 - 2024 — McMap. All rights reserved.