1. Introduction to Java
2. Reference Types and Semantics
3. Method Specifications and Testing
4. Loop Invariants
5. Analyzing Complexity
6. Recursion
7. Sorting Algorithms
8. Classes and Encapsulation
9. Interfaces and Polymorphism
10. Inheritance
11. Additional Java Features
12. Collections and Generics
13. Linked Data
14. Iterating over Data Structures
15. Stacks and Queues
16. Trees and their Iterators
17. Binary Search Trees
18. Heaps and Priority Queues
19. Sets and Maps
20. Hashing
21. Graphs
22. Graph Traversals
23. Shortest Paths
24. Graphical User Interfaces
25. Event-Driven Programming
25. Event-Driven Programming

25. Event-Driven Programming

In the previous lecture, we began our discussion of graphical applications. We saw how we typically separate the design of these applications into three aspects: their model, view, and controller. The model was very similar to the code we’ve been writing all semester, using classes and fields to model the state of the application and methods to access and/or modify this state. The view, however, was completely new, and we spent time understanding Java’s Swing framework and some of the classes that it provides that allow us to draw windows and widgets on the screen.

In today’s lecture, we’ll focus on the third aspect of graphical applications, their controllers. We recall that the controller contains the logic to update the application’s state (both underlying state in the model and visible state in the view) in response to different events. To do this, we’ll need to dive deeper into the Swing framework to understand how it handles events. We’ll also discuss the observer pattern for event handling and the inversion of control in graphical programs. To continue our running example from the previous lecture, we’ll complete the definition of our graphical Tic-Tac-Toe application and end up with a playable game program.

Event Handling

Definition: Imperative Program

In an imperative program, the implementer specifies which instructions should be executed in which order. At runtime, these instructions are followed to produce the program's output or achieve its desired side-effects.

In the previous lecture, we saw that the Swing framework provided many useful abstractions that allowed our code to achieve a much more declarative style.

Definition: Declarative Program

In a declarative program, the implementer specifies the desired outcome of their code. At runtime, the way to achieve these outcomes is determined and carried out behind the scenes.

For example, when we set up the component hierarchy for the view, we did this by constructing objects that modeled what we wanted to appear on the screen, and we used their methods to say how to modify their appearance. We didn’t need to worry about how these objects appeared; the Swing framework took care of this for us. Nevertheless, the view code that we wrote still could be executed sequentially and immediately, constructing and drawing the components to the screen one after another until the view was built out. At this point, the code that we wrote had all finished executing, but this did not cause the program to terminate (as it had in the past). We’ll soon discuss why.

Graphical user interfaces often expose different options for how the user will interact with the program. There may be multiple buttons that a user can click or different text boxes where the user can enter text, and we have no way of knowing in which order the user will interact with these elements. Further, programs may incorporate some element of time if they have some sort of animated component, which may require some parts of the program to continually execute while others await new inputs from the user. Such graphical programs are no longer imperative; they cannot pre-determine what code will need to be run at each point of their execution. Rather, these programs must be responsive to user inputs or other events.

Definition: Event, Responsive, Event-Driven Programs

An event is any occurrence that could necessitate a change in the program's state.

Responsive programs execute certain code instructions in response to events. For this reason, they are also known as event-driven programs.

Event-driven programs behave very differently from imperative programs. Since their instructions are executed after an event takes place, they spend a lot of their time waiting to detect these events. Moreover, since they don’t know how often or in which order these events will occur, they need to repeatedly check (or poll) for new events. We call this process of checking whether a new event has occurred and responding appropriately if it has an event loop.

Definition: Event Loop

An event loop is a piece of code that repeatedly checks whether a new event has occurred and dispatches any detected events to code that can handle them.

The event loop forms the basis for any responsive program. For example, JavaScript’s event loop enables interactivity on web pages. Next, we’ll discuss Java’s Swing event handling framework, which also relies on an event loop.

The Swing Event Loop

Last lecture, we saw that when we wrote the main() method for a Swing application, we used a somewhat complicated syntax:

TicTacToeGraphical.java

1
2
3
4
5
6
public static void main(String[] args) {
  SwingUtilities.invokeLater(() -> {
    TicTacToeGraphical game = new TicTacToeGraphical();
    game.setVisible(true);
  });
}
1
2
3
4
5
6
public static void main(String[] args) {
  SwingUtilities.invokeLater(() -> {
    TicTacToeGraphical game = new TicTacToeGraphical();
    game.setVisible(true);
  });
}

