16.3 A Table with Custom Editing and Rendering


Recall from the previous chapter that you can build your own editors and renderers for the cells in your table. One easy customization is altering the properties of the DefaultTableCellRenderer, which is an extension of JLabel. You can use icons, colors, text alignment, borders, and anything that can change the look of a label.

You don't have to rely on JLabel, though. Developers come across all types of data. Some of that data is best represented as text some isn't. For data that requires (or at least enjoys the support of) alternate representations, your renderer can extend any component. To be more precise, it can extend Component, so your options are boundless. We look at one of those options next.

16.3.1 A Custom Renderer

Figure 16-3 shows a table containing audio tracks in a mixer format, using the default renderer. We have some track information, such as the track name, its start and stop times, and two volumes (left and right channels, both using integer values from to 100) to control.

Figure 16-3. A standard table with cells drawn by the DefaultTableCellRenderer
figs/swng2.1603.gif

We'd really like to show our volume entries as sliders. The sliders give us a better indication of the relative volumes. Figure 16-4 shows the application with a custom renderer for the volumes.

Figure 16-4. A standard table with the volume cells drawn by VolumeRenderer
figs/swng2.1604.gif

The code for this example involves two new pieces and a table model for our audio columns. First, we must create a new renderer by implementing the TableCellRenderer interface ourselves. Then, in the application code, we attach our new renderer to our volume columns. The model code looks similar to the models we have built before. The only real difference is that we now have two columns using a custom Volume class. The following Volume class encodes an integer. The interesting thing about this class is the setVolume( ) method, which can parse a String, Number, or other Volume object.

// Volume.java // A simple data structure for track volumes on a mixer // public class Volume {   private int volume;   public Volume(int v) { setVolume(v); }   public Volume( ) { this(50); }   public void setVolume(int v) { volume = (v < 0 ? 0 : v > 100 ? 100 : v); }   public void setVolume(Object v) {      if (v instanceof String) {       setVolume(Integer.parseInt((String)v));     }     else if (v instanceof Number) {       setVolume(((Number)v).intValue( ));     }     else if (v instanceof Volume) {       setVolume(((Volume)v).getVolume( ));     }   }   public int getVolume( ) { return volume; }   public String toString( ) { return String.valueOf(volume); } }

Here's the model code. We store a simple Object[][] structure for our data, a separate array for the column headers, and another array for the column class types. We make every cell editable by always returning true for the isCellEditable( ) method. The setValue( ) method checks to see if we're setting one of the volumes, and if so, we don't simply place the new object into the array. Rather, we set the volume value for the current object in the data array. Then, if someone builds an editor that returns a String or a Number rather than a Volume object, we still keep our Volume object intact.

// MixerModel.java // An audio mixer table data model. This model contains the following columns: //  Track name (String) //  Track start time (String) //  Track stop time (String) //  Left channel volume (Volume, 0 . . 100) //  Right channel volume (Volume, 0 . . 100) // import javax.swing.table.*; public class MixerModel extends AbstractTableModel {   String headers[] = {"Track", "Start", "Stop", "Left Volume", "Right Volume"};   Class columnClasses[] = {String.class, String.class, String.class,                            Volume.class, Volume.class};   Object  data[][] = {     {"Bass", "0:00:000", "1:00:000", new Volume(56), new Volume(56)},     {"Strings", "0:00:000", "0:52:010", new Volume(72), new Volume(52)},     {"Brass", "0:08:000", "1:00:000", new Volume(99), new Volume(0)},     {"Wind", "0:08:000", "1:00:000", new Volume(0), new Volume(99)},   };   public int getRowCount( ) { return data.length; }   public int getColumnCount( ) { return headers.length; }   public Class getColumnClass(int c) { return columnClasses[c]; }   public String getColumnName(int c) { return headers[c]; }   public boolean isCellEditable(int r, int c) { return true; }   public Object getValueAt(int r, int c) { return data[r][c]; }   // Do something extra here so that if we get a String object back (from a text   // field editor), we can still store that as a valid Volume object. If it's just a   // string, then stick it directly into our data array.   public void setValueAt(Object value, int r, int c) {     if (c >= 3) { ((Volume)data[r][c]).setVolume(value);}     else {data[r][c] = value;}   }   // A quick debugging utility to dump the contents of our data structure   public void dump( ) {     for (int i = 0; i < data.length; i++) {       System.out.print("|");       for (int j = 0; j < data[0].length; j++) {         System.out.print(data[i][j] + "|");       }       System.out.println( );     }   } }

