|
Bookshelf Index
Chapter 18, Tables
By Matthew Robinson and Pavel Vorobiev
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]
|