Chapter 5: User Interfaces With Awt
AWT (Abstract Window Toolkit) is the Java package used to create and maintain an applet’s user interface. It attempts to abstract/objectify all the elements provided by the native GUI, whether that be Solaris, Win32 or Presentation Manager. Within AWT you’ll find button controls, text items, edit fields, dialog boxes, menu items and a host of other graphical objects that make providing a pleasing and efficient user interface possible.
In order to understand the process of writing an applet user interface, we have to look first at the object hierarchy that our applet rests upon. We know already that our applet sub-classes the class Applet, which subclasses Panel, Container, Component and Object. Between all these lower level objects there is enough functionality to make our simple applet from Chapter 4 work. Now we’ll take a closer look at some of the awt classes and try to make them do some more interesting work.
[Figure 5-1: The AWT package class hierarchy.]
<ch5_fig1.tif>
In the native GUI libraries, what typically happens is that a programmer constructs a hierarchy of ‘windows’, where his application main window is a child of the desktop, and all the other windows within the application are children of the applications main window. In these systems a ‘window’ is both something you can draw on, and something that can contain other windows.
AWT splits these two functions into two descendent classes - Component and Container. Container is simply a Component that can ‘contain’ other Components. It keeps a list of the Components that belong to it, and through the LayoutManager it can arrange its Components according to various predefined rules. Component encapsulates all the rest of the traditional ‘window’ functionality including event notification (mouse, keyboard ...), painting, sizing, font and color management.
If you look again at Figure 5-1, you can see that while Applet (which will be our top-level class) subclasses Container and by that route, Component, it doesn’t subclass either Frame or Window. When you first look Java, you might assume that Window and Frame are two of the key classes. As it turns out, applet programmers have literally no use for either Frame or Window objects.
What happens when a browser starts up, is that the browser itself is an instance of the class Frame. Our Applet is instantiated by this Frame, but, in true OO fashion, it knows nothing about the Frame. This means that anything that’s part of the Frame or Window class is not visible to an Applet.
Components, Containers and Panels
The Container class is a special class created to automatically manage the appearance of, and message handling for, a collection of Components. A class like this is absolutely essential for showing HTML documents, where the appearance of the document is generally described in relative, not absolute terms, and often changes with the shape of the window.
The Component class is where you find most of the functionality we traditionally associate with GUI windows. Like most base classes, it contains methods that are useful by themselves, and others that must be implemented by the subclass.
The key method for any Component is the paint/update method. As we saw in Chapter 1 with our basic Applet, paint/update is where the actual screen drawing happens. The base GUI (Win32, Xwin ...) sends a message indicating the window needs to be repainted, and Java turns that into an invocation of paint/update.
Within our Applet, we can cause paint/update to be called by calling repaint(), but in many cases, paint/update will be invoked directly by the system, because the user moved the window, or minimized it, or resized it. Be aware that while a call to repaint will eventually result in Java invoking the paint/update method, repaint itself does not call paint/update. repaint typically puts a message into the GUIs message queue, which in turn causes paint/update to be called. The actual invocation of paint/update will occur in a completely separate thread from the one that ran repaint.
The Container class is designed to keep a collection of Components, and arrange them neatly on the screen. The basic theory of creating a user interface with AWT is to create a Container, then create a Component for each of your visual elements and add them all to the Container. Each Container has a LayoutManager attached to it, which arranges the Components within the Container. Though we’ve talked only about Containers, when we need a Container, what we’ll actually subclass is Panel, which is merely a subclass of Container that attaches a default LayoutManager (FlowLayout) to the Container.
The arrangement of Components within a Container is dictated by the type of LayoutManager that’s attached to the Container. There are five types of layouts: Grid, Flow, Card, Border, and GridBag, which we can attach to our Container via the Container.setLayout method. In addition, if we subclass Panel, we can use the default FlowLayout LayoutManager. Listing 5-1 contains an applet that illustrates the first four layout types.
Listing 5-1: A simple applet illustrating layout managers.
<ch5_fig1.java>
Figure 5-2: The layout applet running.
<ch5_fig2.tif>
GridLayout takes a number of rows and columns as constructor arguments, and arranges the Components along the grid in the order in which they’re added to the Layout. Notice that GridLayout not only manages the location of Components but also their size. In fact, only FlowLayout does no sizing.
FlowLayout will look more familiar to browser users. It tries to intelligently center Components horizontally within the Panel. If you play with resizing the Applet of Listing 5-1, you’ll see that while the Grid Components change size to maintain the grid, the Flow Components keep their original size and change position, even to the point of lining up in a single column if you size the Applet thin enough. FlowLayout also works well with Components that are different sizes, a distinction it shares with GridBagLayout.
CardLayout tries to mimic a deck of cards. The only card you see is the one on top of the deck. You can change which card is on top using methods of the CardLayout class. In Listing 5-1, we’ve coded the Applet so that clicking any of the buttons causes the card panel to ‘flip’ to the next card.
BorderLayout postulates a world divided into, at most, five areas: North, South, East, West and Center. You add each BorderLayout Component to its Panel with one of these five areas attached to it. Figure 5-3 shows how the 5 BorderLayout areas are aligned on screen.
Figure 5-3: alignment of BorderLayout.
<ch5_fig3.tif>
GridBagLayout is the most useful, and most complicated, of the LayoutManagers. Basically, it’s a form of grid that doesn’t insist that its Components all be a uniform size.
[KEITH: This item is important, but it doesn’t work yet in beta. I have a call in to Sun to find out about this.]
Layout in the AgentLauncher
Within the AgentLauncher, the screen is divide into only two parts - the button panel at the top, and the field of running agents beneath it. The Agent Launcher needs to represent each running Agent with a picture (the AgentFace) which later we’ll turn it into an animation. Each of these pictures occupies a small square, anchored at a fixed location.
Figure 5-5: A running AgentLauncher.
<ch5_fig5.tif>
As you can see from figure 5-5, we want the AgentFaces to line up like the windows in a skyscraper, left to right, top to bottom. This is exactly how a FlowLayout would set up a series of consistently sized Panels. By implementing the field of AgentFaces as a FlowLayout Panel, we get, for free, automatic and dynamic repositioning of our picture as the user sizes the browser window, along with a very easy to use collection management via Container.add and Container.remove. This leads us pretty directly to the implementation of our AgentField and AgentFace classes.
Listing 5-2: the AgentField and AgentFace classes.
<ch5_fig2.java>
The AgentFace only has two types of methods, one that changes a state variable, and one that implements the ever-popular Component.update. This is an important point. Up to now, we’ve only written applets that had a single paint routine. Now we’re constructing a world where many, potentially independent objects are writing directly to the screen, rather than funnelling everything through one paint method.
Getting User Input Via Predefined Controls
Back in Listing 5-1 we used buttons to illustrate the effect of various LayoutManagers. In addition, we arranged it so that hitting any of the buttons caused the CardLayout Panel to flip over the next card. Well, now that you’ve seen Buttons in action, we might as well explain them. The AgentLauncher needs five buttons, Pick, Dispatch, Stop, Text-Only and Configuration. Dispatch, Stop and Text-Only are used only to call methods within the AgentLauncher. Listing 5-3 shows the source for these buttons.
Listing 5-3: The AgentButton class and other button classes.
<ch5_fig3.java>
We make a Panel to hold the buttons, create the buttons, then add them to the Panel. This results in a more or less reasonable arrangement of buttons, no matter how the user resizes the window.
How we want to deal with the mouse clicks determines how we’ll design our Buttons. There are a number of places where it looks like you can catch the mouse click for a button. The most obvious choice is to overload Component.mouseUp/mouseDown. Another less obvious possibility is to deal with it in the MOUSE_UP/DOWN case of handleEvent (we talk more about Events later on). Unfortunately, neither of these works. Java’s Button class handles the MOUSE_UP/DOWN Events and redispatches them as a single ACTION_EVENT. The MOUSE_UP/DOWN never reaches any of our Components.
The proper way to catch a button click is to overload action, as we do in AgentButton (you can also get the same effect by handling ACTION_EVENT in a handleEvent case). There is only one type of action that can happen in a Button, so within action we simply do whatever that button requires. The Event passed as an argument is just an ACTION_EVENT, so the Event.id is useless, and Event.x/y is zeroed out, making them useless too. The second argument to action, an Object, is a little more interesting though. This is always the Component within which the Event occurred. This allows us to implement a very OO dodge.
Our original implementation in Listing 5-3 overloaded action within a new class, AgentButton, and all our buttons had to subclass AgentButton. Listing 5-4 reworks this to eliminate the AgentButton class entirely. What we do is overload action within AgentLauncher. Within this new action, we figure out which Button was clicked by comparing the Object to the string labels of our Buttons. This works because the toString method of a Button returns the Button’s label, which we passed as an argument to the Button constructor. Once we’ve figured out which Button was clicked we can easily call the AgentLauncher method that Button previously called from its clicked method.
Listing 5-4: Listing 5-3 reworked to eliminate AgentButton.
<ch5_fig4.java>
This approach has obvious limitations. Button labels must be unique. The Button label itself also has to appear in two places, the Button constructor and the action handling method - a possible source of problems if you try to use literal strings rather than constants. It is, however, more modular in that the Buttons have no knowledge of the AgentLauncher applet. Overloading action in AgentButton forces us to use static methods within the AgentLauncher or pass the AgentLauncher as an argument. Both approaches are valid.
There is no image button supplied with Java, but creating one is not such a big deal. We’ve already seen all the pieces we need to implement one. What we need is to create a Component, ImageButton, whose paint/update method displays one image if the mouse button is down, and another if the button is not. The ImageButton should also catch the mouseUp Event and dispatch an ACTION_EVENT. This way, we can use ImageButtons exactly the same way we use text buttons. Listing 5-5 shows a simple ImageButton. Note that we still pass the label ‘stop’ so that the button can be identified in the Applets action method.
Figure 5-5: An ImageButton class.
<ch5_fig5.java>
Our simple ImageButton has a couple of problems that makes it less than completely compatible with the native GUI buttons you get with the Button class. For one thing, FlowLayout doesn’t lay them out properly. The doc states, rather innocently, that “FlowLayout is used to layout buttons in a panel”. In addition, our ImageButton should have a third image for use when the Component is disabled.
The Agent Launcher can launch ANY one of a number of agents. In order to provide the user with a choice of available agents, we need to implement a single form that lists all the available agents and allows the user to pick/launch one from the list.
Visually, functionally, we have two ‘styles’ of form to choose from. The first is a traditional Windows style dialog box, where all the controls that make up the form are contained in a separate, moveable window with its own frame, that pops up on top of the browser window. The user hits the OK button to ‘accept’ the values entered into the form, and the window then disappears.
The second style is the in-line, Web style form, where the components of the form all appear within the browser window, as if they were part of the document. When the document scrolls, the form scrolls too. Typically, the user ‘accepts’ the data entered by hitting an Accept button. There is usually no frame around the forms components and the form appears to be just a part of a larger document (though the user is often hyperlinked away when the accept button is hit). The mailto form is a good example of a Web style form. Though it consists entirely of buttons, our ButtonPanel is another example of an in-line form.
Though both styles are equally valid, and equally easy to implement in Java, dialog style has several benefits. The first is that it can be ‘modal’. A modal dialog box stays on the screen until the user has filled it in to the satisfaction of the applet writer. Second, when a modal, pop up dialog box comes up on top of the browser window, it is obvious to even the thickest user that the data needs to be filled in. Forms that scroll within the browser window often require extensive inline documentation to ensure that the user fills them out correctly. Third, and perhaps most important to Web page designers, dialog boxes go away when you don’t need them. A scrollable form built into a page always takes up screen real estate, even if the user has already filled it out correctly.
The downside of dialog boxes is that the designers of Java really don’t want you to be creating your own Window objects (of which Dialog is one) from within an applet. In fact, the basic theory of applets is that they should be confined to the area within the frame of the browser. When we start creating our own Window objects, we escape the boundaries of the browser window, a prospect that browser architects find disturbing.
Most GUIs provide a facility for compiling a static description of a dialog box into an application, such that the source code for the application only has to say “Load the dialog box with ID 100” and a functional dialog box appears with all the components in the right place. You can’t do this in Java, at least not yet. Each dialog box must be constructed on-the-fly. This means that, in the dialog constructor, you have to go through the following process:
find the Frame Window that will be the parent of this dialog
create the Window and Container (via the Dialog class constructor) for the dialog
attach a layout manager to the Container
create all the Components (Buttons, TextFields ...) of the dialog
add all the Components to the Container
show the dialog
Listing 5-6: The PickDialog class.
<ch5_fig6.java>
The one argument you absolutely must provide to any Window constructor is the Frame object that makes up the main window for the application, in this case, the Frame of the browser. In order to prevent applets from creating Windows, this object is not just lying around, publicly available for you to use, but there is a way to get it. This is the function of the getFrame method. Fair warning: this is not guaranteed to work forever.
Listing 5-7: The getFrame method.
<ch5_fig7.java>
Component.getParent returns the Container that holds the Component. But every Container is also a Component, so we can follow the Container chain all the way back to the first Container. As we go along, we see if the parent Container is the one, and only one, instance of a Frame, because, as you can see in figure 5-1, Frame subclasses Container, and the Frame Container must contain all the Components in the system.
Now we have enough information to create an empty dialog box, we can proceed to fill it up with useful controls. In Listings 5-6 and 5-8 we implement two dialog boxes, a PickDialog that contains the list of available agents, an Ok button and a Cancel button and a MessageBox that contains only a text string and an Ok button.
The hardest part of constucting dialog boxes on the fly is making sure that the controls end up where they belong. You have three options when it comes to locating controls in a dialog; do it yourself, use an existing LayoutManager, or write your own LayoutManager.
Listing 5-8: The message box dialog.
<ch5_fig8.java>
Especially for Components that are not resizeable, like most dialogs, there’s no good reason to use a LayoutManager. The major benefit of a LayoutManager is that it makes intelligent layout decisions based on the size of the window as set by the user. Dialog box sizes are generally fixed.
The key to successfully laying out a dialog is to size and layout everything relative to the sizes of the basic controls. Most controls implement a couple of methods to help LayoutManagers - preferredSize and minimumSize. Label and Button controls set their preferred size to dimensions that are appropriate for the data they contain, the text of the Label, or the label on the Button. If we base all our sizing and positioning on the preferred sizes of these controls, everything should work out okay
HTML pages scroll all by themselves, and if you plunk an applet into the middle of a page, it’ll scroll with the page. The AgentLauncher needs scrolling capability so that the user can troll through the potentially huge array of running agents, only a small number of which can fit on the screen at any one time. We could define our applet height as something huge, and let the HTML scrolling take care of it, but we still might spawn more agents than would fit, so defining our own scrolling region is the only way to go.
Creating the scrollbar is relatively straightforward. Figure 5-6 shows the horizontal scrollbar that results when you use the constructor:
Scrollbar( Scrollbar.HORIZONTAL, 50, 10, 0, 100 );
There are three ways the user can move the slider, each of which results in a different value change.
1. drag the slider with the mouse. This can leave the slider anywhere.
2. click on one of the arrows at the ends of the scrollbar. This results in the slider value incrementing or decrementing by one.
3. click in the empty space on either side of the slider. This results in the slider value changing by ‘visible’ units - in our example that would be 10.
Figure 5-6: the construction of a scrollbar.
<ch5_fig6.tif>
Just like our ButtonPanel buttons, the scrollbar generates actions which we catch in the AgentLauncher action method. The action event doesn’t tell us what happened, only that something happened within the scrollbar. It’s up to us to then query the scrollbar and rebuild our scrolling region appropriately. Listing 5-9 shows the Scrollbar, our call to the constructor and the AgentLauncher action method modified to deal with scroll events.
Listing 5-9: the scrollbar source.
<ch5_fig9.java>
When we enter the Agent Launcher applet, we want Agent Launcher help to be available, yet unobtrusive. The cool way to do this would be to add a menu item to the browser help menu - Agent Launcher help. Unfortunately, menus, like Windows, fall into the category of things that you aren’t supposed to do with applets. By going through the same getFrame nonsense as the dialog boxes, we can get access to the menu bar and all its submenus, adding and removing items at will. What we can’t do is get ourselves called when a menu item is selected. All the menu item select Events are swallowed up by the handleEvent function of the Frame. So, unfortunately for us, though it belongs on the menu bar, our applet help has to be a button.
There are other controls - Checkbox, Choice, TextField, and TextArea. Of these Choice and TextField are used later on when we talk about fonts in figure 5-13. Using Checkboxes and TextAreas is left as an exercise for the reader. Just what you needed, right?
We’ve already seen how Java deals with a mouse click in a button, absorbing the mouse-up and mouse-down messages from the native GUI and turning them into a single ACTION_EVENT. This, in general terms, is how Java abstracts the message handling mechanism of the native GUIs. With some small amount of processing, Java turns ‘messages’ from the native GUI into Event objects which it directs to the appropriate Components.
By way of background, GUIs like Windows, X11 and Presentation Manager generate a message for all the things that happen outside the application that the application needs to know about. These events include keypresses, hitting the scrollbar, selecting an item from a list, dragging with the mouse, mouse movements, changing which window has the focus. A C Windows program generally has a single function, the window procedure, with a large switch statement that catches all the messages the application wants to deal with. The application registers this single message-handling function with Windows and thereafter, Windows calls that function each time a message gets directed to the application. The Java analogue to the window procedure is the handleEvent method. You can catch any event in the handleEvent method, but Component provides overloadable methods for most of the likely events. Table 5-2 shows the overloadable event handling methods in Component.
Table 5-2 Overloadable event handlers and their corresponding Java events.
Component method Java
Event.id Description
action ACTION_EVENT Button was clicked.
gotFocus GOT_FOCUS Window is now the input focus.
keyDown KEY_ACTION, KEY_PRESS,F1-F12,LEFT,RIGHT, HOME,END,PGUP, PGDN,ESC A key was pressed. The key that was pressed is available in Event.key The state of Ctrl, shift and meta keys available via controlDown, shiftDown and metaDown methods
keyUp KEY_ACTION_RELEASE, KEY_RELEASE
lostFocus LOST_FOCUS This window is no longer the input focus.
mouseDown MOUSE_DOWN The left mouse button was clicked down. It may still be down.
mouseDrag MOUSE_DRAG The user is keeping the left mouse button down and is dragging it across the screen.
mouseEnter MOUSE_ENTER The mouse has entered this Component’s window.
mouseExit MOUSE_EXIT The mouse has left this Component’s window.
mouseMove MOUSE_MOVE The mouse is moving. May or may not be dragging.
mouseUp MOUSE_UP The left mouse button has been released.
Drawing and the Graphics Context
Up to now, we’ve used the supplied graphical objects, and avoided drawing directly to the screen ourselves. The time has come. Whatever GUI our applet is running under, that GUIs key task is to notify the applet when the window needs to be refreshed. Java delivers this ‘notification’ to the applet by calling one of two override methods: Component.paint or Component.update.
With paint, the entire Component is cleared to the background color before paint is invoked. With update, the window is not cleared. update methods are inherently more difficult to write since you have to clear any areas that need clearing manually rather than relying on the automatic clear in paint.
Listings 5-10 and 5-11 show two versions of an Applet that displays two strings. One of the strings sits in the upper left corner of the applet, and one marches around the screen. The first applet uses paint. As the string marches across the screen, both the marching string and the stationary string flicker. In the applet that uses update, not only does the marching string flicker less, the stationary string doesn’t flicker at all. The update method is slightly more complicated by having to clear the area where the string used to be.
Listing 5-10: The paint version of the marching string Applet.
<ch5_fig10.java>
Listing 5-11: The update method from the second version of the marching string Applet.
<ch5_fig11.java>
One of the immediate advantages of using a multi-platform package is that up and down always mean the same thing. The Java coordinate system puts 0,0 in the upper left of the screen, and that’s the way it is (so sayeth Sun). x coordinates increase to the right, y coordinates increase downward. This is okay, as far as it goes, but it leads to something of an inconsistency. If you draw a String with a height of 10, width 5 at (0,0) it occupies a rectangle with corners (0,0) (0,-10) (5,0) (5,-10). However, if you clear (or fill, or draw ...) a rectangle of that same size located at (0,0) the cleared rectangle has corners (0,0) (0,10) (5,0) (5,10 ). You can check this out for yourself in Figure 5-14b, where the cleared rectangle has its origin at the top left of the string, while the string itself is drawn with the origin at the bottom left of the string. Figure 5-7 shows a call to drawString, and fillRect with the same origin and dimensions.
Figure 5-7: Drawing text and rectangle with the same origin.
<ch5_fig7.tif>
The following rules govern origin and drawing direction for various operations:
1. The origin of a drawText (or drawChar) operation is the left end of the baseline (see fig 5-7) of the text. The text will appear to draw up, and to the right of the origin.
2. The origin of GUI controls, like Buttons, Labels, Choices ..., is the upper left corner of the control. The control will appear to draw DOWN, and to the right of the origin. Ditto for rectangles (as in Graphics.drawRect) and other shapes.
Within the Graphics class you get two methods for dealing with the coordinate system., translate and scale. translate moves the origin for the current Graphics context, so that all future operations on the Graphics context are relative to the newly set origin. This is very useful when you have margins, or when you split up the screen into distinct sections. This is essentially what happens when Java calls Component.paint. It gets a Graphics object for the entire applet, translates it to the origin of the Component and passes the translated Graphics object to Component.paint.
Listing 5-12 shows an applet that illustrates some of the basics of font and color management as well as some of the things we’ve been saying about using the update method. The point of the applet is to display a string on a ticker, passing the string through a rectangular window at a constant speed in an endless loop, like a stock ticker. We want the ticker to be a fixed width and just a little taller than the string itself.
The size and shape of characters drawn on an applets screen depends entirely on the font attached to that graphics context. There are two classes to think about when dealing with fonts, the Font class itself and the FontMetrics class. The Font class allows us to retrieve/build fonts with the face, point-size and style (bold, italic ...) we need. A Graphics object always has a Font attached to it which we can query via Graphics.getFont.
But the Font only embodies general information about the font: shape, relative size, weight. We need to know specifically how many pixels wide and tall various characters will be. The FontMetrics class provides us with a way of determining how text drawn using a particular Font will appear in a particular Graphics context.
There is nothing tricky about the information in FontMetrics. getHeight returns the maximum vertical dimension of the largest character in the font plus the space between lines. In techno-type terms this is ascent (height of the character above the baseline), descent (height of the character below the baseline) and leading (space between the top of this line and bottom of the next line).
Figure 5-8: Fontmetrics of the letter g.
<ch5_fig8.tif>
The Ticker update method uses FontMetrics for two purposes: to decide how tall the ticker should be and to know how much of the string can fit into the ticker. The one thing to notice when we draw text in update is that we offset the line upward by getMaxDecent. When you say drawChar(0,0) what you’re saying is draw the character and place its left edge at 0 on the x and its BASELINE at 0 on the y. Many characters, like our g, descend below the baseline and if we don’t offset by MaxDecent, the descenders will be chopped off.
Listing 5-12: The ticker applet.
<ch5_fig12.java>
For many applications, the default font suffices. When it doesn’t, we have to go fishing for a font that suits our needs. Listing 5-13 adds font and color choices to our ticker applet.
Listing 5-13: Adding font and color picking to the ticker applet.
<ch5_fig13.java>
Figure 5-9 The new ticker applet in action.
<ch5_fig9.tif>
Notice that both Component and Graphics have setFont methods. The end result of Component.setFont is to set the font in the graphics context that gets passed to the paint/update method. However, if we call Component.setFont within a paint/update method, the graphics context that was passed to paint/update will still have the old font attached to it. This is why, in paintFirst, we call both Component.setFont and Graphics.setFont.
Another thing to notice is how we get the list of available fonts from the AWT Toolkit. The Toolkit is an AWT class that provides a very thin layer around the native GUI API. In most cases, you will not need to deal directly with the Toolkit. Most of the methods in the Toolkit are used by other Java classes to create native GUI elements. So the Choice class, for example, uses Toolkit.createList(). Use the Java classes, not the Toolkit methods. The Java classes are stable, but the Toolkit is not. The only three things available in the Toolkit that you might need in the normal course of business are the font list, the screen resolution and the screen size.
Colors are similar to, but even simpler than fonts. A graphics context will always have two Colors attached to it, the current color and the background color. All the draw operations (drawString, drawChars, fillRect ...) except clearRect use the current color. You can query and change the current color via getColor and setColor. There are 13 basic colors, shown with their RGB values in Table 5-2, embodied in constants in the Color class.
Table 5-2: The basic colors.
Description RGB Value (Win NT)
black 0,0,0
blue
cyan
darkGray
gray
green
lightGray
magenta
orange
pink
red
white
yellow
There are three color constructors that allow you to make a new color based on its RGB values. The first uses the first eight bits of three separate, 32 bit integers to make up the basic 24 bits, eight bits each for red, green and blue. The second uses the first 24 bits of a single 32 bit integer (16-23 for red, 8-15 for green, 0-7 for blue). The third lets you specify the RGB values as floats, allowing for a wider range of values (though the display will probably cut that back to 24 bits max).
Images are fun, but you can often get a lot more done with text. In our case, we can represent a lot more running agents in the same amount of space if we use text strings rather than images. To do this, we create two new classes, AgentText to hold a text-based representation of the running agent, and AgentFace to contain the AgentText and AgentImage and decide which to use when the update method is called.
Listing 5-?? shows the new AgentFace class. Here we embody the idea of the various states of the agent, dispatched, running, returned results and returned-no-results. Each of these states has an AgentImage and an AgentText associated with it, hence the arrays image[] and text[]. We define a set of constants for the state and a status variable to hold the current state. We also define a boolean, bImageDisplay to indicate whether we’re using the text or image displays. The update method checks bImageDisplay, then calls either the AgentImage or AgentText update method associated with the current state of the agent as defined by status.
Now that we have a text display class, we can hook up the button that switches between image and text displays. Back in Figure 5-??, we defined a button with the label “Text-only”. What we want to do now is hook that button to a method that toggles us between image and text displays. In AgentLauncher we define a boolean, bImageDisplay, that is true if we display images, false for text. In AgentLauncher’s action method, we set bImageDisplay based on the label the button has when it’s clicked, then call changeDisplay to run through the Vector of AgentFace’s changing each to the new display type. A call to the Applet’s repaint() method then clears the Applets window, displaying the new AgentFaces.
So far in our example applets, we’ve displayed an image that was loaded from a GIF file, but we haven’t talked much about it. The actual GIF we use to indicate a running Agent is a transparent-background, GIF89a image.
We’ve already created one Panel and filled it with Buttons. Beneath this we want to display a large set of images, so, back in Listing 5-2 we created a new type of Panel, the AgentField. For now, this Panel only contains an array of Images, and an update method that draws those images. We encapsulate the Image itself in a new Object, AgentImage, and give it its own paint method. Listing 5-14 shows the AgentImage class.
Listing 5-14: the new AgentImage class.
<ch5_fig14.java>
Two things to note about the update method. First of all, it uses an offscreen image (and its graphics context) to draw the individual images into, then draws that image into the onscreen graphics context. This provides faster screen updates. We also use that offscreen image to track whether the panel has changed size, in which case it creates a new offscreen image and rearranges the images to fill the new panel. Second, the origin of the graphics context is the upper left corner of the AgentField, not the upper left corner of the Applet. So drawing to 0,0 appears right beneath, not on top of, the buttons in our applet window. Listing 5-?? shows the AnimationPanel class.
Listing 5-15: the new AgentField class.
<ch5_fig15.java>
Now we have the basic classes we need to display a set of buttons and an array of images beneath the buttons. All we need to do is instantiate these classes and make sure they get drawn. Listing 5-16 shows the modified AgentLauncher class. In init, we create a generic Panel, then create our buttons and add them to the Panel’s Container. We add the button Panel to the Applets Container. Then we create an AgentField for the images and add that Panel to the Applet’s Container. Finally, we add a call to the AnimationPanel.repaint in the Applet’s run method, which will cause AnimationPanel.update to be invoked, painting the images on the screen.
Listing 5-16: AgentLauncher modified to Provide ability to change the image displayed.
<AgentLauncher.java>
A huge portion of the network bandwidth consumed by applets involves downloading images. If we try to download images in a sequential fashion, starting the download and then blocking until the complete image arrives, we condemn applet users to long waits where seemingly nothing is happening. This is undesirable, and completely unneccessary. Through the use of three things, Applet.getImage, Component.prepareImage and the ImageObserver interface, we can download images asynchronously, while still being guaranteed to know for certain when the complete image has arrived and is ready to be drawn.
The applet of Listing 5-17 downloads a gallery of pictures and displays them in rows. While the images are being downloaded, the applet tries to entertain the user by drawing an expanding circle in the space to be occupied by the image.
In the class definition, you can see that the applet itself implements ImageObserver. This requires us to do two things within the applet. First, we add a method, imageUpdate, that the asynchronous image loading thread can call as the loading of the image progresses. imageUpdate will get called at various stages during image loading, depending on the characteristics of the image file itself. Some images will give the image dimensions right at the beginning. Other image files are formatted so that partial data gives a reasonable image. Our imageUpdate method recognizes four states: no-information, height&width available, all data available and error. While there’s no-information available, we display a string - “Hang on a sec”. When height and width become available, we frame the space with a rectangle and start drawing the expanding circle. If we hit an error, we fill the space with random bits. If the image loads completely, we draw it into the framed space.
Second, in Applet.init, we call Applet.getImage for all of our image files. Applet.getImage returns a ‘hollow’ Image object, which at this point only contains the image file’s URL. Then, for each hollow Image, we make a call to Component.prepareImage passing this as the ImageObserver argument. This is what actually causes the browser to start downloading the image file. The asynchronous image loading thread then proceeds to fill in the hollow Image object, at the same time calling our imageUpdate method with progress reports.
When applet.run is finally called, our applet is running, and we have access to a valid graphics context, but, in all likelihood, none of our images is ready for drawing yet. In our paint method, though, we call drawImage without regard as to whether or not the image has arrived yet. If the image isn’t drawable, drawImage returns false and doesn’t write anything to the screen
Listing 5-17: The GalleryDisplay applet using ImageObserver.
<ch5_fig17.java>
At this point, we’ve constructed a basic user interface for our AgentLauncher. The user can set his parameters, pick the agent he wants to launch, and view the status of each and every running agent in either text or image format. He can also scroll through the list of running agents easily.
Along the way, we’ve learned how to use all the visual elements that Java provides, from network-loadable image files to native GUI dialog boxes. Our applet has grown fairly complicated, but in very simple increments.
Add a class to listing 13 that encapsulates the color choice control and all its ‘items’, then modify the applet such that the foreground and background color choices are just instances of this class. Has this simplified the program? How?
In listing 13, the super ticker, why does the ticker configuration change when the user scrolls through the choice list. How would you fix this?
The super ticker uses a hard offset, startx and Graphics.translate, to put a left margin between the ticker and the applet border. This short-cut suffers if the user resizes the applet window. Modify Ticker using Panels to center the Ticker within the applet.