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:
You will re-use this output look for this lab:
Do this...
RandomWalkerGUIDriver#displayWalk()
into
BetterGUIDriver
.RandomWalkerCLIDriver#displayWalk()
into
BetterGUIDriver
.In either case, write a method stub for
BetterGUIDriver#makeWalker()
which returns
null
.
Do not put any meaningful code in makeWalker()
.
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?
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.
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.
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:
|
|
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 in this GUI is the way that data is received. Most GUIs are event driven ; 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.
The main()
method of BetterGUIDriver
needs to implement this algorithm:
Algorithm of BetterGUIDriver#main(String[])
- Let
gui
be a newBetterGUIDriver
.- Set
gui
's default close operation to be "exit on close".- Pack
gui
.- 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()
constructorBetterGUIDriver
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
visibleyes 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 JOptionPane
s and
JFrame
s 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).
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()
- Set
myNumberOfSteps
to be a text field with a default value of"20"
.- Set
myExtent
to be a text field with a default value of"2"
.- Set
mySizeOfField
to be a text field with a default value of"50"
.- Let
walkButton
to be a button with"Walk"
for a label.- Let
numberOfStepsLabel
be a label with the text"Number of steps:"
.- Let
extentLabel
be a label with the text"Maximum stepsize:"
.- Let
sizeOfFieldLabel
be a label with the text"Size of field:"
.- Let
layout
be a new flow layout.- Set my layout to be
layout
.- Add
numberOfStepsLabel
to me.- Add
myNumberOfSteps
to me.- Add
extentLabel
to me.- Add
myExtent
to me.- Add
sizeOfFieldLabel
to me.- Add
mySizeOfField
to me.- Add
walkButton
to me.- Let
walkAction
be a new walk action using myself as an argument.- Add
walkAction
towalkButton
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 ofLayout
set my layout yes setLayout(Layout)
JFrame
add a widget to me yes add(Component)
JFrame
Look at all of the inheritance here!
BetterGUIDriver
, that means I'm also
a JFrame
; I'll invoke a lot of methods directly.FlowLayout
is a subclass of
Layout
, it can be passed to
JFrame#setLayout(Layout)
without any hassles.JTextField
, JLabel
, and
JButton
are all subclasses of Component
,
their instances can be used as arguments to
JFrame#add(Component)
.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!
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...
WalkAction
which implements
ActionListener
.WalkAction#actionPerformed(ActionEvent)
.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...
myBetterGUIDriver
(of type
BetterGUIDriver
) in WalkAction
.WalkAction
which receives
a BetterGUIDriver
and initializes the instance
variable appropriately.Now WalkAction#actionPerformed(ActionEvent)
seems a
bit to easy:
Algorithm of WalkAction#actionPerformed(ActionEvent)
- Invoke
displayWalk()
onmyBetterGUIDriver
.
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()
continuedDescription 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()
continuedDescription 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.
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()
- Let
text
be the text frommyNumberOfSteps
.- Let
numberOfSteps
be the parsed integer fromtext
.- 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 aString
yes parseInt(String)
Integer
return the integer yes return
statementbuilt-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()
- Let
numberOfSteps
be my number of steps.- Let
extent
be my extent.- Let
sizeOfField
be my size of field.- Let
walker
be a newRandomWalker
constructed withnumberOfSteps
,extent
, andsizeOfField
.- 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.
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.
action listener, button, event, event driven, event handler, frame, label, layout, listener, program driven, text field