Lecture 16: GUI Basics
1 Introduction to Views
1.1 Outline
2 Stage 1:   Introduction to GUIs
2.1 Frames and controls
3 Stage 2:   Decoupling the view from the model
4 Stage 3:   Enhancing the view
5 Aside:   Low-level and high-level events
6 Stage 4:   Making the controller more flexible using low-level events
7 Stage 5:   Decoupling the view from the controller using high-level events
8 Further enhancements
9 Exercises
7.4

Lecture 16: GUI Basics

This page is a work in progress.

1 Introduction to Views

So far we’ve worked on designing models to represent the data relevant to a problem domain, in a form that encapsulates the data behind an interface that clients can use without having to know any implementation details. The model is responsible for ensuring that it can’t get stuck in a bogus or invalid state, and exposes whatever appropriate observations and operations are needed while still preserving this integrity constraint.

We’ve also worked on simple synchronous controllers that allow users to interact with a model, in a form that encapsulates the user interactions and can provide feedback to users without having to redundantly ensure any integrity constraints. Moreover, controllers can be customized or enhanced without needing to change the model, making the model more convenient to use without making it any more complex.

Now, it’s time to introduce the third part of the MVC trio: views. Views are renderings of the data in the model, and can be as simple as printing debug output to a console, as complex as fancy graphical user interfaces, or anything in between. Dealing with GUIs brings additional challenges, so we’ll focus on those.

1.1 Outline

Our plan in this lecture is to start with poorly-designed but working code, and improve it in three stages. Initially, the code for the view will directly manipulate the model, and so our first improvement will decouple the view from the model so that it need not — and in fact cannot do so. Our second improvement will add a new feature to the view, and add the ability to control that in the controller. Finally, our third improvement will generalize the controller to make its UI triggers more customizable.

The code for this lecture is available at the top of this page, as the MVC code link.

2 Stage 1: Introduction to GUIs

In the BadDesignButFunctional directory, we have a poorly written implementation of a simple program. Our model state (interface IModel and implementation Model) consists of a single String, which our GUI will allow us to update. The model state is deliberately trivial here, to highlight the model-view interactions. Real models naturally would be far more complex, and their interfaces far more detailed.

Our main() method constructs a Model, and constructs a view that is given a reference to the model. The view interface (IView) has only three methods: to obtain the text the user has typed in to the text box, to clear that text box, and to echo a string to the label in the UI. The implementation of the view, JFrameView, is markedly more complicated in appearance than expected, but it breaks down into several simpler parts.

2.1 Frames and controls

Our program uses the Swing framework to show its UI. In Swing, an individual window is known as a frame, which can contain controls known as components. To create our own window, we design a class that subclasses from JFrame, do some work to establish the components in it, and then call setVisible to display the window. Within the constructor of our frame, we create four components: a label to show some text, a text box to edit text, and two buttons. Adding several controls to a frame requires that we give them a layout, which describes the spatial relationships between the controls. In this example, we’re using a FlowLayout, which allows the controls to wrap around as we resize the window. (Try running the project and resizing the window to be narrower than the controls are.) Different layout managers allow adding controls in different ways. Once controls are added to the UI, the pack() method is used tell the layout manager to determine the actual positions and sizes of all the controls.

In order for the controls to do anything, however, we need to add an event handler to them. An event handler, or callback, is simply a function that gets called when something interesting occurs. In our case, the clicking of different buttons should trigger a callback. In the jargon of Swing, clicking on buttons triggers their action, and so we must supply a function object that implements the ActionListener interface. (Other controls have additional events besides “actions”.) For convenience, Swing allows us to label each button with a so-called action command, which is a String of our choosing: when the ActionListener’s callback is invoked, it will be given an ActionEvent object that knows the action command of the button that was clicked. In this way, we can use a single listener to listen to multiple buttons at once, and distinguish them by means of this command string. See the calls to setActionCommand and setActionListener and the implementation of actionPerformed in JFrameView.java for an example:

class JFrameView extends JFrame implements ActionListener {
  public JFrameView(String caption, IModel model) {
    ...
    echoButton = new JButton("Echo");           // Create a button,
    echoButton.setActionCommand("Echo Button"); // set its command,
    echoButton.addActionListener(this);         // set the callback,
    this.add(echoButton);                       // and add it to the UI

    exitButton = new JButton("Exit");           // ditto, for another button
    exitButton.setActionCommand("Exit Button");
    exitButton.addActionListener(this);
    this.add(exitButton);
    ...
  }

