Java Specialists' Java Training Europehome of the java specialists' newsletter

The Java Specialists' Newsletter
Issue 1062005-04-14 Category: GUI Java version: Sun JDKs 1.3.1_12, 1.4.2_05, 1.5.0_02

GitHub Subscribe Free RSS Feed

Multi-line cells in JTable in JDK 1.4+

by Dr. Heinz M. Kabutz

Welcome to the 106th edition of The Java(tm) Specialists' Newsletter, sent to you from Vienna, Austria. The great staff at Alcatel booked me into a hotel that has free ADSL+WiFi access. This is still rather unusual in Europe, I am told. I am in Vienna, Austria, until the 22nd of April, so please let me know if you are available for a cup of coffee and chat after work. We had a great time last night at the Schweizerhaus, really experiencing Austrian hospitality and atmosphere at its best.

On the first day of the courses, we noticed that the projector was playing up, changing colours intermittently. Changing the projector did not help, so we traced the problem to my Dell D800 heavyweight notebook. I phoned DELL Austria at about 15:00 on Tuesday, and by 14:00 on Wednesday someone had arrived at Alcatel and had swopped out my motherboard. No charge at all! Super service, thanks DELL!

NEW: Please see our new "Extreme Java" course, combining concurrency, a little bit of performance and Java 8. Extreme Java - Concurrency & Performance for Java 8.

Multi-line cells in JTable in JDK 1.4+

Three years ago, I played an April Fools' joke on my readers, as is customary in South Africa. However, with 109 countries on the subscription list, I have stopped playing the fool, since this is can cause confusion in cultures that do not celebrate the 1st of April. In a follow-up newsletter, I explained that the unsubscription fees were nothing but a joke.

Coincidentally, almost three years ago to the day, I wrote about Multi-line cells in the JTable. I discussed how to wrap text inside cells of a JTable. My solution only worked pre-JDK 1.4 , so I asked readers to send me a solution if they found it.

Since I wrote that letter, several readers have sent solutions that worked for JDK 1.4 and JDK 5.0: Scott Sauyet, Dave Combs, Hitesh Patel, Debbie Utley, Wolf Siberski, Dirk Hillbrecht, Andrew Cole and Sami. Thank you very much!

Most of the solutions consisted of adding one line to my code:

    setSize(columnModel.getColumn(column).getWidth(), 100000);
  

This magically made the renderer work under JDK 1.4 and 1.5. However, it did not address the other issues that I mentioned in the original newsletter:

  1. It worked when only one column contained the TextAreaRenderer.
  2. I did not implement a TextAreaEditor.

Something else which bothered me with the proposed solutions was that the cells with our renderers looked slightly different to the cells with default renderers.

Here is a renderer followed by an editor that address all these issues:

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

public class TextAreaRenderer extends JTextArea
    implements TableCellRenderer {
  private final DefaultTableCellRenderer adaptee =
      new DefaultTableCellRenderer();
  /** map from table to map of rows to map of column heights */
  private final Map cellSizes = new HashMap();

  public TextAreaRenderer() {
    setLineWrap(true);
    setWrapStyleWord(true);
  }

  public Component getTableCellRendererComponent(//
      JTable table, Object obj, boolean isSelected,
      boolean hasFocus, int row, int column) {
    // set the colours, etc. using the standard for that platform
    adaptee.getTableCellRendererComponent(table, obj,
        isSelected, hasFocus, row, column);
    setForeground(adaptee.getForeground());
    setBackground(adaptee.getBackground());
    setBorder(adaptee.getBorder());
    setFont(adaptee.getFont());
    setText(adaptee.getText());

    // This line was very important to get it working with JDK1.4
    TableColumnModel columnModel = table.getColumnModel();
    setSize(columnModel.getColumn(column).getWidth(), 100000);
    int height_wanted = (int) getPreferredSize().getHeight();
    addSize(table, row, column, height_wanted);
    height_wanted = findTotalMaximumRowSize(table, row);
    if (height_wanted != table.getRowHeight(row)) {
      table.setRowHeight(row, height_wanted);
    }
    return this;
  }

  private void addSize(JTable table, int row, int column,
                       int height) {
    Map rows = (Map) cellSizes.get(table);
    if (rows == null) {
      cellSizes.put(table, rows = new HashMap());
    }
    Map rowheights = (Map) rows.get(new Integer(row));
    if (rowheights == null) {
      rows.put(new Integer(row), rowheights = new HashMap());
    }
    rowheights.put(new Integer(column), new Integer(height));
  }

  /**
   * Look through all columns and get the renderer.  If it is
   * also a TextAreaRenderer, we look at the maximum height in
   * its hash table for this row.
   */
  private int findTotalMaximumRowSize(JTable table, int row) {
    int maximum_height = 0;
    Enumeration columns = table.getColumnModel().getColumns();
    while (columns.hasMoreElements()) {
      TableColumn tc = (TableColumn) columns.nextElement();
      TableCellRenderer cellRenderer = tc.getCellRenderer();
      if (cellRenderer instanceof TextAreaRenderer) {
        TextAreaRenderer tar = (TextAreaRenderer) cellRenderer;
        maximum_height = Math.max(maximum_height,
            tar.findMaximumRowSize(table, row));
      }
    }
    return maximum_height;
  }

  private int findMaximumRowSize(JTable table, int row) {
    Map rows = (Map) cellSizes.get(table);
    if (rows == null) return 0;
    Map rowheights = (Map) rows.get(new Integer(row));
    if (rowheights == null) return 0;
    int maximum_height = 0;
    for (Iterator it = rowheights.entrySet().iterator();
         it.hasNext();) {
      Map.Entry entry = (Map.Entry) it.next();
      int cellHeight = ((Integer) entry.getValue()).intValue();
      maximum_height = Math.max(maximum_height, cellHeight);
    }
    return maximum_height;
  }
}
  

