Online Training Index
101, Part II:
Writing a Simple JavaBean
by Beth Stearns
November 2000
Introduction | Page 2 | Page 3 | Page 4
Manipulating Properties
Automatic Sizing of Beans
So far, you've learned how to supply a default label for a Bean in its constructor. A better design
lets you override this default label by defining a second constructor that accepts a String argument. (The Acme05Bean contains the source code for this lesson.)
For example:
public Acme05Bean(String label) {
super();
this.label = label;
setFont(new Font("Dialog",
Font.PLAIN, 12));
}
|
Displaying a customizable label on a Bean poses new challenges. Keep in mind that the label can be:
- Set programmatically by the constructor.
- Changed at design time by a builder tool.
- Modified at run time by a call to
setLabel.
It is important to build in flexibility to the sizing of the Bean. What happens, for example, if the
label is too long? Obviously the Bean's bounding box must be adjusted to accommodate the label
supplied to the constructor. A call to the resize command, with its parameters hard coded in the constructor, makes no sense once you make it possible to customize the bean's label.
public Acme04Bean() {
// obsolete constructor
resize(60,40);/
// not flexible for customizable label
this.label="Bean";
...
}
|
The JavaBeans API provides a convenient way for you to specify the preferred size of a Bean on the fly. Define methods specifying the preferred size and the minimum size of your Bean.
By using the technique for automatic sizing, you can omit the call to resize from the constructor. Because you have defined a constructor accepting a String argument, the role of the default constructor can be reduced to supplying a default label for the button:
public Acme05Bean() {
this("AcmeBean Serial# 05");
}
Use a long default label intentionally to show how a button can size itself automatically. There are two occasions when a button needs to adjust itself to accommodate the size of its label:
- When it is first created, such as when the button is selected from the palette of a builder tool and dropped onto a form.
- When the button label is modified during design time by editing the label within the property sheet.
Defining Sizes for Beans
You can define both preferred and minimum sizes for Beans. You can define a method named getPreferredSize and BeanBox calls it automatically when you drag a new instance of the Bean from the component palette and drop it on the application form.
public Dimension getPreferredSize() {
FontMetrics fm =
getFontMetrics(getFont());
return new Dimension(fm.stringWidth(label)
+ TEXT_XPAD,
fm.getMaxAscent() + fm.getMaxDescent()
+ TEXT_YPAD);
}
|
This algorithm is similar to the size calculation carried out in the Bean's paintmethod to center the string within the button. Padding is added outside of the string so the label doesn't appear crowded. In this case, however, the method returns a dimension for the preferred size rather than drawing the string inside of the button. Symbolic constants for the padding are defined as instance variables for the bean to be used as padding around the button, as follows:
static final int TEXT_XPAD = 12;
static final int TEXT_YPAD = 8;
Specifiying a Minimum Size
In addition to a preferred size for your Bean, the JavaBeans API provides a getMinimumSize method that lets you specify a minimum size for a Bean. Both methods help builder tools determine how to draw your Bean at design time. To keep things simple, define the minimum size of the Bean to be the same as the preferred size:
public Dimension getMinimumSize() {
return getPreferredSize();
}
Changing the Size at Design Time
Now, you must handle the situation where the Bean size changes at design time due to someone editing
the label from within the property sheet. Up to now, you could change the label at design time, but
the button would not resize itself to accommodate a label longer than a certain length. For typical
buttons, you want the button to change its size each time the label changes from the property editor--that is, with each key stroke the button should grow or shrink according the new size of the label. The workhorse behind this resizing is a new method, sizeToFit.
private void sizeToFit() {
Dimension d = getPreferredSize();
resize(d.width, d.height);
Component p = getParent();
if (p != null) {
p.invalidate();
p.layout();
}
}
|
Notice that sizeToFit calls the getPreferredSize method whenever the size of the Bean must be recalculated. (The BeanBox also calls getPreferredSize when sizing the Bean.) Notice that the call to resize is also made from this method, instead of in the constructor as we did previously. You can give the sizeToFit sizing method any name you want, but you must call it from setLabel. The builder tool (BeanBox, for example) calls the setLabel method automatically with each keystroke modification made to the label property from the property sheet editor. Defining sizeToFit to be a workhorse method simplifies the modifications that must be made to setLabel; you need only add a single line to the end of the method:
public void setLabel(String newLabel) {
String oldLabel = label;
label = newLabel;
sizeToFit();// new line
}
|
Putting in a 3D Effect
The final change in this version of AcmeBean draws it with a 3D effect, making it look more like a
standard AWT button than a Bean. Here's how to accomplish this 3D effect.
- Remove the calls to
fillArc and fillRec.
// obsolete version
public void paint(Graphics g) {
...
g.fillRect(20, 5, 20, 30);
g.fillArc(5, 5, 30, 30, 0, 360);
g.fillArc(25, 5, 30, 30, 0, 360);
...
}
|
- Replace them with a rectangular rendering of the button, based on the width and height of the Canvas
on which it is drawn:
g.fillRect(1, 1, width - 2, height - 2);
g.draw3DRect(0, 0, width - 1,
height - 1, true);
Notice that the last argument to draw3DRect is a boolean that determines whether the rectangle should appear raised or depressed. Later, you'll modify this call to draw3DRect so that the visual display of the button reflects the up or down state of the left mouse button as the mouse button is pressed and released over the Bean.
- Draw a line border just around the outside edge of the button:
g.drawRect(2, 2, width - 4, height - 4);
To keep things tidy, synchronize the
paint
method. Here's the entire new definition:
public synchronized void paint(Graphics g) {
int width = size().width;
int height = size().height;
g.setColor(beanColor);
g.fillRect(1, 1, width - 2, height - 2);
g.draw3DRect(0, 0, width - 1, height - 1,
true);
g.setColor(getForeground());
g.setFont(getFont());
g.drawRect(2, 2, width - 4, height - 4);
FontMetrics fm = g.getFontMetrics();
g.drawString(label, (width -
fm.stringWidth(label)) / 2,
(height + fm.getMaxAscent() -
fm.getMaxDescent())
/ 2);
}
|
Handling Events
A Bean that wants to generate events needs to keep track of interested event targets. In the
delegation event model, the event mechanism is broken up conceptually into event dispatch and event
handling. Event dispatch is the responsibility of the event source; event handling is the
responsibility of the event listener. Any object that wants to know when an event is fired by a Bean
can tell the Bean it wants to be informed about particular events. In other words, an event listener
registers interest in an event by calling a predetermined method in the event source.
Registering Event Listeners
Consider a typical button Bean that generates events when pressed. An interested listener Bean might
increment a counter object each time the button is pressed.
If the button Bean wants to be an event source, it must provide two methods that can be called by
interested objects. One method adds the caller to the list of listeners who are notified when the
event occurs. The other method removes the caller from the list of interested listeners.
public synchronized void addActionListener
(ActionListener l) {...}
public synchronized void removeActionListener
(ActionListener l) {...}
Naming Event Listeners
Similar to properties, the signature of the method names must follow specific patterns. The Java
introspection mechanism detects the pattern of the method's signature and can determine the events the
source Bean generates from the name of the registration methods, together with the type of the
arguments of the registration methods.
Java's introspection mechanism recognizes the following general pattern for event generation capabilities:
public synchronized
void addTYPE(TYPE listener);
public synchronized
void removeTYPE(TYPE listener);
Note that TYPE is replaced by the class name of the particular event listener; for example, MouseListener or MouseMotionListener.
Following the ActionEvent
When the above event registration methods are defined for our button Bean, Java's introspection mechanism is able to determine that an ActionEvent can be generated by the button. If the counter object wants to be notified when an ActionEvent occurs, it calls the button's addActionListener method, giving itself as an argument. For this to work, the counter object has to first implement the ActionListener interface, because the argument to addActionListener is an ActionListener object.
The button Bean needs to track the listeners who register to receive notification of ActionEvents. This is where the Vector import statement comes into play. The button Bean maintains a list (or Vector) of listeners. Thus, the source Bean declares the following line:
private Vector listeners = new Vector();
When the Bean's addActionListener is called, the listener supplied as an argument to the call is appended to the Vector of listeners, as follows:
public synchronized void addActionListener
(ActionListener l) {
listeners.addElement(l);
}
Similarly, when removeActionListener is called, the listener supplied as an argument to the method is removed from the list of listeners:
public synchronized void removeActionListener
(ActionListener l) {
listeners.removeElement(l);
}
Dispatching Events to Event Listeners
When an event is fired, the event source (the button Bean) iterates over the list of listeners and sends each listener a notification of the ActionEvent. Its fireAction method looks as follows:
public void fireAction() {
...
Vector targets;
synchronized (this) {
targets = (Vector) listeners.clone();
}
ActionEvent actionEvt =
new ActionEvent(this, 0, null);
for (int i = 0; i < targets.size(); i++) {
ActionListener target =
(ActionListener)targets.elementAt(i);
target.actionPerformed(actionEvt);
}
...
}
|
Making a debug Property
Notice that we also define two required instance variables:
private boolean debug = true;
private Vector listeners = new Vector();
The new fireAction method uses both instance variables. The debug instance variable controls the printing of stub information when handleEvent is called. By making debug a property, you can change its value inside a builder tool. This is handy in BeanBox--you can turn debugging on and off for each individual button to get just the amount of feedback you need for diagnosing a particular problem.
To make debug a property, define setDebug and getDebug methods.
public void setDebug(boolean x) {
boolean old = debug;
debug = x;
}
public boolean getDebug() {
return debug;
}
|
By making debug a property, you can dynamically alter event reporting through a println stub. You do this by merely changing the value of the debug property from false to true in a property sheet editor. When debug is true calls to fireAction are reported to the users by printing the label of the button that fired the event.
public void fireAction() {
if (debug) {
System.err.println("Button "
+ getLabel() + " pressed.");}
Vector targets;
synchronized (this) {
targets = (Vector) listeners.clone();
}
ActionEvent actionEvt =
new ActionEvent(this, 0, null);
for (int i = 0; i < targets.size(); i++) {
ActionListener target =
(ActionListener)targets.elementAt(i);
target.actionPerformed(actionEvt);
}
Component parent = getParent();
if (parent != null) {
parent.postEvent(new Event(
this, Event.MOUSE_DOWN, null));
}
}
|
The newly defined Bean can now act as an event source for ActionEvent objects. You can verify this by adding the Bean to the BeanBox component palette, then hooking up the Bean's ActionEvent to start or stop the Duke Juggler bean. Notice that the event menu lists an Action item for this Bean, so it can be used like the OurButton Bean provided as a BeanBox example.
Looking at Acme06Bean
Notice that the Acme06Bean version of the button does not provide
methods to register ActionEvent listeners and therefore cannot fire action events. If Java introspection finds no action events defined for a Bean, no action events appear in the Events menu. When comparing property sheets, notice also that when you select an Acme07Bean, you can edit the debug property by using a boolean choice selector.
Improvements in Acme07Bean
The Acme07Bean made a substantial number of additions.
It's helpful to look at the entire source listing for Acme07Bean.
Controlling Bean Behavior with Events
With two simple changes, you can make the button bounce--that is, behave visually like a regular
AWT button. When the button is pressed, it appears to lower itself; when it is released, it appears to
raise itself. The Acme08Bean contains the code for making a Bean bounce
like a button.
- Add an instance variable,
down, to keep track of whether the button is in a pressed or released state:
private boolean down;
- Inside the button's
paint method, change the rendering to depend on the value of down. Change:
g.draw3DRect(0, 0, width - 1, height - 1,
true);
to:
g.draw3DRect(0, 0, width - 1,
height - 1, !down);
Without this modification, the button was always drawn as raised (because the last argument was always
true). With this modification, the button is drawn as raised only when down is false.
To make this all work properly, you must write a small amount of maintenance code to be sure
down always has the right value. Do this is in handleEvent.
-
Add a new
case clause to handle mouse down events.
public boolean handleEvent(Event evt) {
...
switch (evt.id) {
case Event.MOUSE_DOWN:
down = true;
repaint();
return true;
...
}
|
When the mouse is pressed, this code sets the remembered button state to down (by setting
down to true), requests that the button be redrawn, and returns true to indicate the event has been handled.
- Change the
case clause for the mouse up event. Change it from:
case Event.MOUSE_UP:
fireAction();
return true;
to
case Event.MOUSE_UP:
if (down) {
fireAction();
down = false;
repaint();
}
return true;
|
Before these changes to handleEvent, the button was not redrawn on a mouse event. With these changes, it is always redrawn whether the event is a press or a release so that the button position correctly reflects the mouse action. Note that fireAction is called, as in the previous examples, to dispatch the ActionEvent to registered listeners.
Introduction | Page 2 | Page 3 | Page 4
|
|