Hands on Testing Java: Lab #G3b

Textfields, Labels, and Buttons

Introduction

This GUI exercise is a continuation of GUI #3a.

Do this...
Read (or re-read) GUI #3a to understand the problem worked on in this lab exercise.

Do this...

Do this...
Create a driver class BetterGUIDriver; be sure to declare a stub for main(String[]).

In GUI #3a you (would have) built this look for the output:

view of GUI

You will re-use this output look for this lab:

Do this...

In either case, write a method stub for BetterGUIDriver#makeWalker() which returns null.

Do not put any meaningful code in makeWalker().

Yet Another View (Without Changing the Model)

The input so far has been very clunky. Usually a GUI will provide fields and checkboxes and buttons for input; it doesn't prompt you for input with dialog boxes.

Wouldn't your user appreciate something along these lines?

view of GUI

Change one of those fields, hit the "Plot walk" button, and a new random walk could be generated. If this view were to start with those default values, that could be greatly appreciated, too.

Take a look at the GUI interface above. You can type input into the text fields; labels indicate what the text fields are for; and a button will plot a walk (presumably). Everything you see in that picture is provided to you by the Swing library.

It's a Frame!

This new GUI view is itself going to be a frame, Java's term for an application that uses the Swing library. Since this view is a frame, use inheritance: BetterGUIDriver extends JFrame.

Do this...
Implement the inheritance.

The Text Fields

BetterGUIDriver will be based on the GUI itself; this is in contrast to the other drivers which were based on the data itself. Compare and contrast the descriptions of the attributes:

Attributes of RandomWalkerGUIDriver
Description Type Name
the number of steps int myNumberOfSteps
the extent of a step double myExtent
the size of the field double mySizeOfField
Attributes of BetterGUIDriver
Description Type Name
a text field for the number of steps JTextField myNumberOfSteps
a text field for the extent of a step JTextField myExtent
a text field for the size of the field JTextField mySizeOfField

All of the GUI widgets come from the javax.swing package.

A traditional GUI needs to keep track of the input widgets, not the data. As long as the GUI has access to the widgets, they can always be asked for their input. For now it looks like we have to do more work (i.e., deal with a button), but in the long run, we'll do away with the screen and keyboard which makes some things much easier.

Do this...
Implement the attributes of BetterGUIDriver as instance variables.

The Major Change

The major change in this GUI is the way that data is received. Most GUIs are event driven wikipedia; this is in contrast with CLIs and with the GUIs you've written so far which have been program driven.

Drivers that are program driven work off of the code you type in for the program. You are complete control of the control flow.

With event-drivent programming, you set up a framework, and then you let the event handler handle events and take care of the control flow. You give up complete control of your program so that you don't have to worry about all the minutia of dealing with the widgets.

A Event-Driven Driver

The main() method of BetterGUIDriver needs to implement this algorithm:

Algorithm of BetterGUIDriver#main(String[])
  1. Let gui be a new BetterGUIDriver.
  2. Set gui's default close operation to be "exit on close".
  3. Pack gui.
  4. Make gui visible.

These steps may seem familiar to you if you did GUI #3a; you'll find them in RandomWalkerGUIDriver#walkDisplay(). However, that method creates a frame for output. BetterGUIDriver#main(String[]) is a JFrame for input.

Objects of BetterGUIDriver#main(String[])
Description Type Kind Name
the gui BetterGUIDriver variable gui
"exit on close" operation constant int constant JFrame.EXIT_ON_CLOSE
Operations of BetterGUIDriver#main(String[])
Description Predefined? Name Library
create a new BetterGUIDriver yes BetterGUIDriver() constructor BetterGUIDriver
set the default close-operation yes setDefaultCloseOperation(int) JFrame
resize the JFrame to preferred sizes (i.e., "pack" it) yes pack() JFrame
make a JFrame visible yes setVisible(true) JFrame

Keep the inheritance in mind: anything that belongs to the JFrame class also belong to a BetterGUIDriver because a BetterGUIDriver is a JFrame.

Do this...
Implement BetterGUIDriver#main(String[]). Everything should compile, but it won't look like anything when it runs.

The last statement of this method gets you off the hook for controling the program. In fact, you can get into serious trouble if you do try to take control! Once you make your frame visible, do not invoke any more methods. All of the rest of your code will have to go into listeners which will listen for events.

This does mean that there's a potential problem with the code in RandomWalkerGUIDriver from GUI #3a; strictly speaking, you shouldn't use JOptionPanes and JFrames from a program-driven driver. But since that driver is so controlled, you probably won't notice any problems (although that won't stop some Java programmers from taking offense).