Our entire main() method consists of this single instruction. In fact, this instruction executes right away, and we almost immediately return from the main() method, which typically signals the end of our program. However, the invokeLater() method does something that we have not yet seen in the course: it passes computation to another thread. We’ll discuss threads in more detail in the final lectures of the course when we discuss concurrent programming (i.e., programs that split their computation across multiple threads). For today, you can think of a thread as an abstract entity that executes a sequence of code instructions.

The invokeLater() method starts up a special thread called Swing’s event dispatch thread. Behind the scenes, the Swing framework includes the code that will create and manage an event loop on this thread immediately after executing the code that was passed into invokeLater(); recall that the argument to invokeLater() is an object implementing the Runnable functional interface, whose run() method serves as a wrapper around a sequence of Java instructions.

Definition: Event Dispatch Thread

The event dispatch thread handles the execution of almost all code in the Swing framework. In particular, it contains the main Swing event loop that enables event-handling in responsive graphical applications.

The following figure helps to visualize (at a high level) the execution of a Swing application.

Here, the red line visualizes the main application thread, which executes the main() method by calling the SwingUtilities.invokeLater() method. After this, the work of the main thread is done, so the thread stops. The call to SwingUtilities.invokeLater() starts up the event dispatch thread. At first, this thread executes the code in its Runnable parameter, which does all of the work to initialize the view in the main application JFrame and then makes this frame visible. At this point, the event dispatch thread enters its event loop and is ready to respond to user inputs. When the user closes the main JFrame, the event dispatch thread can be stopped, and the application terminates (since it no longer has any running threads).

Understanding the Event Loop

To better understand what actually takes place in the event loop, it can help to make an analogy with a real-life scenario. We’ll think of the event loop as the front desk of an office. The employees at the front desk sit prepared to handle incoming information. This information can come from many different sources. Sometimes, the phone rings, and they need to answer it and route the call to the correct employee. Other times, mail or package deliveries come in and need to be sent to the correct office. Different people with various requests may show up in person at the office lobby. Finally, there are some tasks, like weekly email reminders, that these employees may take care of on a routine schedule. Some points in the day are particularly busy, and multiple different tasks pile up; some calls may need to be placed on hold. At other points in the day, the office is less busy, and these employees can relax a bit or catch up on more long-term tasks.

The event loop works in a similar manner. Events in our graphical application can come from multiple sources:

Incoming events are queued up and then, one by one (in some order determined behind the scenes by the Swing framework), are dispatched out to be handled. In order for this dispatch to happen correctly, the handlers, pieces of code that understand how to respond to a particular event, must register with the event loop. In our analogy, an employee may inform the front desk that they are expecting a UPS delivery that day. Then, the front desk will know where to send the package when it arrives.

At first, this responsive programming paradigm can seem a bit unintuitive, since it runs counter to most programming you have likely done. When we write event-driven code, we specify up front the actions that will be taken when an event occurs, registering this response with the event loop; however, after this, we don’t know if/when these actions will ever be taken. We trust that the event loop will detect an incoming event and follow our instructions appropriately. We sometimes refer to this paradigm as an inversion of control, since we are handing over control of how our code gets executed to the Swing framework.

Definition: Inversion of Control

Inversion of control is used to describe the paradigm where we write custom subroutines but allow an external entity, such as a framework, to control when these subroutines are executed.

Another name for this is the observer design pattern.

Definition: Observer Pattern

In the observer pattern, different units of code (called the observers) register with (or subscribe to) a central module called the event source (i.e., the event loop), which notifies them of any relevant state changes by calling one of their methods so they can respond appropriately.

Rather than requiring each of the observer objects to independently take responsibility for monitoring the state (which would necessitate many threads and add significant complication to our code design), they delegate this responsibility to the event source.

Now that we’ve introduced many of the high-level ideas behind event handling, we are ready to see how to put these concepts into practice in our Java code.

Listeners

The Swing framework manages event handling using EventListeners, interfaces with methods that define what should happen when the event it is “listening for” is triggered. The source of an event (i.e., the entity that causes the event to be triggered in the first place) registers with the event loop using an add_Listener() method (where _ is replaced by the type of listener). By doing this, the event source says to Swing,

"Hey Swing, I'm just letting you know that I may trigger this type of event in the future, and when I do, this object knows how to handle it. Please let it know about the event, so it can respond appropriately."

As a first example of this, let’s revisit our Tic-Tac-Toe game from the previous lecture.

a screenshot from the running graphical Tic-Tac-Toe game

