COMP1406/1006 - Design and Implementation of Computer Applications | W2006 |
3 Events and Listeners |
Here are the individual topics found in this set of notes (click on one to go there):
3.1 Events and Event Handlers 3.2 Listeners and Adapter Classes 3.3 Handling ActionEvents with ActionListeners 3.4 Handling MouseEvents with MouseListeners 3.5 Key Press Events 3.6 Proper Coding Style for Component Interaction
3.1 Events and Event Handlers |
What is an event ?
Here is a picture that describes the process of user interaction with a GUI through events:
Basically...here's how it works:
Notice that incoming events (i.e., customers/clients) are stored in the event queue in the order that they arrive. As we will see later, events MUST be handled by your program. The JVM spends all of its time taking an event out of the queue, processing it and then going back to the queue for another.
While each event is being handled, JAVA is unable to process any other events. You MUST be VERY careful to make sure that your event handling code does not take too long. Otherwise the JVM will not take any more events from the queue. This makes your application seem to "hang" so that the screen no longer updates, and all buttons, window components seem to freeze up !!!
In a way, the JVM event loop acts as a server. It serves (or handles) the incoming events one at a time on a first-come-first-served basis. So when an event is generated, JAVA needs to go to the appropriate method in your code to handle the event. How does JAVA know which method to call ? We will register each event-handler so that JAVA can call them when the events are generated. These event-handlers are called listeners (or callbacks).A listener:
3.2 Listeners and Adapter Classes |
You should understand now that when the user interacts with your user interface, some events will be generated automatically by JAVA. There are many types of events that can occur, and we will choose to respond to some of them, while ignoring others. The JAVA VM is what actually generates the events, so we will have to "speak JAVA's language" in order to understand what the event means. In fact, to handle a particular event, we will have to write a particular method with a predefined name (chosen by JAVA).
Here is a list of the commonly used types of events in JAVA:
There are many types of events that are generated and commonly
handled. Here is a table of some of the common
events. The table gives a short description of when the
events may be generated, gives the interface that must be implemented
by you in order for you to handle the events and
finally lists the necessary methods that need to be
implemented. Note, for a more complete description of these
events, listeners and their methods, see the JAVA API specifications.
Event Type | Generated By | Listener Interface | Methods that "YOU" must Write |
ActionEvent | a button was pressed, a menu item selected, pressing enter key in a text field or a timer event was generated | ActionListener | actionPerformed(ActionEvent e) |
CaretEvent | moving cursor (caret) in a text-related component such as a JTextField | CaretListener | caretUpdate(CaretEvent e) |
ChangeEvent | value of a component such as a JSlider has changed | ChangeListener | stateChanged(ChangeEvent e) |
DocumentEvent | changes have been made to a text document such as insertion, removal in an editor | DocumentListener | changedUpdate(DocumentEvent
e) insertUpdate(DocumentEvent e) removeUpdate(DocumentEvent e) |
ItemEvent | caused via a selection or deselection of something from a list, a checkbox or a toggle button | ItemListener | itemStateChanged(ItemEvent e) |
ListSelectionEvent | selecting (click or double click) a list item | ListSelectionListener | valueChanged(ListSelectionEvent e) |
WindowEvent | open/close, activate/deactivate, minimize/deminimize a window | WindowListener | windowOpened(WindowEvent
e) windowClosed(WindowEvent e) windowClosing(WindowEvent e) windowActivated(WindowEvent e) windowDeActivated(WindowEvent e) windowIconified(WindowEvent e) windowDeiconified(WindowEvent e) |
FocusEvent | a component has gained or lost focus. Pressing tab key changes focus of components in a window | FocusListener | focusGained(FocusEvent e) focusLost(FocusEvent e) |
KeyEvent | pressing and/or releasing a key while within a component | KeyListener | keyPressed(KeyEvent e) keyReleased(KeyEvent e) keyTyped(KeyEvent e) |
MouseEvent | pressing/releasing/clicking a mouse button, moving a mouse onto or away from a component | MouseListener | mouseClicked(MouseEvent e)
mouseEntered(MouseEvent e) mouseExited(MouseEvent e) mousePressed(MouseEvent e) mouseReleased(MouseEvent e) |
MouseEvent | moving a mouse within a component while the button is up or down | MouseMotionListener | mouseDragged(MouseEvent e)
mouseMoved(MouseEvent e) |
ContainerEvent | Adding or removing a component to a container such as a panel | ContainerListener | componentAdded(ContainerEvent
e) componentRemoved(ContainerEvent e) |
So, if you want to handle a button press in your program, you need
to write
an actionPerformed()method:
public void
actionPerformed(ActionEvent e) {
//Do
what needs to be done when the button is clicked
}
If you want to have something happen when the user presses a particular
key
on the keyboard, you need to write a keyPressed()
method:
public void
keyPressed(KeyEvent e) {
//Do
what needs to be done when a key is pressed
}
Once we decide which events we want to handle and then write our event handlers, we then need to register the event handler. This is like "plugging-in" the event handler to our window. In general, many applications can listen for events on the same component. So when the component event is generated, JAVA must inform everyone who is listenning. We must therefore tell the component that we are listening for (or waiting for) an event. If we do not tell the component, it will not notify us when the event occurs (i.e., it will not call our event handler). So, when a component wants to signal/fire an event, it sends a specific message to all listener objects that have been registered (i.e., anybody who is "listening"). For every event, therefore, that we want to handle, we must write the listener (i.e., event handler) and also register that listener.
To help you understand why we need to do this, think of the olympic games. There are various events in the olympics and we may want to particpate (i.e., handle) a particular event. Our training and preparation for the event is like writing the event handler code which defines what we do when the event happens. But, we don't get to particpate in the olympic games unless we "sign-up" (or register) for the events ... right ? So registering our event handlers is like joining JAVA's sign-up list so that JAVA informs us when the event happens and then allows our event handler to participate when the event occurs. | |
public class SimpleEventTest extends JFrame implements ActionListener {
public SimpleEventTest(String name) {
super(name);
JButton aButton = new JButton("Hello");
add(aButton);
setDefaultCloseOperation(EXIT_ON_CLOSE);
// Plug-in the button's event handler
aButton.addActionListener(this);
setSize(200, 200);
}
// Must write this method now since SimpleEventTest
implements the ActionListener
interface public void actionPerformed(ActionEvent e) { System.out.println("I have been pressed"); } |
public static void main(String[] args) {You can also "unregister" from an event (i.e., disable the listener), by merely removing it using a removeXXXListener() method. Here are some examples:
JFrame frame = new SimpleEventTest("Making a Listener");
frame.setVisible(true);
}
}
public class MyApplication extends JFrame implements MouseListener {It does seem a little silly to have to write 4 blank methods when we do not even want to handle these other kinds of events. The JAVA guys recognized this inconvenience and solved it using the notion of Adapter classes. For each listener interface that has more than one method specified, there exists an adapter class with a corresponding name:
...
public void mouseClicked(MouseEvent e) { /* Put your code here */ };
public void mouseEntered(MouseEvent e) {};
public void mouseExited(MouseEvent e) {};
public void mousePressed(MouseEvent e) {};
public void mouseReleased(MouseEvent e {};
...
}
public abstract class MouseAdapter implements MouseListener {They are merely classes that are provided for convenience sake to help us avoid writing empty methods. So, we can simply write subclasses of these adapter classes, then we can take advantage of these blank methods through inheritance.
public void mouseClicked(MouseEvent e) {};
public void mouseEntered(MouseEvent e) {};
public void mouseExited(MouseEvent e) {};
public void mousePressed(MouseEvent e) {};
public void mouseReleased(MouseEvent e {};
}
Well, we don't want to have to make our user interfaces subclass one of these adapter classes ... this would be bad since we would lose the freedom of creating our own arbitrary class hierarchies. Consider handling an event for dealing with a simple mouse click. We could make our own internal class to handle this event.
class MyClickHandler extends MouseAdapter {But this strategy creates an additional .java file. It may make our code more complicated, since we increase the number of source code files. Well, we can actually write this code within our GUI class itself, provided that we don't put the public modifier in front of this class definition. This makes it an internal class. When you compile a .java file that has internal classes, you will notice that you will have additional .class files for these additional internal classes (which will have a $ in their name).
public void mouseClicked(MouseEvent event) {
System.out.println("Do Something fun");
}
}
To reduce clutter, JAVA allows another way for us to create inner classes which uses VERY strange syntax:
new MouseAdapter() {This syntax actually creates an internal class as a subclass of MouseAdapter. The class has no name, it is considered to be an anonymous class. This code actually creates an instance of the anonymous class and returns it to us. It is weird syntax. We will see below how we can "embed" this code inside other code just like we use any other objects.
public void mouseClicked(WindowEvent event) {
System.out.println("Do Something fun");
}
}
public class YourClass
extends JFrame implements MouseListener {
// This
line
must appear in some method, perhaps the constructor // Some more of your code public void
mouseClicked(MouseEvent e) { /* Put your code
here */ }; // Put your
other
methods here |
public class YourClass
extends JFrame {
// This
line
must appear in some method, perhaps the constructor // Some more of
your
code public class MyButtonListener
implements ActionListener
{ |
public class YourClass
extends JFrame {
// This line
must
appear in some method, perhaps the constructor // Some more of your code class MyButtonListener
implements ActionListener
{ |
public class YourClass
extends JFrame {
// This line
must
appear in some method, perhaps the constructor // Some more of
your
code |
3.3 Handling ActionEvents with ActionListeners |
We have already seen how to handle a simple
button
press by writing an ActionPerformed method.
Here is an application that shows how to handle events
for two different buttons. We will make use of the getActionCommand() method for the ActionEvent class that allows us to
determine
the label on the button that generated the event. Take notice of
the
packages that need to be imported. |
|
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
public class Handle2Buttons extends
JFrame
implements ActionListener {
public Handle2Buttons(String
title)
super(title);
JButton aButton1 = new
JButton("Press Me");
JButton aButton2 = new
JButton("Don't Press Me");
setLayout(new
FlowLayout());
add(aButton1);
add(aButton2);
//
Indicate that this class will handle 2 button clicks
//
and that both buttons will go to the SAME event handler
aButton1.addActionListener(this);
aButton2.addActionListener(this);
setDefaultCloseOperation(EXIT_ON_CLOSE);
setSize(250,100);
}
// This is the event
handler for the buttons
public void actionPerformed(ActionEvent e) {
//
Ask the event which button was the source that generated it
if (e.getActionCommand().equals("Press Me"))
System.out.println("That felt good!");
else
System.out.println("Ouch! Stop that!");
}
public static void
main(String args[]) {
Handle2Buttons frame
=
new Handle2Buttons("Handling 2 Button Presses");
frame.setVisible(true);
}
}
Notice that the getActionCommand() method is sent to the ActionEvent. It returns a String containing the text that is on the button that generated the event. We then compare this string with the labels that we put on the buttons to determine which button was pressed. One disadvantage of this approach is that our event handler depends on the label associated with the button. Although this is safe in this particular example, there are many occasions when the label associated with a button could change (e.g., international applications). Therefore, we could use the getSource() method which returns the component (i.e., an Object) that raised the event instead of getActionCommand() to compare the actual button objects instead of the labels. To do this, we need to make two modifications. First, we need to store the buttons we create into instance variables and second, we need to compare the object that generated the event with these buttons using the identity (==) comparison.
// We need to make the
buttons instance variables and assign
// them in the constructor so that we can access
these
objects
// from within our event handler code.
JButton aButton1,
aButton2;
// Change the event
handler
to use getSource() to compare
the
actual objects
public void actionPerformed(ActionEvent e) {
//
Ask the event which button was the source that generated the event
if (e.getSource() == aButton1)
System.out.println("That felt good!");
else
System.out.println("Ouch! Stop that!");
}
Another disadvantage of the previous example is that if more buttons
(or
other components that generate action events) are added, the number of
"if-statements"
in our handler will increase and become more complex, which may not be
desirable.
One way to handle this disadvantage (and the previous one as well) is
by
using anonymous classes. The following code would replace the code in
the
constructor that registers our frame subclass as a listener. The actionPerformed
method of our class would no longer be required. Here, each
button
has its own event handler:
aButton1.addActionListener(new
ActionListener() {
public void actionPerformed(ActionEvent e) {
System.out.println("That felt good!");
}
});
aButton2.addActionListener(new
ActionListener() {
public void actionPerformed(ActionEvent e) {
System.out.println("Ouch! Stop that!");
}
});
Now les us make a more interesting example that
uses
anonymous classes for two buttons. We will create a simple
Slide
Show application. We will create a window that has a JPanel which uses a CardLayout to represent the slides
(one
at a time) and then we will also add two arrow buttons to the window to
rewind
and forward the slides. Notice the following:
|
|
Here is a new application that has a button and
some
text fields. One text field will hold an integer. When the button
is
pressed, it will compute and display (in two other text fields) the
"square"
as well as the "square root" of the value within the first
textfield.
Note a few things about the code:
|
|
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
public class
HandleTextFieldAndButton
extends JFrame {
JTextField valueField, squareField, rootField;
public
HandleTextFieldAndButton(String
title) {
super(title);
setLayout(new BoxLayout(this.getContentPane(),BoxLayout.Y_AXIS));
// Add the value text field, along with
a label
add(new JLabel("Value:"));
valueField = new JTextField("10", 8);
add(valueField);
// Add the compute button
JButton aButton = new JButton("Compute");
add(aButton);
// Add the square text field, along
with a
label
add(new JLabel("Square:"));
squareField = new JTextField("0", 16);
add(squareField);
// Add the square root text field,
along with
a label
add(new JLabel("Square Root:"));
rootField = new JTextField("0", 20);
add(rootField);
// Handle the button click
aButton.addActionListener(new ActionListener() {
public void
actionPerformed(ActionEvent
e) {
int value =
Integer.parseInt(valueField.getText());
squareField.setText(""
+ value * value);
rootField.setText(""
+ Math.sqrt(value));
}
});
setDefaultCloseOperation(EXIT_ON_CLOSE);
setSize(250,180);
}
public static void
main(String args[]) {
HandleTextFieldAndButton frame = new HandleTextFieldAndButton("Working With TextFields");
frame.setVisible(true);
}
}
Notes:
Although not done in our example here, we can actually handle an ActionEvent for a text
field.
An action event is generated when the user presses the ENTER key while
typing
in a text field. As with button clicks, you can handle this
ENTER
key press in the text field by writing an actionPerformed() method for the
text
field. In such a method, the getActionCommand()
method will return the text oinside the text field. We can
also
send the getSource() method to
the action event to get the text field itself and then get its text as
follows: ((JTextField)e.getSource()).getText()
Let us modify the previous example by using
radio
buttons that allow us to decide which kind of operation we will do on
the
value entered in the text field. We will replace the
Compute
button with 4 radio buttons where each radio button, when clicked, will
perform
a different operation on the value from the text field and then display
the
result in the answer text field. Here are some interesting
points
about the code:
|
|
Note that for this example it seems appropriate to have our frame
class act as a listener since the event handling code is the same for
all components. Nothing is gained by using another class to handle the
events.
Note as well that the JCheckBox works
similar to the JRadioButton,
except
that normally JRadioButtons should
have only one on at a time, while JCheckBoxes
may normally have many on at a time. Here is how the
window
would look if JCheckBoxes were used instead (although keep in mind that
in
this application, it doesn't make sense to have more than one button on
at
a time).
In addition to radio buttons and checkbox buttons, JButtons themselves can maintain selected states. For example, we can create an application that allows one of several JButtons to be selected. As it turns out, JButtons have a setSelectedIcon() method that allows us to change the picture on a button when it is selected. Here is an example that makes 4 JButtons with icons on them which may be used to select a shape for drawing. When the user selects one of these buttons, the image changes on the button and so the button appears selected. All we have to do is use the setSelected() and getSelected() methods to change or query the button's state. |
Notice that we did not use a ButtonGroup to ensure that only one button is selected by itself. That is because JButtons do not work with ButtonGroups. Instead, we had to do everything manually. If we wanted to allow multiple buttons on at a time, we would merely change the action performed method to be:
public void actionPerformed(ActionEvent e) {This would allow the buttons to be toggled on and off individually. Notice one good thing, we don't have to worry about changing the icon. It is done automatically for us when we set the button to be selected or not.
JButton src = (JButton)e.getSource();
src.setSelected(!src.isSelected());
}
3.4 Handling MouseEvents with MouseListeners |
In this example we create two components: a yellow JPanel and a white JTextArea within a JScrollPane. Both the JFrame itself as well as the JPanel will respond to all MouseEvents and display an appropriate message within the text area. We will also add an ImageIcon to the JPanel as a JLabel and we will move it around depending on where the user presses the mouse. We make use of the following MouseEvent methods to get information about mouse presses:
|
|
Here is the code for the application:
import java.awt.*;How can we modify the code to only handle clicked events if it was a double-click ?
import java.awt.event.*;
import javax.swing.*;
public class WorkingWithMouseEvents extends JFrame implements MouseListener {
JPanel blankArea;
JTextArea textArea;
JLabel movableImage;
Class latestComponent;public WorkingWithMouseEvents(String title) {
super(title);setLayout(new FlowLayout(FlowLayout.LEFT, 5, 5));
// Create a yellow JPanel
blankArea = new JPanel();
blankArea.setLayout(null);
blankArea.setBackground(new Color(255,255,200));
blankArea.setOpaque(true);
blankArea.setBorder(BorderFactory.createLineBorder(Color.black));
blankArea.setPreferredSize(new Dimension(350, 150));
add(blankArea);// Add an image to the JPanel
blankArea.add(movableImage = new JLabel(new ImageIcon("brain.gif")));
movableImage.setSize(80,80);
movableImage.setLocation(100,100);// Create a text area to display event information
textArea = new JTextArea();
textArea.setEditable(false);
JScrollPane scrollPane = new JScrollPane(textArea);
scrollPane.setPreferredSize(new Dimension(350, 200));
add(scrollPane);//Register for mouse events on the JPanel AND the JFrame
blankArea.addMouseListener(this);
addMouseListener(this);setDefaultCloseOperation(EXIT_ON_CLOSE);
setSize(370,390);
}// Handle the mouse events (pressed, released, entered, exited & clicked)
public void mousePressed(MouseEvent event) {
addToTextArea("MousePressed", event);
if (event.getComponent().getClass() != this.getClass())
movableImage.setLocation(event.getX()-40, event.getY()-40);
}public void mouseReleased(MouseEvent event) {
addToTextArea("MouseReleased", event);
}public void mouseEntered(MouseEvent event) {
addToTextArea("MouseEntered", event);
}public void mouseExited(MouseEvent event) {
addToTextArea("MouseExited", event);
}public void mouseClicked(MouseEvent event) {
String s;
if (event.getButton() == 3)
s = "Right";
else s = "Left"; // Ignores the middle button case
addToTextArea("Mouse" + s + "-Clicked " + event.getClickCount() +
" times successively ", event);
}// Append the specified event-specific text to the text area
private void addToTextArea(String eventDescription, MouseEvent event) {
if (latestComponent != event.getComponent().getClass())
textArea.append("--------------------------------------------\n");
latestComponent = event.getComponent().getClass();
if (latestComponent == this.getClass())
textArea.append("JFrame event: ");
else
textArea.append("JPanel event: ");
textArea.append(eventDescription + "\n");
}public static void main(String args[]) {
JFrame frame = new WorkingWithMouseEvents("Mouse Event Example");
frame.setVisible(true);
}
}
public void mouseClicked(MouseEvent
event)
{
if
(event.getClickCount() == 2)
// do something
}
3.5 Key Press Events |
Every Component in JAVA can
listen
for KeyEvents. KeyEvents are generated
when
the user presses, releases or types a key while in a component.
In
order for the event to be generated, the component MUST have the
focus.
The focus represents the current component that is
selected
(e.g., we all understand how the TAB key moves the focus from one
component
to another in many windows applications). Thus, if a
particular
component is listening for a key press, but that component does not
have
the focus, then no events are generated. |
|
Also, we can combine other listeners with key presses ... for example, if we want to detect a SHIFT-CLICK operation.
Here is an example with code that detects three things:
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
public class ShiftButtonTest extends
JFrame
implements ActionListener, KeyListener {
private boolean
shiftPressed;
public ShiftButtonTest(String title) {
super(title);
setLayout(new
FlowLayout(5));
JButton aButton = new
JButton("Press Me With/Without the Shift Key");
JButton bButton = new
JButton("No Listeners here");
add(aButton);
add(bButton);
shiftPressed = false;
//Indicate
that this class will handle the button click
aButton.addActionListener(this);
aButton.addKeyListener(this);
// Change aButton to this if you
want
to ignore focus
setDefaultCloseOperation(EXIT_ON_CLOSE);
setSize(300,100);
}
//This is the event
handler for the button
public void actionPerformed(ActionEvent
e)
{
if (shiftPressed)
System.out.println("You SHIFTED Me!!");
else
System.out.println("You did not SHIFT Me.");
}
public void keyPressed(KeyEvent
e) {
if
(e.getKeyCode() == KeyEvent.VK_SHIFT)
shiftPressed
= true;
else if
(e.getKeyCode() == KeyEvent.VK_A)
{
if (shiftPressed)
System.out.println("You pressed the [SHIFT]+[A]
keys");
else
System.out.println("You pressed the [A] key");
}
}
public void keyReleased(KeyEvent
e) {
if
(e.getKeyCode() == KeyEvent.VK_SHIFT)
shiftPressed
= false;
}
public void keyTyped(KeyEvent
e) {
//
Get and display the character for each key typed
System.out.println("Key
Pressed:
" + e.getKeyChar());
}
public static void
main(String args[]) {
ShiftButtonTest frame = new
ShiftButtonTest("Example: Handling a SHIFT+Button
Press");
frame.setVisible(true);
}
}
If you do not want this "focus-oriented" behaviour (e.g., perhaps
you
want to listen for a particular key press regardless of which component
has
been selected) you can have the JFrame listen for the key
press.
In this case, you must "disable" the focusability for all the
components
on the window (but not the JFrame itself). In our example,
we
would replace the line:
aButton.addKeyListener(this);
with the following lines that disallow the buttons to have the focus:
this.addKeyListener(this);
aButton.setFocusable(false);
bButton.setFocusable(false);
Be aware however, that this disables the "normal" behaviour for the
window
and will prevent standard use of the TAB key to traverse between
components
in the window.
3.6 Proper Coding Style
for
Component Interaction |
There are two questions regarding the updating stage of the components:
So, REMEMBER the two VERY
important
things that you should normally do in every event handler: 1. Change
the model
2. Call update() |
|
Example:
Let us now build the following application which represents a list of "things to do":
The application will work as follows:Let us now look at a basic "working" application. We
will handle the Adding and Removing of items from the list. The
highlighted code
below indicate the code required for handling the events from the
buttons
and updating the interface:
import java.util.*;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
public class TodoListFrame extends JFrame
{
private JTextField newItemField;
private
JList itemsList;
private JButton
addButton;
private JButton
removeButton;
private Vector<String> items; // The model
public TodoListFrame() {
this(new
Vector<String>());
}
public TodoListFrame(Vector<String>
todoEntries) {
super("To Do List");
items = todoEntries;
//
...
// The code for building the
window
has been omitted
// ...
// Add listeners for the buttons and then enable them
addButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
addButtonEventHandler();
}
});
removeButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
removeButtonEventHandler();
}
});
setDefaultCloseOperation(EXIT_ON_CLOSE);
setSize(300,200);
update();
}
// Event Handler for
the Add
button
|
// Update all the components private void update() { itemsList.setListData(items); } |
public static void main(String[] args) {
//
Set
up the items to be put into the list
Vector<String>
todoItems = new Vector<String>();
todoItems.add("wash my car");
todoItems.add("go to dentist");
todoItems.add("shovel the laneway");
todoItems.add("pick up milk");
todoItems.add("laundry");
TodoListFrame frame = new
TodoListFrame(todoItems);
frame.setVisible(true);
}
}
Notice:
So, REMEMBER the two VERY
important
things that you should normally do in your update() method: 1. Read
the model's information
2. Change the "look" of the interface components |
|
There are two problems with the application:
For the remove problem, we would like to have a way of determining whether or not anything was selected from the list. To do this, we merely ask if the selected list value is null:private void addButtonEventHandler() {items.add(newItemField.getText());
if (newItemField.getText().length() > 0) {
update();
}
}
private void removeButtonEventHandler() {items.remove((String)itemsList.getSelectedValue());
if (itemsList.getSelectedValue() != null) {
update();
}
}
So ... we will keep track of the item that was selected and this
will
be part of our model.
Of course this means that we will have to handle the selection
event
for the JList component.
Here is what we need to add/change:
public TodoListFrame(Vector<String> todoEntries) {
super("To Do List");
items = todoEntries;
if (items.size() > 0)
selectedItem = (String)items.firstElement();
else
selectedItem = null;
// ... Some code has been omitted ...
// Add listeners for the buttons and then enable them
addButton.addActionListener(...);
removeButton.addActionListener(...);
itemsList.addListSelectionListener(new ListSelectionListener() {
public void valueChanged(ListSelectionEvent e) {
listSelectionEventHandler();
}
});
setDefaultCloseOperation(EXIT_ON_CLOSE);
setSize(300,200);
update();
}
private void listSelectionEventHandler()
{
selectedItem =
(String)itemsList.getSelectedValue();
update();
}
private void removeButtonEventHandler() {update();
if (selectedItem != null) {
items.remove(selectedItem);
selectedItem == null;
}
}
private void update() {
itemsList.setListData(items);
itemsList.setSelectedValue(selectedItem, true);
}
private ListSelectionListener itemsListListener;
itemsList.addListSelectionListener(itemsListListener =
new ListSelectionListener() {
public void valueChanged(ListSelectionEvent e) {
listSelectionEventHandler();
}
}
);
private void update() {
itemsList.setListData(items);
itemsList.removeListSelectionListener(itemsListListener);
itemsList.setSelectedValue(selectedItem, true);}
itemsList.addListSelectionListener(itemsListListener);
removeButton.setEnabled(selectedItem != null);
For the Add button, we can add a similar line:
addButton.setEnabled(newItemField.getText().length() > 0);
There is a small problem. When the interface starts up,
the text field is empty and so the Add button is disabled. That's
good. But when the user starts typing in the text field, there is
then text in the text field but the Add button remains
enabled. The problem is that update() is not being called unless
an event occurs. So we need to have some kind of event for
when the user types text in the text field. So we will need
to make some changes. In addition, this approach to
enabling the Add button results in a dependency on there being
a text field. We should create another instance variable to
indicate whether or not there is any text in the text field. We
can
just use a boolean, but we may as well keep the whole item that is in
there
instead of just a flag. First we need to make the following
additions:
// Add this to the constructor
newItem = "";
// Add this to the constructor
newItemField.getDocument().addDocumentListener(newItemFieldListener
=
new DocumentListener()
{
public
void changedUpdate(DocumentEvent theEvent) {
handleTextFieldEntry();
}
public
void insertUpdate(DocumentEvent theEvent) {
handleTextFieldEntry();
}
public
void removeUpdate(DocumentEvent theEvent) {
handleTextFieldEntry();
}
}
);
// Add this event handler for the text
field
private void handleTextFieldEntry() {
newItem
= newItemField.getText();
update();
}
addButton.setEnabled(newItem.length()
> 0);
private void addButtonEventHandler() {Notice however, that at this point, the newItem variable will be "", but there will still be some contents in the text field ... so they are not in agreement. To fix this, we will have to actually clear out the text field contents. This, of course, has to do with the appearance of the text field, so we could try placing the following code within the update() method:
if (newItem.length() > 0) {
items.add(newItem);
selectedItem = newItem;
newItem = "";
update();
}
}
newItemField.setText(newItem);But, we have a small problem. If we were to run our code right now, we would notice a bug when we tried to type into the text field. Our code would generate the following exception:
// Update called by Document Listeners directlyThe make the following change in the handleTextFieldEntry() method:
private void update(boolean calledFromTextField) {
itemsList.removeListSelectionListener(itemsListListener);
newItemField.getDocument().removeDocumentListener(newItemFieldListener);
itemsList.setListData(items);
itemsList.setSelectedValue(selectedItem, true);
removeButton.setEnabled(selectedItem != null);
addButton.setEnabled(newItem.length() > 0);
if (!calledFromTextField) newItemField.setText(newItem);
itemsList.addListSelectionListener(itemsListListener);
newItemField.getDocument().addDocumentListener(newItemFieldListener);
}// Update used by all the event handlers, except Document event handlers
private void update() {
update(false);
}
Now the last improvement is within the update()
method. When
we make changes to our code by adding or removing components, we must
go
to the update method and make changes. It is difficult to
determine what code pertains to which components. We can extract
this update code
so that we make separate methods such as updateTextField(), updateList(),
and updateButtons(). These will
do the corresponding updates for the individual components. This
way,
when we modify or remove a component, it is clear as to what code
should
be modified/removed. This alternative method also allows us
to update only those components that have changed (not all). This
is good if the interface becomes slow
in drawing the components. We can speed everything up by only
updating
necessary components. We can also extract the code for
disbaling/enabling the listsners into separate methods as
well.
The final completed code is shown below (the class name has been
changed to ToDoListFrame2):
import java.util.*;
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import javax.swing.event.*;
public class ToDoListFrame2 extends JFrame {
private JTextField
newItemField;
private JList
itemsList;
private JButton
addButton;
private JButton
removeButton;
// Listeners
that need to be disabled/enabled during update()
private ListSelectionListener
itemsListListener;
private DocumentListener
newItemFieldListener;
private Vector<String>
items; // The model
private String
selectedItem; // item selected in the list
private String
newItem;
// String contained
in text field
public ToDoListFrame2()
{
this(new
Vector<String>());
}
public ToDoListFrame2(Vector<String>
todoEntries) {
super("To Do List");
items = todoEntries;
if (items.size() > 0)
selectedItem =
(String)items.firstElement();
else
selectedItem =
null;
newItem = ""; // nothing in the text field yet
initializeComponents();
// Add listeners for the buttons, list
and text field
addButton.addActionListener(new ActionListener() {
public void
actionPerformed(ActionEvent e) {
addButtonEventHandler();
}
});
removeButton.addActionListener(new ActionListener()
{
public void
actionPerformed(ActionEvent e) {
removeButtonEventHandler();
}
});
itemsList.addListSelectionListener(itemsListListener =
new ListSelectionListener() {
public void
valueChanged(ListSelectionEvent e) {
listSelectionEventHandler();
}
}
);
newItemField.getDocument().addDocumentListener(newItemFieldListener
=
new DocumentListener() {
public void changedUpdate(DocumentEvent
theEvent) {
handleTextFieldEntry();
}
public void
insertUpdate(DocumentEvent theEvent) {
handleTextFieldEntry();
}
public void removeUpdate(DocumentEvent
theEvent) {
handleTextFieldEntry();
}
}
);
setDefaultCloseOperation(EXIT_ON_CLOSE);
setSize(300,200);
update();
}
// Build the
frame by adding all the components
private void
initializeComponents() {
GridBagLayout layout = new GridBagLayout();
GridBagConstraints
constraints = new GridBagConstraints();
setLayout(layout);
newItemField = new JTextField();
constraints.gridx = 0;
constraints.gridy = 0;
constraints.gridwidth = 1;
constraints.gridheight = 1;
constraints.fill =
GridBagConstraints.BOTH;
constraints.insets = new Insets(12, 12, 3, 3);
constraints.weightx = 1;
constraints.weighty = 0;
layout.setConstraints(newItemField, constraints);
add(newItemField);
addButton = new JButton("Add");
addButton.setMnemonic('A');
constraints.gridx = 1;
constraints.gridy = 0;
constraints.fill =
GridBagConstraints.HORIZONTAL;
constraints.insets = new Insets(12, 3, 3, 12);
constraints.anchor =
GridBagConstraints.NORTHWEST;
constraints.weightx = 0;
constraints.weighty = 0;
layout.setConstraints(addButton, constraints);
add(addButton);
itemsList = new JList();
itemsList.setPrototypeCellValue("xxxxxxxxxxxxxxxxxxxxxxxxxxxxx");
JScrollPane scrollPane = new JScrollPane(itemsList,
ScrollPaneConstants.VERTICAL_SCROLLBAR_ALWAYS,
ScrollPaneConstants.HORIZONTAL_SCROLLBAR_AS_NEEDED);
constraints.gridx = 0;
constraints.gridy = 1;
constraints.fill =
GridBagConstraints.BOTH;
constraints.insets = new Insets(3, 12, 12, 3);
constraints.weightx = 1;
constraints.weighty = 1;
layout.setConstraints(scrollPane, constraints);
add(scrollPane);
removeButton = new JButton("Remove");
removeButton.setMnemonic('R');
constraints.gridx = 1;
constraints.gridy = 1;
constraints.fill =
GridBagConstraints.HORIZONTAL;
constraints.insets = new Insets(3, 3, 0, 12);
constraints.anchor =
GridBagConstraints.NORTH;
constraints.weightx = 0;
constraints.weighty = 0;
layout.setConstraints(removeButton, constraints);
add(removeButton);
}
// Event
Handler for the Add button
private void
addButtonEventHandler() {
if
(newItem.length() > 0) {
items.add(newItem);
selectedItem = newItem; // select
the newly added item
newItem =
"";
// clear the text
update();
}
}
// Event
Handler for the Remove button
private void
removeButtonEventHandler() {
if (selectedItem != null) {
items.remove(selectedItem);
selectedItem = null;
update();
}
}
// Event
Handler for List Selection
private void
listSelectionEventHandler() {
selectedItem =
(String)itemsList.getSelectedValue();
update();
}
// Handler for
entering text in the text field
private void
handleTextFieldEntry() {
newItem =
newItemField.getText();
update(true);
}
// Update all
the components
private void
update(boolean calledFromTextField)
{
disableListeners();
updateList();
updateButtons();
if (!calledFromTextField)
updateTextField();
enableListeners();
}
private void
update() {
update(false);
}
private void
disableListeners() {
itemsList.removeListSelectionListener(itemsListListener);
newItemField.getDocument().removeDocumentListener(newItemFieldListener);
}
private void
enableListeners() {
newItemField.getDocument().addDocumentListener(newItemFieldListener);
itemsList.addListSelectionListener(itemsListListener);
}
private void
updateList() {
itemsList.setListData(items);
itemsList.setSelectedValue(selectedItem, true);
}
private void
updateButtons() {
removeButton.setEnabled(selectedItem != null);
addButton.setEnabled(newItem.length() > 0);
}
private void
updateTextField() {
newItemField.setText(newItem);
}
public static void
main(String[] args) {
// Set up the items to be put into the
list
Vector<String>
todoItems = new Vector<String>();
todoItems.add("wash my car");
todoItems.add("go to dentist");
todoItems.add("shovel the laneway");
todoItems.add("pick up milk");
todoItems.add("laundry");
ToDoListFrame2 frame = new ToDoListFrame2(todoItems);
frame.setVisible(true);
}
}
Exercise:
But wait a minute! There is still a problem with our
code. If we try adding two
or more items with the same name, JAVA will not allow us to select any
of these items except the topmost one!