The javaPRogramming Tutorial: Vol. 2 Classes, Threads, and Applets
by Mark C. Reynolds Reprinted from Web Developer? magazine, Vol. 2 No. 2 May/June 1996 ? 1996 The Java language provides everything one would eXPect in a modern object-orie nted programming language. It has all the standard control constrUCts, a reaso nably comprehensible object model, and tools for creating standalone applicati ons or smaller applets that are designed to run inside Web pages.
The power of Java does not reside primarily in the language itself, however; i t resides in the libraries that accompany it: the Java Class Hierarchy. The fi rst part of this tutorial, in the Spring 1996 issue of Web Developer?, dea lt with the Java language and the construction of simple applets. This part wi ll focus on the set of Java classes associated with graphics, known as the Adv anced Windowing Toolkit, or AWT. Applet construction will also be revisited, w ith an eye toward event handling and multitaSKINg.
Programming low-level graphics is often like constructing a sand castle one gr ain of sand at a time. Higher-level toolkits allow more rapid construction of user interfaces, but they lack fine control and involve certain design comprom ises and limitations. Very high-level GUI builders can be extremely powerful, but they can also be the source of enormous frustration when asked to do somet hing outside their repertoire. Where does Java s AWT fit in?
In many ways, the Advanced Windowing Toolkit will seem very familiar to anyone who has ever used a toolkit to write a graphics program under almost any wind owing system. The design issues are the same: the suite of components (buttons , labels, scrollbars, cuboctohedra) that are available, how these components a re arranged visually, and the mechanisms involved in communicating between the components and the application program itself. You want a button labeled Canc el, you want it in the lower right-hand corner of your graphic, and you want t o tie it to a function named CancelAction(), for example. The AWT gives you se veral ways to accomplish these goals. It also provides a set of very powerful classes and methods that give Java programmers some distinctly different highe r-level tools (as well as some distinctly different programming challenges).
There are four aspects to programming with the AWT: establishing the layout, i mplementing the components, manipulating global resources such as colors and f onts, and handling events. Each of these aspects interacts with the others in well-defined ways.
One of the strengths of the AWT is that it was designed to be platform-indepen dent. This does not guarantee that any piece of AWT code will look exactly the same on a Unix machine, a Windows 95 box, or a Macintosh, but only that the b ehavior of the AWT will be the same on each. We will consider each of the four aspects of AWT programming in turn.
Unlike many toolkits, the AWT does not really enforce a specific layout policy . Instead, it provides five standard layout classes that correspond to commonl y encountered types of layouts. These are the BorderLayout, CardLayout, FlowLa yout, GridLayout, and GridBagLayout classes. It is also possible to create one s own layout class (although this is not for the faint of heart) or to eschew any formalized layout altogether and place one s components at absolute (x,y) coordinates. Most AWT applets will end up using FlowLayout, GridLayout, or Bo rderLayout, which are conceptually the simplest.
In order to understand the idea behind the layout classes, we will start with a simple example using BorderLayout. In a BorderLayout, components may be plac ed at the North, South, East, or West locations, or they may be placed in the Center. It is called a "border" layout because each component that s added sti cks to its corresponding border (with the exception of the Center component, w hich occupies the unused space in the middle). Figure 1 shows a code fragment that creates a border layout with four buttons, one at each cardinal point.
This code first creates a new instance of the BorderLayout class using the new Operator, and then calls the setLayout method with that instance as its sole argument. What is the purpose of this statement? We can guess that this establ ishes the BorderLayout as the default layout for the applet. The real reason t hat it works is that a Java applet is actually an instance of the Applet class , and the Applet class is actually a subclass of a graphical container class k nown as a Panel. Just as any applet must extend the Applet class, so Applet ex tends Panel. The call to setLayout is actually manipulating the panel in which the applet resides.
The next four statements create four buttons named for their intended location s. The text of each button is taken from the instance variables NORTH, SOUTH, EAST, and WEST, which have been declared with the keyWords static and final.
This is the standard approach to declaring constants in Java. Java has no cons t keyword, like C++. Instead it uses the keywords final, which indicates that the instance variable may not be changed, and static, which says that this ins tance variable is shared among all instances of its class. It is very importan t to realize that while the static keyword has the same meaning of permanence it has in C and C++, it does not affect the scope of the instance variable, as it would in C and C++. Other keywords (private and protected) are used for th at purpose.
After the buttons have been created, they are entered into the layout using th e add method. A final call is then made to the show method to ensure that all the components are drawn. Each of the five layout classes has its own peculiar form of the add method. In the case of BorderLayout, it wants add to have two arguments: The first is the placement location, and the second is the compone nt to be added. Note that in this example, the button labels are identical to their placement locations. This is by no means necessary; the buttons could ju st as easily have been named Moe, Larry, Curly, and Shemp. The add method, how ever, must be given one of the cardinal directions exactly as shown or it will fail to understand. You cannot add(Norte, el_amador), even if el_amador is a valid button instance.
This example is, of course, extremely boring. The buttons are not connected to anything, so that pressing them will not have any meaningful results. It is s traightforward to modify this code into a complete Applet that will do somethi ng meaningful. In particular, we can change it so that it will illustrate some thing interesting about applet events. A text-area component will be added at the Center location. We will also add an event handler, which will write infor mation into that text area about the events received by the applet. The comple te code for this simple Evlab (Event Lab) applet is shown in Figure 2.
This code uses a little bit of everything. It declares and uses the BorderLayo ut class; it incorporates three types of graphical components (Button, TextFie ld and TextArea); it makes use of one of the global graphical elements, namely Font; and it incorporates some rudimentary event handling. It also represents a new type of applet, one that is not threaded. This last point will be discu ssed in more detail below.
The init() method of the Evlab applet is similar to the BLex.java code shown p reviously. This code make more extensive use of other graphical components. It still places buttons at the North, East, and West locations. This version of init() creates a TextField and puts that at South, and also creates a TextArea and puts that in the Center position of the BorderLayout. These two text comp onents are almost identical to the Html elements of the same names. A TextFiel d is used to display a single line of text. It may be read/write or read-only. In our example it has been created with no initial text (the first argument t o the constructor is "") and has width 70. By default, TextFields are read/wri te so that it will be possible to type into this particular component.
The construction of the centrally placed TextArea is a little more elaborate. A TextArea is a multiline text entity, which can also be read/write or read-on ly. In our case it has been created with an initial size of 10 x 70. The call to the setEditable() method with the false argument declares that the user wil l not be able to modify the contexts of this text area. A specific font to be used is also set by calling the setFont() method. This is done for both the ed itable text field down South and the text area in the Center.
The font was oBTained by calling the Font() constructor with three arguments: the name of the font, the font style, and the font size. Times Roman is one of a small number of fonts that Java guarantees to provide (the others include H elvetica and Courier). The style argument indicates whether we want the plain version (Font.PLAIN), bold version (Font.BOLD), italic version (Font.ITALIC), or some other style. Styles may be combined, so that if the second argument ha d been Font.BOLD + Font.ITALIC, Java would have tried to find a Times Roman 14 -point font that was both bold and italic. The init() method concludes by succ essively adding each of the five components to the BorderLayout and then calli ng show() to insure that they are shown.
In addition to the init() method, which initializes the graphic environment, t his applet also has an action() method (and two placeholder methods, start() a nd stop(), discussed below).
As one might guess, the action method is used to capture events. The action() method is passed two arguments: an Event instance ev and an Object arg. The Ev ent instance ev describes the event that just happened. The second argument, a rg, represents component-specific information that the AWT gives you to help h andle the event. When a button is pressed, for example, the action method will be called, with arg set equal to the label of the button that was just presse d. Figure 3
In our Evlab example, we do not attempt to do anything to the incoming events and their arguments except to display information about them. The first statem ent of the action() method uses the setText method of the text area tfe to dis play the event id of the incoming event. Using setText ensures that any previo us text in this text area is cleared. The event id is a somewhat mystical numb er describing the type of event. The next four statements of the action() meth od append additional information about the event ev and the argument arg to th e text area. Since each string used ends with a newline "", each will be on its own line. The x and y coordinates of the site of the event are printed, as well as the target of the event (which component was affected), and the arg f ield of the Event ev. It is often the case that ev.arg and the argument arg ar e identical. Figure 3 shows the appearance of the Evlab applet after the West button has been pressed.
The final statement of the action() method is return(false). This is necessary because our action() method overrides a built-in Applet method that returns a Boolean value. The return code is used to indicate if the event has been hand led. Like many windowing systems, the AWT uses a hierarchical event-handling m odel. Just as graphics may be built in a layered fashion, so events may be gen erated and handled at any of the various levels. Our action() method returns f alse to indicate that the event has not been handled.
An Excellent way to use the event lab applet to demonstrate the event hierarch y is to type some text into the text field in the South position, and then hit Return. This will generate an event that will be displayed in the text area i n the center of the applet. The third line of text, the one describing the eve nt target, will be too long, and it will be necessary to scroll the text area using the horizontal scrollbar on the bottom of the text area. When you hit th is scrollbar, it will scroll the central text area, and it will not generate a nother event. In reality, when the scrollbar is activated it does generate ano ther event, but that event is intercepted and processed by the text area itsel f. The result is the scroll action that we see.
Since that event is completely processed by the text area, it never reaches th e action() event handler of the Evlab applet--it has been consumed by the text area s own built-in event handler. The reader is strongly encouraged to exper iment with the Evlab applet by placing other types of graphical components (as described below) in the South position and determining what types of events a re generated.
What of the other layout methods? FlowLayout is the simplest of the five types . When this form of layout is used, components are added from left to right. W hen all the available space in a given row has been used, a new row will be cr eated. Rows can be CENTER justified (the default) or LEFT or RIGHT justified. FlowLayout is typically used for rows of similar items, such as buttons. An ex ample of this layout approach will be given below.
GridLayout is a structured layout format in which graphical components are pla ced on a grid with a fixed size. If a GridLayout is created with a grid of 3 r ows by 5 columns, for example, then we can add a component at location (2,3), which will correspond to the third column of the second row. GridBagLayout is an extensible form of grid layout, in which the grid is permitted to expand as new elements are added. GridBagLayout is the most general layout strategy, an d also the most complex. Finally, CardLayout implements a HyperCard-style layo ut. Graphical components are added to individual cards, which are then display ed sequentially, rather than simultaneously.
Figure 4 lists the graphical components, such as Button, and the global graphi cal elements, such as Font, that are provided by the AWT. If you have ever don e any sort of graphics programming, many of these items will be very familiar to you. A List is actually a scrolling listbox, rather than a Lisp-like List. A CheckboxGroup is called a radio button by most other graphics systems.
Note that the list of graphical components includes items that are containers, such as Panel. This makes the hierarchical organization described above possi ble. We could modify the event lab applet to put a Panel in the central positi on, give that panel its own layout, and then proceed to add components to that panel. This panel might have a GridLayout, even though the applet s panel has a BorderLayout.
The Java API provides all the details on using the various components and glob al elements. Rather than attempt an exhaustive description of all of them, thi s article will conclude by focusing on a small set of classes related to anima tion. We have already seen a very simple demonstration of image loading in the first part of this tutorial. A Java VCR will now be developed that will allow us to load a set of images and then smoothly display them under user control. To do this, however, we must make a short detour and discuss the topic of thr eads in Java.
Threads Most modern operating systems support the concept of multitasking, in which mu ltiple independent processes timeshare the same physical CPU. Soon after the f irst multitasking OS was created, it was realized that it would be delightful to support the same sort of multiple-task model inside application programs, r ather than just at the command-line level. The first implementations of this i dea were cumbersome, and involved copying the entire code and data space of th e parent task to all newly created child tasks. Eventually, the concept of thr eads was born. A thread is a lightweight task, without much context. Good thre ad implementations should allow a top-level application to create multiple thr eads, assign each a piece of work, coordinate them, and ultimately clean up wh en all of them are done. Java supports just such a threaded programming model.
The concept of threads not only applies to stand-alone Java applications, but also to Java applets. The Evlab.java applet in Figure 2 does not make explicit use of threads. It has an init() method, which is called when the applet is l oaded; a start() method, which is called after loading is complete (and when a previously loaded Java page is revisited); and a stop() method, which is call ed when a page containing the Java applet is unloaded, or when another page is visited. Both the tiny applets shown in the first part of the tutorial, as we ll as the Vcr.java applet shown in Figure 5, are threaded applets. We know thi s because they implement the Runnable interface: The phrase "implements Runnab le" is part of the applet s class declaration. A runnable applet will also hav e a run() method, which will be entered whenever a new thread is created for t he applet s class. If this explanation seems a bit obscure, it should become m ore clear as we work through the code of the Java VCR.
The init() method does a number of very familiar things. Its goal is to load a number of images, specified by the HTML PARAM attribute "nimgs." These images are to be found at a location specified by the PARAM "imgloc" relative to the document base. Each image has a numerical suffix, as well as the suffix .gif. Thus if nimgs is 10 and imgloc is images/myanim, then the applet will look fo r its images in files named images/myanim1.gif through images/myanim10.gif. Th e first few lines of the init() method retrieve these parameters from the HTML environment using getParameter() and do some sanity checking. If everything s eems reasonable, a new instance of the MediaTracker class is created and store d in the instance variable tracker. Space is also allocated for an array of ni mgs images in the instance variable imgs. A for loop is then used to attempt t o load each of the images into that array. Note that two loop variables are us ed, because the imgs[] array is 0-indexed, while the image names have suffixes starting from 1.
The for loop also contains the statement tracker.addImage(imgs[i], 0). The Med iaTracker class is being used to keep track of the progress of the images bein g loaded. This is necessary because getImage() does not actually get the image --it simply arranges to start getting it. The call to the addImage() method of tracker tells the tracker instance to begin keeping track of that image. The second argument to addImage() is an arbitrary ID, 0 in this case, which can be used to subsequently inquire about the progress of all images with that ID. E ffectively, we are placing all the images in a single pool, associated with ID 0, whose status will be queried later.
The init() method next builds a layout in which the images and a series of con trol buttons will be displayed. A BorderLayout with two elements is used for t he applet s panel. The North element will be a Panel that will hold the images as they are being animated, while the South element will be another Panel tha t will hold the control buttons. The variable nodim is used to store the apple t s preferred size (set by the WIDTH and HEIGHT attributes in the APPLET tag). We make the North panel as wide as the entire applet, but carve 50 pixels out of its height for the South Panel to occupy.
The South Panel is constructed using a left-justified FlowLayout. This means t hat the buttons we add to it will be added from left to right. The remainder o f the init() method constructs these buttons, sets the button font to 14 point Helvetica, and then adds those buttons to the South panel. Note that it is ne cessary to explicitly say so.add(). If we had used add(), the buttons would ha ve been added to the top-level BorderLayout. This code illustrates hierarchica l organization using the AWT. The top-level applet Panel is a BorderLayout Pan el with two elements: both Panels themselves. The North Panel contains no sube lements, while the South Panel contains four: the four buttons labeled FWD, RE V, STOP, and EJECT.
Before we discuss the run() method, let us first consider the start() and stop () methods. When init() method completes, the start() method will be invoked. It will examine the instance variable animthread, discover that it is null, an d therefore will create a new Thread with the argument this. This statement ac tually creates a new Java Thread based on the Vcr class. The next statement, a nimthread.start(), runs that thread. By virtue of this statement, the run() me thod will be entered in that separate thread of control. The stop() method is the inverse of the start() method. When the page is unloaded, the stop() metho d will be called. It will terminate the animthread thread using animthread.sto p(), and then reset the animthread variable to null. This ensures that if the page is reloaded or revisited, the start() method will create a new thread yet again.
The run() method and the action() event handler work together to display the i mages according to the controls that the user has pressed. The run() method st arts out by doing some additional sanity checking. It then calls tracker.waitF orID(0). This call will wait until all the images associated with ID 0 (which will be all the images in this case) have been fully loaded and prepared for o n-screen display. This method invocation in encased in a peculiar construction known as a try block. Try blocks are necessary because some methods can raise exceptions, which must be caught. If such an exception occurs (because there is not enough memory for all the images, for example), the run() method return s via the catch clause.
The next two statements are a little bit of thread magic. The run method disco vers the current thread and places that in the local variable me. It then lowe rs its own thread priority to one below the normal thread priority. This ensur es that the other thread--the one actually doing the drawing--has higher prior ity, which is what most animation applications would want. After this is done, the run() method enters a loop. If the instance variable imginc is nonzero, t hen the index of the image to be displayed will be updated and a call to repai nt() made to ensure that the new image is shown. The instance variable whichim g is used to hold the index of the image currently being drawn. Great care is taken to ensure that whichimg stays within range. It must never be less than 0 (the minimum image number) or greater than nimgs-1 (the maximum image number) . After the image has been updated, the thread puts itself to sleep for 100 mi croseconds. This is another thread trick, ensuring that the Java runtime will actually look for another thread to run. After all, there really is only a single CPU and a single applet, so Java must somehow decide when to run the various threads. When Java notices that one thread has put itself tosleep, all other threads become runnable.
The action() method controls the critical-instance variables imginc and done. The action() method looks for events that are associated with Buttons. The ev. target field will always indicate the type of graphical object that was associ ated with the Event. Java has a very convenient operator, instanceof, to test if something opaque like ev.target is actually an instance of a particular cla ss--Button, in this case. If the test passes, then the local variable thelab i s set to the label of the Button. A four-way test is now done. If the user pre ssed FWD, then imginc is set to 1 and the images move forward. If REV was pres sed, imginc becomes -1 and the images move backward. If STOP is pressed, imgin c is set to 0, and the images move no more; they rest at the current image. Fi nally, if EJECT is pressed, the instance variable done is set to true.
The paint() method does the work of actually drawing the images. If done is fa lse, then it performs a number of sanity checks, and finally uses the drawImag e() method to draw the current image, represented by the index whichimg. Becau se of the use of the MediaTracker at the top of the run method, we can be sure that all images are loaded and ready to be drawn so that the animation will g o quite smoothly. If done is true, then the paint() method clears the entire d rawing area, erasing all images but leaving the buttons intact. Note that the paint() method can be called by the browser environment itself, not just by re paint(). If you obscure the applet s drawing area with another window and then reexpose it, the paint() method will be called. This is why it is important t hat the instance variable whichimg always have a sane value. Figure 7
Figure 6 shows a simple HTML file that can be used to load images into the Vcr applet. Figure 7 shows the applet captured in motion, at about image 7. Sun s Tumbling Duke GIFs were used to create this particular animation, although an y set of images could have been used. If you download the Java Development Kit (JDK), these images will be found in the demo/TumblingDuke/images Directory.
This Vcr applet can serve as a template for animation applets, since it contai ns all the basic elements of a threaded graphics applet. The start() and stop( ) methods can be used as shown. The MediaTracker code and the Thread manipulat ion portions of the init() and run() methods are also quite generic. More work needs to be done in order to make this applet robust, however. The applet nev er checks to see if the image dimensions will actually fit in the applet s pan el, for example.
Developing Yourself The Java Development Kit may be obtained at FTP.javasoft. Almost all major pla tforms, including several flavors of Unix, Windows NT, Windows 95, and the Mac intosh now support some version of the JDK. Sun also has a Web server at http://javasoft.com but it is often overloaded. One of the most comprehensive and accessible Java sites, the Gamelan repository, has many useful public- domain classes,applets, and applications. Sun itself is publishing a set of definitive reference works on Java. The reader may also benefit from Lemay and Perkins book Teach Yourself Java in 21 Days.
Figure 1 Creating four buttons using the Border Layout Class public static final String NORTH = "North"; public static final String SOUTH = "South"; public static final String EAST = "East"; public static final String WEST = "West";
public void init() { setLayout(new BorderLayout()); Button no = new Button(NORTH); Button ea = new Button(EAST); Button we = new Button(WEST); Button so = new Button(SOUTH); add(NORTH, no); add(SOUTH, so); add(EAST, ea); add(WEST, we); show(); }
public class Evlab extends Applet { public static final String NORTH = "North"; public static final String SOUTH = "South"; public static final String EAST = "East"; public static final String WEST = "West"; TextArea tfe;
public void init() { setLayout(new BorderLayout()); Button no = new Button(NORTH); Button ea = new Button(EAST); Button we = new Button(WEST); TextField tff = new TextField("", 70); Font fo = new Font("TimesRoman", Font.BOLD, 14); tfe = new TextArea(10, 70); tfe.setEditable(false); tfe.setFont(fo); tff.setFont(fo); add("Center", tfe); add(SOUTH, tff); add(NORTH, no); add(EAST, ea); add(WEST, we); show(); }
public class Vcr extends Applet implements Runnable { MediaTracker tracker; Dimension nodim; Thread animthread = null; Image imgs[]; static final String FWD = "FWD"; static final String REV = "REV"; static final String STOP = "STOP"; static final String EJECT = "EJECT"; int whichimg = 0; int imginc = 0; // +1 = forward, -1 = reverse, 0 = stop int nimgs = 0; boolean done = false;
public void init() { BorderLayout nl; FlowLayout fl; Button bu[]; Font fo; Panel so; Panel no; String tmp; String imgloc;
imgloc = getParameter("imgloc"); if ( imgloc != null ) { tmp = getParameter("nimgs"); if ( tmp != null ) { nimgs = Integer.parseInt(tmp); if ( nimgs > 0 ) { imgs = new Image[nimgs]; tracker = new MediaTracker(this); for(int i = 0, j = 1; i < nimgs; i++, j++) { imgs[i] = getImage(getDocumentBase(), imgloc + j + ".g if"); tracker.addImage(imgs[i], 0); }
nl = new BorderLayout(); setLayout(nl); no = new Panel(); nodim = preferredSize(); no.resize(nodim.width, nodim.height-50); add("North", no); so = new Panel(); fl = new FlowLayout(FlowLayout.LEFT); so.setLayout(fl); add("South", so); bu = new Button[4]; bu[0] = new Button(Vcr.FWD); bu[1] = new Button(Vcr.REV); bu[2] = new Button(Vcr.STOP); bu[3] = new Button(Vcr.EJECT); fo = new Font("Helvetica", Font.PLAIN, 14); for(int i = 0; i < 4; i++) bu[i].setFont(fo); so.add(bu[0]); so.add(bu[1]); so.add(bu[2]); so.add(bu[3]); } } } }
public class Vcr extends Applet implements Runnable { MediaTracker tracker; Dimension nodim; Thread animthread = null; Image imgs[]; static final String FWD = "FWD"; static final String REV = "REV"; static final String STOP = "STOP"; static final String EJECT = "EJECT"; int whichimg = 0; int imginc = 0; // +1 = forward, -1 = reverse, 0 = stop int nimgs = 0; boolean done = false;
public void init() { BorderLayout nl; FlowLayout fl; Button bu[]; Font fo; Panel so; Panel no; String tmp; String imgloc;
imgloc = getParameter("imgloc"); if ( imgloc != null ) { tmp = getParameter("nimgs"); if ( tmp != null ) { nimgs = Integer.parseInt(tmp); if ( nimgs > 0 ) { imgs = new Image[nimgs]; tracker = new MediaTracker(this); for(int i = 0, j = 1; i < nimgs; i++, j++) { imgs[i] = getImage(getDocumentBase(), imgloc + j + ".g if"); tracker.addImage(imgs[i], 0); }
nl = new BorderLayout(); setLayout(nl); no = new Panel(); nodim = preferredSize(); no.resize(nodim.width, nodim.height-50); add("North", no); so = new Panel(); fl = new FlowLayout(FlowLayout.LEFT); so.setLayout(fl); add("South", so); bu = new Button[4]; bu[0] = new Button(Vcr.FWD); bu[1] = new Button(Vcr.REV); bu[2] = new Button(Vcr.STOP); bu[3] = new Button(Vcr.EJECT); fo = new Font("Helvetica", Font.PLAIN, 14); for(int i = 0; i < 4; i++) bu[i].setFont(fo); so.add(bu[0]); so.add(bu[1]); so.add(bu[2]); so.add(bu[3]); } } } }