  @Override
  public void actionPerformed(ActionEvent e) {
    switch (e.getActionCommand()) {
    case "Echo Button": ...
    case "Exit Button": ...
    }
  }

  ...
}

Note: Combining the event handlers of multiple buttons into a single function is only temporarily convenient: often, the code we want to run for one button is completely different from the code we want to run for a different button, so there’s not much benefit from merging them all. Instead, it is more common to create anonymous objects, or (even terser) lambda expressions, so that each button gets its own custom handler. We’ll see other idioms of setting up listeners below.

3 Stage 2: Decoupling the view from the model

The code above technically works, but it is very poorly designed: the view is responsible for mutating the model, which means there’s no separation of concerns between this view and any controller, and if we wanted to use the model with another sort of view, we’d be out of luck. In the BasicMVC directory, we start to remedy this. In particular, we want to separate out all the parts of the code that mutate the model, and isolate them within a controller.

To do this, we create a Controller class that takes in the model and the view — at their interface types, not at their concrete class types. We revise the view so that it no longer has access to the model at all. (This is overly drastic; we merely want to ensure that the view does not have mutable access to the model. We can revisit this later.) We next add a method to the view interface, void setListener(ActionListener), which is the key indirection needed here. Instead of the view directly implementing the response to events, this method allows the view to take in a listener object and forward any events it receives to that listener.

public class JFrameView extends JFrame implements IView {
  public JFrameView(String caption) { // NOTE: No model!
    ...
    echoButton = new JButton("Echo"); // NOTE: No action listener
    echoButton.setActionCommand("Echo Button");
    this.add(echoButton);

    exitButton = new JButton("Exit");
    exitButton.setActionCommand("Exit Button");
    this.add(exitButton);
    ...
  }

  public void setListener(ActionListener listener) {
    echoButton.addActionListener(listener); // Rather adding *this* as a listener,
    exitButton.addActionListener(listener); // add the provided one instead.
  }

  ...
}

public class Controller implements ActionListener {
  public Controller(IModel m, IView v) {
    this.model = m;
    this.view = v;
    view.setListener(this);
    view.display();
  }

  @Override
  public void actionPerformed(ActionEvent e) {
    switch (e.getActionCommand()) {
      case "Echo Button": ...  // same code as before, but now
      case "Exit Button": ...  // it's extracted out of the view
    }
  }
}

The controller is now the only part of the system that has mutable access to the model. Because it requested that the view register itself as the listener for the buttons, the controller gets called exactly when necessary, and it can decide what mutations to perform on the model. The view doesn’t even know that it’s received a controller object: as far as it’s aware, the controller is simply a random ActionListener.

Note: there is a subtle difference between the setListener method we’ve defined on our IView interface, and the addActionListener method present on the Swing components: our method’s name implicitly intends for only one listener at a time, but Swing components allow for multiple listeners. When we have multiple listeners, we’ll sometimes say that the control broadcasts its event to whoever’s listening, or that it publishes its event to whoever’s subscribed to it. There’s nothing limiting us from implementing this more general approach, but that generality wasn’t needed here.

4 Stage 3: Enhancing the view

Our next addition of functionality is shown in the KeyboardEvents directory: we want to add some keyboard-triggered behaviors. Specifically, we’ll add two fancy features to our UI: the ability to toggle the color of the text from black to red and back, and the ability to temporarily show the text in all-caps. We’ll switch colors every time we type the 'd' key, and we’ll temporarily capitalize the text while we’re pressing and holding the 'c' key. Interestingly, only one of these two new features requires adding a new method to our view interface.

First, we’ll need to generalize our setListener method, to take in a KeyListener as well as an ActionListener. A KeyListener is analogous to a ActionListener, but as the name suggests, it listens for keyboard-related events. There are three such events: when a key is pressed, when a key is typed, and when a key is released. Pressing and holding down a key for a while will typically generate one key-pressed event, several key-typed events, and one key-released event. Just as ActionListeners accept ActionEvents, KeyListeners accept KeyEvents containing information about which key was involved. We’ll use the keyTyped event to toggle the color of the text, use the keyPressed event to capitalize the text, and use the keyReleased event to un-capitalize the text.

For now, we’ll simply have our controller implement the KeyListener interface also, and pass itself along as the second argument to view.setListeners. Again, note that the view doesn’t know or care that the exact same object is being passed in as the two distinct listeners: the types ensure that it doesn’t matter.

