Sun Java Solaris Communities My SDN Account Join SDN
 
Swing Introduction

Swing

 

Bookshelf Index


Introduction | Download Chapter 18| Download Chapter 22

Understanding the Code

Class ExpenseReport

Class ExpenseReport extends JFrame and defines three instance variables:

  • JTable m_table: table to edit data.
  • ExpenseReportData m_data: data model for this table.
  • JLabel m_total: label to dynamically display total amount of expenses.
  • The ExpenseReport constructor first instantiates our table model, m_data, and then instantiates our table, m_table. The selection mode is set to single selection and we iterate through the number of columns creating cell renderers and editors based on each specific column. The Approved column uses an instance of our custom CheckCellRenderer class as renderer. All other columns use a DefaultTableCellRenderer. All columns also use a DefaultCellEditor. However, the component used for editing varies: the Category column uses a JComboBox, the Approved column uses a JCheckBox, and all other columns use a JTextField. These components are passed to the DefaultTableCellRenderer constructor.

    Several components are added to the bottom of our frame: JLabel m_total, used to display the total amount of expenses, and three JButtons used to manipulate tables rows. (Note that the horizontal glue component added between the label and the button pushes buttons to the right side of the panel, so they remain glued to the right when our frame is resized.)

    These three buttons, titled Insert before, Insert after, and Delete row, behave as their titles imply. The first two use the insert() method from the ExpenseReportData model to insert a new row before or after the currently selected row. The last one deletes the currently selected row by calling the delete() method. In all cases the modified table is updated and repainted.

    Method calcTotal() calculates the total amount of expenses in column COL_AMOUNT using our table's data model, m_data.

    Class CheckCellRenderer

    Since we use check boxes to edit our table's Approved column, to be consistent we also need to use check boxes for that column's cell renderer (recall that cell renderers just act as rubber stamps and are not at all interactive). The only GUI component which can be used in the existing DefaultTableCellRenderer is JLabel, so we have to provide our own implementation of the TableCellRenderer interface. This class, CheckCellRenderer, uses JCheckBox as a super-class. Its constructor sets the border to indicate whether the component has the focus and sets its opaque property to true to indicate that the component's background will be filled with the background color.

    The only method which must be implemented in the TableCellRenderer interface is getTableCellRendererComponent(). This method will be called each time the cell is about to be rendered to deliver new data to the renderer. It takes six parameters:

  • JTable table: reference to table instance.
  • Object value: data object to be sent to the renderer.
  • boolean isSelected: true if the cell is currently selected.
  • boolean hasFocus: true if the cell currently has the focus.
  • int row: cell's row.
  • int column: cell's column.
  • Our implementation sets whether the JCheckBox is checked depending on the value passed as Boolean. Then it sets the background, foreground, font, and border to ensure that each cell in the table has a similar appearance.

    Class ExpenseData

    Class ExpenseData represents a single row in the table. It holds five variables corresponding to our data structure described in the beginning of this section.

    Class ColumnData

    Class ColumnData holds each column's title, width, and header alignment.

    Class ExpenseReportData

    ExpenseReportData extends AbstractTableModel and should look somewhat familiar from previous examples in this chapter (such as StockTableData), so we will not discuss this class in complete detail. However, we need to take a closer look at the setValueAt() method, which is new for this example (all previous examples did not accept new data). This method is called each time an edit is made to a table cell. First we determine which ExpenseData instance (table's row) is affected, and if it is invalid we simply return. Otherwise, depending on the column of the changed cell, we define several cases in a switch structure to accept and store a new value, or to reject it:

  • For the Date column the input string is parsed using our SimpleDateFormat instance. If parsing is successful, a new date is saved as a Date object, otherwise an error message is displayed.
  • For the Amount column the input string is parsed as a Double and stored in the table if parsing is successful. Also a new total amount is recalculated and displayed in the Total JLabel.
  • For the Category column the input string is placed in the CATEGORIES array at the corresponding index and is stored in the table model.
  • For the Approved column the input object is cast to a Boolean and stored in the table model.
  • For the Description column the input string is directly saved in our table model.
  • Running the Code

    Try editing different columns and note how the corresponding cell editors work. Experiment with adding and removing table rows and note how the total amount is updated each time the Amount column is updated. Figure 18.8 shows ExpenseReport with a combo box opened to change a cell's value.

    18.9A JavaBeans property editor

    Now that we're familiar with the table API we can complete the JavaBeans container introduced in the chapter 4 and give it the capability to edit the properties of JavaBeans. This dramatically increases the possible uses of our simple container and makes it a quite powerful tool for studying JavaBeans.


    Click Figure 18.8 for full-scale image.
    BeanContainer JavaBeans property editor using JTables as editing forms. The Code: BeanContainer.java

    import java.awt.*;
    import java.awt.event.*;
    import java.io.*;
    import java.beans.*;
    import java.lang.reflect.*;
    import java.util.*;
    
    import javax.swing.*;
    import javax.swing.table.*;
    import javax.swing.event.*;
    
    import dl.*;
    
    public class BeanContainer 
                    extends JFrame 
                    implements FocusListener
    {
      protected Hashtable m_editors = 
                        new Hashtable();
    
      // Unchanged code from section 4.7
    
      protected JMenuBar createMenuBar() {
        // Unchanged code from section 4.7
    
        JMenu mEdit = new JMenu("Edit");
        mItem = new JMenuItem("Delete");
        lst = new ActionListener() { 
          public void 
            actionPerformed(ActionEvent e) {
            if (m_activeBean == null)
              return;
            Object obj = 
                  m_editors.get(m_activeBean);
            if (obj != null) {
              BeanEditor editor = 
                            (BeanEditor)obj;
              editor.dispose();
              m_editors.remove(m_activeBean);
            }
            getContentPane().remove(
                        m_activeBean);
            m_activeBean = null;
            validate();
            repaint();
          }
        };
        mItem.addActionListener(lst);
        mEdit.add(mItem);
    
        mItem = new JMenuItem(
                        "Properties...");
        lst = new ActionListener() { 
          public void actionPerformed
                        (ActionEvent e) {
            if (m_activeBean == null)
              return;
            Object obj = m_editors.get(
                          m_activeBean);
            if (obj != null) {
              BeanEditor editor = 
                       (BeanEditor)obj;
              editor.setVisible(true);
              editor.toFront();
            }
            else {
              BeanEditor editor = 
                 new BeanEditor(m_activeBean);
              m_editors.put(m_activeBean, editor);
            }
          }
        };
        mItem.addActionListener(lst);
        mEdit.add(mItem);
        menuBar.add(mEdit);
    
        // Unchanged code from section 4.7
    
        return menuBar;
      }
    
      // Unchanged code from section 4.7
    }
    
    class BeanEditor extends JFrame 
         implements PropertyChangeListener
    {
      protected Component m_bean;
      protected JTable m_table;
      protected PropertyTableData m_data;
        
      public BeanEditor(Component bean) {
        m_bean = bean;
        m_bean.addPropertyChangeListener(this);
    
        Point pt = m_bean.getLocationOnScreen();
        setBounds(pt.x+50, pt.y+10, 400, 300);
        getContentPane().setLayout(
                         new BorderLayout());
    
        m_data = new PropertyTableData(m_bean);
        m_table = new JTable(m_data);
    
        JScrollPane ps = new JScrollPane();
        ps.getViewport().add(m_table);
        getContentPane().add(ps, 
                        BorderLayout.CENTER);
    
        setDefaultCloseOperation(
                     HIDE_ON_CLOSE);
        setVisible(true);
      }
    
      public void propertyChange(
                     PropertyChangeEvent evt) {
        m_data.setProperty(evt.getPropertyName(
                           ), evt.getNewValue());
      }
    
      class PropertyTableData 
            extends AbstractTableModel 
      {
        protected String[][] m_properties;
        protected int m_numProps = 0;
        protected Vector m_v;
    
        public PropertyTableData(
                         Component bean) {
          try {
            BeanInfo info = 
                     Introspector.getBeanInfo(
              m_bean.getClass());
            BeanDescriptor descr = 
                       info.getBeanDescriptor();
            setTitle("Editing "+descr.getName());
            PropertyDescriptor[] props = 
                info.getPropertyDescriptors();
            m_numProps = props.length;
                    
            m_v = new Vector(m_numProps);
            for (int k=0; k<m_numProps; k++) {
              String name = props[k].getDisplayName();
              boolean added = false;
              for (int i=0; i<m_v.size(); i++) {
                String str = ((PropertyDescriptor)
                                 m_v.elementAt(i)).
                  getDisplayName();
                if (name.compareToIgnoreCase(str)
                                       < 0) {
                  m_v.insertElementAt(props[k], i);
                  added = true;
                  break;
                }
              }
              if (!added)
                m_v.addElement(props[k]);
            }
    
            m_properties = new 
                    String[m_numProps][2];
            for (int k=0; k<m_numProps; k++) {
              PropertyDescriptor prop = 
                (PropertyDescriptor)
                       m_v.elementAt(k);
              m_properties[k][0] = 
                     prop.getDisplayName();
              Method mRead = 
                       prop.getReadMethod();
              if (mRead != null && 
               mRead.getParameterTypes(
                            ).length == 0) {
                Object value = 
                  mRead.invoke(m_bean, null);
                m_properties[k][1] = 
                       objToString(value);
              }
              else
                m_properties[k][1] = "error";
            }
          }
          catch (Exception ex) {
            ex.printStackTrace();
            JOptionPane.showMessageDialog(
              BeanEditor.this, "Error: 
                "+ex.toString(),
              "Warning", 
               JOptionPane.WARNING_MESSAGE);
          }
        }
    
        public void setProperty(String name, 
                              Object value) {
          for (int k=0; k<m_numProps; k++)
            if (name.equals(m_properties
                                [k][0])) {
              m_properties[k][1] = 
                        objToString(value);
              m_table.tableChanged(
                    new TableModelEvent(
                               this, k)); 
              m_table.repaint();
              break;
            }
        }
    
        public int getRowCount() 
                { return m_numProps; }
    
        public int getColumnCount() 
                        { return 2; } 
    
        public String getColumnName(
                          int nCol) { 
          return nCol==0 ? 
                 "Property" : "Value";
        }
     
        public boolean isCellEditable(
                    int nRow, int nCol) { 
           return (nCol==1);
        }
    
        public Object getValueAt(int nRow, 
                                int nCol) {
          if (nRow < 0 || nRow>=
                              getRowCount())
            return "";
          switch (nCol) {
            case 0: return m_properties[
                                 nRow][0];
            case 1: return m_properties[
                                 nRow][1];
          }
          return "";
        }
    
        public void setValueAt(Object 
                   value, int nRow, int nCol) {
          if (nRow < 0 || nRow>
                   =getRowCount())
            return;
          String str = value.toString();
          PropertyDescriptor prop = 
                    (PropertyDescriptor)m_v.
            elementAt(nRow);
          Class cls = prop.getPropertyType();
          Object obj = stringToObj(str, cls);
          if (obj==null)
            return;        // can't process
               
          Method mWrite = prop.getWriteMethod();
          if (mWrite == null || 
                mWrite.getParameterTypes(
                           ).length != 1)
            return;
          try {
            mWrite.invoke(m_bean, 
                 new Object[]{ obj });
            m_bean.getParent().doLayout();
            m_bean.getParent().repaint();
            m_bean.repaint();
          }
          catch (Exception ex) {
            ex.printStackTrace();
            JOptionPane.showMessageDialog(
              BeanEditor.this, "Error: 
                   "+ex.toString(),
              "Warning", 
                 JOptionPane.WARNING_MESSAGE);
          }
          m_properties[nRow][1] = str;
        }
    
        public String objToString(Object value) {
          if (value==null)
            return "null";
          if (value instanceof Dimension) {
            Dimension dim = (Dimension)value;
            return ""+dim.width+","+dim.height;
          }
          else if (value instanceof Insets) {
            Insets ins = (Insets)value;
            return ""+ins.left+","+ins.top+",
                 "+ins.right+","+ins.bottom;
          }
          else if (value instanceof 
                               Rectangle) {
            Rectangle rc = (Rectangle)value;
            return ""+rc.x+","+rc.y+",
                "+rc.width+","+rc.height;
          }
          else if (value instanceof Color) {
            Color col = (Color)value;
            return ""+col.getRed()+",
                 "+col.getGreen()+",
                     "+col.getBlue();
          }
          return value.toString();
        }
    
        public Object 
           stringToObj(String str, 
              Class cls) {
          try {
            if (str==null)
              return null;
            String name = cls.getName();
            if (name.equals("java.lang.String"))
              return str;
            else if (name.equals("int"))
              return new Integer(str);
            else if (name.equals("long"))
              return new Long(str);
            else if (name.equals("float"))
              return new Float(str);
            else if (name.equals("double"))
              return new Double(str);
            else if (name.equals("boolean"))
              return new Boolean(str);
            else if 
              (name.equals("java.awt.Dimension")) {
              int[] i = strToInts(str);
              return new Dimension(i[0], i[1]);
            }
            else if (name.equals(
               "java.awt.Insets")) {
              int[] i = strToInts(str);
              return new Insets(i[0], 
                  i[1], i[2], i[3]);
            }
            else if (name.equals(
                      "java.awt.Rectangle")) {
              int[] i = strToInts(str);
              return 
               new Rectangle(i[0], i[1], 
                             i[2], i[3]);
            }
            else if (name.equals("java.awt.Color")) {
              int[] i = strToInts(str);
              return new 
               Color(i[0], i[1], i[2]);
            }
            return null;    // not supported
          }
          catch(Exception ex) { return null; }
        }
    
        public int[] strToInts(
            String str) throws Exception {
          int[] i = new int[4];
          StringTokenizer tokenizer = 
              new StringTokenizer(str, ",");
          for (int k=0; k<i.length && 
           tokenizer.hasMoreTokens(); k++)
            i[k] = Integer.parseInt(
               tokenizer.nextToken());
          return i;
        }
      }
    }
    

    Understanding the Code

    Class BeanContainer

    This class (formerly BeanContainer from section 4.7) has received a new collection, Hashtable m_editors, added as an instance variable. This Hashtable holds references to BeanEditor frames (used to edit beans, see below) as values and the corresponding Components being edited as keys.

    A new menu item titled Properties... is added to the Edit menu. This item is used to create a new editor for the selected bean or activate an existing one (if any). The attached ActionListener looks for an existing BeanEditor corresponding to the currently selected m_activeBean component in the m_editors collection. If such an editor is found it is made visible and brought to the front. Otherwise, a new instance of BeanEditor is created to edit the currently active m_activeBean component, and is added to the m_editors collection.

    The ActionListener attached to menu item Delete, which removes the currently active component, receives additional functionality. The added code looks for an existing BeanEditor corresponding to the currently selected m_activeBean component in the m_editors collection. If such an editor is found it is disposed and its' reference is removed from the hashtable.

    Class BeanEditor

    This class extends JFrame and implements the PropertyChangeListener interface. BeanEditor is used to display and edit the properties exposed by a given JavaBean. Three instance variables are declared:

  • Component m_bean: JavaBean component to be edited.
  • JTable m_table: table component to display a bean's properties.
  • PropertyTableData m_data: table model for m_table.
  • The BeanEditor constructor takes a reference to the JavaBean component to be edited, and stores it in instance variable m_bean. The initial location of the editor frame is selected depending on the location of the component being edited.

    The table component, m_table, is created and added to a JScrollPane to provide scrolling capabilities. Note that we do not add a WindowListener to this frame. Instead we use the HIDE_ON_CLOSE default close operation (see chapter 3):

    setDefaultCloseOperation(HIDE_ON_CLOSE);

    setVisible(true);

    Upon closing, this frame will be hidden but not disposed. Its' reference will still be present in the m_editors collection, and this frame will be re-activated if the user chooses to see the properties of the associated bean again.

    Note that an instance of the BeanEditor class is added as a PropertyChangeListener to the corresponding bean being edited. The propertyChange() method is invoked if the bean has changed it's state during editing and a PropertyChangeEvent has been fired. This method simply triggers a call to the setProperty() method of the table model.

    Class BeanEditor.PropertyTableData

    PropertyTableData extends AbstractTableModel and provides the table model for each bean editor. Three instance variables are declared:

  • String[][] m_properties: an array of data displayed in the table.
  • int m_numProps: number of a bean properties (corresponds to the number of rows in the table).
  • Vector m_v: collection of PropertyDescriptor objects sorted in alphabetical order.
  • The constructor of the PropertyTableData class takes a given bean instance and retrieves it's properties. First it uses the Introspector.getBeanInfo() method to get a BeanInfo instance:

    BeanInfo info = Introspector.getBeanInfo(
            m_bean.getClass());
          BeanDescriptor descr = info.getBeanDescriptor();
          setTitle("Editing "+descr.getName());
          PropertyDescriptor[] props = 
              info.getPropertyDescriptors();
          m_numProps = props.length;
    

    This provides us with all available information about a bean (see chapter 2). We determine the bean's name and use it as the editor frame's title (note that this is an inner class, so setTitle() refers to the parent BeanEditor instance). We then extract an array of PropertyDescriptors which will provide us with the actual information about a bean's properties.

    Bean properties are sorted by name in alphabetical order. The name of each property is determined with the getDisplayName() method. The sorted PropertyDescriptors are stored in our m_v Vector collection. Then we can create the 2-dimensional array, m_properties, which holds data to be displayed in the table. This array has m_numProps rows and 2 columns (for property name and value). To determine a property's value we need to obtain a reference to its getXX() method with getReadMethod() and make a call using the reflection API. We can call only getXX() methods without parameters (since we don't know anything about these parameters). Note that our objToString() helper method is invoked to translate a property's value into a display string (see below).

    The setProperty() method searches for the given name in the 0-th column of the m_properties array. If such a property is found this method sets it's new value and updates the table component.

    Several other simple methods included in this class have already been presented in previous examples and need not be explained again here. However, note that the isCellEditable() method returns true only for cells in the second column (property names, obviously, cannot be changed).

    The setValueAt() method deserves additional explanation because it not only saves the modified data in the table model, but it also sends these modifications to the bean component itself. To do this we obtain a PropertyDescriptor instance stored in the m_v Vector collection. The modified property value is always a String, so first we need to convert it into its proper object type using our stringToObj() helper method (if we can do this, see below). If the conversion succeeds (i.e. the result is not null), we can continue.

    To modify a bean value we determine the reference to it's setXX()method (corresponding to a certain property) and invoke it. Note that an anonymous array containing one element is used as parameter (these constructions are typical when dealing with the reflection API). Then the bean component and it's container (which can also be affected by changes in such properties as size and color) are refreshed to reflect the bean's new property value. Finally, if the above procedures were successful, we store the new value in the m_properties data array.

    The objToString() helper method converts a given Object into a String suitable for editing. In many cases the toString() method returns a long string starting with the class name. This is not very appropriate for editable data values. So for several classes we provide our own conversion into a string of comma-delimited numbers. For instance a Dimension object is converted into a width, height form, Color is converted into red, green, blue form, etc. If no special implementation is provided, an object's toString() string is returned.

    The stringToObj() helper method converts a given String into an Object of the given Class. The class's name is analyzed and a conversion method is chosen to build the correct type of object based on this name. The simplest case is the String class: we don't need to do any conversion at all in this case. For the primitive data types such as int or boolean we return the corresponding encapsulating (wrapper class) objects. For the several classes which receive special treatment in the objToString() method (such as a Dimension or Color object), we parse the comma-delimited string of numbers and construct the proper object. For all other classes (or if a parsing exception occurs) we return null to indicate that we cannot perform the required conversion.

    Running the Code

    Figure 18.9 shows the BeanContainer container and two editing frames displaying the properties of Clock and JButton components. This application provides a simple but powerful tool for investigating Swing and AWT components as well as custom JavaBeans. We can see all exposed properties and modify many of them. If a component's properties change as a result of user interaction, our component properly notifies its' listeners and we see an automatic editor table update. Try serializing a modified component and restoring it from its' file. Note how the previously modified properties are saved as expected.

    It is natural to imagine using this example as a base for constructing a custom Swing IDE (Interface Development Environment). BeanContainer, combined with the custom resize edge components developed in chapters 15 and 16, provides a fairly powerful base to work from.

    Page 1 2 3 4 5 6 7 8 9 10 11

    << INTRO >>
    Download Chapter 18| Download Chapter 22]


    Reader Feedback
    Excellent   Good   Fair   Poor  

    If you have other comments or ideas for future technical content, please type them here:

    Comments:

    If you would like a reply to your comment, please submit your email address:
    Note: We may not respond to all submitted comments.


    Have a question about Java programming? Use Java Online Support.