Introduction |
In this lecture we will examine the most important aspects of a variety
of simple Java classes that act as controllers, extending the
Component class:
JButton, JTextField, JTextArea (and JScrollPane),
JCheckBox, JRadioButton, and JSlider.
We have already learned about some view-related aspects of these components:
how they appear in the view of a GUI, meaning how we can specify their
appearance (e.g, color) and layout.
Now we will learn about some controller-related aspects of these components:
how the user can act upon them to control the state in the model of the
GUI.
In the case of the JRadioButton and JSlider components, we will examine two extensions (subclasses) of the JPanel class that I have written: RadioButtonPanel and PercentSliderPanel. Each generalizes and simplifies the creation and use of these built-in components; the code in each also illustrates a typical use of that component. The code for creating and managing all these components (and specifying how they interact with the model) is typically defined in a Controller class. This is not because it MUST be defined there, but because it is REASONABLE to separate all the control information from the View class. In this case, two smaller classes are better than one bigger one. Typically, the build method in the View class will call methods in the Controller class to create components that it will position in the view; inside the methods written in Controller class is code that will instruct these components as to how to interact with the Model class. We will also discuss the concept of a control pushing and pulling information, and see in general how to accomplish each with various control components. Pushing is when the control calls the model to tell it about an action/value the user has performed. Pulling is when the model calls the control asking it for its current value. Some controls naturally push information, other naturally have information pulled from them; some naturally allow both. Finally, there is a Controllers Demonstrations folder that contains various project folders that illustrate concretely the code described in this lecture. You can easily change the code each contains to experiment with the controllers. Code shown there can be cut and pasted into other programs that use similar controller components. Please download, unzip, and examine its project folders. |
Listeners and their Interfaces, Parameters, and Adapters |
The most important operation that we can perform on a component is to
add a listener object to it.
Whenever the user acts on a component, Java generates an event for it to
handle.
Java then uses the listener object for that component to handle the event.
For example, in the case of clicking the mouse while it is positioned over
some button in the view, Java generates an event representing the action of
pressing that button.
Java then uses the listener added to that button to handle that event.
If no listener has been added, pressing the button has no effect.
To make this concrete now, here is some code that contains all the needed
Java elements; we will discuss each in more detail soon.
When Java senses an "action event" (that the user has pressed a button), Java processes the event by calling the actionPerformed method in any listener objects that were added to the component that the user acted on. Typically, the method in the listener object will itself call a method in the model to update the model's state. Thus, a listener object is an indirect connection from the view to the model It acts as a bridge between the user (acting on the component) and the model: Java calls a method in the listener object, and that method calls some method in the model to change its state accordingly. There is no simpler way in Java to create such a connection. Most interfaces (all the ones that specify more than one method) also have matching adapter classes in the standard Java library, which implement stubs for all the required methods in the interface. We will also use listeners to sense a mouse's movement and process other mouse events (e.g., clicking at locations which do not contain buttons).
To make this concrete now, here is some code that uses a MouseAdapter
instead of the MouseListener interface (that it implements) to specify
just one aspect of the mouse: what to do on a click.
The MouseListener interface actually specifies five methods, which are
all stubbed in the adapter.
So, Java watches for events on a component,and when it detects such an event, it executes the appropriate code for that event. We often write such code to push information to the model (by calling one of its methods, possibly supplying it with relevant parameters). A listener is typically defined by an interface that specifies the methods that it contains (the different events that it must handle). Java allows us to add a variety of listeners to components, most using the anonymous class syntax that we have discussed for use with interfaces and adapters.
Finally, we will eventually see how to use an ActionListener object to add a special action to another action listener (which is used in the case of controller components that both push and pull values). |
JButton |
A JButton object is a complete controller-component.
We can use it to illustrate the general form in which most controllers
are created, and to which listeners are added.
Typically, code in the View class will construct a controller implicitly, by calling a "get" method in the Controller class. In the main GUI that I wrote for this lecture, the code in the View class looks like all.add(controller.getButton("B1"));which adds to the JPanel named all (in the appropriate spot) the button returned by calling controller.getButton (labeled with B1). If we needed to modify the appearance of the JButton, we might instead write something like JButton temp = controller.getButton("B1"); temp.setBackground(Color.green); ... call other methods on temp to change its visual appearance all.add(temp);Note that all method calls that change its visual characteristics should appear in the View class,not the Controller class: this is a convention, not a Java requirement. The code for getButton in the sample Controller class looks like JButton getButton(String label) { final JButton b = new JButton(label); //final is important b.addActionListener( new ActionListener() { public void actionPerformed(ActionEvent e) { System.out.println("Debug-Controller: " + "Button pressed: " + b.getText() ); //System.out.println(" Action-Event = " + e); model.buttonPress(b.getText()); }}); return b; }First this method constructs a JButton using the parameter label to label it; then it stores a reference to it into b (to which an action listener is added, and which is ultimately returned). This local variable must be declared final to be used inside the actionPerformed method in the anonymous class implementing the ActionListener interface: otherwise the compiler will detect this error and report it in a clear manner. In between construction and return, the method adds an action listener to b. It uses the special syntax to create an object from an anonymous class implementing the ActionListener interface; this is similar to creating objects from an anonymous class which extends some adapter; Java allows this syntax to be used for both purposes. In the case of using the name of an interface, the body in brackets must define ALL the methods specified in the interface; in an adapter, only the ones overriding the stubbed methods must be written. The ActionListener interface defines just one method: actionPerformed, which will be called whenever we press the button (that is the action/event that it is listening for). The implementation here, of this method, is to print a debugging message on the console (another debugging print is commented out; you can uncomment it to see what information an ActionEvent stores, when it is converted to a String and printed), and finally to push information to the model by calling its buttonPress method (supplying the button's label as a parameter). Note that the getText method on a JButton returns the String that is the label of that button.
Of course, the anonymous class can also declare and manipulate instance
variables.
We can define a special button whose label changes from one String to
the next, each time it is pressed (and back to the first eventually).
What is interesting about this controller-component is that the class that implements ActionListener not only defines an actionPerformed method, but it also declares an instance variable (currentIndex), which is initialized and used subsequently in the in the actionPerformed method: each time the button is pressed, the index is label incremented (modularly). Because the parameter, labels is used inside the anonymous class, it too must be declared final. This example begins to illustrate the power of using more complicated classes that implement the ActionListener interface. For example, we could use the actionPerformed method to access/update any instance variables stored in the Controller class (since the anonymous classes are inner classes). In this way, the buttons could communicate with each other: for example, pressing one button could allow it to change the labels on many other buttons. Also, the buttons could store information in the Controller class that is pulled by the Model class: by calling some method in the Controller class to retrieve their values. The explanation for why we must use final is both simple yet difficult to understand. As we have seen in inner classes (and the anonymous class is an inner class) they can refer to all the variables specified in any outer methods and outer classes. That is why we can refer to the JButton b and the parameter String[] labels inside the methods in the inner classes. If these names were instance variables in the Controller class, there would be no problem referring to them (as is the case in some of the controllers below). But because these names refer to local variables and method parameters, which disappear when the method returns, Java requires that they be declared final. Then, Java stores the values in these variables/parameters (either primitive, or references to the objects) in some special instance variables that it creates solely for the anonymous class. Because the outer variables are declared final, we know that they can never have their values changed; thus, the specially created/initialized instance variables will always store the "right" values (whether primitive or reference to an object). Understanding this paragraph is difficult, as the idea is subtle; but it illustrate behavior consistent with what we know about Java. Finally, we can call doClick on any button, which is the program's way of clicking the button. We can also call setActionCommand on a button (using a String parameter); doing so allows us to associate the String with the button. By this mechanism, we can associate different Strings with two buttons that happen to have the same label. We can call getActionCommand on the ActionEvent parameter supplied to the actionPerformed method. |
JTextField |
A JTextField object is another complete controller component.
We will see that, like a JButton, its is constructed, instrumented
with listeners, and returned.
The code for getJTextField in the sample Controller class looks
like
JTextField getTextField(final String id, String initialContents, int displaySize) { final JTextField tf = new JTextField(initialContents,displaySize); tf.addActionListener( new ActionListener() { public void actionPerformed(ActionEvent e) { System.out.println("Debug-Controller: " + "Text field activated: " + tf.getText()); //System.out.println(" Action-Event = " + e); model.textFieldEntered(id,tf.getText()); }}); tf.addFocusListener( new FocusAdapter() { public void focusLost(FocusEvent e) { System.out.println("Debug-Controller: " + "Text field loses focus: " + tf.getText()); //System.out.println(" Focus-Event = " + e); model.textFieldEntered(id,tf.getText()); }}); return tf; } The implementation of the actionPerformed method (called when the user pressed "enter" while typing information into the text field), is to print a debugging message on the console and to push information to the model by calling its textFieldEntered method (supplying the id and content of the text field). The id can help the model understand what to do with the content. Note that the getText method on a JTextField returns the String that is typed in the text field. If it is supposed to be an int value, either the control or model can try to convert its value (by calling Integer.parseInt); whichever is more easily able to handle a NumberFormatException. Likewise a focus listener is added to this controller-component. It calls exactly the same model method in the focusLost method. So, the textFieldEntered method in the model doesn't know whether it was called because the user pressed "enter" or because the user moved the cursor outside the text field: in each case it processes the text that the user has typed (and, it often makes no difference). The focusLost method is defined in the anonymous sublcass extending FocusAdapter; it is just one of two methods that deal with the foucus -the only one relevant to this application. |
JCheckBox and JRadioButton |
A JCheckBox object is a complete controller component, and can be
treated similarly to a JButton.
It is often the case in GUIs that a series of check boxes are collected
together.
A JRadioButton is similar to a JCheckBox, but it has one special
wrinkle: whereas any number of boxes can be checked in a series of check
boxes, typically only one of a series or radio buttons should be selected at
any time.
When a new button is selected, whichever button is currently selected becomes
deselected.
We accomplish this behavior by constructing a ButtonGroup object (from the javax.swing package) and adding to it, any number of JRadioButtons (or JCheckBoxes, for that matter). Then, whenever on button is selected, any currently selected button will first be deselected. This behavior is illustrated in a section below. See the RadioButtons class which I have written and uses a ButtonGroup to implement this "only one selected at a time" behavior. A ButtonGroup has no visual appearance; it exists only as a collection of components with a logical purpose. It works by adding its own action listener to each component in the collection. When a component is selected, the actionPerformed it defines iterates through all the other components in the collection and ensures that they are not selected. |
JTextArea |
A JTextArea object is a complete controller component, although it
is most frequently embedded in a JScrollPane as illustrated below.
We can treat it similarly to a JTextField, regarding both its
view-related aspects (e.g., setEditable) and its controller-related
aspects (e.g., adding listeners).
Unlike a JTextField which is always one line (scrollable horizontally if
it is too big to display), a JTextArea contains multiple lines.
In its simplest input/output uses, we can let the user type information into a JTextArea and later retrieve that input, or we can append text into this area (include "\n" to genrate new lines) which is displayed for output. We can do both, but that gets tricky and requires reading more information in this component's Javadoc.
We can create a scrollable text area as follows.
|
JPanel of Radio Buttons |
A series of JRadioButton objects are typically collected together and
processed as a group, to form a single controller component.
To make such a grouping easier to put in a GUI, I have written a simple
RadioButtonPanel class to construct and process a group of radio
buttons; the body of this class also shows how to use JRadioButtons.
The class has only two constructors (we will use only the simpler one), a
getSelectedValue method, and a selectedValue instance variable.
For ease of use, I have made RadioButtonPanel a subclass of
JPanel.
The header of the constructor to this class is defined as public RadioButtonPanel(String[] labels, final ActionListener action)It specifies a first parameter that is a String array (where each entry is the label of a radio button), and an ActionListener that specifies the actionPerformed method to execute when each radio button is pressed; passing null to the second parameter means to perform no special action when a radio button is selected. Recall that we can easily specify a String array filled with literals using Java. An example is new String[]{"A","B","C"}, which returns a reference to the String[] object pictured below |
  |
The sample Controller class defines the following method to construct
and return a radio button controller component.
RadioButtonPanel getDecisionPanel() { return decisionPanel = new RadioButtonPanel( new String[]{"Yes", "No", "Maybe So"}, new ActionListener() { public void actionPerformed(ActionEvent e) {model.decisionPanelUpdate(decisionPanel.getSelectedValue());} }); };This method constructs a RadioButtonPanel, stores a reference to it in the instance variable decisionPanel (defined in the Controller class), and returns that reference (making use of the full semantics of the = operator). The panel will contain three radio buttons, with the labels Yes, No, and Maybe So. When pressed, each radio button will perform the action specified: calling the decisionPanelUpdate method in the model, pasing to it the currently selected radio button value (the label of the selected radio button) in the decisionPanel instance variable. To accomplish all this the body of the constructor for the RadionButtonPanel is defined as ... ActionListener updateSelectedValue = new ActionListener() { public void actionPerformed(ActionEvent e) { System.out.println("Debug-Controller: " + "RadioButtonPanel: " + e.getActionCommand()); //System.out.println(" Action-Event = " + e); selectedValue = e.getActionCommand(); if (action != null) action.actionPerformed(e); }}; ButtonGroup exclusive = new ButtonGroup(); for (int i=0; i<labels.length; i++) { JRadioButton temp = new JRadioButton(labels[i]); add(temp); exclusive.add(temp); temp.addActionListener(updateSelectedValue); } ...This code first stores a reference to an object from an anonymous class that implements ActionListener. This is the action listner that will be executed by each radio button. Notice that it performs two main actions: first it uses e.getActionCommand() to return, and store into an instance variable, the label of the selected radio button; then it calls the actionPerformed method in the action listener passed to the parameter of this method, if non-null (which we saw above called the decisionPanelUpdate method in the model). Once this action listener is constructed, we construct a ButtonGroup to ensure that only one of the radion buttons in this group shows as selected at any given time. Then, for each label in the String[], we
Although this class is sophisticated on the inside, it is pretty easy to use (at least for an intermediate Java programmer) from the outside. Also note that the class defines a getSelectedValue method, so that the model can access the selected radio button from this panel any time it needs it. The model stores a reference to the controller, and can refer to the package-friendly instance variable decisionPanel. Thus, the model can call: controller.decsisionPanel.getSelectedValue() to retrieve the label of the current the pressed radio button (returnining null if none has been pressed; another way to indicate such a problem is to throw an exception). So, this controller-component supplies easy to use push AND pull behavior. By specifying an ActionListener in the constructor, we can have any selection push information to the model. By calling the getSelectedValue method for this panel, the model can pull whatever value it needs from this panel. |
JPanel of JSlider |
Although a JSlider object can be used as a complete controller component
(albeit one with many complicated aspects), I have written a simple
SliderPanel class to construct and process a special kind of
JSlider (and a label for it).
This class has only a constructor, a getSelectedValue method, and a
selectedValue instance variable.
In many ways this class is similar to the RadioButtonPanel class in how it works and how it is used by programmers, but it is more complicated internally. It is beyond the scope of this course to explain this class in detail, but I welcome students looking at my code and reading the Javadoc for the JSlider and HashTable classes to understand what I am doing here. |
Mouse Listeners for JPanels |
The getMouseListener method in the sample Controller class
returns a mouse listener that can be attached to any component (often it is
attached to a JFrame or JPanel object (by the View
class).
Mouse listeners can handle the following events: a mouse is pressed, a
mouse is released, a mouse is clicked (a press/release pair happening
within some time limit), a mouse has moved over some component, and a
mouse has moved to not be over some component).
The one I've written extends the MouseAdapter by defining methods to execute when the mouse is clicked, and when the mouse enters/exits the JPanel to which this object is added as a listener. In the former case, the mouseClicked method calls the mouseClick method in the model, passing it the coordinates of the click and a click count (for double clicking events), which it gets from the MouseEvent parameter. In the latter case, the controller itself contains code that changes the shape of the cursor. The MouseMotionListener includes two more methods/events it can handle: the mouse being moved and the mouse being dragged. The mouse project folder in the Controllers Demonstrations folder contains methods defined (just to print information) for all of these events. |
Keyboard Listeners for JFrames |
The getKeyListener method in the sample Controller class returns
a key listener that can be attached to a JFrame object (by the
View class).
The one I've written extends the KeyAdapter by defining a method to
execute when a key is typed (meaning pressed and then released: it combines
these into one event rather than treating them separately).
The keyTyped method calls the keyTyped method in the
Model, passing it the character that was typed, which it gets from the
KeyEvent parameter.
Note that this method works for Java 1.3 only. Java 1.4 requires a different way to attatch a key listener to a JFrame (and I haven't debugged how to do it yet).
|
The Controller Testbed |
In the grabbag project folder, the Application class, as always,
is very simple.
It
If we look in the View class, we will find that it creates a JPanel whose layout manager is a vertical box, to which controller-components (gotten from the Controller class) are added. In the case of the mouse and key listeners, these controllers are added directly to this JPanel and the JFrame that is the View respectively. If we look in the Model class, we will see that all but the last method just print their name and their parameters; these methods are called by the controller-components, which push information to them. By keeping the view and model trivial, we can focus on the controller. In contrast, the pullPressed method (which is called by the bottom controller-component) illustrates how the model can pull information from the controller. It displays the values currently stored in two radio button panels and a slider. To do so, it refers to package-friendly instance variables in the Controller class, calling their getSelectedValue methods. Technically, one could pull the value in a text field too (via getText), but I did not write the code to do so; I could have stored this information in package-friendly instance variables too, or in a large map from each text field (or some String label) to its contents. Finally, the Controller class is filled with methods that construct controller components, set them up to push information to the model, and return them for placement in the view. Thus, the controller class provides the stated bridge between the view and model. We will briefly go over in class the code for the controller-components discused above, and see how they are created in the View class and push information to the Model class; we will also see how we can make changes to and generally experiment with the components supplied. There is a Controllers Demonstrations that we can use to illustrate all these concepts with a variety of controllers. We can also easily change the code it contains to experiment with other controllers. Code here can be cut and pasted into other programs that use similar controller components. Please download, unzip, and examine this file. The GUI it creates looks like
|
Problem Set |
To ensure that you understand all the material in this lecture, please solve
the the announced problems after you read the lecture.
If you get stumped on any problem, go back and read the relevant part of the lecture. If you still have questions, please get help from the Instructor, a CA, or any other student.
|