At the bottom of the window, there is the “Reset” button. We can expect that the user might want to click this button when they are running our application. Thus, the button needs to tell the event loop what should happen when it is clicked. It does this by registering an ActionListener with its addActionListener() method.

If we open the ActionListener interface, we see that it contains a method actionPerformed() with an ActionEvent parameter. Clicking the button is “performing its action.” Therefore, when a button is clicked, the event loop will call the actionPerformed() method on all ActionListener objects registered to that button (i.e., added with the addActionListener() method). It constructs an ActionEvent object to pass to these listeners that packages up information that may be relevant to handle the event (the event’s source object, a description of what command the action performed, the time of the event, etc.).

For now, let’s create a new class that implements the ActionListener interface called a ResetListener that we will register with the reset button.

ResetListener.java

1
public class ResetListener implements ActionListener { ... }
1
public class ResetListener implements ActionListener { ... }

Resetting the game must primarily take place within the TicTacToeGrid, since this class has the game’s model as one of its fields and has access to the TicTacToeCells that constitute most of the game’s view. Let’s add a reset() method to the TicTacToeGrid class that contains the reset logic. This method will need to reset the model, which we can do by reassigning this field to a new TicTacToe object. Then, it must go through and clear the symbols out of all of the TicTacToeCells. Finally, it should request a repaint() to update the view for the user.

TicTacToeGrid.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
/**
 * Resets the game by generating a new model, clearing the symbols from all the cells, and
 * requesting a repaint.
 */
