Now that you are familiar with the software component assembly model and the JavaBeans development kit, it's time to learn how to develop your own beans. In this chapter, you'll learn how beans work and take a tour of the java.beans packages. You'll also learn how to write bean code in Java. When you finish this chapter, you'll be able to create a few beans of your own.
Chapter 24, "The Software Component Assembly Model," presented the underlying concepts related to component-based software development. Chapter 25, "The JavaBeans Development Kit," showed how these concepts were embodied in an actual visual development tool. The point of the BeanBox tutorial in Chapter 25 was to give you some hands-on experience of how beans are used in software development. This section covers some of the underlying mechanisms that enable beans to be used in this manner. You may want to have your BeanBox up and running when you read through this section so that you can see how these mechanisms are implemented by it.
One of the first things that you probably noticed when you started up the BeanBox was the ToolBox full of JavaBeans. Several of these beans had icons. JavaBeans have the capability to support a variety of icons for display by visual development tools.
Beans themselves can also be graphically displayed by visual development tools. When you placed the Juggler and OurButton beans in the BeanBox, they were displayed exactly how they appear in a final application. You can move them to their intended position and resize them to the desired dimensions.
Some beans are invisible in the sense that they do not have a graphical display. An example of an invisible bean could be a specialized algorithm, such as an image filter. Visual development tools usually create special graphical objects that allow the invisible beans to be manipulated in the same manner as visible beans during software development. Of course, the special graphical objects of the invisible beans are not displayed by the final application or applet.
Properties are attributes of a bean that can be modified to change the appearance or behavior of a bean. They are accessed through special methods, referred to as accessor methods. Visual development tools allow properties to be changed through the use of property sheets, lists of properties that can be specified for a bean. Visual building tools, like the BeanBox, display a property sheet in response to a bean's selection. You used property sheets to change the animationRate property of the Juggler bean and the label property of the OurButton bean.
In addition to the simple property editing capabilities exhibited by the BeanBox example, individual beans can define custom property editors that allow properties to be edited using specialized dialog boxes. These custom property editors are implemented as special classes that are associated with the bean's class. The custom property editors are available to visual development tools, but because they are not part of the bean's class, they do not need to be compiled into applications or applets. This lets you provide extra design capabilities for a bean without having to develop bloated applications.
Suppose that you are using a bean that provides extensive customization support. You change the bean's background color to red and its foreground color to white, change a label associated with the bean, and alter a few other properties. You may wonder what happens to the property changes. How are the changes packaged along with the bean's class?
Beans store any property changes so that new property values come into effect and are displayed when the modified bean is used in an application. The capability to permanently store property changes is known as persistence. JavaBeans implement persistence by serializing bean objects that are instances of a bean class. Serialization is the process of writing the current state of an object to a stream. Because beans are serialized, they must implement the java.io.Serializable or java.io.Externalizable interfaces. Beans that implement java.io.Serializable are automatically saved. Beans that implement java.io.Externalizable are responsible for saving themselves.
NOTE: Chapter 40, "Using Object Serialization and JavaSpaces," covers object serialization in more detail.
When a bean object is saved through serialization, all of the values of the variables of the object are saved. In this way, any property changes are carried along with the object. The only exceptions to this are variables that are identified as transient. The values of transient variables are not serialized.
Beans support a few different types of properties. In the BeanBox tutorial, you saw examples of simple properties. The animationRate property of the Juggler bean used a simple numeric value, and the label property of the OurButton bean used a text value.
An indexed property is a property that can take on an array of values. Indexed properties are used to keep track of a group of related values of the same type. For example, an indexed property could be used to maintain the values of a scrollable list.
A bound property is a property that alerts other objects when its value changes. For example, you could use a bound property to implement a temperature control dial. Whenever the user changes the control, notification of the change is propagated to objects that regulate temperature.
A constrained property differs from a bound property in that it notifies other objects of an impending change. Constrained properties give the notified objects the power to veto a property change. You could use a constrained property to implement a bean that fires a missile under two-person control. When one person initiates a missile launch, a notification is sent to a second user, who could either confirm or deny the launch.
All properties are accessed through accessor methods. There are two types of accessor methods: getter methods and setter methods. Getter methods retrieve the values of properties, and setter methods set property values. The names of getter methods begin with get and are followed by the name of the property to which they apply. The names of setter methods begin with set and are followed by the property name.
Methods Used with Simple Properties
If a bean has a property named fooz of type foozType that can be read and written, it should have the following accessor methods:
public foozType getFooz() public void setFooz(foozType foozValue)
A property is read-only or write-only if one of the preceding accessor methods are missing.
NOTE: If a property is boolean, getter methods are written using is instead of get. For example, isFooz() would be used instead of getFooz() if fooz is a boolean property.
Methods Used with Indexed Properties
A bean that has an indexed property will have methods that support the reading and writing of individual array elements as well as the entire array. For example, if a bean has an indexed widget property in which each element of the array is of type widgetType, it will have the following accessor methods:
public widgetType getWidget(int index) public widgetType[] getWidget() public void setWidget(int index, widgetType widgetValue) public void setWidget(widgetType[] widgetValues)
Methods Used with Bound Properties
Beans with bound properties have getter and setter methods, as previously identified, depending upon whether the property values are simple or indexed. Bound properties require certain objects to be notified when they change. The change notification is accomplished through the generation of a PropertyChangeEvent. Objects that want to be notified of a property change to a bound property must register as listeners. Accordingly, the bean that's implementing the bound property supplies methods of the form:
public void addPropertyChangeListener(PropertyChangeListener l) public void removePropertyChangeListener(PropertyChangeListener l)
NOTE: The PropertyChangeEvent class and PropertyChangeListener interface are defined in java.beans.
The preceding listener registration methods do not identify specific bound properties. To register listeners for the PropertyChangeEvent of a specific property, the following methods must be provided:
public void addPropertyNameListener(PropertyChangeListener l)
public void removePropertyNameListener(PropertyChangeListener l)
In the preceding methods, PropertyName is replaced by the name of the bound property.
Objects that implement the PropertyChangeListener interface must implement the propertyChange() method. This method is invoked by the bean for all of its registered listeners to inform them of a property change.
Methods Used with Constrained Properties
The previously discussed methods used with simple and indexed properties also apply to constrained properties. In addition, the following event registration methods are provided:
public void addVetoableChangeListener(VetoableChangeListener l) public void removeVetoableChangeListener(VetoableChangeListener l) public void addPropertyNameListener(VetoableChangeListener l) public void removePropertyNameListener(VetoableChangeListener l)
Objects that implement the VetoableChangeListener interface must implement the vetoableChange() method. This method is invoked by the bean for all of its registered listeners to inform them of a property change. Any object that does not approve of a property change can throw a PropertyVetoException within its vetoableChange() method to inform the bean whose constrained property was changed that the change was not approved.
In order for beans to be used by visual development tools, the beans must be able to dynamically inform the tools of their interface methods and properties and also what kind of events they may generate or respond to. This capability is referred to as introspection. The Introspector class of java.beans provides a set of static methods for tools to obtain information about the properties, methods, and events of a bean.
The Introspector supports introspection in the following ways:
Beans, being primarily GUI components, generate and respond to events. Visual development tools provide the capability to link events generated by one bean with event- handling methods implemented by other beans. For example, a button component may generate an event as the result of the user clicking on that button. A visual development tool would enable you to connect the handling of this event to the interface methods of other beans. The bean generating the event is referred to as the event source. The bean listening for (and handling) the event is referred to as the event listener.
Now that you have a feel for what beans are, how they are used, and some of the mechanisms they employ, let's take a look at the classes and interfaces of the java.beans packages. These classes and interfaces are organized into the categories of design support, introspection support, and change event-handling support.
The classes in this category help visual development tools to use beans in a design environment.
The Beans class provides seven static methods that are used by application builders:
The Visibility interface is implemented by classes that support the capability to answer questions about the availability of a GUI for a bean. It provides the avoidingGui(), dontUseGui(), needsGui(), and okToUseGui() methods. The VisibilityState interface provides the isOkToUseGui()method.
The methods of the PropertyEditor interface are implemented by classes that support custom property editing. These methods support a range of property editors, from simple to complex. The setValue() method is used to identify the object that is to be edited. The getValue() method returns the edited value. The isPaintable() and paintValue() methods support the painting of property values on a Graphics object. The getJavaInitializationString() method returns a string of Java code that is used to initialize a property value. The setAsText() and getAsText() methods are used to set and retrieve a property value as a String object. The getTags() method returns an array of String objects that are acceptable values for a property. The supportsCustomEditor() method returns a boolean value indicating whether a custom editor is provided by a PropertyEditor. The getCustomEditor() method returns an object that is of a subclass of Component and is used as a custom editor for a bean's property. The addPropertyChangeListener() and removePropertyChangeListener() methods are used to register event handlers for the PropertyChangeEvent associated with a property.
The PropertyEditorManager class provides static methods that help application builders find property editors for specific properties. The registerEditor() method is used to register an editor class for a particular property class. The getEditorSearchPath() and setEditorSearchPath() methods support package name lists for finding property editors. The findEditor() method finds a property editor for a specified class. Unregistered property editors are identified by the name of the property followed by Editor.
The PropertyEditorSupport class is a utility class that implements the PropertyEditor interface. It is subclassed to simplify the development of property editors.
The methods of the Customizer interface are implemented by classes that provide a graphical interface for customizing a bean. These classes are required to be subclasses of java.awt.Component so that they can be displayed in a panel. The addPropertyChangeListener() method is used to enable an object that implements the PropertyChangeListener interface as an event handler for the PropertyChangeEvent of the object being customized. The removePropertyChangeListener() method is used to remove a PropertyChangeListener. The setObject() method is used to identify the object that is to be customized.
The classes and interfaces in this category provide information to application builders about the interface methods, properties, and events of a bean.
The Introspector class provides static methods that are used by application builders to obtain information about a bean's class. The Introspector gathers this information using information explicitly provided by the bean designer whenever possible and uses reflection and design patterns when explicit information is not available. The getBeanInfo() method returns information about a class as a BeanInfo object. The getBeanInfoSearchPath() method returns a String array to be used as a search path for finding BeanInfo classes. The setBeanInfoSearchPath() method updates the list of package names used to find BeanInfo classes. The decapitalize() method is used to convert a String object to a standard variable name in terms of capitalization.
The methods of the BeanInfo interface are implemented by classes that want to provide additional information about a bean. The getBeanDescriptor() method returns a BeanDescriptor object that provides information about a bean. The getIcon() method returns an Image object that is used as an icon to represent a bean. It uses the icon constants defined in BeanInfo to determine which type of icon should be returned. The getEventSetDescriptors() method returns an array of EventSetDescriptor objects that describe the events generated (fired) by a bean. The getDefaultEventIndex() method returns the index of the most commonly used event of a bean. The getPropertyDescriptors() method returns an array of PropertyDescriptor objects that support the editing of a bean's properties. The getDefaultPropertyIndex() method returns the most commonly updated property of a bean. The getMethodDescriptors() method returns an array of MethodDescriptor objects that describe a bean's externally accessible methods. The getAdditionalBeanInfo() method returns an array of objects that implement the BeanInfo interface.
The SimpleBeanInfo class provides a default implementation of the BeanInfo interface. It is subclassed to implement BeanInfo classes.
The FeatureDescriptor Class and Its Subclasses
The FeatureDescriptor class is the top-level class of a class hierarchy that is used by BeanInfo objects to report information to application builders. It provides methods that are used by its subclasses for information gathering and reporting.
The BeanDescriptor class provides global information about a bean, such as the bean's class and its Customizer class, if any. The EventSetDescriptor class provides information on the events generated by a bean. The PropertyDescriptor class provides information on a property's accessor methods and property editor. It is extended by the IndexedPropertyDescriptor class, which provides access to the type of the array implemented as an indexed property and information about the property's accessor methods.
The MethodDescriptor and ParameterDescriptor classes provide information about a bean's methods and parameters.
The PropertyChangeEvent is generated by beans that implement bound and constrained properties as the result of a change in the values of these properties. The PropertyChangeListener interface is implemented by those classes that listen for the PropertyChangeEvent. It consists of a single method, propertyChange(), that is used to handle the event.
The VetoableChangeListener interface is implemented by classes that handle the PropertyChangeEvent and throw a VetoableChangeEvent in response to certain property changes. The vetoableChange() method is used to handle the PropertyChangeEvent.
The PropertyChangeSupport class is a utility class that can be subclassed by beans that implement bound properties. It provides a default implementation of the addPropertyChangeListener(), removePropertyChangeListener(), and firePropertyChange() methods.
The VetoableChangeSupport class, like the PropertyChangeSupport class, is a utility class that can be subclassed by beans that implement constrained properties. It provides a default implementation of the addVetoableChangeListener(), removeVetoableChangeListener(), and fireVetoableChange() methods.
The Aggregate interface has been added in JDK 1.2 as a means of aggregating several objects into a single bean. It is extended by the Delegate interface, which provides methods for accessing Aggregate objects. The AggregateObject class is an abstract class that implements the Delegate interface and provides a foundation for creating other aggregate classes. Note that aggregation has nothing to do with inheritance. It is just a way of combining multiple objects into a single bean.
JDK 1.2 introduces the java.beans.beancontext package, which provides classes and interfaces for enabling beans to access their execution environment, referred to as their bean context. The BeanContextChild interface provides methods for getting and setting this context and for managing context-related event listeners. BeanContextChild is extended by the BeanContext interface, which provides methods by which beans can access resources and services that are available within their context. Objects that implement BeanContext function as containers for other beans. The BeanContextMemberShipListener interface provides an event listener interface for events that occur as the result of changes to the beans that are members of a bean context. The DesignMode interface of java.beans provides the capability for a BeanContext object to determine whether it is being executed in a design or execution mode.
In addition to the interfaces described in the previous paragraph, the java.beans.beancontext package provides the following six classes:
The easiest way to implement a bean context is to extend the BeanContextSupport class. BeanContextSupport provides numerous methods for managing beans that are contained within a particular context.
In this section, you'll learn how to create your own beans and use them in an applet. First, you'll create a simple gauge that can be used as a widget for applets and applications. Next, you'll create a bean that can be used to display text without the use of a TextArea or TextField object. After that, you'll learn how to use these beans in an applet that displays multiple-choice quiz questions.
When you studied all of the classes and interfaces of java.beans in previous sections, you might have been left with the impression that beans are complicated and hard to develop. In fact, the opposite is true. You can easily convert existing classes to beans with minimal programming overhead.
Listing 26.1 contains the code for a bean that displays a simple gauge. The gauge is displayed as a 3D-style box that is filled somewhere between its minimum and maximum values. The color of the gauge's border and its fill color are both configurable. So are its dimensions and horizontal/vertical orientation.
import java.io.Serializable; import java.beans.*; import java.awt.*; import java.awt.event.*; public class Gauge extends Canvas implements Serializable { // Set constants and default values public static final int HORIZONTAL = 1; public static final int VERTICAL = 2; public static final int WIDTH = 100; public static final int HEIGHT = 20; public int orientation = HORIZONTAL; public int width = WIDTH; public int height = HEIGHT; public double minValue = 0.0; public double maxValue = 1.0; public double currentValue = 0.0; public Color gaugeColor = Color.lightGray; public Color valueColor = Color.blue; public Gauge() { super(); } public Dimension getPreferredSize() { return new Dimension(width,height); } // Draw bean public synchronized void paint(Graphics g) { g.setColor(gaugeColor); g.fill3DRect(0,0,width-1,height-1,false); int border=3; int innerHeight=height-2*border; int innerWidth=width-2*border; double scale=(double)(currentValue-minValue)/ (double)(maxValue-minValue); int gaugeValue; g.setColor(valueColor); if(orientation==HORIZONTAL){ gaugeValue=(int)((double)innerWidth*scale); g.fillRect(border,border,gaugeValue,innerHeight); }else{
gaugeValue=(int)((double)innerHeight*scale);
g.fillRect(border,border+(innerHeight-ÂgaugeValue),innerWidth,gaugeValue); } } // Methods for accessing bean properties public double getCurrentValue(){ return currentValue; } public void setCurrentValue(double newCurrentValue){ if(newCurrentValue>=minValue && newCurrentValue<=maxValue) currentValue=newCurrentValue; } public double getMinValue(){ return minValue; } public void setMinValue(double newMinValue){ if(newMinValue<=currentValue) minValue=newMinValue; } public double getMaxValue(){ return maxValue; } public void setMaxValue(double newMaxValue){ if(newMaxValue >= currentValue) maxValue=newMaxValue; } public int getWidth(){ return width; } public void setWidth(int newWidth){ if(newWidth > 0){ width=newWidth; updateSize(); } } public int getHeight(){ return height; } public void setHeight(int newHeight){ if(newHeight > 0){ height=newHeight; updateSize(); } } public Color getGaugeColor(){ return gaugeColor; } public void setGaugeColor(Color newGaugeColor){ gaugeColor=newGaugeColor; } public Color getValueColor(){
return valueColor; } public void setValueColor(Color newValueColor){ valueColor=newValueColor; } public boolean isHorizontal(){ if(orientation==HORIZONTAL) return true; else return false; } public void setHorizontal(boolean newOrientation){ if(newOrientation){ if(orientation==VERTICAL) switchDimensions(); }else{ if(orientation==HORIZONTAL) switchDimensions(); orientation=VERTICAL; } updateSize(); } void switchDimensions(){ int temp=width; width=height; height=temp; } void updateSize(){ setSize(width,height); Container container=getParent(); if(container!=null){ container.invalidate(); container.doLayout(); } } }
To see how the bean works, copy the Gauge.jar file from your ch26 directory to the \bdk\jars directory and then start up the BeanBox using the following commands:
copy Gauge.jar \bdk\jars
cd \bdk\beanbox
run
The BeanBox opens and displays the ToolBox, BeanBox, and PropertySheet windows. You will notice a new bean at the bottom of the ToolBox. This is the Gauge bean from Listing 26.1. Note that it comes with its own icon, as shown in Figure 26.1.
FIGURE 26.1. The Gauge bean now has an icon in the ToolBox.
Click the Gauge bean's ToolBox icon and then click in the BeanBox. The bean is displayed as a horizontal 3D box, as shown in Figure 26.2.
FIGURE 26.2. Adding the Gauge bean to the BeanBox.
The bean's property sheet displays a number of properties that may be changed to alter the bean's appearance, as shown in Figure 26.3. The foreground, background, and font properties are the default properties of visible beans. These properties reflect the getForeground(), setForeground(), getBackground(), setBackground(), getFont(), and setFont() methods of the Component class.
The rest of the properties shown in the style sheet were defined in Listing 26.1. The minValue and maxValue properties identify the minimum and maximum values associated with the gauge. The currentValue property identifies the current value of the gauge.
Figure 26.3. The Gauge bean's PropertySheet.
The width and height properties control the gauge's dimensions. The horizontal property takes on boolean values. When set to True, the gauge is displayed horizontally. When set to False, the gauge is displayed vertically. When the orientation of a gauge is switched, so are its width and height.
The gaugeColor and valueColor properties identify the color of the gauge's border and the color to be displayed to identify the gauge's current value.
To see how the gauge's properties work, make the following changes:
Figure 26.4 shows the results of these changes on the bean's display.
FIGURE 26.4. Changing the Gauge bean's properties.
The first thing that you'll notice about the Gauge bean's source code is that it imports java.io.Serializable. All bean classes implement Serializable or Externalizable. These interfaces support bean persistence, allowing beans to be read from and written to permanent storage (for example, hard disk). When a bean implements Serializable, serialization of the bean's data is performed automatically by Java, meaning you don't have to figure out how to write objects to streams or read them back in. When a bean implements Externalizable, the bean is responsible for performing all the serialization overhead. Serializable is obviously the easiest to implement of the two interfaces. All you have to do is add Serializable to your class's implements clause and poof!--serialization is automatically supported. Chapter 40, "Using Object Serialization and JavaSpaces," covers object serialization.
Besides serialization, you won't notice anything bean-specific about the rest of the Gauge class. In fact, it looks just like any other custom AWT class. Gauge extends Canvas so that it can draw to a Graphics object. It defines a few constants for use in initializing its field variables. You should note that these field variables correspond to the Gauge bean's properties.
The getPreferredSize() method is an important method for visible beans to implement. It tells application builder tools how much room is needed to display a bean. All of your visible beans should implement getPreferredSize().
The paint() method draws the bean on a Graphics object. Visible beans need to implement paint() in order to display themselves. The paint() method of Gauge works by drawing a 3D rectangle using the gaugeColor and then drawing an inner rectangle using the valueColor. The dimensions of the inner rectangle are calculated based on the value of currentValue and the orientation variables.
Gauge provides getter and setter methods for each of its properties. These methods adhere to the naming conventions used for bean properties. The Introspector class of java.beans automatically reports the properties corresponding to these methods to application builder tools, such as the BeanBox.
The switchDimensions() method is used to switch the values of width and height when the bean's orientation is switched.
The updateSize() method is invoked when the bean changes its size. It invokes setSize() to inform a layout manager of its new size. It invokes the invalidate() method of its container to invalidate the container's layout and doLayout() to cause the component to be redisplayed.
You may be wondering, "What about all of those other classes and interfaces of java.beans?" For simple beans, you don't really need them. However, we created a GaugeBeanInfo class so that the bean's icon can be displayed. The GaugeBeanInfo class is shown in Listing 26.2.
import java.beans.*; import java.awt.*; public class GaugeBeanInfo extends SimpleBeanInfo { // Return icon to be used with bean public Image getIcon(int size) { switch(size){
case ICON_COLOR_16x16:
return loadImage("gauge16c.gif"); case ICON_COLOR_32x32: return loadImage("gauge32c.gif"); case ICON_MONO_16x16: return loadImage("gauge16m.gif"); case ICON_MONO_32x32: return loadImage("gauge32c.gif"); } return null; } }
The GaugeBeanInfo class extends the SimpleBeanInfo class and implements one method--getIcon(). The getIcon() method is invoked by application builders to obtain an icon for a bean. It uses the constants defined in the BeanInfo interface to select a color or monochrome icon of size 16¥16 or 32¥32 bits.
The Gauge.mf manifest file is used to build the Gauge.jar file. It identifies the Gauge.class file as a bean. To create the Gauge.jar file, use the following command:
jar cfm Gauge.jar Gauge.mf Gauge*.class gauge*.gif
All the files that you need for this example are installed in your ch26 directory. Remember to copy your beans' .jar files from the ch26 directory to the \bdk\jars directory to have them loaded by the BeanBox. The contents of the Gauge.mf file are as follows:
Manifest-Version: 1.0 Name: Gauge.class Java-Bean: True
Did you ever wish that you could draw text on a canvas without having to fiddle around with fonts and font metrics? The bean that you'll develop next will make your wish come true. You can use it in place of the TextArea and TextField classes to display text in applets and window applications.
The name of this bean is TCanv, which is short for Text Canvas. The source code for the TCanv bean is shown in Listing 26.3. Let's start by learning how TCanv works. Copy the TCanv.jar file from the ch26 directory to the \bdk\jars directory using the following command:
copy TCanv.jar \bdk\jars
When you open up your BeanBox, you will notice the TCanv bean at the bottom of your ToolBox, as shown in Figure 26.5.
Click on the TCanv icon and then in the BeanBox. The TCanv bean is displayed, as shown in Figure 26.6. Its property sheet is shown in Figure 26.7. The background, foreground, and font properties are the default properties of visible beans. The leftMargin and topMargin properties are used to insert space between the edges of the bean and the text it displays. The border property is used to display a border around the perimeter of the bean. The width and height properties control the bean's dimensions.
FIGURE 26.5. The TCanv bean is now in the ToolBox.
Figure 26.6. The TCanv bean is in the BeanBox.
FIGURE 26.7. The TCanv bean's PropertySheet.
The text property identifies the actual text that is displayed by the bean. Because some application builders, such as the BeanBox, do not let you enter a new line character in a text property, the vertical bar character (|) is used to indicate a new line. At least one character should be contained on a line for the line to be displayed.
To distinguish it from the rest of the BeanBox and to show how its properties work, make the following changes:
Figure 26.8 shows the effect of these changes on the bean.
FIGURE 26.8. The result of changing the TCanv bean's properties.
import java.io.*; import java.util.*; import java.beans.*; import java.awt.*; import java.awt.event.*; public class TCanv extends Canvas implements Serializable { // Set constants and default values public static final int WIDTH = 200; public static final int HEIGHT = 200; public int width = WIDTH; public int height = HEIGHT; public int leftMargin = 5; public int topMargin = 5; public String text = ""; public boolean border = true; public TCanv() { super(); } public Dimension getPreferredSize() { return new Dimension(width,height); } // Draw bean public synchronized void paint(Graphics g) { if(border) g.drawRect(0,0,width-1,height-1); Font font = g.getFont(); FontMetrics fm = g.getFontMetrics(font); int lineHeight = fm.getHeight(); int y=fm.getLeading()+fm.getAscent(); StringTokenizer tokenizer = new StringTokenizer(text,"|"); String line; while(tokenizer.hasMoreTokens()){ line=tokenizer.nextToken(); if(border) g.drawString(line,leftMargin+1,topMargin+y+1); else g.drawString(line,leftMargin,topMargin+y); y+=lineHeight; } } // Methods for accessing bean properties public String getText(){ return text; } public void setText(String newTextValue){ text=newTextValue; } public int getWidth(){ return width; } public void setWidth(int newWidth){ if(newWidth > 0){ width=newWidth; updateSize(); } } public int getHeight(){ return height; } public void setHeight(int newHeight){ if(newHeight > 0){ height=newHeight; updateSize(); } } public int getLeftMargin(){ return leftMargin; } public void setLeftMargin(int newLeftMargin){ if(newLeftMargin >= 0) leftMargin=newLeftMargin; } public int getTopMargin(){ return topMargin; } public void setTopMargin(int newTopMargin){ if(newTopMargin >= 0) topMargin=newTopMargin; } public boolean isBorder(){ return border; } public void setBorder(boolean newBorder){ border = newBorder; } void updateSize(){ setSize(width,height); Container container=getParent(); if(container!=null){
container.invalidate();
container.doLayout(); } } }
The TCanv class, like the Gauge class, extends Canvas and implements Serializable. It defines the field variables corresponding to its properties and implements getPreferredSize() and paint(). It also implements a few getter and setter methods. Starting to recognize a pattern?
The paint() method checks the border variable and draws a border around the bean, as required. It then gets the value of the current font and the font's FontMetrics object. It invokes the getHeight() method of the FontMetrics class to get the line height of the current font in pixels. It then uses a StringTokenizer object to parse the String object of the text variable based on the | delimiter. Finally, the text is displayed one line at a time. The hasMoreTokens() and nextToken() methods of StringTokenizer are used to step through the parsed text string and to display them on the Graphics object of the bean's canvas.
Listing 26.4 shows the code for the TCanvBeanInfo. This class is similar to GaugeBeanInfo and is used to provide icons to application builders.
import java.beans.*; import java.awt.*; public class TCanvBeanInfo extends SimpleBeanInfo { // Return TCanv icon public Image getIcon(int size) { switch(size){ case ICON_COLOR_16x16: return loadImage("tcanv16c.gif"); case ICON_COLOR_32x32: return loadImage("tcanv32c.gif"); case ICON_MONO_16x16: return loadImage("tcanv16m.gif"); case ICON_MONO_32x32: return loadImage("tcanv32c.gif"); } return null; } }
The manifest file used to create TCanv.jar is as follows:
Manifest-Version: 1.0 Name: TCanv.class Java-Bean: True
This file is used to identify the TCanv class as a bean. The following command is used to create the TCanv.jar file:
jar cvfm TCanv.jar TCanv.mf TCanv*.class tcanv*.gif
Now that you have a couple of beans under your belt, let's use them in an applet. The Quiz applet, shown in Listing 26.5, uses both beans. It displays arithmetic multiple-choice quiz questions to the user. These questions are displayed in a TCanv bean. A second TCanv bean displays status information. A Gauge bean displays the user's quiz score in graphical form.
FIGURE 26.9. The Quiz applet as displayed by appletviewer.
Figure 26.10. Keeping track of the score using the Gauge bean.
Figure 26.9 shows how the applet is initially displayed by appletviewer. The applet is displayed by opening the quiz.htm file contained in your ch26 directory. The questions are randomized to reduce the likelihood of a question being asked twice. When you click on an answer, the TCanv beans are updated with new questions and status information. The Gauge bean updates the user's quiz score, as shown in Figure 26.10.
import java.applet.*; import java.awt.*; import java.awt.event.*; public class Quiz extends Applet { // Declare bean objects TCanv question = new TCanv(); Gauge gauge = new Gauge(); String labels[]={" A "," B "," C "," D "}; Button button[] = new Button[labels.length]; TCanv status=new TCanv(); int questions = 0; int correctAnswers = 0; int currentAnswer; public void init() { Panel mainPanel = new Panel(); Panel gaugePanel = new Panel(); Panel bottomPanel = new Panel(); Panel buttons = new Panel(); // Set bean properties question.setLeftMargin(20); question.setTopMargin(20); gauge.setHorizontal(false); gauge.setMaxValue(100.0); gauge.setCurrentValue(100.0); gauge.setHeight(200); gauge.setWidth(20); status.setHeight(20); status.setWidth(200); status.setTopMargin(0); status.setBorder(false); mainPanel.setLayout(new BorderLayout()); mainPanel.add("Center",question); gaugePanel.add(new Label("Score: (0-100%)")); gaugePanel.add(gauge); mainPanel.add("East",gaugePanel); bottomPanel.setLayout(new BorderLayout()); for(int i=0;i<labels.length;++i){ button[i] = new Button(labels[i]); button[i].addActionListener(new ButtonHandler()); buttons.add(button[i]); } buttons.add(status); bottomPanel.add("Center",buttons); mainPanel.add("South",bottomPanel); add(mainPanel); } public void start(){ displayQuestion(); } void displayQuestion() { question.setText(nextQuestion()); if(questions==0) status.setText("Click the correct answer."); else{ String s="Questions: "+String.valueOf(questions); s+=" Correct: "+String.valueOf(correctAnswers); status.setText(s); } } String nextQuestion() { String q = "What is "; String operand[] = {"+","-","*"}; int op1 = randomInt(100); int op2 = randomInt(100); int op = randomInt(3); String operator = operand[op]; int ans=0; switch(op){ case 0: ans=op1+op2; break; case 1: ans=op1-op2; break; case 2: ans=op1*op2; break; } currentAnswer=randomInt(labels.length); q+=String.valueOf(op1)+operator+String.valueOf(op2)+"?| "; for(int i=0;i<labels.length;++i){ q+="|"+labels[i]; if(i==currentAnswer) q+=String.valueOf(ans); else{ int delta = randomInt(10); if(delta==0) delta=1; int add = randomInt(2); if(add==1) q+=String.valueOf(ans+delta); else q+=String.valueOf(ans-delta); } } return q; } int randomInt(int max){ int r = (int) (max*Math.random()); r %= max; return r; } void answer(int i){ ++questions; if(i==currentAnswer){ ++correctAnswers; displayQuestion(); }else{ status.setText("Try again!"); } // Update bean properties double score = (double) correctAnswers/(double) questions; gauge.setCurrentValue(score*100.0); gauge.repaint(); question.repaint(); status.repaint(); } class ButtonHandler implements ActionListener { public void actionPerformed(ActionEvent e){ String s = e.getActionCommand(); for(int i=0;i<labels.length;++i){ if(labels[i].equals(s)){ answer(i); break; } } } } }
The Quiz applet provides a very crude example of using beans in an applet. Normally, if you were using beans, you would slap together an applet using a visual programming tool. In this case, you could avoid having to do most of the applet programming.
The Quiz applet is valuable in that it shows you how beans can be used in the same manner as other GUI components. A second purpose of the applet is to make you appreciate the use of serialization. You'll learn about beans, serialization, and applets later in this chapter when you study a serialized clone of Quiz, named Quiz2.
The Quiz applet creates two TCanv beans and assigns them to the question and status variables. A Gauge bean is created and assigned to the gauge variable. The bean assigned to the question variable displays the text of a question. The bean assigned to the status variable displays the status information to the right of the answer buttons.
The applet's init()method lays out the applet and sets the properties of the beans. The left and top margins of the question bean are set to 20. The Gauge bean is changed to vertical and its maximum value is set to 100. Its current value is also set to 100, giving the user a vote of confidence. The gauge's width and height dimensions are also modified. The dimensions of the status bean are adjusted. Its top margin is set to 0 and its border is turned off.
The applet's start() method simply invokes the displayQuestion() method to display a quiz question to the user. The displayQuestion() method invokes the question bean's setText() method to display the text of the question. The setText() method of the status bean is invoked to display status information to the user.
Questions are created by the nextQuestion() method. This method generates an arithmetic question based on the addition, subtraction, and multiplication of integers between 0 and 100. It displays the answer along with three other incorrect answers. These answers are displayed in random order.
The randomInt() method generates a random integer from zero to one less than a specified maximum.
The answer() method supports the handling of the answer buttons by checking if the user answered correctly and then updating and displaying the score accordingly. The repaint() methods of the beans are invoked to cause the beans to update their respective displays.
The ButtonHandler class supports the handling of the events associated with clicking the answer buttons.
The quiz.htm file, shown in Listing 26.6, is used to display the Quiz applet.
<HTML> <HEAD> <TITLE>Quiz</TITLE> </HEAD> <BODY> <APPLET CODE="Quiz.class" WIDTH=400 HEIGHT=300> [Quiz applet] </APPLET> </BODY> </HTML>
While reading through the source code of the Quiz applet, you probably were wondering what benefit, if any, was derived from using beans. That's a legitimate concern. The answer is that in the absence of an application builder tool, beans are just a little easier to work with than other classes. The one feature of beans that is apparent, whether you are using them as part of an application builder or by hand, is their support for persistence.
The Quiz applet did not make use of persistence. Instead of customizing beans using the BeanBox, the Quiz applet included special code in the init() method to accomplish bean editing and customization. The Quiz2 applet, shown in Listing 26.7, which is a takeoff on the Quiz applet, does show how persistence is used. Listing 26.8 shows the quiz2.htm file used to display the applet. Go ahead and display quiz2.htm using the appletviewer. You should notice that the Quiz2 applet behaves in the same way as Quiz.
import java.applet.*; import java.awt.*; import java.awt.event.*; import java.beans.*; public class Quiz2 extends Applet { // Declare beans TCanv question, status; Gauge gauge; String labels[]={" A "," B "," C "," D "}; Button button[] = new Button[labels.length]; int questions = 0; int correctAnswers = 0; int currentAnswer; public void init() { Panel mainPanel = new Panel(); Panel gaugePanel = new Panel(); Panel bottomPanel = new Panel(); Panel buttons = new Panel(); try{ // Load serialized beans question = (TCanv) Beans.instantiate(null,"qcanv"); gauge = (Gauge) Beans.instantiate(null,"vgauge"); status = (TCanv) Beans.instantiate(null,"scanv");
}catch(Exception ex){
} mainPanel.setLayout(new BorderLayout()); mainPanel.add("Center",question); gaugePanel.add(new Label("Score: (0-100%)")); gaugePanel.add(gauge); mainPanel.add("East",gaugePanel); bottomPanel.setLayout(new BorderLayout()); for(int i=0;i<labels.length;++i){ button[i] = new Button(labels[i]); button[i].addActionListener(new ButtonHandler()); buttons.add(button[i]); } buttons.add(status); bottomPanel.add("Center",buttons); mainPanel.add("South",bottomPanel); add(mainPanel); } public void start(){ displayQuestion(); } void displayQuestion() { question.setText(nextQuestion()); if(questions==0) status.setText("Click the correct answer."); else{ String s="Questions: "+String.valueOf(questions); s+=" Correct: "+String.valueOf(correctAnswers); status.setText(s); } } String nextQuestion() { String q = "What is "; String operand[] = {"+","-","*"}; int op1 = randomInt(100); int op2 = randomInt(100); int op = randomInt(3); String operator = operand[op]; int ans=0; switch(op){ case 0: ans=op1+op2; break; case 1: ans=op1-op2; break; case 2: ans=op1*op2; break; } currentAnswer=randomInt(labels.length); q+=String.valueOf(op1)+operator+String.valueOf(op2)+"?| "; for(int i=0;i<labels.length;++i){ q+="|"+labels[i]; if(i==currentAnswer) q+=String.valueOf(ans); else{ int delta = randomInt(10); if(delta==0) delta=1; int add = randomInt(2); if(add==1) q+=String.valueOf(ans+delta); else q+=String.valueOf(ans-delta); } } return q; } int randomInt(int max){ int r = (int) (max*Math.random()); r %= max; return r; } void answer(int i){ ++questions; if(i==currentAnswer){ ++correctAnswers; displayQuestion(); }else{ status.setText("Try again!"); } // Update bean properties double score = (double) correctAnswers/(double) questions; gauge.setCurrentValue(score*100.0); gauge.repaint(); question.repaint(); status.repaint(); } class ButtonHandler implements ActionListener { public void actionPerformed(ActionEvent e){
String s = e.getActionCommand();
for(int i=0;i<labels.length;++i){ if(labels[i].equals(s)){ answer(i); break; } } } } }
Quiz2 works in the same manner as Quiz. The only difference is in how the properties of its beans are initialized. In Quiz, these properties are initialized through Java code in the init() method. In Quiz2, the question, status, and gauge beans were customized in the BeanBox and written to the qcanv.ser, scanv.ser, and vgauge.ser files. These files not only contain class information but also the values of the bean's customized properties. The instantiate() method of the Beans class is used to read the beans from serialized storage in the .ser files.
<HTML> <HEAD> <TITLE>Quiz</TITLE> </HEAD> <BODY> <APPLET CODE="Quiz2.class" WIDTH=400 HEIGHT=300> [Quiz applet] </APPLET> </BODY> </HTML>
FIGURE 26.11. Using the SerializeComponent command.
You probably want to know how the .ser files were created in the first place. I used the BeanBox to customize each of the beans used by Quiz2 and saved them to .ser files using the SerializeComponent command from the BeanBox File menu. (see Figure 26.11). I could have written a program to create the .ser file, but working with the BeanBox is much easier.
Let's change the font used to display the text of the status bean. Run the BeanBox and move a TCanv object into the box, as shown in Figure 26.12. Now edit the properties of the TCanv object as follows:
FIGURE 26.12. Displaying the changed properties.
When you have finished making these property changes, save the customized bean to a file using the SerializeComponent command under the File menu. Save the new file over the scanv.ser file that's in your ch26\ directory.
Now open quiz2.htm with appletviewer, as shown in Figure 26.12. Note how the status text is displayed in the new font you just selected.
In this chapter, you learned how beans work and examined the classes and interfaces of the java.beans packages. You also learned how to write bean code in Java. In the next chapter, you'll look at some of the notable beans that have been developed by the industry.
© Copyright 1998, Macmillan Publishing. All rights reserved.