Here's the application that displays the window and table. Notice how we attach a specific renderer to the Volume class type without specifying which columns contain that data type, using the setDefaultRenderer( ) method. The table uses the results of the TableModel's getColumnClass( ) call to determine the class of a given column and then uses getDefaultRenderer( ) to get an appropriate renderer for that class.

// MixerTest.java // import java.awt.*; import javax.swing.*; public class MixerTest extends JFrame {   public MixerTest( ) {     super("Customer Editor Test");     setSize(600,160);     setDefaultCloseOperation(EXIT_ON_CLOSE);     MixerModel test = new MixerModel( );     test.dump( );     JTable jt = new JTable(test);     jt.setDefaultRenderer(Volume.class, new VolumeRenderer( ));     JScrollPane jsp = new JScrollPane(jt);     getContentPane( ).add(jsp, BorderLayout.CENTER);   }   public static void main(String args[]) {     MixerTest mt = new MixerTest( );     mt.setVisible(true);   } }

Now we build our renderer. As you saw with the DefaultTableCellRenderer class, to create a new renderer, we often just extend the component that does the rendering. Then we initialize the component with the getTableCellRendererComponent( ) method and return a reference to that component. That is exactly what we do with this rather simple VolumeRenderer class. We extend the JSlider class and, in the getTableCell-RendererComponent( ) method, set the position of the slider's knob and return the slider. This doesn't allow us to edit volume values, but at least we get a better visual representation.

// VolumeRenderer.java // A slider renderer for volume values in a table // import java.awt.Component; import javax.swing.*; import javax.swing.table.*; public class VolumeRenderer extends JSlider implements TableCellRenderer {   public VolumeRenderer( ) {     super(SwingConstants.HORIZONTAL);     // Set a starting size. Some 1.2/1.3 systems need this.     setSize(115,15);   }   public Component getTableCellRendererComponent(JTable table, Object value,                                                  boolean isSelected,                                                  boolean hasFocus,                                                  int row,int column) {     if (value == null) {       return this;     }     if (value instanceof Volume) {       setValue(((Volume)value).getVolume( ));     }     else {       setValue(0);     }     return this;   } } 

16.3.2 A Custom Editor

Of course, the next obvious question is how to make the sliders usable for editing volume values. To do that, we write our own VolumeEditor class that implements the TableCellEditor interface.

The following code implements a TableCellEditor for Volume objects from scratch. The fireEditingCanceled( ) method shows how to cancel an editing session, and fireEditingStopped( ) shows how to end a session and update the table. In both cases, it's a matter of firing a ChangeEvent identifying the editor for the appropriate listeners. When editing is canceled, you must restore the cell's original value.

To help with this new editor, we provide a pop-up window inner class that has two buttons: an "OK" button (which displays a green checkmark) and a "cancel" button (a red "x"). This pop up extends the JWindow class and gives us room to grow if we need to. (The JSlider class can take advantage of the Enter key to accept a new value, and the Escape key can be used to cancel. If we need anything else, our pop-up helper is the right way to go.) As a bonus, the pop up clearly indicates which volume is actively being edited, as you can see in Figure 16-5.

Figure 16-5. Our custom volume editor in action with the pop-up accept and cancel buttons
figs/swng2.1605.gif

Here's the source code for this editor. Pay special attention to the code we use to connect the pop-up helper to the editor (so we can properly stop or cancel editing when the user clicks a button).