Constructing a Real GUI Application

There are three tasks to be done when constructing a GUI application: create the widgets, add the widgets to the application's frame, and add listeners to the widgets (at least some of them) to be notified of any events. We end up with a rather long algorithm!

Since you'll have so many widgets in this one frame, you need to worry about the layout of the frame. Layouts in Java are, simply put, a pain. Some of the layout managers require too much of your attention; others don't really work very well. Fortunately, for BetterGUIDriver, we can use one of the easiest layout managers: FlowLayout. This layout manager makes the widgets flow "naturally" in the frame.

Remember: internal perspective indicates clearly when I'm using instance variables or my own instance methods. Be sure to call my own instance methods directly (even (especially?) if they are inherited from JFrame).

Algorithm of BetterGUIDriver#BetterGUIDriver()
  1. Set myNumberOfSteps to be a text field with a default value of "20".
  2. Set myExtent to be a text field with a default value of "2".
  3. Set mySizeOfField to be a text field with a default value of "50".
  4. Let walkButton to be a button with "Walk" for a label.
  5. Let numberOfStepsLabel be a label with the text "Number of steps:".
  6. Let extentLabel be a label with the text "Maximum stepsize:".
  7. Let sizeOfFieldLabel be a label with the text "Size of field:".
  8. Let layout be a new flow layout.
  9. Set my layout to be layout.
  10. Add numberOfStepsLabel to me.
  11. Add myNumberOfSteps to me.
  12. Add extentLabel to me.
  13. Add myExtent to me.
  14. Add sizeOfFieldLabel to me.
  15. Add mySizeOfField to me.
  16. Add walkButton to me.
  17. Let walkAction be a new walk action using myself as an argument.
  18. Add walkAction to walkButton as an action listener.

Here are the objects and operations for all but the last two statements:

Objects of BetterGUIDriver#BetterGUIDriver()
Description Type Kind Name
my number-of-steps text field JTextField instance variable myNumberOfSteps
my extent text field JTextField instance variable myExtend
my size-of-field text field JTextField instance variable mySizeOfField
the walk button JButton variable walkButton
the label for the number-of-steps text field JLabel variable numberOfStepsLabel
the label for the extent text field JLabel variable extentLabel
the label for the size-of-field text field JLabel variable sizeOfFieldLabel
the flow layout FlowLayout variable layout
Operations of BetterGUIDriver#BetterGUIDriver()
Description Predefined? Name Library
create a text field with a default value yes JTextField(String) JTextField
create a button with text on it yes JButton(String) JButton
create a label with text on it yes JLabel(String) JLabel
create a new flow layout yes FlowLayout() FlowLayout, a subclass of Layout
set my layout yes setLayout(Layout) JFrame
add a widget to me yes add(Component) JFrame

Look at all of the inheritance here!

Do this...
Implement BetterGUIDriver#BetterGUIDriver() except for its last two statements. Compile, and run this driver.

Everything in the driver should work except for the button. Now, a non-working button seems like a lot, but consider what is working without you really trying:

All of these things (the look of the output, entering data, the order of entering data) were determined precisely in previous programs. Not now!

Listening to Events

The key here is that you are not in charge of this program. Once you make an application visible, you hand over complete control to the event handler. The event handler hands the control of the program until the program ends. So the key now is to get the event handler to let you know when something interesting happens. You're told of interesting events (e.g., a button pushed, text typed in, etc.) through listeners.

The Swing library has already added some listeners to the widgets in your GUI. There's a listener for the button that reacts when the button is pressed so that the button looks like its pressed. There's a listener for each text field that processes every keypress made in the text field. There's a listener that redraws the application when you resize it.

Thankfully, we don't have to write any of this code!

Instead, you want to listen to events that will trigger some action in your model (most likely).

You don't need to listen to the text fields because the Swing library is already listening to them to handle all the mundane (and obvious) details. You'll want to get the text from the text fields, but that doesn't involve listening to them.

So, for you, the only widget worth listening to is the button.

A "listener" is a thing, a noun. Nouns are implemented as classes. The event handler needs to be able to execute a particular method. So: you need to implement a class and the event handler demands a particular method. Answer: an interface!

Buttons use an action listener specified by the ActionListener interface (from the java.awt.event package). This interface requires just one method:

public void actionPerformed(ActionEvent event)

The ActionEvent parameter specifies the details of the event, but it often can be safely ignored. Pressing the button is pressing the button, and the mere fact that your method is called is all the information you need from the event handler.

So you need to write a class that implements the ActionListener interface. Let's call this class WalkAction.

Do this...

