Let's say you need to whip up a control GUI, and you need to do it fast. You need to control variables of different types, and there are a few dozen of them...maybe even more. The task at first seems daunting. You could lay out all of your components in a GUI builder, hook up all the events, and test each one. You could code by hand if you need practice typing. Or, if you do not feel like dealing with layout styles (
GridBagLayout is my favorite), and you need your GUI to be quickly extensible, you can use a JTable to hold your controls. This requires a small amount of effort to enhance the typical behavior of a JTable, where there is only one type of renderer and editor per column. After all, we don't want simple text areas for all of our variables. Some of them are hexadecimal numbers, combo box options, check boxes, and more.With this entry, I'll walk myself through the process of creating custom cell renderers and editors, and hopefully you'll be able to follow along in the process. I'll assume that you are familiar with the
AbstractTableModel and JTable classes.First step: create a class that extends
JTable. We need this so we can modify the behavior of two key functions, namely:public TableCellEditor getCellEditor(int row, int column)
public TableCellRenderer getCellRenderer(int row, int column)Let's start with the
getCellRenderer function. Here's what mine looks like:
public TableCellRenderer getCellRenderer(int row,
int column)
{
if(column != 2)
return super.getCellRenderer(row, column);
TableModel mdl = getModel();
if(init)
{
createDefaultRenderers();
/* System.out.println("BEGIN");
init = false;
Collection col =
defaultRenderersByColumnClass.values();
Iterator iter = col.iterator();
while(iter.hasNext())
{
System.out.println(
iter.next().getClass());
}
System.out.println("END");
for (Enumeration e =
defaultRenderersByColumnClass.keys() ;
e.hasMoreElements() ;)
{
System.out.println(e.nextElement());
}
System.out.println("END");
*/
}
if(mdl.getValueAt(row,column).getClass() == Boolean.class)
return rnd_bool;
else if(mdl instanceof MyCustomTableModel &&
((MyCustomTableModel)mdl).useComboBox(row))
return rnd_edit_quad;
else
return rnd_hex;
}
I used the commented-out section to see what default renderers were supported by JTable, so I could refer to them appropriately if need be. You will see this come into play later. For columns other than number 2, my particular table has static text displayed, so I chose to use the default cell renderer. To do this, I simply call
getCellRenderer on the parent class, and I can expect the default behavior.The table model, obtained by calling
getModel(), is the data provider. For a given row and column index, the cell may be a boolean, hex number, choice, or more. It's convenient to use the data type from the table model as a source for driving which type of renderer to use.You can see toward the end of the function that the return value (the renderer) can be based on any data your underlying model provides. You could create a function in your table model called "
getRendererType," and have it return a number or enumeration value that represents the type of renderer to use. Six in one hand, half a dozen in the other.Next, let's look at what the custom renderers look like (the variables
rnd_hex, rnd_bool, rnd_edit_quad). As a note, I've declared these objects inside the custom JTable class, which is why there is no declaration in the function.The
HexRenderer class, used to represent Long values in hexadecimal form (and preceded by '0x'), is extremely simple:
class HexRenderer extends DefaultTableCellRenderer {
public HexRenderer() { super(); }
public void setValue(Object value) {
setText("0x"+Long.toHexString((Long)value));
}
}
The
HexRenderer class simply changes the formatting of the text before placing it into the cell. The boolean renderer is a little different, as we'd like a check box as an indicator:
public class BoolRenderer extends JCheckBox
implements TableCellRenderer
{
public BoolRenderer()
{
super("Render");
}
public Component getTableCellRendererComponent(
JTable table,
Object value,
boolean isSelected,
boolean hasFocus,
int row, int column)
{
this.setSelected((Boolean)value);
return this;
}
public void validate() {}
public void revalidate() {}
protected void firePropertyChange(
String propertyName,
Object oldValue,
Object newValue) {}
public void firePropertyChange(
String propertyName,
boolean oldValue,
boolean newValue) {}
}
The class
BoolRenderer extends the component it will be displayed as, in this case a check box, and it implements the interface TableCellRenderer. If you read up in the Java documentation, you'll find that table cell renderers are re-used across cells. In other words, you do not need 40 check box objects to render 40 cells that should be rendered as check boxes. When the JTable calls getTableCellRendererComponent, it provides the value of the cell, the location, and a reference to itself. You now have all the information you need to set up this GUI component as it should look before it's actually rendered. In our case, we set the state of the check box based on the value of the cell. Any time the cell needs to be re-painted, this component will be used.The empty functions in the
BoolRenderer class are there on purpose to improve performance in larger data sets. Because BoolRenderer does not need all the functionality of a real JCheckBox, we can snip out these features and gain additional performance. We're just using this component for painting our cell, remember?Custom Cell Editors
Next on our list is how to create custom editors. We have the means of displaying values, but how do we edit them using similar components? Let's take a look at the
getCellEditor function of our class that extends JTable.
public TableCellEditor getCellEditor(int row,
int column)
{
if(column != 2)
return super.getCellEditor(row, column);
TableModel mdl = getModel();
if(mdl.getValueAt(row,column).getClass() ==
Boolean.class)
return edit_bool;
else if(mdl instanceof MyCustomTableModel
&& ((MyCustomTableModel)mdl).useComboBox(row))
return rnd_edit_quad;
else if(mdl.getValueAt(row,column)
instanceof Long)
return edit_hex;
else
return (TableCellEditor)defaultEditorsByColumnClass.get(Number.class);
}
This seems pretty self-explanatory. Simply use whatever logic you desire to determine what kind of editor to use. If you are looking to use the default editor for a given class, you can use
(TableCellEditor)defaultEditorsByColumnClass.get(Number.class);. I wasn't sure what classes were available to use, so I used the commented code inside getCellRenderer to enumerate all the available default renderers.Let's take a look at the
BoolEditor class:
class BoolEditor extends JCheckBox implements
TableCellEditor, ItemListener
{
Vector editorListeners;
public BoolEditor()
{
super("Edit");
addItemListener(this);
editorListeners = new Vector();
}
public void itemStateChanged(ItemEvent e)
{
System.out.println("Firing!");
for(int i=0; i<editorListeners.size(); ++i)
{
((CellEditorListener)editorListeners.
elementAt(i)).editingStopped(
new ChangeEvent(this));
}
}
public Component getTableCellEditorComponent(
JTable table,
Object value, ]
boolean isSelected,
int row,
int column)
{
this.setSelected((Boolean)value);
return this;
}
public Object getCellEditorValue()
{
return this.isSelected();
}
public boolean isCellEditable(EventObject anEvent)
{
return true;
}
public boolean shouldSelectCell(EventObject anEvent)
{
return true;
}
public boolean stopCellEditing()
{
return true;
}
public void cancelCellEditing()
{
}
public void addCellEditorListener(CellEditorListener l)
{
editorListeners.add(l);
}
public void removeCellEditorListener(CellEditorListener l)
{
editorListeners.remove(l);
}
}
It seems like a lot, but it really isn't. Let's follow the code step-by-step. First, note that this component extends
JCheckBox. This is necessary, as we'd like to borrow the functionality of the check box and use it for editing some of our cells. Next, note that the class implements TableCellEditor and ItemListener. The TableCellEditor interface requires that we implement the following functions:getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) - Note this is just like the case for getTableCellRendereraddCellEditorListener
cancelCellEditing
getCellEditorValue
isCellEditable
removeCellEditorListener
shouldSelectCell
stopCellEditingBasically, the table will add a listener to a cell and wait for the user to finish editing the cell. Our cell editor notifies the JTable that the user has finished editing the cell by using the
CellEditorListener object added to the component and calling its editingStopped event. For example, as soon as the user releases the mouse button inside the checkbox, the value of the check box changes, the event is captured by the itemStateChanged event (part of the ItemListener interface), and any CellEditorListener objects are notified. When the table is done listening for events, it can remove the listener from the object's list by calling removeCellEditorListener.That was a nice example -- now let's check out the class used for editing hexadecimal numbers,
HexEditor:
class HexEditor extends JTextField implements
TableCellEditor, ActionListener
{
Vector editorListeners;
long thevalue;
long oldvalue;
public HexEditor()
{
super("Edit");
addActionListener(this);
editorListeners = new Vector();
}
public void actionPerformed(ActionEvent e)
{
System.out.println("HexEditor actionPerformed() Firing!");
try
{
if(getText().indexOf("x") >= 0)
thevalue = Long.parseLong(
getText().substring(getText().indexOf("x")+1),16);
else
thevalue = Long.parseLong(
getText(),16);
}
catch(Exception ex)
{
thevalue = oldvalue;
}
for(int i=0; i<editorListeners.size(); ++i)
{
((CellEditorListener)editorListeners.
elementAt(i)).editingStopped(new ChangeEvent(this));
}
}
public Component getTableCellEditorComponent(JTable table,
Object value, boolean isSelected, int row, int column)
{
this.setText("0x"+Long.toHexString((Long)value));
thevalue = (Long)value;
oldvalue = (Long)value;
return this;
}
public Object getCellEditorValue()
{
return thevalue;
}
public boolean isCellEditable(EventObject anEvent)
{
return true;
}
public boolean shouldSelectCell(EventObject anEvent)
{
return true;
}
public boolean stopCellEditing()
{
return true;
}
public void cancelCellEditing()
{
}
public void addCellEditorListener(CellEditorListener l)
{
editorListeners.add(l);
}
public void removeCellEditorListener(CellEditorListener l)
{
editorListeners.remove(l);
}
}
The
HexEditor class contains the same boilerplate functions that handle the CellEditorListeners, but this time it extends JTextArea to provide text input. As a note, validation is not performed as the user types. If the user were to type some other than a hexadecimal number preceeded by '0x', the cell will revert to the previous value it contained. This class was designed to support unsigned integers. Since there are no unsigned types in Java (aarrrgg!), I opted to use a number with twice the amount of bits (a Long) and ignore the top 32 bits.Finally, let's take a look at a cell editor (and renderer) that utilizes a combo box to provide the user with a set of 4 choices:
class QuadRenderer extends JComboBox implements TableCellRenderer, TableCellEditor, ItemListener
{
Vector editorListeners;
public QuadRenderer()
{
super();
editorListeners = new Vector();
addItem("Choice A");
addItem("Choice B");
addItem("Choice C");
addItem("Choice D");
addItemListener(this);
}
public void itemStateChanged(ItemEvent e)
{
if(e.getStateChange()==ItemEvent.DESELECTED)
{
System.out.print("Firing!!");
for(int i=0; i<editorListeners.size(); ++i)
{
System.out.print(".");
((CellEditorListener)editorListeners.
elementAt(i)).editingStopped(
new ChangeEvent(this));
}
System.out.print("\n");
}
}
public Component getTableCellRendererComponent(
JTable table, Object value,
boolean isSelected, boolean hasFocus,
int row, int column)
{
System.out.println("Setting index to: "+value);
this.setSelectedIndex(((Long)value).intValue());
return this;
}
public Component getTableCellEditorComponent(JTable table,
Object value, boolean isSelected, int row, int column)
{
System.out.println("Setting (editing) index to: "+value);
this.setSelectedIndex(((Long)value).intValue());
return this;
}
public Object getCellEditorValue()
{
return this.getSelectedIndex();
}
public boolean isCellEditable(EventObject anEvent)
{
return true;
}
public boolean shouldSelectCell(EventObject anEvent)
{
return true;
}
public boolean stopCellEditing()
{
return true;
}
public void cancelCellEditing()
{
}
public void addCellEditorListener(CellEditorListener l)
{
editorListeners.add(l);
}
public void removeCellEditorListener(CellEditorListener l)
{
editorListeners.remove(l);
}
}
This class implements everything in one place; both the rendering interface and the editing interface. By now you should be able to easily recognize what's going on in this class, and you should be able to implement something comparable should you need it.
2 comments:
Hi Tommy,
Its a mindblowing example you have blogged. I searched and tryed hours to find a solution.
My problem was ,i was having a JTable with first column as checkbox. There are few buttons outside the JTable. In any of the checkbox (row) is checked then the buttons has to be activated.
My code has JTable created with vector rows and vector columns. JTable was written as annonymons inner class and itemChanged event is coded for the JCheckbox which is obtained from the render. Uses the JTable renders as the value was Boolean. So it displayed the Checkbox and itemStateChanged event fired. But the state value is correct and event not firing properly.
Thanks very much for ur post.
How to set the background for the cell to white(jcheckbox cell) and center it in your code.
Plese see my code
import javax.swing.*;
import javax.swing.table.*;
import javax.swing.event.*;
import java.awt.event.*;
import java.util.*;
import java.awt.*;
class TestTable1
{
Vector rowData = new Vector();
public JButton open = new JButton("Open");
public JButton delete = new JButton("Delete");
public void createWindow(){
JFrame frame = new JFrame();
JPanel panel = new JPanel();
frame.setLayout(new FlowLayout());
JScrollPane scrollPane = new JScrollPane(createTable());
panel.add(scrollPane);
frame.getContentPane().add(panel);
JPanel panelButtons = new JPanel();
panelButtons.add(open);
panelButtons.add(delete);
frame.getContentPane().add(panelButtons);
open.setEnabled(false);
delete.setEnabled(false);
frame.setVisible(true);
frame.setSize(300,300);
}
public static void main(String[] args)
{
new TestTable1().createWindow();
}
public JTable createTable(){
Vector row ;
for(int i=0;i<2;i++){
row = new Vector();
row.add(new Boolean(false));
row.add("Data "+i);
rowData.add(row);
}
Vector header = new Vector();
header.add("select");
header.add("Data");
final JTable table = new JTable(rowData, header) {
//set all cells except first non-editable
public boolean isCellEditable(int row, int column) {
if (column == 0) {
return true;
}
return false;
}
//set checkbox in the first column
public Class getColumnClass(int col) {
if (col == 0) {
return Boolean.class;
} else {
return super.getColumnClass(col);
}
}
};
table.setName("archreport.table");
table.setName("archreport.table");//NOI18N
DefaultTableCellRenderer dcl= (DefaultTableCellRenderer)table.getTableHeader().getDefaultRenderer();
dcl.setHorizontalAlignment(SwingUtilities.CENTER);
// table.getColumn(I18NProvider.getString("archreport.col.header1","Select")).setWidth(5);
//TableUtility.autoResizeTable (table, false);
table.setRowSelectionAllowed(false);
table.setColumnSelectionAllowed(false);
table.setCellSelectionEnabled(false);
//ToolTipHeader toolTipHeader = new ToolTipHeader(table.getColumnModel());
//toolTipHeader.setToolTipStrings(toolTips);
//table.setTableHeader(toolTipHeader);
table.getTableHeader().setReorderingAllowed(false);
table.getColumnModel().getColumn(1).setMaxWidth(0);
table.getColumnModel().getColumn(1).setMinWidth(0);
table.getColumnModel().getColumn(1).setPreferredWidth(0);
table.getColumnModel().getColumn(1).setResizable(false);
TableCellRenderer renderer = table.getColumnModel().getColumn(0).getCellRenderer();
((JCheckBox)renderer).addItemListener(new ItemListener(){
public void itemStateChanged(ItemEvent e) {
if(rowData.size()==0){
return ;
}
boolean state = false;
for (int rowCount = 0; rowCount < rowData.size(); rowCount++) {
state |= ((Boolean) table.getValueAt(rowCount, 0)).booleanValue();
}
//set the button state.
//PReportListMainDialogFactory.getPReportListMainDialog("").setArchiveOpenButtonState(state);
//PReportListMainDialogFactory.getPReportListMainDialog("").setArchiveDeleteButtonState(state);
}
});
return table;
}
}
Post a Comment