COMP1406/1006 - Design and Implementation of Computer Applications | Winter 2006 |
4 A Traffic Light Application |
Here are the individual topics found in this set of notes (click on one to go there):
4.1 Application Description |
Understanding the interface:
First let us describe the application. The application is a window with the following components:
Here is how the behaviour of the interface is described:
4.2 Developing the Model |
Note:
public class TrafficLight
{
int currentState;
// Constructor that
makes
a red traffic light
public TrafficLight()
{
currentState = 1;
}
// Advance the traffic
light to the next state
public int advanceState()
{
currentState =
++currentState
% 3 + 1;
return
currentState;
}
// Return the state of
the traffic light (as a number from 1 to 3)
public int getState()
{
return
currentState;
}
// Set the state of the
traffic light (as a number from 1 to 3)
// If the integer is out
of range, do nothing
public void setState(int
newState) {
if ((newState
> 0) && (newState <4))
currentState = newState;
}
// Return a string
representation
of the traffic light
public String toString()
{
String[] colours = {"Red", "Yellow", "Green"};
return colours[currentState] + " Traffic Light";
}
}
Making a traffic light is easy:
new TrafficLight();
We can ask for the state or change the state. A state of 1 is
a Red light, 2 is a Yellow light and 3 is a Green light.
The advance method will cause the state to cycle as
follows:
1, 3, 2, 1, 3, 2, 1, 3, 2, 1, ...
Notice that there are no System.out.println()
messages here, nor is there any keyboard input. That is because,
those
are
actually I/O operations and they depend heavily on the type of user
interface
that will be used. We leave that kinda "stuff" out of the model
let
the user interface worry about those issues. (Sometimes we may
have println statements for debugging/testing purposes, but ultimately
these should be removed from the model class code). That way, we
can
"plug-in"
the model into any user interface and the model becomes modular, clean,
shared code. So remember,
Model classes
should NOT:
|
![]() |
4.3 Designing the User Interface Layout |
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.event.*;
public class TrafficLightFrame extends JFrame {
// These are the window's components
private JRadioButton[]
buttons = new JRadioButton[3];
private JButton
advButton;
private JProgressBar
progressBar;
private JSlider
slider;
private JComboBox
actionList;
private JCheckBox
autoButton;
public TrafficLightFrame(String
title) {
super(title);
buildWindow();
setDefaultCloseOperation(EXIT_ON_CLOSE);
setSize(400, 250);
}
// Add all
components to the frame's panel
private void buildWindow()
{
GridBagLayout
layout = new GridBagLayout();
GridBagConstraints
constraints = new GridBagConstraints();
setLayout(layout);
// Add all the labels
JLabel label = new JLabel("Manual");
constraints.gridx = 0;
constraints.gridy = 0;
constraints.gridwidth = 1;
constraints.gridheight = 1;
constraints.weightx = 0;
constraints.weighty = 0;
constraints.fill =
GridBagConstraints.NONE;
constraints.anchor =
GridBagConstraints.NORTHWEST;
constraints.insets = new Insets(5, 5, 0, 0);
layout.setConstraints(label,
constraints);
add(label);
label = new JLabel("Action");
constraints.gridx = 1;
layout.setConstraints(label,
constraints);
add(label);
label = new JLabel("Advance");
constraints.gridx = 2;
layout.setConstraints(label,
constraints);
add(label);
label = new JLabel("Timer Progress");
constraints.gridx = 0;
constraints.gridy = 4;
layout.setConstraints(label,
constraints);
add(label);
// Add the Radio Buttons
ButtonGroup lights = new ButtonGroup();
JPanel aPanel = new JPanel();
aPanel.setLayout(new BoxLayout(aPanel,
BoxLayout.Y_AXIS));
aPanel.setBackground(Color.black);
for (int i=0; i<3; i++) {
buttons[i] = new JRadioButton("", false);
buttons[i].setBackground(Color.black);
lights.add(buttons[i]);
aPanel.add(buttons[i]);
}
buttons[0].setText("Red");
buttons[1].setText("Yellow");
buttons[2].setText("Green");
buttons[0].setForeground(Color.red);
buttons[1].setForeground(Color.yellow);
buttons[2].setForeground(Color.green);
constraints.gridx = 0;
constraints.gridy = 1;
constraints.gridheight = 3;
constraints.fill =
GridBagConstraints.BOTH;
layout.setConstraints(aPanel, constraints);
add(aPanel);
// Make the Actions List
String[] actions = {"Stop", "Yield", "Go"};
actionList = new JComboBox(actions);
constraints.gridx = 1;
constraints.gridy = 1;
constraints.gridheight = 1;
constraints.weightx = 1;
constraints.fill =
GridBagConstraints.HORIZONTAL;
layout.setConstraints(actionList, constraints);
add(actionList);
// Make the Slider
slider = new JSlider(JSlider.HORIZONTAL, 0,
20, 1);
slider.setMajorTickSpacing(5);
slider.setMinorTickSpacing(1);
slider.setPaintTicks(true);
slider.setPaintLabels(true);
constraints.gridx = 1;
constraints.gridy = 3;
layout.setConstraints(slider, constraints);
add(slider);
// Add the auto checkbox button
autoButton = new JCheckBox("Auto Advance");
constraints.gridx = 1;
constraints.gridy = 2;
constraints.fill =
GridBagConstraints.BOTH;
layout.setConstraints(autoButton, constraints);
add(autoButton);
// Add the Advance Picture button
advButton = new JButton(new ImageIcon("RedLight.jpg"));
constraints.gridx = 2;
constraints.gridy = 1;
constraints.gridheight = 3;
constraints.weightx = 0;
constraints.weighty = 1;
constraints.fill =
GridBagConstraints.BOTH;
constraints.insets = new Insets(5, 5, 0, 5);
layout.setConstraints(advButton, constraints);
add(advButton);
// Add the progress bar
progressBar = new JProgressBar(JProgressBar.HORIZONTAL,
0, 8);
constraints.gridx = 0;
constraints.gridy = 5;
constraints.gridwidth = 3;
constraints.gridheight = 1;
constraints.weightx = 1;
constraints.weighty = 2;
constraints.fill =
GridBagConstraints.BOTH;
constraints.insets = new Insets(5, 5, 5, 5);
layout.setConstraints(progressBar, constraints);
add(progressBar);
}
public static void
main(String args[]) {
TrafficLightFrame frame
= new TrafficLightFrame("Traffic Light");
frame.setVisible(true);
}
}
When we run the application at this point, the components all appear on the window, although the interface does not really do anything useful yet. Some interesting things to note are:
4.4 Connecting it all Together |
We begin by adding an instance variable representing the model:
// This is the model
private TrafficLight aTrafficLight =
new
TrafficLight();
This model MUST ALWAYS be synchronized with the user
interface. That is, the user interface should always
reflect perfectly the state of the traffic light. That
means, upon startup, the application should show the default traffic
light state in the radio buttons, the combo box and the picture on the
advance button. To do this, we will make sure that our update() method always updates the
components properly. In fact, it is best to first write the
update() method BEFORE writing any event handlers. That
way, the interface always reflects the model, making debugging the
event handlers easier. Also, it ensures that you put your
code in the correct place.
Always follow
these steps:
|
![]() |
So now let us write our update method. It is often the
case that we write helper methods (one for each component or group of
components) to help keep our code neat and tidy. We will
start by updating the radio buttons, combo box and advance button only,
since they deal directly with the traffic light state:
// Update all relevant
components according to the traffic light state
public void update()
{
updateRadioButtons();
updateComboBox();
updateAdvanceButton();
}
Notice the three helper methods. These methods should put the appropriate data into the components based on the current state of the model. These methods will also be used later on when we make changes to the model and need to reflect these changes in the window.
private
void updateRadioButtons()
{
for (int
i=0; i<3; i++)
buttons[i].setSelected(aTrafficLight.getState() == (i+1));
}
private
void updateComboBox()
{
actionList.setSelectedIndex(aTrafficLight.getState()
- 1);
}
private void updateAdvanceButton()
{
String[] iconNames = {"RedLight.jpg","YellowLight.jpg","GreenLight.jpg"};
advButton.setIcon(new ImageIcon(iconNames[aTrafficLight.getState()-1]));
}
}
Notice a couple of things:
In our constructor, we will call the update() method (after we add the components) so that when the frame is created, the components will all be updated to reflect the initial state of the model.
public TrafficLightFrame(String
title) {
super(title);
buildWindow();
update(); |
setDefaultCloseOperation(EXIT_ON_CLOSE);
setSize(400, 250);
}
OK. When we run our window now, it should represent our
default traffic light state (which is red). So, the top
radio button should be selected, the combo box should indicate "Stop"
and the red light image should be on the advance button.
It is now time to write the event handlers.
How can we get the radio buttons to change the state of the model ? We need to add an action listener for each button. We need to write the following in the constructor in order to register the listener for each Radio Button:
//
Register the JRadioButton Listeners
for (int i=0; i<3; i++)
buttons[i].addActionListener(new ActionListener() {
public void
actionPerformed(ActionEvent e) {
handleRadioButtonPress((JRadioButton)e.getSource());
}
});
Then, we can write the handleRadioButtonPress()
helper method. What should it do ? Do you
remember ... its simple ... change the model ... then call update(). But how does it
change the model ? Well, the model's state should match the
index of the button, shouldn't it ?
// This is the radio
button event handler
private void
handleRadioButtonPress(JRadioButton source) {
for (int i=0; i<3; i++) {
if (source
== buttons[i])
aTrafficLight.setState(i+1);
}
update();
}
Notice that the event handler determines which button was pressed by using getSource() and comparing this to the actual buttons by using the identity operator. Notice also, that we simply ask the model to change its state and then call update(). The update() method will take care of making sure that all other components will reflect the recent model changes. If we were to test things now, we would see that by clicking the radio buttons, the ComboBox and icon on the Advance button would be updated properly to reflect the model changes as desired.
To get this to happen for selecting combo box items as well, we merely add an ActionListener to the ComboBox (again in out constructor):
//
Register the JComboBox Listener
actionList.addActionListener(new ActionListener() {
public void
actionPerformed(ActionEvent e) {
handleComboBoxSelection((JComboBox)e.getSource());
}
});
Now here is the helper method which
simply determines the index of the selected item and updates the model
accordingly
// The ComboBox
selection event handler
private void
handleComboBoxSelection(JComboBox source) {
aTrafficLight.setState(source.getSelectedIndex() + 1);
update();
}
Once again, we can use getSource() to get the
component
(i.e. the combo box). Alternatively, we could have just
accessed
the actionList instance variable (simpler code, but it would be
dependent on the variable name ... which is not too bad). After
making
these changes, both the radio buttons and combo box are "synchronized"
in
that
selection from one causes selection in the other. Neat isn't it ?
There is a slight problem. The setSelectedIndex() call in our updateComboBox() method will
generate an ActionEvent again,
leading to another call to update()
and we are led into an endless loop as with our "todo List"
example. So we need to disable the ActionListener for the combobox
while we are updating. We can store the ActionListener into an instance
variable when we create it and then remove/add it in the update() method:
// Add this new instance variable
private ActionListener
comboBoxListener;
public TrafficLightFrame(String
title) {
...
actionList.addActionListener(comboBoxListener
= new ActionListener() {
public void actionPerformed(ActionEvent
e) {
handleComboBoxSelection((JComboBox)e.getSource());
}
});
...
}
public void update() {
actionList.removeActionListener(comboBoxListener);
updateRadioButtons();
updateComboBox();
updateAdvanceButton();
actionList.addActionListener(comboBoxListener);
}
So what about the Advance button ? Well, it too should advance the state of the model and then update all components. We add this code to the constructor:
advButton.addActionListener(new ActionListener()
{
public void
actionPerformed(ActionEvent e) {
handleAdvanceButtonPress();
}
});
And then add this very simple helper method:
// This is the Advance
button event handler
private
void handleAdvanceButtonPress() {
aTrafficLight.advanceState();
update();
}
Wow! Isn't this getting really easy now. We just ask the
model to do the advancing of state and then call update(). Now
everything
is just peachy.
4.5 Hooking up the Timer |
private Timer aTimer;
Now we must make the timer. To make one, we simply call a
constructor which specifies the number of milliseconds that we would
like between timer events as well as the listener (i.e., event
handler). As it turns out, the listener is simply an
ActionListener ... just as with JButtons. We can write the
following code in our constructor to generate a timer tick twice per
second (i.e., 1secons / 500ms = 2 seconds):
// Add a timer for
automode. Set it to go off every 500 milliseconds
aTimer = new Timer(500, new ActionListener() {
public void
actionPerformed(ActionEvent e) {
handleTimerTick();
}
});
After doing this, the timer has NOT yet started and so no events are
actually generated. We have to explicitly start and/or stop the
timer with separate methods. When do we want the timer to start
anyway ? Well, if the "Auto Advance" checkbox is turned on, we
should start the timer. If it is turned off, we should stop the
timer. We need to add an event handler for the check box.
We can do this by adding the following code to the constructor:
autoButton.addActionListener(new ActionListener() {
public
void actionPerformed(ActionEvent e) {
handleAutoButtonPress((JCheckBox)e.getSource());
}
});
And here is the helper method:
// This is the Auto
button event handler
private void
handleAutoButtonPress(JCheckBox source) {
if (source.isSelected())
aTimer.start();
else
aTimer.stop();
update();
}
As can be seen, when the checkbox is turned on, the timer is
started.
When turned off, it is stopped.
Let us now look at what we must do on each timer event. We
can write the following code as our TimerEventHandler:
// This is the Timer
event
handler
private
void handleTimerTick() {
aTrafficLight.advanceState();
update();
}
Wow! It is the same code as the advance button. In fact, we could have used the exact same event handler ! If you were to test this, you would see the traffic light change state every 0.5 seconds as long as the check box remains selected. Once turned off, the advancing stops. Notice that the advance button will still cause the light to change state as well.
We forgot one of our criteria ... we must have the lights remain in
a certain state for
different
amounts of time. Remember, red for 6 seconds, green for 8
seconds, then yellow for 3 seconds. We will have to keep a
counter of some kind to
keep
track of how long the light has been in the current state. Once
it
has been on long enough, we advance.
So what about the progress bar ? How can we have it reflect
the current count ? We first need to go back to our code
that built our window and now use the static constant that we defined
in our model representing the maximum limit for the progress bar:
progressBar = new JProgressBar(JProgressBar.HORIZONTAL,
0, TrafficLight.MAX_TIME_COUNT);
We will need to update the progress bar every time that the traffic
light increments its stateCount. We will write an update
method for this:
// Update all relevant
components according to the traffic light state
public void update()
{
actionList.removeActionListener(comboBoxListener);
updateRadioButtons();
updateComboBox();
updateAdvanceButton();
updateProgressBar(); |
actionList.addActionListener(comboBoxListener);
}
// Update the progress bar
private void updateProgressBar() {
progressBar.setValue(aTrafficLight.getStateCount());
}
That's it. The progress bar will now show the amount of time that the traffic light remains in its current state.
The last remaining task for us to complete is to get the slider
working. We need to register for a stateChanged event. We
can add this to our constructor:
slider.addChangeListener(new ChangeListener() {
public
void
stateChanged(ChangeEvent e) {
handleSlider((JSlider)e.getSource());
}
});
Now of course, we need to write the event handler. We only
want to make a change to the timer delay when the user lets go of the
slider. So while the user is adjusting the value, we do not
want to handle the event. We can check for this with a getValueIsAdjusting() method call to
our slider. Then, we can get the value of the slider with getValue(). It will
return an integer within the range that we specified when creating the
slider (from 0 to 20 in our case). Lastly, we just need to call setDelay(int) for our timer, passing
it in the new delay value. Of course, if the delay is 0, we
probably want to pick a HUGE delay such as
Integer.MAX_VALUE. Also, we will need to restart the timer
after making the change (as long as it has already been started by the
check box):
// This is the Slider event handler
private void handleSlider(JSlider
source) {
if (!source.getValueIsAdjusting())
{
int delay
= source.getValue();
if (delay
> 0) {
aTimer.setDelay(1000/delay);
if (aTimer.isRunning())
aTimer.restart();
}
else
aTimer.setDelay(Integer.MAX_VALUE);
update();
}
}
Well ... that is it! We are done.
4.6 Splitting up the Model, View and Controller |
Recall that a view is:
If we make our model inform all interested applications when it has
changed,
then
it must somehow keep track of all applications that need to be updated
when
a change occurs. The model becomes a kind of subject
which the applications observe. To make a clean
connection
between the two, we will make our model class implement a standardized Subject
interface that allows observers to register or un-register with
it. We will write the following interface:
Let us take a look at how the model now looks. Notice
the changes highlighted:
// Making the TrafficLight model implement
the Subject interface allows
// it to inform all of the
observers whenever there has
been a
change
import java.util.ArrayList;
public class TrafficLight implements Subject {
public static final
int MAX_TIME_COUNT = 8;
private int
currentState; // 1=red,
2=yellow, 3=green
private int
stateCount; //
amount of time in this state
ArrayList<Observer> observers = new ArrayList<Observer>(); |
//
Constructor that makes a red traffic light
public TrafficLight()
{
currentState = 1;
stateCount = 0;
}
// Advance the
traffic light to the next state
public int
advanceState() {
currentState =
++currentState % 3 + 1;
stateCount = 0;
updateObservers(); // Tell the observer applications about this change |
return currentState;
}
// Simulate a
single time unit of time passing by
public void
advanceTime() {
// The number of seconds (i.e., time
units) that
// the traffic light remains in each state
int[]
stateTimes = {6, 3, 8};
// advance the time spent in the
current state
stateCount++;
updateObservers(); // Tell the observer applications about this change |
// Check if we have reached time limit
for current state
if (stateCount >
stateTimes[currentState-1])
advanceState();
}
// Return the
amount of time spent in the current state
public int
getStateCount() {
return stateCount;
}
// Return the
state of the traffic light (as a number from 1 to 3)
public int
getState() {
return currentState;
}
// Set the
state of the traffic light (as a number from 1 to 3)
// If the
integer is out of range, do nothing
public void
setState(int newState) {
if ((newState > 0) &&
(newState <4)) {
currentState = newState;
stateCount = 0;
updateObservers(); // Tell the observer applications about this change |
}
}
// Return a
string representation of the traffic light
public String
toString() {
String[] colours = {"Red", "Yellow", "Green"};
return colours[currentState] + " Traffic Light";
}
public void registerObserver(Observer
observer) { observers.add(observer); } public void unregisterObserver(Observer observer) { observers.remove(observer); } // This method is called whenever there is a change to the model. // It informs all registered observer applications of the change. private void updateObservers() { for (Observer anObserver: observers) anObserver.update(); }
|
}
What portion of code represents the view ? All of the stuff related to adding Frames/Panels/Components etc.. We can take the TrafficLightFrame class and "strip away" all of the behaviour and model related stuff (i.e., remove the Listeners etc..) If we do this, then the controller MUST add the behaviour-related stuff (i.e., event handlers, updates, listeners etc..).
One problem is that the controller must be able to access the view's
components in order to add listeners, get their contents, change them
etc..
One solution to this problem is to make instance variables for all the
components and then supply public "get" methods for
each.
We will make our views as separate JPanels
that hold the entire contents of the window:
import
java.awt.*;
import javax.swing.*;
public class TrafficLightPanel extends JPanel {
// These are
the components
private JRadioButton[]
buttons = new JRadioButton[3];
private JButton
advButton;
private JProgressBar
progressBar;
private JSlider
slider;
private JComboBox
actionList;
private JCheckBox
autoButton;
// Make some
get methods so that the controller can access this view
public JRadioButton
getButton(int i) {
return buttons[i]; }
public JButton
getAdvanceButton() { return advButton;
}
public JProgressBar
getProgressBar() { return
progressBar; }
public JSlider
getSlider() {
return slider; }
public JComboBox
getActionList() { return
actionList; }
public JCheckBox
getAutoButton() { return
autoButton; }
public TrafficLightPanel()
{
GridBagLayout
layout = new GridBagLayout();
GridBagConstraints
constraints = new GridBagConstraints();
setLayout(layout);
// Add all the labels
JLabel label = new JLabel("Manual");
constraints.gridx = 0;
constraints.gridy = 0;
constraints.gridwidth = 1;
constraints.gridheight = 1;
constraints.weightx = 0;
constraints.weighty = 0;
constraints.fill =
GridBagConstraints.NONE;
constraints.anchor =
GridBagConstraints.NORTHWEST;
constraints.insets = new Insets(5, 5, 0, 0);
layout.setConstraints(label,
constraints);
add(label);
label = new JLabel("Action");
constraints.gridx = 1;
layout.setConstraints(label,
constraints);
add(label);
label = new JLabel("Advance");
constraints.gridx = 2;
layout.setConstraints(label,
constraints);
add(label);
label = new JLabel("Timer Progress");
constraints.gridx = 0;
constraints.gridy = 4;
layout.setConstraints(label,
constraints);
add(label);
// Add the Radio Buttons
ButtonGroup lights = new ButtonGroup();
JPanel aPanel = new JPanel();
aPanel.setLayout(new BoxLayout(aPanel,
BoxLayout.Y_AXIS));
aPanel.setBackground(Color.black);
for (int i=0; i<3; i++) {
buttons[i] = new JRadioButton("", false);
buttons[i].setBackground(Color.black);
lights.add(buttons[i]);
aPanel.add(buttons[i]);
}
buttons[0].setText("Red");
buttons[1].setText("Yellow");
buttons[2].setText("Green");
buttons[0].setForeground(Color.red);
buttons[1].setForeground(Color.yellow);
buttons[2].setForeground(Color.green);
constraints.gridx = 0;
constraints.gridy = 1;
constraints.gridheight = 3;
constraints.fill =
GridBagConstraints.BOTH;
layout.setConstraints(aPanel, constraints);
add(aPanel);
// Make the Actions List
String[] actions = {"Stop", "Yield", "Go"};
actionList = new JComboBox(actions);
constraints.gridx = 1;
constraints.gridy = 1;
constraints.gridheight = 1;
constraints.weightx = 1;
constraints.fill =
GridBagConstraints.HORIZONTAL;
layout.setConstraints(actionList, constraints);
add(actionList);
// Make the Slider
slider = new JSlider(JSlider.HORIZONTAL, 0,
20, 1);
slider.setMajorTickSpacing(5);
slider.setMinorTickSpacing(1);
slider.setPaintTicks(true);
slider.setPaintLabels(true);
constraints.gridx = 1;
constraints.gridy = 3;
layout.setConstraints(slider, constraints);
add(slider);
// Add the auto checkbox button
autoButton = new JCheckBox("Auto Advance");
constraints.gridx = 1;
constraints.gridy = 2;
constraints.fill =
GridBagConstraints.BOTH;
layout.setConstraints(autoButton, constraints);
add(autoButton);
// Add the Advance Picture button
advButton = new JButton(new ImageIcon("RedLight.jpg"));
constraints.gridx = 2;
constraints.gridy = 1;
constraints.gridheight = 3;
constraints.weightx = 0;
constraints.weighty = 1;
constraints.fill =
GridBagConstraints.BOTH;
constraints.insets = new Insets(5, 5, 0, 5);
layout.setConstraints(advButton, constraints);
add(advButton);
// Add the progress bar
progressBar = new JProgressBar(JProgressBar.HORIZONTAL,
0, TrafficLight.MAX_TIME_COUNT);
constraints.gridx = 0;
constraints.gridy = 5;
constraints.gridwidth = 3;
constraints.gridheight = 1;
constraints.weightx = 1;
constraints.weighty = 2;
constraints.fill =
GridBagConstraints.BOTH;
constraints.insets = new Insets(5, 5, 5, 5);
layout.setConstraints(progressBar, constraints);
add(progressBar);
}
}
Notice the following:
What portion of code represents the controller ? All of the stuff related to adding listeners, event handlers, update methods etc.. as well as any behaviour-related code such as the Timer code. All of the stuff that we removed from the View must be added here. We will put it all in the TrafficLightFrame class that will serve as a Mediator between the model and the view. It will also contain the "main" method which will be responsible for coordinating the startup of the application. The controller will keep hold of the model and the view (as instance variables) as part of this coordination.
Here is the code:
// This is the view private TrafficLightPanel aView; |
aView = view; setContentPane(aView); //Replace old panel with ours |
// !!! IMPORTANT !!! // Register with the model so that when it changes, we get informed aTrafficLight.registerObserver(this); |