2.3 Hello Web! III: The Button Strikes!Well, now that we have those concepts under control, we can move on to some fun stuff. HelloWeb3 brings us a new graphical interface component: the Button. We add a Button component to our applet that changes the color of our text each time the button is pressed. Our new example is shown below.
import java.applet.Applet; import java.awt.*; import java.awt.event.*; public class HelloWeb3 extends Applet implements MouseMotionListener, ActionListener { int messageX = 125, messageY = 95; String theMessage; Button theButton; int colorIndex = 0; static Color[] someColors = { Color.black, Color.red, Color.green, Color.blue, Color.magenta }; public void init() { theMessage = getParameter("message"); theButton = new Button("Change Color"); add(theButton); addMouseMotionListener(this); theButton.addActionListener(this); } public void paint( Graphics gc ) { gc.drawString( theMessage, messageX, messageY ); } public void mouseDragged( MouseEvent e ) { messageX = e.getX(); messageY = e.getY(); repaint(); } public void mouseMoved( MouseEvent e ) { } public void actionPerformed( ActionEvent e ) { if ( e.getSource() == theButton ) { changeColor(); } } synchronized private void changeColor() { if ( ++colorIndex == someColors.length ) colorIndex = 0; setForeground( currentColor() ); repaint(); } synchronized private Color currentColor() { return someColors[ colorIndex ]; } } Create HelloWeb3 just as the other applets and put an <applet> tag referencing it in an HTML document. An <applet> tag just like the one for HelloWeb2 will do nicely. Run the example, and you should see the display shown in Figure 2.6. Drag the text. Each time you press the button the color should change. Call your friends! They should be duly impressed. The New OperatorSo what have we added this time? Well, for starters we have a new variable:
Button theButton; The theButton variable is of type Button and is going to hold an instance of the java.awt.Button class. The Button class, as you might expect, represents a graphical button, which should look like other buttons in your windowing system. Two additional lines in init() actually create the button and cause it to be displayed in our applet:
theButton = new Button("Change Color"); add(theButton); The first line brings us to something new. The new keyword is used to create an instance of a class. Recall that the variable we have declared is just an empty reference and doesn't yet point to a real object--in this case, an instance of the Button class. This is a fundamental and important concept. We have dealt with objects previously in our examples. We have assigned them to variables, and we have called methods in them. So far, however, these objects have always been handed to us ready to go, and we have not had to explicitly create them ourselves. In the case of our paint() method, we were given a Graphics object with which to draw. The system created this instance of the Graphics class for our area of the screen and passed it to us in the parameter variable gc. Our theMessage variable is of type String, and we used it to hold a String that was returned by the getParameter() method. In each case, some other method instantiated (created a new instance of) the class for us. The closest we came to actually instantiating an object was when we passed the name of the HTML <applet> parameter as an argument to the getParameter() method. In that case, we created a String object and passed it as the argument, but we did it in a rather sneaky fashion. As I mentioned previously, String objects have special status in the Java language. Because strings are used so frequently, the Java compiler creates an instance of the String class for us whenever it comes across quoted text in our source code. String objects are, in all other respects, normal objects. (See Chapter 7, Basic Utility Classes.) The new operator provides the general mechanism for instantiating objects. It's the feature of the Java language that creates a new instance of a specified class. It arranges for Java to allocate storage for the object and then calls the constructor method of the objects' class to initialize it. ConstructorsA constructor is a special method that is called to set up a new instance of a class. When a new object is created, Java allocates storage for it, sets variables to their default values, and then calls the constructor method for the class to do whatever application-level setup is required. A constructor method looks like a method with the same name as its class. For example, the constructor for the Button class is called Button(). Constructors don't have a return type; by definition they return an object of that class. But like other methods, constructors can take arguments. Their sole mission in life is to configure and initialize newly born class instances, possibly using whatever information is passed to them in parameters. It's important to understand the difference between a constructor and a method like our init() method. Constructors are special methods of a class that help set up and initialize an instance of a class when it's first created. The init() method of the Applet class serves a very similar purpose; however, it's quite different. Constructors are a feature of the Java language. Every class, including Applet, has constructors. init(), however, is just a method of the Applet class like any other. It's an application-level phenomenon that happens to perform initialization. An object is created by using the new operator with the constructor for the class and any necessary arguments. The resulting object instance is returned as a value. In our example, we create a new instance of Button and assign it to our theButton variable:
theButton = new Button("Change Color"); This Button constructor takes a String as an argument and, as it turns out, uses it to set the label of the button on the screen. A class could also, of course, provide methods that allow us to configure an object manually after it's created or to change its configuration at a later time. Many classes do both; the constructor simply takes its arguments and passes them to the appropriate methods. The Button class, for example, has a public method, setLabel(), that allows us to set a Button's label at any time. Constructors with parameters are therefore a convenience that allows a sort of short hand to set up a new object. Method OverloadingI said this Button constructor because there could be more than one. A class can have multiple constructors, each taking different parameters and possibly using them to do different kinds of setup. When there are multiple constructors for a class, Java chooses the correct one based on the types of arguments that are passed to it. We call the Button constructor and pass it a String argument, so Java locates the constructor method of the Button class that takes a single String argument and uses it to set up the object. This is called method overloading. All methods in Java, not just constructors, can be overloaded; this is one aspect of the object-oriented programming principle of polymorphism. A constructor method that takes no arguments is called the default constructor. As you'll see in Chapter 7, Basic Utility Classes, default constructors play a special role in the initialization of inherited class members. Garbage CollectionI've told you how to create a new object with the new operator, but I haven't said anything about how to get rid of an object when you are done with it. If you are a C programmer, you're probably wondering why not. The reason is that you don't have to do anything to get rid of objects when you are done with them. The Java run-time system uses a garbage collection mechanism to deal with objects no longer in use. The garbage collector sweeps up objects not referenced by any variables and removes them from memory. Garbage collection is one of the most important features of Java. It frees you from the error-prone task of having to worry about details of memory allocation and deallocation. ComponentsI have used the terms component and container somewhat loosely to describe graphical elements of Java applications. However, you may recall from Figure 2.3 that these terms are the names of actual classes in the java.awt package. Component is a base class from which all of Java's GUI components are derived. It contains variables that represent the location, shape, general appearance, and status of the object, as well as methods for basic painting and event handling. The familiar paint() method we have been using in our example is actually inherited from the Component class. Applet is, of course, a kind of Component and inherits all of its public members, just as other (perhaps simpler) types of GUI components do. The Button class is also derived from Component and therefore shares this functionality. This means that the developer of the Button class had methods like paint() available with which to implement the behavior of the Button object, just as we did when creating our applet. What's exciting is that we are perfectly free to further subclass components like Button and override their behavior to create our own special types of user-interface components. Both Button and Applet are, in this respect, equivalent types of things. However, the Applet class is further derived from a class called Container, which gives it the added ability to hold other components and manage them. ContainersA Button object is a simple GUI component. It makes sense only in the context of some larger application. The Container class is an extended type of Component that maintains a list of child components and helps to group them. The Container causes its children to be displayed and arranges them on the screen according to a particular scheme. A Container may also receive events related to its child components. As I mentioned earlier, if a component doesn't respond to a particular type of event by overriding the appropriate event-handling method and handling the event, the event is passed to the parent Container of the component. This is the default behavior for the standard Java AWT components, which gives us a great deal of flexibility in managing interface components. We could, for example, create a smart button by subclassing the Button class and overriding certain methods to deal with the action of being pressed. Alternatively, we could simply have the Button's container note which Button is pressed and handle the event appropriately. In the interest of keeping our examples contained in a single class, I am using the gestalt view and letting our Button's container, HelloWeb3, deal with its events. Remember that a Container is a Component too and, as such, can be placed alongside other Component objects in other Containers, in a hierarchical fashion, as shown in Figure 2.7. Our HelloWeb3 applet is a kind of Container and can therefore hold and manage other Java AWT components and containers like buttons, sliders, text fields, and panels. In Figure 2.7, the italicized items are components, and the bold items are containers. The keypad is implemented as a container object that manages a number of keys. The keypad itself is contained in the GizmoTool container object. LayoutAfter creating the Button object, we'd like to stick it in our applet. To do so, we invoke the add() method of Applet, passing the Button object as a parameter:
add(theButton); add() is a method inherited by our Applet from the Container class. It appends our Button to the list of components HelloWeb3 manages. Thereafter, HelloWeb3 is responsible for the Button: the applet causes the button to be displayed, it determines where in our part of the screen the button should be placed, and it receives events when the button is pushed. Java uses an object called a LayoutManager to determine the precise location in HelloWeb3's screen area the Button is displayed. A LayoutManager object embodies a particular scheme for arranging components on the screen and adjusting their sizes. You'll learn more about layout managers in Chapter 12, Layout Managers. There are several standard layout managers to choose from, and we can, of course, create new ones. In our case, we have not specified a layout manager, so we get the default one, which means our button appears centered at the top of the applet. Subclassing and SubtypesIf you look up the add() method of the Container class, you'll see that it takes a Component object as an argument. But in our example we've given it a Button object. What's going on? Well, if you check the inheritance diagram in Figure 2.3 again, you'll see that Button is a subclass of the Component class. Because a subclass is a kind of its superclass and has, at minimum, the same public methods and variables, we can use an instance of a subclass anywhere we use an instance of its superclass. This is a very important concept, and it's a second aspect of the object-oriented principle of polymorphism. Button is a kind of Component, and any method that expects a Component as an argument will accept a Button. More Events and InterfacesNow that we have a Button, we need some way to communicate with it: that is, to get the events it generates. We could just listen for mouse clicks, figure out whether they were close enough to the button, and act accordingly. But that would take a lot of work, and would give up the advantages of using a pre-built component. Buttons generate a special kind of event called an ActionEvent when someone clicks on them. To receive these events, we have added another method to our class:
public void actionPerformed( ActionEvent e ) { if ( e.getSource() == theButton ) { changeColor(); } } If you understood the previous applet, you shouldn't be surprised to see that HelloWeb3 now declares that it implements the ActionListener interface, in addition to MouseMotionListener. ActionListener requires us to implement an actionPerformed() method, which is called whenever an ActionEvent occurs. You also shouldn't be surprised to see that we added a line to init(), registering the applet as a listener for the button's action events: this is the call to theButton.addActionListener(this). The actionPerformed() method takes care of any action events that arise. First, it checks to make sure that the event's source (the component generating the event) is what we think it should be: theButton, the only button we've put in the applet. This may seem superfluous; after all, what else could possibly generate an action event? In this applet, nothing. But it's a good idea to check, because another applet may have several buttons, and you may need to figure out which one is meant. Or you may add a second button to this applet later, and you don't want the applet to break something. To make this check, we call the getSource() method of the ActionEvent object, e. Then we use the "==" operator to make sure that the event source matches theButton. Remember that in Java, == is a test for identity, not equality; it is true if the event source and theButton are the same object. The distinction between equality and identity is important. We would consider two String objects to be equal if they have the same characters in the same sequence. However, they might not be the same object. In Chapter 5, Objects in Java, we'll look at the equals() method, which tests for equivalence. Once we establish that the event e comes from the right button, we call our changeColor() method, and we're done. You may be wondering why we don't have to change mouseDragged() now that we have a Button in our applet. The rationale is that the coordinates of the event are all that matter for this method. We are not particularly concerned if the event happens to fall within an area of the screen occupied by another component. This means that you can drag the text right through the Button and even lose it behind the Button if you aren't careful: try it and see! Color CommentaryTo support HelloWeb3's colorful side we have added a couple of new variables and two helpful methods. We create and initialize an array of Color objects representing the colors through which we cycle when the button is pressed. We also declare an integer variable that serves as an index to this array, specifying the current color:
Color[] someColors = { Color.black, Color.red, Color.green, Color.blue, Color.magenta }; int colorIndex; A number of things are going on here. First let's look at the Color objects we are putting into the array. Instances of the java.awt.Color class represent colors and are used by all classes in the java.awt package that deal with color graphics. Notice that we are referencing variables such as Color.black and Color.red. These look like normal references to an object's instance variables; however Color is not an object, it's a class. What is the meaning of this? Static MembersIf you recall our discussion of classes and instances, I hinted then that a class can contain methods and variables that are shared among all instances of the class. These shared members are called static variables and static methods. The most common use of static variables in a class is to hold predefined constants or unchanging objects all of the instances can use. There are two advantages to this approach. The more obvious advantage is that static members take up space only in the class; the members are not replicated in each instance. The second advantage is that static members can be accessed even if no instances of the class exist. A hypothetical Component class might have a static variable called maximumWidth. Some other class that has to deal with this component, such as a layout manager, might want to know the maximum width of such a component, even if there aren't any around at the moment. Since maximumWidth is a static variable, the layout manager can get this information. An instance of the Color class represents a color. For convenience, the Color class contains some static, predefined color objects with friendly names like green, red, and (my favorite) magenta. Color.green is thus a predefined Color object that is set to a green color. In this case, these static members of Color are not changeable, so they are effectively constants and can be optimized as such by the compiler. Constant static members make up for the lack of a #define construct in Java. However, static variables don't, in general, have to be constant. I'll say more about static class members in Chapter 5, Objects in Java. The alternative to using these predefined colors is to create a color manually by specifying its red, green, blue (RGB) components using a Color class constructor. ArraysNext, we turn our attention to the array. We have declared a variable called someColors, which is an array of Color objects. Arrays are syntactically supported by the Java language; however, they are true, first-class objects. This means that an array is, itself, a type of object that knows how to hold an indexed list of some other type of object. An array is indexed by integers; when you index an array, the resulting value is the object in the corresponding slot in the array. Our code uses the colorIndex variable to index someColors. It's also possible to have an array of simple primitive types, rather than objects. When we declare an array, we can initialize it by using the familiar C-like curly brace construct. Specifying a comma-separated list of elements inside of curly braces is a convenience that instructs the compiler to create an instance of the array with those elements and assign it to our variable. Alternatively, we could have just declared our someColors variable and, later, allocated an array object for it and assigned individual elements to that array's slots. See Chapter 4, The Java Language for a complete discussion of arrays. Our Color MethodsSo, we now have an array of Color objects and a variable with which to index them. What do we do with them? Well, we have declared two private methods that do the actual work for us. The private modifier on these methods specifies that they can be called only by other methods in the same instance of the class. They are not visible outside of the object. We declare members to be private to hide the detailed inner workings of a class from the outside world. This is called encapsulation and is another tenet of object-oriented design, as well as good programming practice. Private methods are also often created as helper functions for use solely within the class implementation. The first method, currentColor(), is simply a convenience routine that returns the Color object representing the current text color. It returns the Color object in the someColors array at the index specified by our colorIndex variable:
synchronized private Color currentColor() { return someColors[ colorIndex ]; } We could just as readily have used the expression someColors[colorIndex] everywhere we use currentColor(); however, creating methods to wrap common tasks is another way of shielding ourselves from the details of our class. In an alternative implementation, we might have shuffled off details of all color-related code into a separate class. We could have created a class that takes an array of colors in its constructor and then provided two methods: one to ask for the current color and one to cycle to the next color (just some food for thought). The second method, changeColor(), is responsible for incrementing the colorIndex variable to point to the next Color in the array. changeColor() is called from our action() method whenever the button is pressed.
synchronized private void changeColor() { if ( ++colorIndex == someColors.length ) colorIndex = 0; setForeground( currentColor() ); repaint(); } We increment colorIndex and compare it to the length of the someColors array. All array objects have a variable called length that specifies the number of elements in the array. If we have reached the end of the array, we reset our index to zero and start over. After changing the currently selected color, we do two things. First, we call the applet's setForeground() method, which changes the color used to draw text in the applet and the color of the button's label. Then we call repaint() to cause the applet to be redrawn with the new color for the draggable message. So, what is the synchronized keyword that appears in front of our currentColor() and changeColor() methods? Synchronization has to do with threads, which we'll examine in the next section. For now, all you need know is that the synchronized keyword indicates these two methods can never be running at the same time. They must always run one after the other. The reason is that in changeColor() we increment colorIndex before testing its value. That means that for some brief period of time while Java is running through our code, colorIndex can have a value that is past the end of our array. If our currentColor() method happened to run at that same moment, we would see a run-time "array out of bounds" error. There are, of course, ways in which we could fudge around the problem in this case, but this simple example is representative of more general synchronization issues we need to address. In the next section, you'll see that Java makes dealing with these problems easy through language-level synchronization support. |
|