I know that the mechanism for handling the cell renderer for several columns is rather complex and inefficient. However, it caters for the general case where one instance of the cell renderer is used for several columns, or even for several tables.

You will notice that I am borrowing the formatting of the DefaultTableCellRenderer, so that the Text Area looks the same as the other cells, for the particular platform and JVM version.

As predicted in my newsletter three years ago, it was a "Kinderspiel" (child's play) to write the TextAreaEditor. A few points to note: I subclass the DefaultCellEditor, which only caters for JTextField, JComboBox and JCheckBox as editors. I therefore replace the editorComponent with my own, and also set my own delegate. In order to make it possible to edit more than the current cell allows, I put the TextArea for editing into a JScrollPane, and I set its border to null, to prevent it from showing the scrollbar unnecessarily.

import javax.swing.*;

public class TextAreaEditor extends DefaultCellEditor {
  public TextAreaEditor() {
    super(new JTextField());
    final JTextArea textArea = new JTextArea();
    textArea.setWrapStyleWord(true);
    textArea.setLineWrap(true);
    JScrollPane scrollPane = new JScrollPane(textArea);
    scrollPane.setBorder(null);
    editorComponent = scrollPane;

    delegate = new DefaultCellEditor.EditorDelegate() {
      public void setValue(Object value) {
        textArea.setText((value != null) ? value.toString() : "");
      }
      public Object getCellEditorValue() {
        return textArea.getText();
      }
    };
  }
}
  

I tested this on JDK 1.3.1 right up to JDK 1.5.0, under the Windows, Motif and Metal Look and Feels, and it appears the same as a normal JTable cell. Here is some test code:

import javax.swing.*;
import javax.swing.table.*;

public class TextAreaRendererTest extends JFrame {
  private final JTable table = new JTable(10, 4);

  public TextAreaRendererTest() {
    super(System.getProperty("java.vm.version"));

    // We use our cell renderer for columns 1, 2, 3
    TableColumnModel cmodel = table.getColumnModel();
    TextAreaRenderer textAreaRenderer = new TextAreaRenderer();

    cmodel.getColumn(1).setCellRenderer(textAreaRenderer);
    cmodel.getColumn(2).setCellRenderer(new TextAreaRenderer());
    // I am demonstrating that you can have several renderers in
    // one table, and they communicate with one another in
    // deciding the row height.
    cmodel.getColumn(3).setCellRenderer(textAreaRenderer);
    TextAreaEditor textEditor = new TextAreaEditor();
    cmodel.getColumn(1).setCellEditor(textEditor);
    cmodel.getColumn(2).setCellEditor(textEditor);
    cmodel.getColumn(3).setCellEditor(textEditor);

    String test = "The lazy dog jumps over the quick brown fox";

    for (int column = 0; column < 4; column++) {
      table.setValueAt(test, 0, column);
      table.setValueAt(test, 4, column);
    }
    test = test + test + test + test + test + test + test + test;
    table.setValueAt(test, 4, 2);

    getContentPane().add(new JScrollPane(table));
    setSize(600, 600);
    setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    setVisible(true);
  }
  public static void main(String[] args) {
    new TextAreaRendererTest();
  }
}
  

Well, it is late, and I need to hit the sack so that I can be fresh tomorrow for the Design Patterns Course at Alcatel Austria :)

Don't forget to let me know if you are currently in Austria!

Kind regards

Heinz

GUI Articles Related Java Course

Java Master
Java Concurrency
Design Patterns
In-House Courses



© 2010-2014 Heinz Kabutz - All Rights Reserved Sitemap
Oracle and Java are registered trademarks of Oracle and/or its affiliates. Other names may be trademarks of their respective owners. JavaSpecialists.eu is not connected to Oracle, Inc. and is not sponsored by Oracle, Inc.
@CORE_THE_BAND #RBBJGR