To implement the color changing, we’ll need to add a method to our view interface to toggle the color of the text. This is intrinsically a view-specific thing, since the controller cannot know exactly how the text is displayed or which control it needs to change color. (That would leak internal implementation details of the view, and in any case, the controller only knows it has an IView rather than a particular view class.)

To implement the capitalization, note that we do not actually mutate the model! This is both a good thing and a necessary thing: suppose the model text contained a mixture of upper- and lower-case letters. If we mutated the model and capitalized everything, then we would not be able to undo that change later. Instead, we ask the model for its content, and inform the view that it should display a capitalized version of the that content. (This view-only change is analogous to “zooming in” while editing a picture in Photoshop or some other image editor: the view is technically displaying only a subset of the pixels of the document, and moreover is displaying them at far more than one screen pixel per document pixel! If “zooming in” actually mutated the document, then we’d lose information and be unable to “zoom out” again.)

5 Aside: Low-level and high-level events

Within the KeyListener interface, there is a qualitative difference between the key-pressed and key-released events, and the key-typed event. Individual key events are incredibly, tediously low-level. Just trying to type a capital 'A' generates five events: the Shift key was pressed; the A key was pressed; the letter 'A' was typed; the A key was released; the Shift key was released. (The last two events might happen in either order, depending on which key was released first.) Notice that only one key-typed event occurred, though, and it contained exactly the text that was typed.

If we had to deduce which keys were typed, merely from the key-pressed and key-released events, we’d quickly lose track. Java (and most GUI toolkits) thankfully translate those sequences into higher-level key-typed events. And this translation has an addtional benefit: consider typing non-English text on a typical QWERTY keyboard. We clearly need to type mutliple physical keys to produce one logical character (this is sometimes known as “chord” input, by analogy with pressing multiple keys on a piano keyboard), and this translation lets us ignore the individual key-pressed and key-released events if we only want to consider what text was typed.

On the other hand, if we want to keep track of which keys are pressed (e.g. to control a player’s motion while a key is held down), we need to resort to the lower-level events.

This low-level/high-level distinction is not clearly defined, and depends on perspective. Would we consider ActionEvents to be low-level or high-level? On the one hand, they’re clearly much higher-level than individual MouseEvents, which are analogous to KeyEvents and indicate when a mouse button is pressed, released, or clicked, or when it enters or exits some area. Indeed, JButtons register themselves as MouseListeners, and translate the relevant mouse-clicked event into an ActionEvent! (They also register themselves as KeyListeners, and generate ActionEvents when the Enter key is pressed.) But at the same time, the user of our view might not care which particular buttons we happened to use to implement the view, and there might well be multiple buttons that trigger identical actions: from that perspective, action events are too low-level and should be implementation details hidden behind some abstraction barrier.

Designing a view and controller properly requires considering what level of detail we want to expose in the events that the view forwards to the controller. Our current designs expose far too low-level detail: “something happened with the following action command”, or “some key was pressed/typed/released”. These events are very general, and have no specific semantics for our application. Let’s consider the different enhancements we can make, using either low-level or high-level events. We’ll find that we might want to translate generic low-level events into application-specific high-level ones.

6 Stage 4: Making the controller more flexible using low-level events

Many applications allow the user to customize the hotkeys that control the application. Our prior attempt hard-coded the keys in the various key event-listeners. In the KeyboardMaps directory, we generalize this so that we can reconfigure hotkeys at runtime. To accomplish this, we design a new KeyListener implementation that uses dictionaries of Runnables instead of switch statements in its event handlers. Specifically, our KeyboardListener will contain a Map<Integer, Runnable> dictionary for its key-pressed event handler, another such dictionary for its key-released handler, and a Map<Character, Runnable> dictionary for its key-typed handler. (These Runnables are examples of the command pattern, which we talked about several lectures ago.)

The handlers are pleasingly straightforward:

// In the KeyboardListener class
@Override
public void keyTyped(KeyEvent e) {
  if (keyTypedMap.containsKey(e.getKeyChar()))
    keyTypedMap.get(e.getKeyChar()).run();
}

// analogously for the other two events

Because the dictionaries are data, we can mutate them at runtime if we so choose, and therefore change which keys are mapped to which responses. (For variety’s sake, we show three different syntaxes for creating Runnables: explicit classes, anonymous classes, and lambda expressions.)