public void reset() {
  model = new TicTacToe();
  for (int i = 0; i < 3; i++) {
    for (int j = 0; j < 3; j++) {
      buttons[i][j].reset();
    }
  }
  repaint();
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
/**
 * Resets the game by generating a new model, clearing the symbols from all the cells, and
 * requesting a repaint.
 */
public void reset() {
  model = new TicTacToe();
  for (int i = 0; i < 3; i++) {
    for (int j = 0; j < 3; j++) {
      buttons[i][j].reset();
    }
  }
  repaint();
}

Then, the actionPerformed() method of our ResetListener simply needs to call the reset() method of the TicTacToeGrid. To do this, it will need to have an alias reference to the grid, which we can initialize in its constructor. Overall, this results in the following class definition.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
/**
 * Responds to a click of the "Reset" button.
 */
public class ResetListener implements ActionListener {
  /** The grid that this listener will reset. */
  private final TicTacToeGrid grid;

  public ResetListener(TicTacToeGrid grid) {
    this.grid = grid;
  }

  @Override
  public void actionPerformed(ActionEvent e) {
    grid.reset();
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
/**
 * Responds to a click of the "Reset" button.
 */
public class ResetListener implements ActionListener {
  /** The grid that this listener will reset. */
  private final TicTacToeGrid grid;

  public ResetListener(TicTacToeGrid grid) {
    this.grid = grid;
  }

  @Override
  public void actionPerformed(ActionEvent e) {
    grid.reset();
  }
}

We connect this listener to the reset button by calling the addActionListener() method from within the TicTacToeGraphical constructor:

TicTacToeGraphical.java

1
2
3
4
5
6
7
8
9
public class TicTacToeGraphical extends JFrame {
  public TicTacToeGraphical() {
    // ...
    JButton resetButton = new JButton("Reset");
    resetButton.setFont(resetButton.getFont().deriveFont(20.0f)); // increase font size
    add(resetButton, BorderLayout.SOUTH);
    resetButton.addActionListener(new ResetListener(grid));
    // ...
  }
1
2
3
4
5
6
7
8
9
public class TicTacToeGraphical extends JFrame {
  public TicTacToeGraphical() {
    // ...
    JButton resetButton = new JButton("Reset");
    resetButton.setFont(resetButton.getFont().deriveFont(20.0f)); // increase font size
    add(resetButton, BorderLayout.SOUTH);
    resetButton.addActionListener(new ResetListener(grid));
    // ...
  }

When we run the code from the end of the previous lecture, we see that the reset button now works as intended, clearing away the symbols that we “hard-coded” into the TicTacToeGrid constructor.

Simplified Listener Creation

While the reset button controller that we implemented in the previous section worked as intended, it was rather cumbersome to implement, requiring us to create a new listener class in a separate file. If our application contains more buttons, we don’t want to have to create separate files to make each button work. We’d like to find a simpler way. Fortunately, we can rely on ideas from earlier in the course (along with some new ones) to attain this simplification.

Step 1: Nest the Listener Class

There is no reason that we need our ResetListener class to live in its own file. This is an auxiliary class that will only ever be used from within the TicTacToeGraphical class, so we can nest it to promote better maintainability (fewer files to track) and encapsulation. To simplify the design of this class even more, we can make it a static nested class (it does not need access to any TicTacToeGraphical fields or instance methods) or even a record class.

TicTacToeGraphical.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
public class TicTacToeGraphical extends JFrame {
  public TicTacToeGraphical() {
    // ...
    JButton resetButton = new JButton("Reset");
    resetButton.setFont(resetButton.getFont().deriveFont(20.0f)); // increase font size
    add(resetButton, BorderLayout.SOUTH);
    resetButton.addActionListener(new ResetListener(grid));
    // ...
  }

  /** Responds to a click of the "Reset" button. */
  private record ResetListener(TicTacToeGrid grid) implements ActionListener {
    @Override
    public void actionPerformed(ActionEvent e) {
      grid.reset();
    }
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
public class TicTacToeGraphical extends JFrame {
  public TicTacToeGraphical() {
    // ...
    JButton resetButton = new JButton("Reset");
    resetButton.setFont(resetButton.getFont().deriveFont(20.0f)); // increase font size
    add(resetButton, BorderLayout.SOUTH);
    resetButton.addActionListener(new ResetListener(grid));
    // ...
  }

  /** Responds to a click of the "Reset" button. */
  private record ResetListener(TicTacToeGrid grid) implements ActionListener {
    @Override
    public void actionPerformed(ActionEvent e) {
      grid.reset();
    }
  }
}

Step 2: Use an Anonymous Class

Since our ResetListener is only instantiated once (within the addActionListener()) argument, we do not actually need to create a new named type for this listener. Instead, we can use a Java feature called an anonymous class, writing:

TicTacToeGraphical.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public class TicTacToeGraphical extends JFrame {
  public TicTacToeGraphical() {
    // ...
    JButton resetButton = new JButton("Reset");
    resetButton.setFont(resetButton.getFont().deriveFont(20.0f)); // increase font size
    add(resetButton, BorderLayout.SOUTH);
    resetButton.addActionListener(new ActionListener(){
      @Override
      public void actionPerformed(ActionEvent e) {
        grid.reset();
      }
    });
    // ...
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
public class TicTacToeGraphical extends JFrame {
  public TicTacToeGraphical() {
    // ...
    JButton resetButton = new JButton("Reset");
    resetButton.setFont(resetButton.getFont().deriveFont(20.0f)); // increase font size
    add(resetButton, BorderLayout.SOUTH);
    resetButton.addActionListener(new ActionListener(){
      @Override
      public void actionPerformed(ActionEvent e) {
        grid.reset();
      }
    });
    // ...
  }
}

Notice that the new expression in the addActionListener() parameter includes the name of the interface type ActionListener, followed by the constructor parameters, followed by a curly-brace delimited scope containing the class’ body. Behind the scenes, Java will create a new class (with some automatically assigned name) with the given body implementing the ActionListener interface. Then, it will call the constructor of this new class with the provided parameters (in this case, no parameters).

The main benefit of this anonymous class is that it places the class definition within the TicTacToeGraphical constructor, providing access to the local grid variable. We say that the variable grid is captured by the anonymous class.

Definition: Variable Capture

An anonymous class (or lambda expression) can capture a variable declared within a surrounding scope, making the value of that variable accessible within the anonymous class (or lambda expression).

Remark:

Variable capture always works for fields and for static variables. These variables are allocated on the heap, so they will not be automatically deallocated when the current method returns. This ensures that they will remain accessible within the anonymous class. Local variables can only be captured when they are explicitly declared as final, or when they are "effectively final", meaning they are never modified after their initial assignment. This allows an anonymous class to safely copy the current value of this variable without needing to contend with the possibility of later modifications.

This variable capture allows us to avoid declaring an additional grid field in our anonymous class, which in turn allows us to forgo supplying a constructor.

Step 3: Use a Lambda Expression

Observe that (although it is not annotated in the documentation), ActionListener is a functional interface. It declares only one method, actionPerformed(). Therefore, we can instantiate an ActionListener using a lambda expression within the parameter of addActionListener().

TicTacToeGraphical.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public class TicTacToeGraphical extends JFrame {
  public TicTacToeGraphical() {
    // ...
    JButton resetButton = new JButton("Reset");
    resetButton.setFont(resetButton.getFont().deriveFont(20.0f)); // increase font size
    add(resetButton, BorderLayout.SOUTH);
    resetButton.addActionListener(e -> grid.reset());
    // ...
  }
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public class TicTacToeGraphical extends JFrame {
  public TicTacToeGraphical() {
    // ...
    JButton resetButton = new JButton("Reset");
    resetButton.setFont(resetButton.getFont().deriveFont(20.0f)); // increase font size
    add(resetButton, BorderLayout.SOUTH);
    resetButton.addActionListener(e -> grid.reset());
    // ...
  }
}

This is, by far, the most elegant solution. The lambda expression syntax clearly shows exactly what will happen when this event is triggered; it will execute grid.reset().

Remark:

While we prefer using lambda expressions to model listeners, this will not always be possible. This is the reason why we presented the other approaches as well.

  • When the EventListener sub-interface we are realizing includes more than one method (for example, a MouseListener or a KeyListener), it cannot be instantiated with a lambda expression, and we will need to write out a more expressive anonymous class. Typically, we prefer anonymous classes extending the adapter classes, such as MouseAdapter and KeyAdapter, since they can minimize the code we need to write.
  • When we need to instantiate the same listener class multiple times (for example, to register it to multiple event sources), we will need to give this class a name, likely defining it as a nested (record) class within whatever outer class we are writing.
  • When the same listener class needs to be instantiated from multiple different files, it likely makes sense to define it in its own file

Adding More Listeners

Let’s complete the controller for our TicTacToeGraphical application by adding the other necessary listeners. First, we need to add a listener that detects when a grid cell is clicked. Since this is a more involved operation, we’ll extract it out into its own cellAction() factory method that returns a suitable ActionListener:

TicTacToeGrid.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class TicTacToeGrid extends JPanel {
  /** Represents the current state of the TicTacToe game */
  private TicTacToe model;

  /** The grid of buttons in each cell of this TicTacToe grid */
  private final TicTacToeCell[][] buttons;

  /** Construct a JPanel with a 3 x 3 GridLayout of TicTacToeCell buttons. */
  public TicTacToeGrid() {
    model = new TicTacToe();
    setBackground(Color.BLACK);
    buttons = new TicTacToeCell[3][3];
    setLayout(new GridLayout(3, 3, 10, 10));

    for (int i = 0; i < 3; i++) {
      for (int j = 0; j < 3; j++) {
        buttons[i][j] = new TicTacToeCell();
        add(buttons[i][j]);
        buttons[i][j].addActionListener(cellAction(i,j));
      }
    } 
    repaint();
  }

  /** Returns an ActionListener that responds to a user click in cell (i,j). */
  private ActionListener cellAction(int i, int j) {
      return e -> {
          // TODO
      };
  }

  // reset() method 
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class TicTacToeGrid extends JPanel {
  /** Represents the current state of the TicTacToe game */
  private TicTacToe model;

  /** The grid of buttons in each cell of this TicTacToe grid */
  private final TicTacToeCell[][] buttons;

  /** Construct a JPanel with a 3 x 3 GridLayout of TicTacToeCell buttons. */
  public TicTacToeGrid() {
    model = new TicTacToe();
    setBackground(Color.BLACK);
    buttons = new TicTacToeCell[3][3];
    setLayout(new GridLayout(3, 3, 10, 10));

    for (int i = 0; i < 3; i++) {
      for (int j = 0; j < 3; j++) {
        buttons[i][j] = new TicTacToeCell();
        add(buttons[i][j]);
        buttons[i][j].addActionListener(cellAction(i,j));
      }
    } 
    repaint();
  }

  /** Returns an ActionListener that responds to a user click in cell (i,j). */
  private ActionListener cellAction(int i, int j) {
      return e -> {
          // TODO
      };
  }

  // reset() method 
}

Take some time to think about what needs to be done within the cellAction() method. It may help to look back at the TicTacToe model class definition from Lecture 24 to see what methods are available.

cellAction() definition

This action listener must update both the model and the view to reflect that the current player has chosen to place a symbol in this cell. First, we'll need to know which player's turn it is, which we can obtain by calling model.currentPlayer(). Once we get this, we can update the model by calling model.processMove(i, j). Then, we can update the view by calling the addSymbol() method (passing in the current player) on the TicTacToeCell object referenced by buttons[i][j]. Lastly, we'll need to determine whether this move ended the game (either with a win for the current player or a tie). We can check this using the model.gameOver() method. For now, we'll leave a "TODO" stub for this behavior, which we'll revisit at the end of the lecture.

TicTacToeGrid.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
/** Returns an ActionListener that responds to a user click in cell (i,j). */
private ActionListener cellAction(int i, int j) {
  return e -> {
    char player = model.currentPlayer();
    model.processMove(i, j); // update model
    buttons[i][j].addSymbol(player); // update view
    if (model.gameOver()) {
        // TODO
    }
  };
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
/** Returns an ActionListener that responds to a user click in cell (i,j). */
private ActionListener cellAction(int i, int j) {
  return e -> {
    char player = model.currentPlayer();
    model.processMove(i, j); // update model
    buttons[i][j].addSymbol(player); // update view
    if (model.gameOver()) {
        // TODO
    }
  };
}

If we run the game at this point, we’ll see that most of the functionality is working; we can click the cell buttons to register moves on the board, and we can click the reset button to clear away these moves. The main missing feature is updating the JLabel at the top of the application window to reflect which player has the next turn.

This is a new type of situation that we need to handle. In this case, an event in one class (clicking a button that triggers a listener defined within the TicTacToeGrid class) results in a side effect that affects a view component in another class (the JLabel referenced by turnLabel in the TicTacToeGraphical class). Swing enables these “class-to-class” effects through property changes. In particular, the event source can call firePropertyChange() to create a PropertyChangeEvent object on the event dispatch thread, which is intercepted by a PropertyChangeListener in another class.

Let’s see this in action. Within our cellAction() method, we’ll fire the property change with the propertyName “Turn”, signaling that the player’s turn has changed.

TicTacToeGrid.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
/** Returns an ActionListener that responds to a user click in cell (i,j). */
private ActionListener cellAction(int i, int j) {
  return e -> {
    char player = model.currentPlayer();
    model.processMove(i, j); // update model
    buttons[i][j].addSymbol(player); // update view
    firePropertyChange("Turn", player, model.currentPlayer());
    if (model.gameOver()) {
        // TODO
    }
  };
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
/** Returns an ActionListener that responds to a user click in cell (i,j). */
private ActionListener cellAction(int i, int j) {
  return e -> {
    char player = model.currentPlayer();
    model.processMove(i, j); // update model
    buttons[i][j].addSymbol(player); // update view
    firePropertyChange("Turn", player, model.currentPlayer());
    if (model.gameOver()) {
        // TODO
    }
  };
}

Then, we’ll add a PropertyChangeListener to our grid variable in the TicTacToeGraphical class (since the TicTacToeGrid object it references is the one that fires the property change) that listens for changes to the “Turn” property and updates the label text accordingly.

TicTacToeGraphical.java

1
2
3
grid.addPropertyChangeListener("Turn",
    e -> turnLabel.setText("It's your turn Player " + e.getNewValue()
            + ". Select a cell to claim."));
1
2
3
grid.addPropertyChangeListener("Turn",
    e -> turnLabel.setText("It's your turn Player " + e.getNewValue()
            + ". Select a cell to claim."));

There is one case that we’ve missed for updating the turnLabel text. Take a minute to try to identify it.

click to show

We must also change the text when the grid is reset(), since our model class always begins the game by giving the "X" player the first turn. We can achieve this by adding the following code to our reset() method within the TicTacToeGrid class.

TicTacToeGrid.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public void reset() {
  model = new TicTacToe();
  for (int i = 0; i < 3; i++) {
    for (int j = 0; j < 3; j++) {
      buttons[i][j].reset();
    }
  }
  firePropertyChange("Turn", null, model.currentPlayer());
  repaint();
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
public void reset() {
  model = new TicTacToe();
  for (int i = 0; i < 3; i++) {
    for (int j = 0; j < 3; j++) {
      buttons[i][j].reset();
    }
  }
  firePropertyChange("Turn", null, model.currentPlayer());
  repaint();
}

Final Thoughts

Dialog Boxes

To finish our Tic-Tac-Toe application, we want to add a pop-up dialog with a message displaying the game’s result (our outstanding “TODO” in the cellAction() method). We can do this using the JOptionPane class. In this case, a message dialog box is the most appropriate, and we can construct one using the showMessageDialog() method.

TicTacToeGrid.java

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
/** Returns an ActionListener that responds to a user click in cell (i,j). */
private ActionListener cellAction(int i, int j) {
  return e -> {
    char player = model.currentPlayer();
    model.processMove(i, j); // update model
    buttons[i][j].addSymbol(player); // update view
    firePropertyChange("Turn", player, model.currentPlayer());
    if (model.gameOver()) {
      JOptionPane.showMessageDialog(this, switch (model.winner()) {
          case 'X', 'O' -> "Congratulations Player " + model.winner() + "!";
          default -> "Tie game. Please play again!";
      }, "Game Over", JOptionPane.PLAIN_MESSAGE);
      reset();
    }
  };
}
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
/** Returns an ActionListener that responds to a user click in cell (i,j). */
private ActionListener cellAction(int i, int j) {
  return e -> {
    char player = model.currentPlayer();
    model.processMove(i, j); // update model
    buttons[i][j].addSymbol(player); // update view
    firePropertyChange("Turn", player, model.currentPlayer());
    if (model.gameOver()) {
      JOptionPane.showMessageDialog(this, switch (model.winner()) {
          case 'X', 'O' -> "Congratulations Player " + model.winner() + "!";
          default -> "Tie game. Please play again!";
      }, "Game Over", JOptionPane.PLAIN_MESSAGE);
      reset();
    }
  };
}

Note here that we used a switch expression to more simply select the appropriate message from among the two possibilities (the win and tie messages). In addition, we reset() the game after the user closes this dialog box so they can play again.

Handling Many Events

As we discussed earlier, the Swing framework relies on the event dispatch thread to manage its event loop. In practice, this means that all Swing widgets must be created and updated on the event dispatch thread to ensure that the event state is managed correctly. All of the event handling code (i.e., the bodies of all listeners) also executes on the event dispatch thread.

Our TicTacToeGraphical application is relatively simple. The user interacts with the game only by clicking on its buttons, and we can expect enough time between these button presses for the event dispatch thread to be able to process the events and complete the required updates of the model and the view. In more complicated applications, such as those with periodic timer events that render each frame of an animation, it can become more noticeable when event listeners try to carry out expensive computations. This can block the event dispatch thread from making progress on updating the view and introduce a lag into the program. For this reason, it is critical that event handling code is quick and simple. Any larger computations should be passed off to another thread (for example, by using a SwingWorker). We’ll talk more about spreading work across multiple threads in our final unit on concurrent programming.

We've reached the end of our very short introduction to graphical applications and event-driven programming. You now have the tools that you need to start building these programs, and we encourage you to take some time to experiment and see what you can build (small games, like our Tic-Tac-Toe example, make great exercises). You'll likely find that you'll need to access additional Swing properties, classes, or features to handle specific cases in the applications that you're building. For this, the extensive Swing documentation (primarily in the Swing tutorial) is a great reference!

Main Takeaways:

  • All Swing framework components are constructed and managed by Swing's event dispatch thread, which also executes its main event loop. Long event handler code can block this thread and result in lag, so it should be avoided.
  • Graphical applications leverage an inversion of control to be responsive to user inputs. The implementer packages behaviors into methods and delegates control over calling these methods to the framework's event loop. This is also referred to as the observer pattern.
  • An event source registers one or more listeners, which contain code for responding to that event.
  • Event listeners can often be instantiated as anonymous classes or lambda expressions.

Exercises

Exercise 25.1: Check Your Understanding
(a)
Programming with a GUI framework often entails an inversion of control. Which of the following is associated with this style of programming?
Check Answer
(b)

Consider the following snippet of Swing code for changing some text in a window when a button is clicked:

1
2
3
JLabel text = new JLabel("text");
JButton b = new JButton("Click me!");
b.addActionListener(v -> text.setText("Action: " + v.getActionCommand()));
1
2
3
JLabel text = new JLabel("text");
JButton b = new JButton("Click me!");
b.addActionListener(v -> text.setText("Action: " + v.getActionCommand()));
Which of the following is the event source?
Check Answer
(c)

A student accidentally wrote the following as their main() method for TicTacToeGraphical.

1
2
3
4
public static void main(String[] args) {
  TicTacToeGraphical game = new TicTacToeGraphical();
  game.setVisible(true);
}
1
2
3
4
public static void main(String[] args) {
  TicTacToeGraphical game = new TicTacToeGraphical();
  game.setVisible(true);
}
What happens when the code is run?
Check Answer
Exercise 25.2: From Concrete Implementation to Lambda Expression
Suppose we define the following graphical application to allow users to interact with a checkbox. When the checkbox is selected, the label should display "Compliant"; otherwise, it should say "Not compliant". For each of the following subproblems, define a listener in the specified way and attach it to checkbox. Note that JCheckBox has an instance method called addItemListener(), which is invoked anytime the checkbox is clicked. addItemListener() takes in an ItemListener as a parameter. You may also find ItemEvent helpful.
1
2
3
4
5
6
7
8
9
/** A simple window with a label and a checkbox. */
public class AppWindow extends JFrame {
  public AppWindow() {
    JCheckBox checkbox = new JCheckBox("Agree to Terms & Conditions.");
    JLabel label = new JLabel("Not compliant");
    add(label, BorderLayout.NORTH);
    add(checkbox);
  }
}
1
2
3
4
5
6
7
8
9
/** A simple window with a label and a checkbox. */
public class AppWindow extends JFrame {
  public AppWindow() {
    JCheckBox checkbox = new JCheckBox("Agree to Terms & Conditions.");
    JLabel label = new JLabel("Not compliant");
    add(label, BorderLayout.NORTH);
    add(checkbox);
  }
}
(a)

Define an external listener class.

1
public class CheckListener implements ItemListener { ... }
1
public class CheckListener implements ItemListener { ... }
(b)
Define a nested class within AppWindow.
(c)
Use an anonymous class.
(d)
Use a lambda expression.
Exercise 25.3: Event Tracing
Suppose that player 1 clicks the center cell in the Tic-Tac-Toe grid.
(a)
Which lines of code in which file are run due to this event? How does the model change?
(b)
When the user makes this move, the center cell will now be filled in. What line allows the view of the grid to be updated as a result of the event?
(c)
This listener also fires a property change event. What components are listening to this change? How do the view and model of the other components change?
Exercise 25.4: When Lambda Expressions Fail
A developer wants to add a border to the cell of the grid that the user is currently hovering over. To do this, they notice that Swing has a MouseListener interface that can accomplish this.
(a)
Why can’t the developer use a lambda expression to attach a MouseListener to each button?
(b)

The developer instead decides to make TicTacToeCell implement MouseListener.

1
public class TicTacToeCell extends JButton implements MouseListener { ... }
1
public class TicTacToeCell extends JButton implements MouseListener { ... }
Modify the constructor to register the MouseListener.
(c)
Override mouseEntered() and mouseExited() to draw the border only when a user is hovering over this cell. You may use BorderFactory to quickly create a Border. You can leave the bodies of the other three MouseListener methods blank.
Exercise 25.5: Player Customization Controller
Extending on Exercise 24.3, let's add the controller for the player customization.
(a)
Add ActionListeners to the JComboBoxes to change the shape of the user. This action listener should fire a property change.
(b)
Register a property change listener with your grid that modifies its model to the selected shape.
(c)
Add ActionListeners to the JRadioButtons to change the color scheme.
Exercise 25.6: Undo Button for Tic-Tac-Toe
Let's add an undo button to our Tic-Tac-Toe game.
(a)
We’ll first start with the view. Add a button to the right of the Reset button. You may need to refactor the existing component hierarchy to properly show both buttons.
(b)
Moving on to the model, what data structure can we use to model a history of previous moves (excluding undos) that would efficiently, in \(O(1)\) time, support looking up the last move? Add this data structure to the model of TicTacToeGrid.
(c)

Define an undo() method that undoes a move.

TicTacToeGrid.java

1
2
3
4
/**
 * Undoes the last move. Requires that a move was previously done.
 */
public void undo() { ... }
1
2
3
4
/**
 * Undoes the last move. Requires that a move was previously done.
 */
public void undo() { ... }
(d)
Finally, complete the controller, and attach an action listener to the undo button.
Exercise 25.7: Connect 4 Controller
Add the controller to the Connect 4 graphical application we defined in Exercise 24.5. You'll need to attach listeners to each of the columns that we represented as buttons and the reset button.
Exercise 25.8: Minesweeper
In Minesweeper, you are given a grid of tiles, some of which contain mines. Your goal is to uncover all non-mine (safe) tiles. Each safe tile reveals how many bombs it is 8-directionally adjacent to. Try playing a game of Minesweeper here on Google.
(a)
As with our other two games, start by defining a model for the game. You should randomly generate a board containing mines. When a user clicks to reveal a cell, if it has 0 adjacent mines, all cells that can reach the clicked cell through a path of cells that also have 0 adjacent mines should be automatically revealed, along with the numbered border cells around that region.
(b)
Extend this to a graphical application. Plan out your component hierarchy first, then implement the view of Minesweeper. You should at least contain the grid, a reset button, and the expected number of bombs left. However, feel free to add any other features.
(c)
Make your application interactive by adding listeners. Your controller should contain listeners for left-clicking and right-clicking on each cell. Left-clicking reveals the cell, while right-clicking places a flag on the cell. A cell that has a flag on it cannot be left-clicked and should decrease the expected number of bombs. The flag can be removed with a second right-click.