If you have ever served on a committee, you know how hard it is for a group of people to work together to reach a common goal. Without leadership, everyone seems to go their own way. Without well-coordinated communication, duplication of effort can occur. Likewise, if you try to put together a Java applet with several AWT controls, it may seem like you have a big committee-lots of activity but no leadership and no communication.
In this chapter, you learn how to combine AWT components. Unlike forming a committee, this text establishes leadership, communication, and division of labor. This cooperative effort produces a whole that is much greater than the sum of the parts. Once you have formed the composite controls, they will act like all the rest of the AWT controls. You can use these new controls anywhere that you use regular AWT controls.
To demonstrate composite controls, you create a scrolling picture window control. This control takes an image and makes it scrollable. All of the interaction between the AWT components that make up the control gets handled internally. To use one, all you have to do is create one and add it to your layout.
Before you look at combining controls, you need to look at some key AWT classes. These three classes-Component, Container, and Panel-are illustrated in Figure 10.1.
Figure 10.1 : The core AWT classes.
First, you examine the Component class. Component is the base class from which all AWT components are derived. Java does not allow you to instantiate Component or to directly subclass Component.
The Component class also implements the ImageObserver interface. ImageObserver provides a means of keeping track of image properties as they are loaded.
Container is a class derived from Component. Containers are objects that may contain other AWT Components. When a Container paint method is called, all the embedded Components' paint methods are called as well. Container provides functions to manage embedded Components. Like Components, you do not actually instantiate Container objects.
The last of the key classes is Panel. Panel is derived from Container and may be instantiated or subclassed. Panels are Java's multi-purpose Containers and do not provide any special functionality, except the capability to embed other GUI objects. Panels form the foundation of the composite controls you build.
Encapsulation is the technique used in object-oriented programming to combine data and methods into classes. This principle is the idea of grouping similar things. You also use encapsulation to combine classes into composite classes. In Chapter 9, "Extending AWT Components," you used subclassing to extend controls.
In this chapter, you use containment to extend controls,
which means that you instance variables in your classes that are
themselves AWT classes.
Note |
In Object-Oriented Programming (OOP), the containment relationship is called hasa. (For example, class Foo hasa member of type class Bar.) The subclassing relationship is called isa. (For example, class SubFoo isa derived class of class Foo.) |
The base class for all of the composite controls is Panel. The Panel class allows you to embed other AWT components. This class is derived from Container so it can contain UI components. The Panel class contains functions for managing embedded components.
Some functions can retrieve references to the embedded components. These functions allow the class to iteratively call methods in the embedded components. Other functions handle layout issues.
The primary advantage of using Panel as your composite component base class is that it is a Component itself. You can use your composite components like any other AWT components. You can take these new components and combine them to form composite components from other composite components and so on.
The new components can be added to layouts; they can generate existing Events or create new ones. They are full-fledged UI components and may be used anywhere that the AWT components are used.
The composite controls will be more versatile if you implement them with the appropriate layout manager. Because you would like the controls to be self-contained, they should be able to lay themselves out properly no matter what size they are. Chapter 8, "All About GridBagLayout and Other Layout Managers," discusses the use of layout managers, including the versatile GridBagLayout and designing your own layout.
When you build user interfaces, you will use components that generate and respond to events. One of the critical decisions to make is who should handle a given event. In the case of your composite controls, you have three choices: (1)the Component, (2)the Panel, or (3)the Applet.
Some components handle their own events. The SelfValidatingTextField from Chapter 9 is an example of such a component. Your composite components will also handle many of their own events.
Some Events you will want the Panel to handle. Because the Panel has information about all of the embedded components, it has the option of handling the events they develop.
The Applet class is derived from Panel, which means that the applets can contain embedded controls. Applets may also handle the events generated by these controls.
Your composite components will use the Panel class as a component manager. The Panel will handle any Events that are not handled in the components and will maintain information about its embedded components.
You use the panel's LayoutManager to size the embedded controls. When an event occurs in a control, the panel will respond. An animation control might have a panel and some VCR style buttons. When a user presses the start button, the animation begins. Clicking on the stop button ends the animation. While the animation plays, you disable the play button and enable the stop button.
In this example, you create a scrolling picture window. You derive a class from Panel called ScrollingPictureWindow. The class will contain three member objects, a Canvas to hold the picture, and two scrollbars.
This composite control will provide a self-contained way of displaying a picture. A user simply needs to pass an Image object to the control, and it does the rest. The control handles scrolling and updating the image. When the LayoutManager resizes the control, the scrollbars and image location automatically get adjusted.
Unlike some committees, each member of the ScrollingPictureWindow class has well-defined responsibilities. You will define specific roles for each component. You also design a means of handling communication between member components.
All committees need leadership. This committee needs a leader as well. The chairman of your committee is the ScrollingPictureWindow object. This class is derived from Panel and contains the two scrollbars and the Canvas object. The relationship of these classes is shown in the organizational chart of Figure 10.2.
Figure 10.2 : The organization of your committee.
The first member of the committee is the ImageCanvas
object. ImageCanvas is a
class derived from Canvas.
This object actually displays the image. The ScrollingPictureWindow
object will tell the ImageCanvas
how to display the image.
Note |
The Canvas class is an AWT component and serves as a generic UI component. It is meant to be subclassed to provide application (or applet) specific behavior. It is often used to display images. Canvas generates all of the key and mouse events so that it can be used to create powerful new UI classes. |
The last two members of the committee are the scrollbars. These scrollbars do not handle any of their own events, but simply report them to the ScrollingPictureWindow object. The ScrollingPictureWindow class will also inform the scrollbars when they need to reposition themselves.
The ImageCanvas class is derived from Canvas. You use this class to display your image. The class defined contains one instance variable:
Image canvasImg ;
The ImageCanvas constructor takes an Image object as a parameter. Because parameters of class type are passed by reference, this makes a local reference to the Image object in the class.
public ImageCanvas( Image img ) {
canvasImg = img ;
}
Note |
Java uses pass-by-value for parameters of simple types. Pass-by-value means that these variables are copied into the local space for a function. This is the normal passing paradigm of C++ and the only passing paradigm of C. Parameters of class type use pass-by-reference. This is like the C++ implementation. Pass-by-reference creates a local reference to a parameter in the function space. |
The only other method provided in the ImageCanvas class is paint(). The paint method will actually draw the image. Because the picture scrolls, the class will need to know where to draw it.
The location of the image depends on the position of the scrollbars. In your scheme, the ScrollingPictureWindow object handles communication between the member objects. You need to query the ScrollingPictureWindow object to determine where to draw the image.
public void paint(Graphics g) {
g.drawImage( canvasImg,
-1 * ((ScrollingPictureWindow)getParent()).imgX,
-1 * ((ScrollingPictureWindow)getParent()).imgY,
this ) ;
}
To get the information, use the getParent() method. The getParent() method is a member of the Component class. This method returns a reference to the Container object that holds the Component.
When you call getParent(), you get a reference to the ScrollingPictureWindow object. Because this reference is the Container type, you need to cast it to a ScrollingPictureWindow reference. Now you can access the public instance variables in the ScrollingPictureWindow object.
If you feel uncomfortable with directly accessing the members of the parent class, an alternative method would be to provide public methods or access functions. These functions would return the x and y values at which to draw the image.
The imgX and imgY members contain the x and y coordinates of the point (in terms of the image) that will be displayed in the upper left corner. If you want the point (10,5) to be displayed in the upper left corner, you pass -10 and -5 to drawImage(). As the example in Figure 10.3 shows, the Canvas class clips the image to fit within its boundaries.
Figure 10.3 : Drawing and clipping the image.
ImageCanvas provides the basic drawing for the ScrollingPictureWindow class. This class shows a typical usage of the Canvas class.
The ScrollingPictureWindow class contains several instance variables. These variables include the embedded controls and state variables. The embedded controls will be stored as:
ImageCanvas imageCanvas ;
Scrollbar vertBar ;
Scrollbar horzBar ;
Image image ;
The last instance variable in this list is a reference to an Image object, which gets passed in by the owner of your class object.
The remaining instance variables all contain state information. The first two contain the size in pixels of the entire image:
int imgWidth ;
int imgHeight ;
The next two instance variables contain the current position of the image. These variables also reflect the current position of the scrollbars. Because the scrollbars and the image are tied together, both classes use these variables. The scrollbars will set their value, and the ImageCanvas uses the value to place the image.
int imgX ;
int imgY ;
The last variable is used by the scrollbars. This value specifies the amount that the scrollbar moves when you request a pageup or pagedown.
int page ;
The class constructor performs all of the initialization for your class. The constructor must
Begin construction by setting the local Image reference to the Image argument:
public ScrollingPictureWindow ( Image img ) {
image = img ;
The next step in the construction process is simple. You need to initialize imgX and imgY to 0. What this really does is set the initial position of the image and scrollbars. These two instance variables contain the x and y offsets at which to display the image:
imgX = 0 ;
imgY = 0 ;
The ImageCanvas class will need these variables to determine how to place the image. The ImageCanvas paint() method accesses these instance variables directly and uses them in its call to drawImage().
Your composite control needs to know how large its image is. Once you have this information, it will remain constant. Unfortunately, determining the image size is not as straightforward as you might think. You have this difficulty by design.
Your class has been designed to take an Image object as a parameter, giving the users of the class a great deal of flexibility. They may load the image anyway they want. The image you receive may be one of many in an array. It may be in use by other objects in the applet. It may also have been just recently loaded by the calling applet. It is this last case that causes problems.
The sample applet used to test the class is very simple. It loads an image, creates a ScrollingPictureWindow object, and adds it to the layout. The Applet code follows:
package COM.MCP.Samsnet.tjg ;
import java.applet.*;
import java.awt.*;
import ScrollingPictureWindow ;
public class Test extends Applet {
ScrollingPictureWindow pictureWindow ;
public void init() {
Image img = getImage( getCodeBase(), "picture.gif" ) ;
pictureWindow = new ScrollingPictureWindow( img ) ;
setLayout( new BorderLayout() );
add( "Center", pictureWindow ) ;
}
};
The first line of the init() method calls getImage(). The getImage() method loads a specified image file. The problem is that getImage() returns immediately before the image actually loads. The image is not really loaded (i.e., its bits read into memory) until it is needed. Therefore, when you pass an image to the ScrollingPictureWindow constructor, it may not be fully loaded.
Thus in your class constructor, it is possible that the reference you receive is to an image that is not yet fully loaded. To get the image size, you make a call to Image.getHeight(). If the image is not fully loaded, however, getHeight() returns -1. To get the size of the image, you will loop until getHeight() returns a value other than -1. Both while loops below have null bodies:
while ((imgHeight = image.getHeight(this)) == -1 ) {
// loop until image loaded
}
while ((imgWidth = image.getWidth(this)) == -1 ) {
// loop until image loaded
}
Next, you need to create the embedded member objects. The ImageCanvas takes the Image as a parameter. Each scrollbar constructor takes a constant that determines whether the scrollbar is vertical or horizontal.
imageCanvas = new ImageCanvas( image ) ;
vertBar = new Scrollbar( Scrollbar.VERTICAL ) ;
horzBar = new Scrollbar( Scrollbar.HORIZONTAL ) ;
You use a GridBagLayout to lay out the embedded control. GridBagLayout is the most versatile LayoutManager in the AWT, which provides precisely the control you need to arrange the components. While GridBagLayout is the most powerful LayoutManager, many Java programmers have been slow to accept it.
Why is GridBagLayout so mysterious? Chapter 8 hopefully helped to de-mystify the topic. The Java phenomenon has developed so quickly that it seems almost comical to talk about the history of Java. Early Java books and even the beta versions of the online documentation from Sun omitted GridBagLayout. Many people who were doing Java in the early days still hesitate to use GridBagLayout.
You may be wondering why this control does not use a BorderLayout. BorderLayout is simple to use and a good choice in many situations. Figure 10.4 shows the control using a BorderLayout.
Figure 10.4 : A ScrollingPicture-Windows with a BorderLayout.
What's wrong with this picture? Take a look at the lower-right-hand corner of Figure 10.4. Do you see how the horizontal scrollbar is wider than the image area? Look for Java applets on the Internet; many have scrollbars arranged just like these. Nothing is intrinsically wrong with this layout. Compare it, however, to the TextField applet in Figure 10.5.
Figure 10.5 : A TextField with scrollbars.
This applet simply creates and displays a multiline TextArea. The scrollbars are part of the TextArea. Look at how they are arranged. In the built-in AWT component the scrollbars do not overlap. This is the suggested look for the ScrollingPictureWindow control. The best way to achieve this layout is to use GridBagLayout.
First, you create a GridBagLayout object. Then, you call setLayout() to make it the current layout manager.
GridBagLayout gridbag = new GridBagLayout();
setLayout( gridbag ) ;
The GridBagLayout class uses the GridBagConstraints class to specify how the controls get laid out. First, you create a GridBagConstraints object. You will then use the GridBagConstraints object to determine how to layout your individual components.
GridBagConstraints c = new GridBagConstraints();
You add the ImageCanvas object to your panel first. Because the ScrollingPictureWindow control is supposed to act like the native AWT controls, it may be resizeable. Therefore, you need to specify that it can grow in both x and y directions. So you set the fill member to BOTH.
c.fill = GridBagConstraints.BOTH ;
You want the image to fill all the available space with no padding, so set the weight parameters to 1.0.
c.weightx = 1.0;
c.weighty = 1.0;
You finish laying out the image by calling setConstraints() to associate the ImageCanvas object with the GridBagConstraints object. Then, you add the image to the layout.
gridbag.setConstraints(imageCanvas, c);
add( imageCanvas ) ;
Next, you layout the scrollbars. Start with the vertical scrollbar. The vertical scrollbar should shrink or grow vertically when the control is resized, so you set the fill member to VERTICAL.
c.fill = GridBagConstraints.VERTICAL ;
Look at your layout in terms of rows. You see that the first row contains two controls: the ImageCanvas and the vertical scrollbar. You indicate that the scrollbar is the last control in the row by setting the gridwidth member to REMAINDER.
c.gridwidth = GridBagConstraints.REMAINDER ;
You complete the vertical scrollbar layout by associating it with the constraint object and then adding it to the layout.
gridbag.setConstraints(vertBar, c);
add( vertBar ) ;
Finally, you layout the horizontal scrollbar. Because this scrollbar should be horizontally resizeable, set the fill member to HORIZONTAL.
c.fill = GridBagConstraints.HORIZONTAL ;
The reason for using a GridBagLayout is to prevent the horizontal scrollbar from filling the entire width of the control. You need to guarantee that the horizontal scrollbar remains the same width as the ImageCanvas object. Fortunately, the GridBagConstraint class provides a means of tying the width of one object to the width of another.
You use the gridWidth member of the GridBagConstraint class to specify the width of the scrollbar in terms of grid cells. Set this member to 1 so that the horizontal scrollbar takes up the same width as the ImageCanvas object (they are both one cell wide). It is the ImageCanvas object that sets the cell size.
c.gridwidth = 1 ;
The last thing you need to do is add the horizontal scrollbar. First associate it with the constraints object; then add it to the layout.
gridbag.setConstraints(horzBar, c);
add( horzBar ) ;
One of the most important features of the composite control is that it is resizeable. When you resize the control, you expect it to: resize its components, reposition the image, and adjust the scrollbars. You also need to update the class's status variables.
Start by examining what happens when you resize the control. First, size the control so that the control is smaller than the image it displays. You should be able to use the scrollbar to see all of the image. Figure 10.6 shows the control.
Figure 10.6 : The ScrollingPicture-Window.
Next, you make the control wider until it is wider than the image. The control now shows the entire width of the image. The horizontal scrollbar is no longer necessary, so you disable it. By making the control taller than the image, you disable the vertical scrollbar. If you make the control both wider and taller than the image, you disable both scrollbars. Figure 10.7 displays the control enlarged so that both scrollbars are disabled.
Figure 10.7 : The ScrollingPictureWindow resized, with scrollbars disabled.
Figure 10.8 : The ScrollingPictureWindow resized, with scrollbars enabled.
When you shrink the control, you enable the scrollbars again.
You will handle all of the resizing by overriding the Component.reshape() method. This function is called every time a control gets resized. The first thing that your function does is call the superclass (baseclass) reshape method. The superclass method does the real work of sizing. Because you are using a GridBagLayout, the LayoutManager resizes the individual components.
public synchronized void reshape(int x,
int y,
int width,
int height) {
super.reshape( x, y, width, height ) ;
You let the superclass do the resizing, so now you must update the image and scrollbars. First, determine whether the width of the control is greater than the image width plus the width of the vertical scrollbar. If the control width is greater, then you disable the horizontal scrollbar.
if ( width > imgWidth +
vertBar.preferredSize().width ) {
horzBar.disable() ;
If the control width is not greater, then you enable the horizontal scrollbar.
} else {
horzBar.enable() ;
Next, you determine how to reposition the horizontal scrollbar. Start by getting the size of the entire control and the width of the vertical scrollbar.
lllll Rectangle bndRect = bounds() ;
int barWidth = vertBar.preferredSize().width ;
Note |
When working with scrollbars, you have to set several values: (1)the thumb position, (2)the maximum and minimum values, (3)the size of the viewable page, and (4)the page increment. |
Now you can calculate the maximum value for the scrollbar. You always set the minimum of the scrollbar to 0. The maximum value will be the image width minus the width of the ImageCanvas. You set the page size and page increment to one-tenth of the maximum size.
int max = imgWidth - (bndRect.width - barWidth);
page = max/10 ;
Before setting the new values, you have to determine how to translate the old position to the new scale. Start by getting the old maximum value. If the old value is 0, you make the position 0.
int oldMax = horzBar.getMaximum() ;
if ( oldMax == 0) {
imgX = 0 ;
If the old maximum is not 0, you calculate the new position. First, express the old position as a fraction of the old maximum. Then, multiply the fraction by the new maximum. The resulting value gives you the new position.
} else {
imgX = (int)(((float)imgX/(float)oldMax) *
(float)max) ;
}
The last thing you need to do is set the scrollbar parameters.
horzBar.setValues( imgX, page, 0, max ) ;
horzBar.setPageIncrement( page ) ;
}
You use the same algorithm for setting the vertical scrollbar.
Whenever the user interacts with your control, the system generates an Event (see Chapter 11, "Advanced Event Handling"). This program is especially concerned about scrollbar Events. All other types of Events get passed on and handled outside your program.
You start by overriding the Component.handleEvent() method. In this method, you look for Events generated by the horizontal scrollbar. If the Event is one of the seven scrollbar Events, you reset the imgX variable and call repaint(). You return true if you can handle the Event.
public boolean handleEvent(Event e) {
if ( e.target == horzBar ) {
switch( e.id ) {
case Event.SCROLL_PAGE_UP:
case Event.SCROLL_LINE_UP:
case Event.SCROLL_ABSOLUTE:
case Event.SCROLL_LINE_DOWN:
case Event.SCROLL_PAGE_DOWN:
imgX = horzBar.getValue() ;
imageCanvas.repaint();
return true ;
}
The code for handling the vertical scrollbar is the same as for the horizontal scrollbar. If you do not handle the Event, call the superclass handleEvent method and return.
return super.handleEvent(e) ;
}
You now have a composite control that can become a drop-in replacement for other AWT controls. It handles its own events, and it responds to external resizing. The complete ScrollingPictureWindow class is as follows:
package COM.MCP.Samsnet.tjg ;
import java.awt.*;
public class ScrollingPictureWindow extends Panel {
ImageCanvas imageCanvas ;
Scrollbar vertBar ;
Scrollbar horzBar ;
Image image ;
int imgWidth ;
int imgHeight ;
int imgX ;
int imgY ;
int page ;
public ScrollingPictureWindow ( Image img ) {
image = img ;
imgX = 0 ;
imgY = 0 ;
while ((imgHeight = image.getHeight(this)) == -1 ) {
// loop until image loaded
}
while ((imgWidth = image.getWidth(this)) == -1 ) {
// loop until image loaded
}
imageCanvas = new ImageCanvas( image ) ;
vertBar = new Scrollbar( Scrollbar.VERTICAL ) ;
horzBar = new Scrollbar( Scrollbar.HORIZONTAL ) ;
GridBagLayout gridbag = new GridBagLayout();
setLayout( gridbag ) ;
GridBagConstraints c = new GridBagConstraints();
c.fill = GridBagConstraints.BOTH ;
c.weightx = 1.0;
c.weighty = 1.0;
gridbag.setConstraints(imageCanvas, c);
add( imageCanvas ) ;
c.fill = GridBagConstraints.VERTICAL ;
c.gridwidth = GridBagConstraints.REMAINDER ;
gridbag.setConstraints(vertBar, c);
add( vertBar ) ;
c.fill = GridBagConstraints.HORIZONTAL ;
c.gridwidth = 1 ;
gridbag.setConstraints(horzBar, c);
add( horzBar ) ;
}
public synchronized void reshape(int x,
int y,
int width,
int height) {
super.reshape( x, y, width, height ) ;
if ( width > imgWidth + vertBar.bounds().width ) {
horzBar.disable() ;
} else {
horzBar.enable() ;
Rectangle bndRect = bounds() ;
int barWidth = vertBar.preferredSize().width ;
int max = imgWidth - (bndRect.width - barWidth);
page = max/10 ;
int oldMax = horzBar.getMaximum() ;
if ( oldMax == 0) {
imgX = 0 ;
} else {
imgX = (int)(((float)imgX/(float)oldMax) *
(float)max) ;
}
horzBar.setValues( imgX, page, 0, max ) ;
horzBar.setPageIncrement( page ) ;
}
if (height > imgHeight + horzBar.bounds().height) {
vertBar.disable() ;
} else {
vertBar.enable() ;
Rectangle bndRect = bounds() ;
int barHeight = horzBar.preferredSize().height ;
int max = imgHeight - (bndRect.height -
&nbs p; barHeight) ;
page = max/10 ;
int oldMax = vertBar.getMaximum() ;
if ( oldMax == 0) {
imgY = 0 ;
} else {
imgY = (int)(((float)imgY/(float)oldMax) *
(float)max) ;
}
vertBar.setValues( imgY, page, 0, max ) ;
vertBar.setPageIncrement( page ) ;
}
}
public boolean handleEvent(Event e) {
if ( e.target == horzBar ) {
switch( e.id ) {
case Event.SCROLL_PAGE_UP:
case Event.SCROLL_LINE_UP:
case Event.SCROLL_ABSOLUTE:
case Event.SCROLL_LINE_DOWN:
case Event.SCROLL_PAGE_DOWN:
imgX = horzBar.getValue() ;
imageCanvas.repaint();
return true ;
}
} else if ( e.target == vertBar ) {
switch( e.id ) {
case Event.SCROLL_PAGE_UP:
case Event.SCROLL_LINE_UP:
case Event.SCROLL_ABSOLUTE:
case Event.SCROLL_LINE_DOWN:
case Event.SCROLL_PAGE_DOWN:
imgY = vertBar.getValue() ;
imageCanvas.repaint();
return true ;
}
}
return super.handleEvent(e) ;
}
};
class ImageCanvas extends Canvas {
Image canvasImg ;
public ImageCanvas( Image img ) {
canvasImg = img ;
}
public void paint(Graphics g) {
g.drawImage( canvasImg,
-1 * ((ScrollingPictureWindow)getParent()).imgX,
-1 * ((ScrollingPictureWindow)getParent()).imgY,
this ) ;
}
}
The ScrollingPictureWindow class you created in this chapter is a good example of a composite control. This class combines the techniques of subclassing and encapsulation. It also is a subclass of Panel and serves to encapsulate the Canvas and the two scrollbars.
The goal in developing composite controls is to provide plug-in replacements for the existing AWT controls. Because the ScrollingPictureWindow is a subclass of Panel, it inherits all the properties of the Panel class. Therefore, you can use a ScrollingPictureWindow object anywhere that you would use a Panel.
When you create a composite control, you need to provide a mechanism for encapsulating the embedded controls. In this chapter you used the AWT Panel class to contain the other controls.
The embedded controls must also communicate with each other. The handleEvent() method handles scrollbar events and enables the Canvas class to determine how to draw the image.
When you design an applet or application in Java, you have at your disposal the basic AWT controls. Now you can create composite controls like the ScrollingPictureWindow. Common tasks (like scrolling an image) are good candidates for combined controls. These new controls will become part of your personal Java toolbox, and you can use them in all of your future Java programming.