// In the Controller class
  private void configureKeyBoardListener() {
    Map<Character, Runnable> keyTypes = new HashMap<>();
    Map<Integer, Runnable> keyPresses = new HashMap<>();
    Map<Integer, Runnable> keyReleases = new HashMap<>();

    // Uses an explicit function-object class to provide the implementation
    keyReleases.put(KeyEvent.VK_C, new MakeOriginalCase());
    // Uses an anonymous object to provide the implementation
    keyPresses.put(KeyEvent.VK_C, new Runnable() {
      public void run() {
        String text = model.getString();
        text = text.toUpperCase();
        view.setEchoOutput(text);
      }
    });
    // Uses lambda-syntax to provide the implementation
    keyTypes.put('r', () -> { view.toggleColor() });

    KeyboardListener kbd = new KeyboardListener();
    kbd.setKeyTypedMap(keyTypes);
    kbd.setKeyPressedMap(keyPresses);
    kbd.setKeyReleasedMap(keyReleases);

    this.view.addKeyListener(kbd);
  }

(In the same manner as this KeyboardListener, our implementation also generalizes the ActionListener implementation into a dictionry that maps action commands to Runnables.)

7 Stage 5: Decoupling the view from the controller using high-level events

The previous generalization relied on the view exposing its low-level events to the controller. However, we might reasonably want to trigger the same behavior from multiple UI controls. In the GeneralCommandCallbacks directory, we take this approach: our view can toggle the color of the text either via a button, or via typing the 't' key. (This is a simplified example, but is a stand-in for typical toolbar buttons doing the same thing as hotkey shortcuts.)

The key innovation in this design is that we’ve eliminated the ActionListeners and KeyListeners from the controller and also from the IView interface. Instead, we have a new addFeatures method that takes in a new interface, Features, whose methods are the various high-level features and abilities of our view. Our prior interfaces exposed low-level events saying, for instance, “Hey, a button with this action command has been clicked; what should be done?” These new callbacks say, for example, “The user has requested to make the display uppercase; what should be done?” This interface is bigger than the ActionListener interface, but it’s also much more application-specific, and it advertises exactly the responsibilities a controller has to uphold for this view. It also successfully encapsulates the action commands that we leaked in prior designs: the view is now free to change those commands without breaking the controller at all.

This design is quite elegant, and does the best job of loosening the coupling between the view and the controller: by encapsulating the details of which physical controls do what, the view is much more abstracted away, and the logical interface that it presents to the controller is much more application-specific. When possible, aspire to interfaces like this one, but be prepared to fall back to lower-level events when necessary.

Note: It might seem odd that the toggleColor method is both a callback on the Features interface and a method on the IView interface — why can’t the view handle this internally? Indeed, it possibly could! (And in some circumstances, the view definitely should handle some events entirely internally: for instance, when implementing a text editor, the view should probably internally handle the sequence of low-level events that indicate the user has selected a stretch of text. Once the user does something with that text — deletes it, replaces it, copies it, etc. — the view can raise a semantically relevant high-level event with the selected content.) But consider a “bigger” version of our current program, where we have two views that we want to keep synchronized: if either of them has toggled its color, both should toggle their colors together. The only way to ensure this synchronization is for the view to forward the event to the controller, which can in turn decide to tell both views how to update themselves.

8 Further enhancements

Our last revision eliminated the flexibility of dynamically changing hotkeys and reverted to hard-coding the keys in the view’s implementation of its own key listener. Combining the flexibility of the KeyboardListener and its dictionaries, with the high-level events of the Features interface, is aesthetically tricky: who should control which keys do what? In some sense, it almost feels like the choice should be made not by our current controller, which is controlling a particular model and view, but rather to some hypothetical “application controller”, that controls the overall application. We’ve already encountered this in practice: in IntelliJ, there are both project-specific settings and application-wide settings, and different dialogs control those different features.

9 Exercises

At the top of this lecture are starter files for a GUI version of the Turtles example we worked through in Lecture 9. The TurtleGraphicsView does not currently do anything. Enhance this code with a new TurtlePanel class that extends JPanel, and override its paintComponent method to draw whatever you want, just to confirm that it works.

Next, enhance the IView interface so you can pass the relevant information from the model, through the controller, into the view and into your TurtlePanel class. Once you’ve connected the pieces, use this information in your paintComponent implementation to draw the turtle’s trace.

The links at the top of the lecture include a “solution” implementation; do not to look at that until you’ve tried to implement this yourself.

The IView interface contains one method for setting up an event listener. What is its signature? Does it seem like a high-level event to you, or a low-level one? If you think it’s too low-level, can you think of a better, higher-level signature to use? If you think it’s sufficiently high-level, why do you think so?