Summary

Java > Core SWING advanced programming > 7. TABLE EDITING > Using Table Editors

 

Using Table Editors

Now that you've seen how the table manages the editing process, it's time to look at a couple of examples. The first example you'll see is a very straightforward demonstration of a JComboBox as a table cell editor. Because the table component includes support for combo box editors, this example is little more than a demonstration of how to make straightforward use of existing features. The second example, however, is more complex. Here, you'll see how to give the appearance of adding a button to a table. As you know, the table cells are not actually individual components, so you can't just add a button to a table and have it behave like a fully functioning button. Using the knowledge gained in the last section, however, you'll see how to create the illusion that your table has an embedded JButton. This technique can also be applied to "add" more sophisticated controls to your table.

A Table with a Combo Box Editor

Using a JComboBox as a table editing component is very simple all you need to do is create and populate the combo box and wrap it with an instance of DefaultCellEditor, and then assign the editor to the appropriate table column or install it as the default editor for a specific data type. The table and DefaultCellEditor jointly provide the code to arrange for the combo box to appear when necessary and for the selected item to be written back to the table model. You can see an example of a combo box being used to edit a table cell in Figure 7-2. To try this example for yourself, use the command:

Figure 7-2. Using JComboBox as a cell editor.
graphics/07fig02.gif
 java AdvancedSwing.Chapter7.ComboBoxTable 
