Introduction
to Interactive Programming
by Lynn Andrea
Stein
A Rethinking
CS101 Project
- How can event-generator and event-consumer behaviors be separated into different objects?
- How can intermediate (listener) objects be used to couple together system components that don't know about one another?
In an earlier chapter, we saw how event driven programming can be used to implement painting in java.awt components. In this chapter, we will extend that event model to include other (input) event types using the Java 1.1 AWT event model.
The problem of GUI design is illustrative of larger design issues. The event-delegation approach described in this chapter arises from our desire to separate what happens in the GUI (such as clicking a button) from the behavior that this causes (such as playing a song). To make this work, we connect GUI objects (such as Buttons) to application objects (such as SongPlayers) indirectly, through special EventListener objects. The EventListener records the appropriate connection between GUI events and application behavior, keeping these details out of both GUI and application components. This allows signifcant flexibility: a single application behavior may be invoked by many different GUI events; one GUI event may give rise to many application behaviors; or the relationship between GUI events and application behavior may be remapped by a running program, for example.
This kind of indirect coupling through a Listener object is a useful technique in a wide range of applications.
In the simple event model that we considered above, each component had an event handler method (e.g., paint) that was invoked every time that the appropriate event was triggered (e.g., by uncovering a window or by an explicit call to repaint()). The event handler method received any appropriate arguments (such as the relevant Graphics context) necessary to enable it to do its job. Recall that this also means that the component doesn't (necessarily) have an always-active animacy (Thread); instead, it is woken whenever an appropriate event occurs.
While painting works this way, many of the events used in building GUIs use a more complicated two-layer model. The idea is that the appearance of a GUI object and its underlying behavior may actually be implemented by two different Java Objects. For example, the GUI object that implements a set of radio buttons may be a Panel containing a number of CheckBoxes. When the appropriate buttons are pressed, a song may be played. The Panel is responsible for keeping track of the on-screen appearance of the CheckBoxes (with their help, of course). The Panel need not be responsible for playing the song, though. Logically, we want to separate out the GUI appearance (and GUI behavior, e.g., buttons looking pressed or not pressed) from the underlying application behavior.
Java's 1.1 AWT event model lets us do just that. Each java.awt.Component specifies other AWT objects to listen for and handle its events. This is like leaving a (specialized) forwarding address with the post office: If any large packages come, please send them to the main office, and if any mouse motion events occur, please send them to the mouse motion listener that's waiting for them.
For example, a mouse click generates a MouseEvent with four especially relevant methods. (MouseEvent and all the other specialized Event classes are part of the java.awt.event package.) If the MouseEvent is labelled mickey, then mickey.getX() returns an int specifying the mouse click's mickey-relative screen coordinates (in pixels starting at the upper left-hand corner of mickey's screen-space). mickey.getY() similarly returns mickey's y coordinate. If you prefer to get both coordinates at once, you can retrieve a java.awt.Point object using mickey.getPoint().
Finally, if you want to verify that mickey is a mouse click event, you can call mickey.getID(). An event's ID is a static final int specifying what kind of event it is. These are defined by each of the event classes. Some significant event types include
(The event types listed above are an assortment to give you some sense of the possible range. You should look at the specific event type for its particular IDs. For example, there are a whole lot of KeyEvent types. Most of the time, you can avoid them by using a text widget, though.)
Every ComponentEvent also has a getComponent() method that specifies the component that the event happened to. MouseEvents, WindowEvents, and KeyEvents are all ComponentEvents. (You can see the complete hierarchy on the Java api site.)
A mouse click is typically handled by the mouseClicked event handler. This event handler takes a MouseEvent as a parameter: mouseClicked(MouseEvent mickey). The mouseClicked method, like every other method, must belong to an object. In this case, mouseClicked(MouseEvent mickey) is a part of the java.awt.event.MouseListener interface. So the mouse click will be handled by an instance of a class that implements MouseListener.
Every AWTEvent type has an associated Listener type. This means that when the AWT event occurs -- the mouse is clicked or the key is pressed, etc. -- there's a type of object equipped to handle that event. (Actually, MouseEvent is an exception, as it has two associated listener types: MouseListener, which handles clicks, entry and exit, presses and releases, and MouseMotionListener, which handles drags and moves. Most event types only have one Listener.)
For example, the java.awt.TextField component class generates an ActionEvent whenever you hit the return key. (A TextField is a widget that allows you to enter a single line of text.) ActionEvents are handled by ActionListeners. Here is the definition of the ActionListener interface:
public interface ActionListener extends EventListener {public void actionPerformed( ActionEvent ae );}
We can implement this interface in a class that provides an actionPerformed method with a body:
public class FieldHandler implements ActionListener {public void actionPerformed( ActionEvent ae ) { TextField f = (TextField) ae.getSource(); System.out.println( f.getText() ); }
In this method, we've used the ActionEvent's getSource() method (inherited from java.awt.EventObject, if you go looking for it). Since ActionEvent isn't a ComponentEvent, it doesn't have a getComponent() method. The getSource() method tells you which object generated the event. In this case, we blindly assume that it was a TextField. (If we're wrong, we'd get a class cast exception. It might be wise to check this explicitly before making the cast.) If it is, we can use the TextField's getText() method to find out what text the user just typed in. Then, we print it out (just to show that we can).
A TextField only generates ActionEvents when the user types the return (or enter) key. If you type without hitting return, lots of KeyEvents and TextEvents are generated. Which you choose to intercept depends on which things you care about. The TextField takes care of things like having the characters you type appear on the screen, making the delete key work, etc., all by itself.
So far, so good. However, we haven't specified how the FieldHandler gets the event in the first place. Of course, part of the story is that Java's event manager identifies that a carriage return has been hit in the text field and generates an appropriate ActionEvent. But this event happens to the TextField; how does the FieldHandler get hold of it?
The answer is that the TextField has to specify to Java that the FieldHandler will take care of its (action) events for it. This is accomplished with the TextField's addActionListener method, which takes an ActionListener as an argument and tells Java that that particular ActionListener will handle ActionEvents on behalf of this TextField. (One ActionListener can in principle handle events for multiple TextFields, and one TextField can in principle have multiple ActionListeners, but they typically go in pairs.)
We might, in fact, want to establish this relationship when the TextField is first created. We could do this in the code that creates the TextField:
TextField txt = new TextField(); FieldHandler f = new FieldHandler(); txt.addActionListener( f );
Alternately, we could make our own class -- our own specialized TextField -- that is born with its own FieldHandler:
public class HandledTextField extends TextField {public HandledTextField() { FieldHandler f = new FieldHandler(); this.addActionListener( f ); }}
Not much too it, but now this TextField will print out (on the command line) whatever you type to it (provided that you end with a carriage return)!
There are add methods for every type of listener that is appropriate to a component. The Nutshell book has a nice list of which methods are implemented by which classes; it is a good place to look for reference details.
The ActionListener defined above will do the trick quite nicely for our TextField. The ActionListener interface only had a single method to implement. Other listener interfaces are more complex, though. For example, the MouseListener interface defines five methods:
public interface MouseListener extends EventListener {public void mouseClicked( MouseEvent mickey ); public void mouseEntered( MouseEvent mickey ); public void mouseExited( MouseEvent mickey ); public void mousePressed( MouseEvent mickey ); public void mouseReleased( MouseEvent mickey );}
(Note the not-entirely-coincidental similarities between these names and the event IDs defined by java.awt.event.MouseEvent.)
It would be rather annoying to have to implement each of these methods just to be able to write the one (mouseClicked) that we need. Our class definition might say
public class MouseHandler implements MouseListener {public void mouseClicked( MouseEvent mickey ) { // Interesting code goes here... } public void mouseEntered( MouseEvent mickey ) {} public void mouseExited( MouseEvent mickey ) {} public void mousePressed( MouseEvent mickey ) {} public void mouseReleased( MouseEvent mickey ) {}
Not very concise or beautiful, but necessary if we are to implement the interface directly.
To avoid this ugliness, java.awt.event gives us a more concise way of saying the same thing. There is a class called MouseAdapter that implements MouseListener, providing all of the (non-interesting but also non-abstract) method bodies required. We can just extend MouseAdapter in our class, eliminating the need to implement all of the extra (extraneous) methods:
public class MouseHandler extends MouseAdapter {public void mouseClicked( MouseEvent mickey ) { // Overrides MouseAdapter's mouseClicked method. // Interesting code goes here... }
Much nicer!
Each of the listener interfaces that declares more than one method has a corresponding adapter class.
Let's return to the TextField handler class from above. There are still some improvements in functionality that we can make.
First, we can clean this code up a bit by making the FieldHandler aware of which TextField it was supposed to be handling. Here are some new definitions:
public class FieldHandler implements ActionListener {private TextField f; public FieldHandler( TextField f ) { this.f = f; } public void actionPerformed( ActionEvent ae ) { System.out.println( this.f.getText() ); }
public class HandledTextField extends TextField {
public HandledTextField() { FieldHandler f = new FieldHandler( this ); this.addActionListener( f ); }
}
Note the reference to this.f, rather than f, in the System.out.println statement. Also note the added constructor argument in the call to new FieldHandler( this ); in the HandledTextField constructor.
These changes improve the code in the following ways: Because the FieldHandler knows whose ActionEvent it has received, it doesn't have to check which Object generated the event. This avoids the dangerous cast (or the lengthy checking for cast validity). The two objects are now intertwined in a way that reflects their intimate bonding.
In fact, we can exploit a special Java mechanism called inner classes to make this relationship even tighter. Inner classes are a relatively advanced feature of Java 1.1, and they add only to the aesthetics of this program, not to its functionality. (They do provide a little bit more protection for code from unanticipated use.) Using inner classes, we can write:
public class HandledTextField extends TextField {public HandledTextField() { FieldHandler f = new this.FieldHandler(); this.addActionListener( f ); } class FieldHandler implements ActionListener {public void actionPerformed( ActionEvent ae ) { System.out.println( HandledText.this.getText() ); }}
Since FieldHandler is defined inside HandledTextField, it has access to its containing instance directly (through HandledTextField.this), and we can eliminate the constructor argument (and the constructor itself!) for FieldHandler. Pretty neat, huh?
This course is a part of Lynn Andrea Stein's Rethinking CS101 project at the MIT AI Lab and the Department of Electrical Engineering and Computer Science at the Massachusetts Institute of Technology.
Questions or comments: