Wednesday, August 5, 2009

Custom JTable Cell Renderer (Part 2)

While overriding getTableCellRendererComponent() in DefaultTableCellRenderer allows you to render any arbitrary JComponent in a cell, it is unfortunately all it does. If getTableCellRendererComponent() returns a JCheckBox, then JTable will render it in the appropriate cell including its check state. But the rendered checkbox will not be responsive to any mouse click applied, which of course defeats the purpose of displaying a checkbox inside a cell in the first place.

It is relatively easy to make a cell interactive. First, register a MouseListener on the JTable. When a mouseClicked(MouseEvent evt) is called, you can determine the cell picked using:

int row = table.rowAtPoint(evt.getPoint());
int col = table.columnAtPoint(evt.getPoint());

If we determine that the cell at this row and column can respond to a selection, then we need a way to keep track of the state of that cell. For example, a cell rendering a checkbox needs to keep track of its check state. The JCheckBox returned by getTableCellRendererComponent() cannot be used for this purpose because there is only one and is used as a rendering template. Here is where an arbitrary data object comes in handy. For example, if the cell is a checkbox, then data object for that cell can be a Boolean object. Then you can call the JTable's model's setValueAt(row, col) to store the boolean value of the cell clicked, making it true if it were false and vice versa.

This data object will be passed as the argument to getTableCellRenderer()'s value object parameter. Using the checkbox example above, this will be a Boolean object and depending on whether the value is true or false, set the selection state of the JCheckBox template before returning it. The checked state will be used to render the affected cell accordingly.

Tuesday, August 4, 2009

Custom JTable Cell Renderer (Part 1)

If the data is not a supported type, or if not all the cells in a single column is of the same type, then you will need to write a custom renderer for your JTable.

JTable asks the renderer's getTableCellRendererComponent() what kind of display component each cell should look like in the course of painting the table. Note that the JTable does not actually place a JComponent in each cell. Instead, each cell simply visually mimics the JComponent returned by getTableCellRendererComponent() by painting it in the cell.

For example, if getTableCellRendererComponent() returns a JLabel, then the attributes of that JLabel is used to paint the cell so that the cell looks like a JLabel (but is not really one). In other words, the JLabel is simply used like a data structure to pass information. For this reason, it is important that getTableCellRendererComponent() should not create a new component with each call. For example, the default JTable renderer, DefaultTableCellRenderer (an extension of JLabel), always returns itself when its getTableCellRendererComponent() is called.

In order to display a different "component" in each cell, the simplest method is to extend the DefaultTableCellRenderer. Instantiate in advance (say, in the constructor) all the different types of JComponent you will need (only one of each type is needed). Then when getTableCellRendererComponent() is called, determine which JComponent is to be returned, adjusting their attributes if necessary.

public class MyTableCellRenderer extends DefaultTableCellRenderer
{
private JCheckBox checkbox;
private JButton button;

public MyTableCellRenderer()
{
// Create one checkbox and one button. We will always return the same ones.
checkbox = new JCheckBox("Check Box");
button = new JButton("Button");
}

public Component getTableCellRendererComponent(JTable table, Object value,
boolean isSelected, boolean hasFocus, int row, int column)
{
// Render row 0 column 0 as a JCheckBox. Return the pre-created
// JCheckBox after setting the "selected" attribute appropriately
// based on its value in the JTable model.
if (row == 0 && column == 0)
{
checkbox.setSelected(table.getValue(row, column));
return checkbox;
}

// Render row 0 column 2 as a JButton.
if (row == 1 && column == 2)
return button;

// Everywhere else, return the usual DefaultTableCellRenderer.
return this;
}
}

Monday, August 3, 2009

Using Arbitrary Objects as JTable Data

While we normally supply Strings, JTable can actually accept any arbitrary objects as data. To display the data, its toString() method is called for a string that the JTable can display.

If the data is not of the supported type, then its toString() method must be implemented to return something for JTable to display intelligibly. For example, given a class called ArbitraryObject as follows:

public class ArbitraryObject
{
private String name;

public ArbitraryObject(String name)
{
this.name = name;
}

public String toString()
{
return "The number is " + name; // prepend "The number is" to name
}
}
Then you can create a JTable initialized with instances of ArbitraryObject as data:

private ArbitraryObject[][] data =
{ {new ArbitraryObject("1"), new ArbitraryObject("1"), new ArbitraryObject("1")},
{new ArbitraryObject("2"), new ArbitraryObject("2"), new ArbitraryObject("2")},
{new ArbitraryObject("3"), new ArbitraryObject("3"), new ArbitraryObject("3")},
{new ArbitraryObject("4"), new ArbitraryObject("4"), new ArbitraryObject("4")},
{new ArbitraryObject("5"), new ArbitraryObject("5"), new ArbitraryObject("5")},
{new ArbitraryObject("6"), new ArbitraryObject("6"), new ArbitraryObject("6")} };
private String[] headerLabels = { "Column One", "Column Two", "Column Three" }

:
JTable table = new JTable(data, headerLabels);
add(table, BorderLayout.CENTER);
:
Here's the screenshot of this JTable:


The utility of this is that you can easily associate each cell in the JTable with an object it represents. JTable "displays" the object with the value returned by the object's toString(). For example, if you select a cell based on what's displayed in it, a listener registered for the JTable can easily retrieve its associated object using getValueAt(row, col) without going through convolutions to look it up. Think of it as a link from the table back to the data it represents:

table.getSelectionModel().addListSelectionListener(new ListSelectionListener()
{
ublic void valueChanged(ListSelectionEvent event)
{
int row = table.getSelectedRow();
int col = table.getSelectedColumn();
ArbitraryObject o = table.getValueAt(row, col);
}
);

Sunday, August 2, 2009

JTable Cell Display Based on Data Type

JTable is capable of doing some special rendering based on the data type, such as JCheckBox for boolean data. The supported data type are listed in Sun's Tutorial on JTable. The only restriction is that each column has to be uniformly the same type. To enable this, the getColumnClass() of the JTable's model has to return the type of the data to be displayed:
   DefaultTableModel model = new DefaultTableModel()
{
/**
* Override to return the data class.
*/
public Class getColumnClass(int c)
{
return getValueAt(0, c).getClass();
}
};

table.setModel(model);

If getColumnClass() is not overridden, then the returned class is Object. In this case, JTable invokes the toString() method of the Object and displayed it as a JLabel.