Lecture 12: GUI Basics

1Introduction 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.1Outline

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.

2Stage 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.1Frames 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:

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.

3Stage 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.

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.

4Stage 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.)

5Aside: 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.

6Stage 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 same manner as this KeyboardListener, our implementation also
generalizes the ActionListener implementation into a dictionry that maps
action commands to Runnables.)

7Stage 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.

8Further 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.

9Exercises

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?