Let's design the WalkAction#actionPerformed(ActionEvent) method.

Behavior of WalkAction#actionPerformed(ActionEvent)

I am a walk action. When my button is pressed, I will ask the BetterGUIDriver to display a walk using data from its text fields.

Wait a second...

Where is this BetterGUIDriver going to come from? It cannot come as an argument because the interface says that all I will receive is an ActionEvent. I should have this as an instance variable.

Let's try that again...

Behavior of WalkAction#actionPerformed(ActionEvent)

I am a walk action. When my button is pressed, I will ask myBetterGUIDriver to display a walk (using data from its own text fields).

This suggests an instance variable and a constructor:

Do this...

Now WalkAction#actionPerformed(ActionEvent) seems a bit to easy:

Algorithm of WalkAction#actionPerformed(ActionEvent)
  1. Invoke displayWalk() on myBetterGUIDriver.

That's all the behavior asks for!

So the rest of the work is back in BetterGUIDriver#BetterGUIDriver().

Here are the objects and operations for the last two statements of that constructor.

Objects of BetterGUIDriver#BetterGUIDriver() continued
Description Type Kind Name
the walk action that listens to the button WalkAction variable walkAction
myself (to be passed as an argument to the WalkAction constructor) BetterGUIDriver keyword this

As indicated in this table, this is a reserved word in Java. In an instance method or constructor, it refers to myself (i.e., the object being constructed or receiving the message). Usually we don't need this, but when I have to pass myself in as an argument (as with the WalkAction constructor), then I need to use this.

Think of this as an undeclared constant. You cannot set its value; you cannot declare it as your own variable.

Operations of BetterGUIDriver#BetterGUIDriver() continued
Description Predefined? Name Library
create a new WalkAction yes WalkAction(BetterGUIDriver) WalkAction
add an action listener to a widget yes addActionListener(ActionListener) Component

That last operation is nothing but inheritance! Since a JButton is a Component, you can invoke the method on any JButton. Since WalkAction is a ActionListener, you can pass an instance of WalkAction as an argument.

Do this...
Implement the last two statements of BetterGUIDriver#BetterGUIDriver().

Most things are in place.

Do this...
Compile your code, and run the better GUI. When you press the button, you should get a horrible error message (probably about a null-pointer exception) on your console. If you get no message at all, then probably something isn't hooked up right yet.

Constructing a Random Walker

The missing piece is the BetterGUIDriver#makeWalker() method. It exists, but it should be returning null (like a good method stub).

Do this...
Look at the makeWalker() methods from the other drivers. Note where they get their data from.

You cannot go to instance variables directly to get the data for a random walker. The data in BetterGUIDriver is tied up in text fields.

Your code will be a bit more readable if you create some pseudo-accessors.

Algorithm of BetterGUIDriver#getNumberOfSteps()
  1. Let text be the text from myNumberOfSteps.
  2. Let numberOfSteps be the parsed integer from text.
  3. Return numberOfSteps.
Objects of BetterGUIDriver#getNumberOfSteps()
Description Type Kind Movement Name
my number-of-steps text field JTextField instance variable instance myNumberOfSteps
the text from the text field String variable local text
the parsed integer from the text int variable out numberOfSteps
Operations of BetterGUIDriver#getNumberOfSteps()
Description Predefined? Name Library
get the text from a text field yes getText() JTextField
parse an int from a String yes parseInt(String) Integer
return the integer yes return statement built-in

Do this...
Write a pseudo-accessor method for each of the text fields.

I call these "pseudo-accessors" because their named as if they were accessors and to the outside they kind of behave like accessors.

Now BetterGUIDriver#makeWalker() is easier to write.

Algorithm of BetterGUIDriver#makeWalker()
  1. Let numberOfSteps be my number of steps.
  2. Let extent be my extent.
  3. Let sizeOfField be my size of field.
  4. Let walker be a new RandomWalker constructed with numberOfSteps, extent, and sizeOfField.
  5. Return walker.

The first three statements are written using the pseudo-accessors.

Do this...
Implement BetterGUIDriver#makeWalker(). Compile your code, and run this GUI. It should work quite nicely except for one small flaw.

One Small Flaw

There's a small problem: if you close one of the displays of a walk, it stops the entire program. That doesn't seem quite right. What if the user makes a data-entry mistake and wants to close up a bad display? You're forcing the user to restart the whole program. Instead, closing a display should just close that one display. (It does seem fair that closing the frame with the text fields and the button should stop the whole program.)

Do this...
This is fixed by removing one line of code from the program. Do that.

Terminology

action listener, button, event, event driven, event handler, frame, label, layout, listener, program driven, text field