// VolumeEditor.java // A slider editor for volume values in a table // import java.awt.*; import java.awt.event.*; import java.util.*; import javax.swing.*; import javax.swing.table.*; import javax.swing.event.*; public class VolumeEditor extends JSlider implements TableCellEditor {   public OkCancel helper = new OkCancel( );   protected transient Vector listeners;   protected transient int originalValue;   protected transient boolean editing;   public VolumeEditor( ) {     super(SwingConstants.HORIZONTAL);     listeners = new Vector( );   }   // Inner class for the OK/cancel pop-up window that displays below the active   // scrollbar. Its position will have to be determined by the editor when   // getTableCellEditorComponent( ) is called.   public class OkCancel extends JWindow {     private JButton okB = new JButton(new ImageIcon("accept.gif"));     private JButton cancelB = new JButton(new ImageIcon("decline.gif"));     private int w = 50;     private int h = 24;     public OkCancel( ) {       setSize(w,h);       setBackground(Color.yellow);       JPanel p = new JPanel(new GridLayout(0,2));       // p.setBorder(BorderFactory.createLineBorder(Color.gray));       // okB.setBorder(null);       // cancelB.setBorder(null);       p.add(okB);       p.add(cancelB);       setContentPane(p);       okB.addActionListener(new ActionListener( ) {         public void actionPerformed(ActionEvent ae) {           stopCellEditing( );         }       });       cancelB.addActionListener(new ActionListener( ) {         public void actionPerformed(ActionEvent ae) {           cancelCellEditing( );         }       });     }   }    public Component getTableCellEditorComponent(JTable table, Object value,                                                boolean isSelected,                                                int row, int column) {     if (value == null) {       return this;     }     if (value instanceof Volume) {       setValue(((Volume)value).getVolume( ));     }     else {       setValue(0);     }     table.setRowSelectionInterval(row, row);     table.setColumnSelectionInterval(column, column);     originalValue = getValue( );     editing = true;     Point p = table.getLocationOnScreen( );     Rectangle r = table.getCellRect(row, column, true);     helper.setLocation(r.x + p.x + getWidth( ) - 50, r.y + p.y + getHeight( ));     helper.setVisible(true);     return this;   }   // CellEditor methods   public void cancelCellEditing( ) {     fireEditingCanceled( );     editing = false;     helper.setVisible(false);   }   public Object getCellEditorValue( ) {return new Integer(getValue( ));}   public boolean isCellEditable(EventObject eo) {return true;}      public boolean shouldSelectCell(EventObject eo) {     return true;   }   public boolean stopCellEditing( ) {     fireEditingStopped( );     editing = false;     helper.setVisible(false);     return true;   }   public void addCellEditorListener(CellEditorListener cel) {     listeners.addElement(cel);   }      public void removeCellEditorListener(CellEditorListener cel) {     listeners.removeElement(cel);   }   protected void fireEditingCanceled( ) {     setValue(originalValue);     ChangeEvent ce = new ChangeEvent(this);     for (int i = listeners.size( ) - 1; i >= 0; i--) {       ((CellEditorListener)listeners.elementAt(i)).editingCanceled(ce);     }   }   protected void fireEditingStopped( ) {     ChangeEvent ce = new ChangeEvent(this);     for (int i = listeners.size( ) - 1; i >= 0; i--) {       ((CellEditorListener)listeners.elementAt(i)).editingStopped(ce);     }   } } 

You can make this the active editor for volume objects by using the setDefaultEditor( ) method:

JTable table = new JTable(new MixerModel( )); table.setDefaultEditor(Volume.class, new VolumeEditor( ));

Once it's in place, you can use the sliders to edit the volumes. Because we always return true when asked isCellEditable( ), the sliders are always active. If you want, you can make them a muted color until the user double-clicks on one and then make that slider active. When the user stops editing by selecting another entry, the active slider should return to its muted color. The DefaultCellEditor does a lot of the work for you.



Java Swing
Graphic Java 2: Mastering the Jfc, By Geary, 3Rd Edition, Volume 2: Swing
ISBN: 0130796670
EAN: 2147483647
Year: 2001
Pages: 289
Authors: David Geary

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