Listing 7-2 A Table with a Combo Box Editor Installed
 package AdvancedSwing.Chapter7; import javax.swing.*; import javax.swing.table.*; import java.awt.event.*; public class ComboBoxTable {    public static void main(String[] args) {       JFrame f = new JFrame("Combo Box Table");       JTable tbl = new JTable(new ComboBoxTableModel());       // Create the combo box editor       JComboBox comboBox = new JComboBox(                          ComboBoxTableModel.getValidStates());       comboBox.setEditable(true);       DefaultCellEditor editor =                       new DefaultCellEditor(comboBox);       // Assign the editor to the second column       TableColumnModel tcm=tbl.getColumnModel();       tcm.getColumn(1).setCellEditor(editor);       // Set column widths       tcm.getColumn(0).setPreferredWidth(200);       tcm.getColumn(1).setPreferredWidth(100);       // Set row height       tbl.setRowHeight(20);       tbl.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);       tbl.setPreferredScrollableViewportSize(                            tbl.getPreferredSize());       f.getContentPane().add(new JScrollPane(tbl), "Center");       f.pack();       f.addWindowListener(new WindowAdapter() {          public void windowClosing(WindowEvent evt) {             System.exit(0);          }       });       f.setVisible(true);    } } class ComboBoxTableModel extends AbstractTableModel {    // Implementation of TableModel interface    public int getRowCount() {       return data.length;    }    public int getColumnCount() {       return COLUMN_COUNT;    }    public Object getValueAt(int row, int column) {       return data[row][column];    }    public Class getColumnClass(int column) {       return (data[0][column]).getClass();    }    public String g%tColumnName(int column) {       return columnNames[column];    }    public boolean isCellEditable(int row, int column) {       return column == 1;    }    public void setValueAt(Object value, int row, int column) {       if (isValidValue(value)) {          data[row][column] = value;          fireTableRowsUpdated(row, row);       }    }    // Extra public methods    public static String[] getValidStates() {       return validStates;    }    // Protected methods    protected boolean isValidValue(Object value) {       if (value instanceof String) {          String sValue = (String)value;          for (int i = 0; i < validStates.length; i++) {             if (sValue.equals(validStates[i])) {                return true;             }          }       }       return false;    }    protected static final int COLUMN_COUNT = 2;    protected static final String[] validStates = {       "On order", "In stock", "Out of print"    };    protected Object[][] data = new Object[][] {       { "Core Java Volume 1", validStates[0] },       { "Core Java Volume 2", validStates[0] },       { "Core Web Programming", validStates[0] },       { "Core Visual Basic 5", validStates[0] },       { "Core Java Foundation Classes", validStates[0] }    };    protected static final String[] columnNames = {       "Book Name", "Status"    }; } 

The implementation of this example is shown in Listing 7-2, which combines the table and its data model. Let's look first at the data model, which is provided by the ComboBoxTableModel class. Like the oth%r data models that have been used in this chapter, this one is derived from AbstractTableModel and stores its data in a two-dimensional array, each entry of which is a string. This table lists some of the more popular Java books and their current stock state in an imaginary bookstore. The second column of the table can have one of three values:

  • On order

  • In stock

  • Out of print

The table model must be able to supply a book's current state and allow the user to change the state of any book to one of the three legal values (but no other). Most of the table model implementation should be familiar from previous examples. The isCellEditable method allows only the second column to be edited. Editable tables also require a suitable setValueAt method; here, setValueAt makes use of the protected method isValidValue to make sure that the book's state can only be assigned a legal value an attempt to supply a value that is not a string or not from the list shown previously is ignored. The only other method of any interest is getvalidstates, which returns an array of strings that represent the valid book states. These are the only values that will be acceptable to setValueAt.

Now let's look at the ComboBoxTable class, which sets up the table. Most of this code deals with creating the table, fixing its column sizes, and mounting it in a JScrollPane within the main JFrame of the application. Here is the most important part of this setup:

 // Create the combo box editor JComboBox comboBox = new JComboBox (                      ComboBoxTableModel.getvalidstates ()); comboBox.setEditable (true); DefaultCellEditor editor = new DefaultCellEditor (comboBox); // Assign the editor to the second column TableColumnModel tcm = tbl.getColumnModel(); tcm.getColumn (1).setCellEditor (editor); 

The JComboBox will be used in the second column and initialized with its set of legal state values. To avoid hard-wiring these states outside the table model itself, the table model's getvalidstates method is used to get the set of possible stock states. The next step is to create a DefaultCellEditor and associate the combo box with it. As you saw in the last section, this causes the DefaultCellEditor to register an ItemListener to receive notification when the combo box's selection is changed. Lastly, the editor is installed in the table's second column.

Core Note

This is, of course, a highly simplified example. In a real-world application, the table model would probably be populated from a database, using an SQL SELECT statement that would generate one row of two columns for each book in the imaginary bookstore. The values returned by the getvalidstates method would most likely be obtained on demand using another SQL SELECT statement the first time they were required and cached for subsequent calls of getvalidstates. Keeping the data in a table model and providing an interfacing method like getvalidstates allows details like this to be kept out of the graphical user interface (GUI) implementation.



The last thing to note about the ComboBoxTable class is the following line of code:

 tbl.setRowHeight(20); 

The rows in a JTable are all the same height. In this case, each row will be 20 pixels high. Using a JTable is not the same as using a container with a layout manager you cannot rely on the table to choose the correct height for its rows based on the data that its model contains and the renderers that will be used to displays its contents. If you don't set an explicit row height, you get a default value, which may or may not be suitable for your application. In this case, because a JComboBox is being used, it is necessary to add some vertical space to account for the selection window of the combo box.

If you run this example and click the mouse over a cell in the second column, you'll find that the cell content is replaced by the combo box and its drop-down menu appears, as shown in Figure 7-2. You can use the mouse to select one of the three legal book states and install it by clicking. When you do this, the new value is written to the table and the editing process ends, because the JComboBox generates an event that is caught by the DefaultCellEditor and causes the editing process to terminate, as described in "The Mechanics of the Editing Process".

Another way to end the edit is to click in another cell of the table. This also causes the drop-down menu to disappear and the cell's usual renderer will redraw the cell with the original value installed.

Core Note

For the sake of illustration, the combo box in this example has been made editable, which means that you can edit the selected value in the combo box selection window. If you do this, you could attempt to install a value that is not one of the legal stock states for books in this bookstore. However, you'll find that the table won't allow you to select an illegal state, because the setValueAt method accepts only the values in the combo box drop-down list



Including Buttons in a Table

Having seen how to use a standard editor, let's now look at a more complex problem that requires a proper understanding of how table editing works to produce a working solution. A common misconception among developers using JTable for the first time is that the table's cells are components that are added to a container. Because of this, there is an expectation that you can use a JButton as a cell renderer and have the button behave like a real button. Unfortunately, this is not the case. If you use a renderer that draws a button, all you get is a static image of the button the button is not actually present and can't be clicked. However, with a little ingenuity, you can give the illusion of a working button.

To see how this is done, let's return to our currency table example. The last version of this example changed the original read-only table model to an editable one. Changing the data is not of any use, of course, unless you can save the changes somewhere. Assuming that this data was loaded into the table model from a database server, it would be a good idea to write back any updated values. One way to do this might be to have a single button outside the table that would trigger the update process. Implementing that solution would not, of course, show us any new JTable features, so we'll adopt a slightly different solution here: each row of the table will be given its own button. The intent of this is that the person performing updates would press the button to commit the change on a particular line to the original data source. You can see what this arrangement looks like in Figure 7-3. To keep this example as simple as possible, the code that would, in real application, load the table model from the database and commit the updates when the buttons are pressed will not be shown. Instead, we'll just print a message when an update is requested.

Figure 7-3. A JTable with active buttons.
graphics/07fig03.gif

To create this table, three enhancements need to be made to the editable version of the currency table:

  1. A column needs to be added for the "Update" button.

  2. A renderer must be written to draw the button.

  3. The code that handles the user's button "press" must be written.

Lets look at these changes one by one.

Adding the Update Button Column

The simplest way to add a column for the Update button is just to add a column to the table's TableColumnModel. However, every column in the TableColumnModel must correspond to a column in the TableModel, so that the data for the column can be obtained. At first, this seems like an unnatural and clumsy arrangement after all, the button is not really part of the table data. In fact, though, as you'll see when we look at how to implement the button in "Activating the Button", having a TableModel column for it simplifies the implementation and also allows the action taken when it is pressed to be dependent on the model, which is exactly as it should be. Aside from this as yet unexplained advantage, the most direct benefit of having a column for the button is that the button's label can be held in the data model.

Core Note

You might not consider this last point to be such a worthwhile gain, and you would probably be right. It does, however, avoid the need to hard-code the button label in the source code and allows you, if you wish, to use a different button label for each row. While this flexibility doesn't really fit very well for the currency table, it might be useful in other contexts. In any cose, there really is no choice about adding an extra column to the TableModel, so it might as well be put to some use.



Fortunately, adding a column to the TableModel is a very simple matter. The existing functionality can be preserved by deriving the new model from EditableCurrencyTableModel, as shown in Listing 7-3.

Listing 7-3 An Updatable Currency Table Model
 package AdvancedSwing.Chapter7; import javax.swing.*; //An updatable version of the currency table model public abstract class UpdatableCurrencyTableModel                  extends EditableCurrencyTableModel {    public int getColumnCount() {       return super.getColumnCount() + 1;    }    public Object getValueAt(int row, int column) {       if (column == BUTTON_COLUMN) {          return "Update";       }       return super.getValueAt(row, column);    }    public Class getColumnClass(int column) {       if (column == BUTTON_COLUMN) {          return String.class;       }       return super.getColumnClass(column);    }    public String getColumnName(int column) {       if (column == BUTTON_COLUMN) {          return "";       }       return super.getColumnName(column);    }    public boolean isCellEditable(int row, int column) {       return column == BUTTON_COLUMN ||                     super.isCellEditable(row, column);    }    public void setValueAt(Object value, int row, int column) {       if (column == BUTTON_COLUMN) {          // Button press - do whatever is needed to update          // the table source          updateTable(value, row, column);          return;       }       // Other columns - use superclass       super.setValueAt (value, row, column);    }    // Used to implement the table update    protected abstract void updateTable (                            Object value, int row, int column);    protected static final int BUTTON_COLUMN = 4; } 

Most of this code should be self-explanatory. This table model creates the extra column while preserving the existing data by delegating anything that concerns the first four columns of the table to EditableCurrencyTableModel and handling the last column itself. The getColumnCount method returns the correct number of columns by invoking the same method in its superclass and then adding 1 to account for the button column. The getvalueAt method is enhanced to return the string Update for any row in the button column. The value returned from this method will actually be used to set the buttons label; as noted above you can, if you wish, make the label dependent on the row number. For example, you might do this:

 public Object getValueAt (int row, int column) {    if (column == BUTTON_COLUMN) {       return "Update row " + row;    }    return super.getValueAt(row, column); } 

As you can see, data for the other columns in the table is obtained directly from the EditableCurrencyTableModel.

There is a similar pattern in the getColumnClass, getColumnName, and isCellEditable methods, which directly handle requests for the last column and delegate others to the superclass. The getColumnClass method returns String. class for the button's column, because the data used to supply the buttons label is a string. In this example, a column-based renderer will be used to draw the button, so the exact class returned by this method is not critically important. The getColumnName method returns an empty string for the last column, so the column heading will be empty as you can see in Figure 7-3. Finally, the isCellEditable method returns true for the column occupied by the buttons data and whatever the superclass returns for the other columns. It might seem strange to return true for the button's column, but there is a good reason for this, as you'll see shortly.

The most important methods in this class are the last two. The setvalueAt method is called when the table content is being updated. Updates for most columns go directly to the EditableCurrencyTableModel setValueAt method. What does it mean to update the button column's data and why does isCellEditable return true for this column? The reason for making this column editable is tied to the implementation of the button, which you'll see in "Activating the Button". For now, notice that calling the setValueAt method to change the button column's content does not affect the button label this much is obvious anyway, because getValueAt returns a constant value for that column. Instead, attempting to update this column results in a call to the abstract updateTable method. This method can be implemented in a concrete subclass of updatableCurrencyTableModel to provide the code needed to save the affected row's contents back to its original source.

The Button Renderer

The second thing you need for this table is a cell renderer that can draw a button. This is the simplest part, because everything you need to know to create this renderer was covered in the previous chapter. Because the renderer will draw a button, the simplest implementation is just to implement it as a subclass of JButton and return this from getTableCellRendererComponent. The code is shown in Listing 7-4.

Listing 7-4 A Button Renderer
 package AdvancedSwing.Chapter7; import javax.swing.*; import javax.swing.border.*; import javax.swing.table.*; import java.awt.*; import AdvancedSwing.Chapter6.DataWithIcon; //A holder for data and an associated icon public class ButtonRenderer extends JButton                implements TableCellRenderer {    public ButtonRenderer() {       this.border = getBorder();       this.setOpaque(true);    }    public void setForeground(Color foreground) {       this.foreground = foreground;       super.setForeground(foreground);    }    public void setBackground(Color background) {       this.background = background;       super.setBackground(background);    }    public void setFont(Font font) {       this.font = font;       super.setFont(font);    }    public Component getTableCellRendererComponent(                    JTable table, Object value,                    boolean isSelected,                    boolean hasFocus,                    int row, int column) {       Color cellForeground = foreground !=                  null ? foreground : table.getForeground();       Color cellBackground = background !=                  null ? background : table.getBackground();       setFont(font != null ? font : table.getFont());       if (hasFocus) {           setBorder(UIManager.getBorder(                          "Table.focusCellHighlightBorder"));           if(table.isCellEditable (row, column)) {           cellForeground = UIManager.getColor(                           "Table.focusCellForeground");           cellBackground = UIManager.getColor(                           "Table.focusCellBackground");           }       } else {          setBorder(border);       }       super.setForeground (cellForeground);       super.setBackground(cellBackground);       // Customize the component's appearance       setValue(value);       return this;    }    protected void setValue(Object value) {       if (value == null) {          setText("");          setIcon (null);       } else if (value instanceof Icon) {          setText ("");          setIcon ((Icon)value);       } else if (value instanceof DataWithIcon) {          DataWithIcon d = (DataWithIcon)value;          setText(d.toString());          setIcon(d.getIcon());       } else {          setText(value.toString());          setIcon(null);       }    }    protected Color foreground;    protected Color background;    protected Font font;    protected Border border; } 

Because you should by now be thoroughly familiar with writing renderers, not much needs to be said about this code. The only points worth noting are the border and label handling. Every button is created with a border that depends on the current look-and-feel. The table, however, uses its own border (also look-and-feel specific) to indicate the cell that currently has the focus. To preserve this mechanism for the button renderer, a reference to the buttons original border is saved by the constructor. In the getTableCellRendererComponent method, this original border is installed unless the button cell has the focus, when the same border as that used by the other table cells is used instead. The label is controlled by the setvalue method, which is called from getTableCellRendererComponent.

Core Note

Here, as with the other renderers that you have seen in this book, a setvalue method is implemented for the benefit of potential subclasses of ButtonRenderer, avoiding the need for them to re-implement the entire getTableCellRendererComponent method to make a small change to the way the button looks.



The value argument passed to getTableCellRendererComponent method (and hence to setvalue) comes from the button's column in the table model. Using the UpdatableCurrencyTableModel, this value will always be a string. As you can see, string values are simply used to set the buttons label. The setValueAt implementation is, however, more powerful. If you wish, you can subclass UpdatableCurrencyTableModel and have its getValueAt method return an Imagelcon or a DataWithIcon object to get a button with an icon or a button with an icon and accompanying text. Using Java's compact inner class notation, you can even embed this kind of extension directly into the code that creates the table. For example:

 JTable tbl = new JTable (new UpdatableCurrencyTableModel() {    public void updateTable (Object value, int row, int column) {       // Code not shown    };    public Object getValueAt(int row, int column) {       if (column == BUTTON_COLUMN) {          return new DataWithIcon("Save",                     new ImageIcon(getClass().                     getResource("images/save.gif")));       }       return super.getValueAt (row, column);    } }); 

This modified table would display buttons with the label Save and whatever the icon in the file images/save.gif represents.

Activating the Button

Now we come to the tricky part. If you created a new version of the EditableHighlightCurrencyTable example with the changes made so far, you would see a table with five columns, the rightmost of which contained a button with the label Update in every row. However, if you click any of these buttons with the mouse or tab over to one of them using the keyboard and press space or return, you wouldn't get a very useful response.

Core Note

Actually, the table would respond if you double-click the button or if you start typing into it, because a text editor would be used for the button column. This happens because the UpdatableCurrencyTableModel returns the type string and isCellEditable returns true for this column, so the default editor for object, a text editor, will be used. If you committed the edit by pressing RETURN, the table model setvalueAt method would be called and the table update would actually occur. This would, of course, be very confusing for the user, who would expect the button to click, not offer its label to be edited!



The question is, how to get the button to click? When you click the button's drawn image with the mouse, the table will consider starting an editor. If you implement an editor that looks like a button, with the same label as the one shown by the renderer and that activates itself on the first mouse click, you could give the illusion that the table contains a real button and, for the short period of time during which the button is active, there really would be a button in the table cell. To make this work, you need to implement an editor that returns a JButton as its editing component.

It would be nice to be able to implement this editor by extending DefaultCellEditor. This would make it possible to reuse existing code that registers CellEditorListeners and fires events when editing is stopped or abandoned. However, DefaultCellEditor has three constructors that require as arguments a JTextField, a JComboBox, or a JCheckBox. There is no default constructor and no way to supply a JButton, not even as a Component. This leaves three choices:

  1. Extend DefaultCellEditor and supply a dummy JTextField, JcomboBox, or JCheckBox just to satisfy its constructors.

  2. Implement a class that provides the cellEditor interface with a constructor that accepts a JButton and add the logic required for a button editor to that class.

  3. Implement a new base class that provides the CellEditor interface but which accepts an arbitrary component passed to its constructor.

The first of these would be the cheapest and fastest in implementation terms. Its drawbacks are that it requires more resource (in the shape of an addition component that is never used) and that it is not a neat and tidy solution. The difference between the second and third options is largely one of code reuse. The second option is undoubtedly faster to implement, but it would be very difficult to reuse, for the same reasons as DefaultCellEditor is hard to use in this case. The approach adopted here is to take the third alternative and implement a new base class called BasicCellEditor that is more flexible than DefaultCellEditor.

Creating a Custom Editor Base Class

The code for the new custom editor base class, which will be the basis for our button editor, is shown in Listing 7-5.

There is little to say about most of this code, because much of it will be overridden by derived classes. There are two constructors, one of which accepts any Component as its argument and a default constructor that doesn't require you to supply a Component. If a Component is supplied, its reference is simply stored as a convenience for derived classes. In most cases, a derived class will create a suitable Component and pass it to the constructor. However, if this is not possible because, for example, the Component's attributes need to be explicitly set, the default constructor can be used and the setcomponent method used to register the editing component later.

Listing 7-5 A Cell Editor Base Class
 package AdvancedSwing.Chapter7; import javax.swing.*; import javax.swing.table.*; import javax,swing.event.*; import java.awt.*; import java.beans.*; import java.util.*; public class BasicCellEditor implements CellEditor,             PropertyChangeListener {    public BasicCellEditor() {       this.editor = null;    }    public BasicCellEditor(Component editor) {       this.editor = editor;       editor.addPropertyChangeListener(this);    }    public Object getCellEditorValue() {       return null;    }    public boolean isCellEditable(EventObject evt) {       editingEvent = evt;       return true;    }    public boolean shouldSelectCell(EventObject evt) {       return true;    }    public boolean stopCellEditing() {       fireEditingStopped();       return true;    }    public void cancelCellEditing() {       fireEditingCanceled();    }    public void addCellEditorListener(CellEditorListener 1) {       listeners.add(CellEditorListener.class, 1);    }    public void removeCellEditorListener(CellEditorListener 1) {       listeners.remove(CellEditorListener.class, 1);    }    // Returns the editing component    public Component getComponent() {       return editor;    }    // Sets the editing component    public void setComponent(Component comp) {       editor = comp;    }    // Returns the event that triggered the edit    public EventObject getEditingEvent() {       return editingEvent;    }    // Method invoked when the editor is installed in the table.    // Overridden in derived classes to take any convenient    // action.    public void editingStarted(EventObject event) {    }    protected void fireEditingStopped() {       Object[] 1 = listeners .getListenerList ();       for (int i = 1.length -2; i >= 0; i -= 2) {          if (l[i] == CellEditorListener.class) {             if (changeEvent == null) {                changeEvent = new ChangeEvent(this);             }             ((CellEditorListener)l [i+1]).                     editingStopped(changeEvent);          }       }    }    protected void fireEditingCanceled ) {        Object[] 1 = listeners.getListenerList();        for (int i = 1.length - 2; i >= 0; i -= 2) {           if (l[i] == CellEditorListener.class) {              if (changeEvent == null) {                 changeEvent = new ChangeEvent(this);             }             ((CellEditorListener)1[i+1]).                     editingCanceled(changeEvent);          }       }    }    // Implementation of the PropertyChangeListener interface    public void propertyChange(PropertyChangeEvent evt) {       if (evt.getPropertyName().equals("ancestor") &&             evt.getNewValue() != null) {          // Added to table - notify the editor          editingStarted(editingEvent);       }    }    protected static JCheckBox checkBox = new JCheckBox();    protected static ChangeEvent changeEvent;    protected Component editor;    protected EventListenerList listeners =                                       new EventListenerList();    protected EventObject editingEvent; } 

The other CellEditor methods of interest are addCellEditorListener, removeCellEditorListener, fireEditingStopped, and fireEditingCanceled, all of which provide the event handling for the editor; the code used in these methods is taken almost directly from DefaultCellEditor. All CellEditorListeners for an instance of an editor are registered with an instance of the EventListenerList class, which is part of the javax.swing.event package. When registering a listener, you supply the listener's reference (that is, the CellEditorListener reference in this case) and the class that represents the listener, which, in this case, will always be CellEditorListener.class. The same arguments are used to remove a listener. This calling interface makes the implementation of the addCellEditorListener and removeCellEditorListener methods trivial.

Internally, the EventListenerList maintains its state as an array of Objects that are manipulated in pairs. The first item in each pair is the type of listener (for example, CellEditorListener. class) and the second is the listener's reference (that is, the reference passed to addCellEditorListener). Storing the type and reference together allows objects that support multiple event types to use a single EventListenerList to hold all listener details in one place.

Although EventListenerList provides the means for storing listeners, it doesn't have any code that can be used to deliver the events. This code is provided here in the editors fireEditingStopped and fireEditingCanceled methods, which process the array returned by the EventListenerList getListenerList method. Because this list could (theoretically) contain multiple listener types, the first item of each pair is checked to see whether it represents a CellEditorListener and, if so, the second item is used as the listener reference (after appropriate casting). Notice that only a single changeEvent is created, no matter how many listeners there are and how many events actually occur. This is possible because a ChangeEvent only holds the event source, which is constant for a given CellEditor.

The implementations of fireEditingStopped and fireEditingCanceled process the listener list from the end to the front, because DefaultCellEditor does it that way. This is done for compatibility. It does, however, cause a problem for our button editor, as you'll see shortly. Changing the listener invocation order would make it difficult to migrate to an improved version of DefaultCellEditor if one is ever produced, so we choose to live with some slightly odd behavior to avoid potential problems in the future.

In addition to implementing the methods required by the CellEditor interface, this class also supplies a little extra functionality that we'll use to implement our button editor. Although the table calls the editor's isCellEditable method to determine whether an edit should start and its getTableCellEditorComponent method to obtain the editing component, the editor is not actually notified that the editing process has started. In Swing 1.0, the editor could rely on the table to call its shouldSelectCell method after the editor had been assigned to the table, which constituted notification that an edit was in progress. However, later versions changed this, so that now shouldSelectCell is only called if the editing process is started in response to a mouse click; if the user begins editing using the keyboard or if the application calls editCellAt, then shouldSelectCell will not be called. It is often useful to be able to arrange for some work to be done once it is known that editing is in progress, so to make this possible the BasicCellEditor class includes the following method:

 public void editingStarted (EventObject evt); 

This method is invoked after the editing component has been added to the table. It is called no matter how the editing process was started. If you need to know what caused the edit to be initiated, you need to override the editingStarted method in your custom edit and use the evt argument, which is a copy of the originating event, or null if the edit was caused by the application invoking the two-argument form of editCellAt. We'll see shortly how to make use of this method. As an alternative, BasicCellEditor also provides the method

 public EventObject getEditingEvent () 

which returns the same event. This method can be used at any time during the editing process.

The implementation of this feature is simple. To invoke the editingStarted method, the editor needs to know that the editing component has been added to the table. To do this, it listens to PropertyChangeEvents generated by the editing component. When a JComponent is added to a container, it sends a PropertyChangeEvent for a property called ancestor in which the new value is a reference to its new parent. BasicCellEditor registers itself as a PropertyChangeListener of the editing component and monitors these events, calling editingstarted when the ancestor property changes to a non-null value. Because the editingstarted method needs the event that started the edit, a reference to it is saved by BasicCellEditor's isCellEditable method for use in the PropertyChangeListener propertyChange method. As a result, if you subclass BasicCellEditor and override its isCellEditable method, you must remember to invoke super.isCellEditable so that the edit event will be saved.

Extending the Base Class to Create a Button Editor

Having implemented a base class, the next task is to subclass it to create the button editor. The real problem to solve here is how to actually make the button work. To see what needs to be done, let's recap on what the table will do for us.

First, when the user clicks the button image, the JTable editCellAt method will be called. Because the button column in the UpdatableCurrencyTableModel is editable, it will allow editing to commence and the getTableCellEditorComponent method of the button editor will be called. This method will return a suitably configured JButton, which will subsequently be added to the table. If nothing else were done, the button would appear in the table and, if the user started the "edit" using a mouse, the mouse event would be passed to the button, causing it to click its appearance will change as the user expects and an ActionEvent will be generated. Unfortunately, this would not happen if the user attempts to activate the button using the keyboard by pressing the space key, for example, which is the usual shortcut for activating a focused button.

Having arranged for the button to click, at least when using the mouse, we now need to stop the editing phase immediately because there is nothing more for the user to do with the button it should be removed and the usual renderer restored. Also, the action implied by the button press, which is actually implemented by the table model, needs to be performed. To stop the edit, the editors stopCellEditing method needs to be called. This can be done by arranging for the button editor to be an ActionListener of the button and invoking stopCellEditing from its actionPerformed method, which will be called when the button is clicked.

The stopCellEditing method of BasicCellEditor calls fireEditingStopped, which in turn invokes the editingStopped method of all registered CellEditorListeners. In this case, only the JTable is registered. As you saw in "Stopping the Edit" earlier in this chapter, when the edit terminates it removes the editing component from the table and stores the value from the component in the table's data model. What does this mean for the button editor? There really is no "value" to store, but the editors getEditorValue method will still be called to get whatever the value might be. Fortunately, when the editor component is obtained using getTableCellEditorComponent, it is given an initial value from the table model. As you know, this will usually be the button's label. Whatever it might be, the button editor just returns it when getEditorValue is called. The JTable then calls the setvalueAt method of the table model with this value, which results in no change.

The table model that we'll use in our example will be derived from UpdatableCurrencyTableModel. You know that the setValueAt method of this class calls the abstract updateTable method, which will be implemented in the model subclass. This is how the row content would be written back to the database, or whatever persistent storage it was loaded from.

You can see the implementation of the button editor in Listing 7-6

Listing 7-6 A Button Cell Editor
 package AdvancedSwing.Chapter7; import javax.swing.*; import javax.swing.table.*; import java.awt.*; import java.awt.event.*; import java.util.*; import AdvancedSwing.Chapter6.DataWithIcon; public class ButtonEditor extends BasicCellEditor                   implements ActionListener,                   TableCellEditor {    public ButtonEditor(JButton button) {       super(button);       button.addActionListener(this);    }    public void setForeground(Color foreground) {       this.foreground = foreground;       editor.setForeground(foreground);    }    public void setBackground(Color background) {       this.background = background;       editor.setBackground(background);    }    public void setFont(Font font) {       this.font = font;       editor.setFont(font);    }    public Object getCellEditorValue() {       return value;    }    public void editingStarted(EventObject event) {       // Edit starting - click the button if necessary       if (!(event instanceof MouseEvent)) {          // Keyboard event - click the button          SwingUtilities.invokeLater(new Runnable() {             public void run() {                ((JButton)editor).doClick();             }          });       }    }    public Component getTableCellEditorComponent(             JTable tbl,             Object value, boolean isSelected,             int row, int column) {       editor.setForeground(foreground != null ? foreground :                                       tbl.getForeground());       editor.setBackground(background != null ? background :                                       tbl.getBackground());       editor.setFont(font != null ? font : tbl.getFont());       this.value = value;       setValue(value);       return editor;    }    protected void setValue(Object value) {       JButton button = (JButton)editor;       if (value == null) {          button.setText("");          button.setIcon(null);       } else if (value instanceof Icon) {          button.setText("");          button.setIcon((Icon)value);       } else if (value instanceof DataWithIcon) {          DataWithIcon d = (DataWithIcon)value;          button.setText(d.toString());          button.setIcon(d.getIcon());       } else {          button.setText(value.toString() ) ;          button.setIcon(null);       }    }    public void actionPerformed(ActionEvent evt) {       // Button pressed - stop the edit       stopCellEditing();    }    protected Object value;    protected Color foreground;    protected Color background;    protected Font font; } 

Creating a table that uses this editor is a simple matter of installing the editor in the column occupied by the buttons and creating a subclass of UpdatableCurrencyTableModel with a suitable implementation of the updateTable method. The code for the application is shown in Listing 7-7 and for the table model in Listing 7-8.

Listing 7-7 An Updatable Currency Table
 package AdvancedSwing.Chapter7; import javax.swing.*; import javax.swing.table.*; import javax.swing.event.*; import java.awt.*; import java.awt.event.*; import AdvancedSwing.Chapter6.*; public class UpdatableHighlightCurrencyTable {    public static void main(String[] args) {       JFrame f = new JFrame("Updatable Highlighted Currency                                                       Table");       JTable tbl = new JTable (                       new TestUpdatableCurrencyTableModel());       tbl.setDefaultRenderer(java.lang.Number.class,             new FractionCellRenderer(10, 3,             SwingConstants.RIGHT));       TableColumnModel tcm=tbl.getColumnModel();       tcm.getColumn(0).setPreferredWidth(150);       tcm.getColumn(0).setMinWidth(150);       TextWithIconCellRenderer renderer =                                new TextWithIconCellRenderer();       tcm.getColumn(0).setCellRenderer(renderer) ;       tbl.setShowHorizontalLines(false);       tbl.setIntercellSpacing(new Dimension(1, 0));       // Add the stripe renderer in the leftmost four columns.       StripedTableCellRenderer.installInColumn(tbl, 0,                   Color.lightGray, Color.white,                   null, null);       StripedTableCellRenderer.installInColumn(tbl, 1,                   Color.lightGray, Color.white,                   null, null);       StripedTableCellRenderer.installInColumn(tbl, 2,                   Color.lightGray, Color.white,                   null, null);       StripedTableCellRenderer.installInColumn(tbl, 3,                   Color.lightGray, Color.white,                   null, null);       // Add the highlight renderer to the difference column.       // The following comparator makes it highlight       // cells with negative numeric values.       Comparator cmp = new Comparator() {          public boolean shouldHighlight(JTable tbl, Object                                  value, int row, int column) {             if (value instanceof Number) {                double columnValue =                             ((Number)value).doubleValue();                return columnValue < (double)0.0;             }             return false;          }       };       tcm.getColumn(3).setCellRenderer(                             new HighlightRenderer(cmp,                             null, Color.pink,                             Color.black, Color.pink.darker(),                             Color.white));       // Install a button renderer in the last column       ButtonRenderer buttonRenderer = new ButtonRenderer();       buttonRenderer.setForeground(Color.blue);       buttonRenderer.setBackground(Color.lightGray);       tcm.getColumn(4).setCellRenderer(buttonRenderer);       // Install a button editor in the last column       TableCellEditor editor = new ButtonEditor(new JButton());       tcm.getColumn(4).setCellEditor(editor);       // Make the rows wide enough to take the buttons       tbl.setRowHeight(20);       tbl.setAutoResizeMode(JTable.AUTO_RESIZE_OFF);       tbl.setPreferredScrollableViewportSize(              tbl.getPreferredSize());       JScrollPane sp = new JScrollPane(tbl);       f.getContentPane().add(sp, "Center");       f.pack();       f.addWindowListener(new WindowAdapter() {          public void windowClosing(WindowEvent evt) {             System.exit(0);          }       });       f.setVisible(true);    } } 
Listing 7-8 An Updatable Currency Table Model
 package AdvancedSwing.Chapter7; import AdvancedSwing.Chapter7.UpdatableCurrencyTableModel; public class TestUpdatableCurrencyTableModel                extends UpdatableCurrencyTableModel {    public void updateTable(Object value, int row, int column) {       System.out.println("Update for row " + row + "                                                    required.");       System.out.println("Values are " +               getValueAt(row, 1} +               ", " + getValueAt(row, 2) +               "; diff is " + getValueAt(row, 3));    } } 

For this simple application, the updateTable method simply records that it has been called and shows the value, row, and column numbers passed from the table. Typically, in a real application, the row number might be used to extract all the data for that row and write it back to a database using Java Database Connectivity (JDBC).

That almost completes the implementation of the button editor and its sample application, but there is one small catch. As we said earlier, when the button editor is activated, the button will be automatically clicked by the mouse event that caused the table to start the edit. However, what happens if the user navigates to the button using the keyboard and presses a key? Here, the editor will be installed correctly, but the key press will not be passed to it, as you saw in "Editing the Cell Data". As a result, the button won't click if you try to activate it with a key press and the edit will not end until you move the focus to another cell and activate that instead. Fortunately, there is a simple way to solve this problem. It is possible to programmatically click a button by calling its doClick method, which causes the button's appearance to change as the user would expect and generates the ActionEvent. When can the doClick method be called? It must be called some time after the button has been added to the table. Fortunately, the button editor is subclassed from our BasicCellEditor class, which calls its editingStarted method when the editing component has been added to the JTable. As you can see from Listing 7-6, the button cell editor overrides this method to get control at this point and calls the button's doClick method to simulate pressing the button. Note, however, the following two points:

  1. The call is not unconditional it happens only if the event passed to it is not a MouseEvent.

  2. The call is not made inline it is deferred using the SwingUtilities invokeLater method.

The reason for the event check is simple there is no need to programmatically click the button if the edit was started by a mouse click because the MouseEvent will be passed directly to the button by the JTable and cause the button to clock of its own accord. This check ensures that button will be clicked if the edit is started from the keyboard or (for some reason) as a result of an application calling editCellAt directly on a cell in the button column.

The reason for deferring doClick is slightly more subtle. If doClick were called inline, it would immediately call the actionPerformed method of the editor, which, in turn, would call the editingStopped method of BasicCellEditor. This would result in the table trying to remove the button from the table and clean up the entire edit, while it was still setting that edit up in editCellAt! Not surprisingly, this just doesn't work. Deferring the button click allows the edit setup to complete before starting the cleanup process. This is a technique that is generally useful and you'll see another example of it in the next section.

 

 



Core Swing
Core Swing: Advanced Programming
ISBN: 0130832928
EAN: 2147483647
Year: 1999
Pages: 55
Authors: Kim Topley

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net