Reading

Required: These notes!
Recommended: Java in a Nutshell, TO APPEAR

Overview

Gooey goo for chewy chewing, that's what the Goo Goose is doing.

GUIs ... what are some things we want?

GUIs have some basic ideas: Even something as simple as a button in a GUI is actually hugely complicated: it has to be drawn, and redrawn whenever its window gets minimized than maximized again, or redrawn when it is partially covered by a window layed over it that subsequently moves out of the way; it has to detect when it has been clicked (and possibly change appearance while the mouse is depressed) and actually cause some kind of action when clicked; it might be grayed out and unclickable for some periods of time; it might have a "tooltip" that tells what it does if the mouse hovers over it for long enough; and so on. And this for only one button! Certainly many, many lines of code must be written (at some level) to handle all this complexity. What do we want as future GUI programmers? We want to not have to worry about all that stuff! We want:
  1. to be able to use components like buttons, windows, text-fields, etc. without having to mess with or worry about how they are implemented,
  2. to be able to modify specific aspects of components like buttons, windows, text-fields, etc. when necessary without messing up all the other aspects (e.g. making a window change colors for 10 second intervals shouldn't make the click-X-to-close stop working) and without having to mess with or worry about how they are implemented,
  3. to be able to have our programs react to user actions on GUI componenets (like a button click) without having to mess with or understand how those GUI components are implemented.
Doesn't this sound like the sepration of interface from implementation concerns that motivated object oriented programming in the first place? It should scream out to you that these design goals can be met by making GUI components "objects", i.e. instances of classes, that we can "use" by simply calling mathods, that we can modify/customize/build-on via inheritance, all without having to understand or worry about the implementation. So there is a class for buttons (JButton), and a class for windows (JFrame), and a class for text input fields (JTextField) and so on. A button needs one interface for a "click-reactor" that you (and every other programmer) can implement in different ways for each different button you create. So we just have a Java interface for that (ActionListener) and we hand the button an object that implements that interface and say "when you are clicked, call the react-to-a-click method from the click-reactor interface on this object". Polymorphism then provides the mechanism for allowing many implementations for that one click-reactor interface.

In this lesson, we're just going to learn how to create the most basic GUI component - a window - and how to react to the most basic user action - clicking the x to close the window. Although that seems very limited, it will show us the basic mechanisms that permeate the whole Java Swing API (the GUI API we'll use).

Windows: creating, displaying, destroying

The basic Java class for a window is JFrame. You create a JFrame like you do anything else: you use new!
JFrame f = new JFrame();
However, while this creates the JFrame, it does not display it. It only gets displayed when you set it to be visible.
f.setVisible(true);
Now if you create a program with only those two lines, it "works" but is more than a bit underwhelming. There are several more things that you almost certainly want to do whenever you create a JFrame.
    JFrame f = new JFrame();
    f.setTitle("IC211 GUI Ex0"); // sets title that appears on the top bar
    f.setSize(300,400);          // sets the size (in pixels) of the frame
    f.setLocation(100,100);      // sets the top-left corner of the window on the desktop
    f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); // makes it so that closing window exits program
    f.setVisible(true);
With this minimal code you get a Window you can be proud of.

Now, to emphasize the fact that a window is nothing more than JFrame object, let's create 5 windows and pop them all up on the screen at once. The following program will create all five first, wait for the user to enter an 'x', and only then will it actually display the five on the screen.

You'll notice that the "EXIT_ON_CLOSE" line is commented out. Why? With that uncommented, closing any one window exits the program. On the other hand, with it commented out like it is, even when all five windows have been closed, the program is still running. We have to do a ctrl-c in the terminal window to exit it. That's something we'll fix in a bit.

Despite the hiccough of window closing in our example, I hope you'll see that having windows simply be objects like everything else in Java, is pretty nice. We can create them, store them, and manipulate them with methods calls, just as we do for more familiar things like Strings and Exceptions.

But main is over!?!? Why doesn't the program just end as soon as it starts?

One thing about the programs we've just looked at should be bothering you. By the time we finishing executing the setVisible(true)'s, main() is done. Shouldn't the program stop at that point? The reason the program doesn't stop at that point is because programs that use the Swing GUI components (like JFrame) automatically use at least two threads. A thread is an executing stack of function calls. All programs you've seen up to this point have been single-threaded, which means that there is only one executing stack of function calls. In a multi-threaded program there are more than one simultaneously executing stacks of function calls. A multi-threaded program doesn't exit until all threads have finished executing. So, going back to our Swing program with the JFrames, we have the main thread (the stack of function calls with the call to main() on the bottom) and we have a second thread called the event dispatch thread in which executes all code related to displaying the GUI and responding to user actions within the GUI. So, even though the main thread ends as soon as the setVisible(true)'s are done, the event dispatch thread continues running ... and so the program continues running.

The fact that all GUI actions run in this even dispatch thread is going to be an important topic in the future. But for now, we'll comfortably ignore that fact!

Handling events in Swing: counting dead windows

The program we ended with in the above had the unfortunate problem that it kept running even after the last window was closed. To handle this properly, i.e. to exit the program only when all five windows have closed, we need to simply count the number of window closures. When that count gets to five, we should exit the program.

The closing of a window is an event, just like button pushes, combo-box selections, or text box changes, all of which we'll consider in the future. So dealing with this one little problem will give us an opportunity to examine the general design of event handling in Java's GUI API. That basic model is quite simple: a GUI component keeps a list of Listener objects that are listening to the various events that can occur with that component, and when an event happens the component, it calls a method on the Listener object that is associated with that kind of event, and passes it an Event object that contains information about the details of the event that occurred.

To be concrete, actions on JFrames generate WindowEvent objects, listeners for these events are classes that implement the WindowListener interface. That interface is

WindowListener
{
    void windowActivated(WindowEvent e);
    void windowClosed(WindowEvent e);
    void windowClosing(WindowEvent e);
    void windowDeactivated(WindowEvent e);
    void windowDeiconified(WindowEvent e);
    void windowIconified(WindowEvent e);
    void windowOpened(WindowEvent e);
}
... which shows you all the events that might occur. When you create a class that implements that interface, you need to define each of these methods, which means providing the code that you want to execute in response to each of these actions. For starters, let's simply print out a message everytime a window closes, just to show that we can react to these events.
Now, if you are like everyone else in the world, you are horrified at all the code you have to write just to print out a message when a window is closed. As a convenience, just to ease that pain, the Java API has defined a class called WindowAdapter that implements WindowListener, simpy defining all the methods with empty bodies, { }. This way, we can define our WindowDisposer class as extending WindowAdapter and only override the one method we care about, windowClosed(WindowEvent e);. The new version of the program below, is functionally the same as the previous version, but somewhat shorter.

So, finally, we are able to solve our original problem by making WindowDisposer do a simple bit of work: each time a window gets closed, decrement a counter. When the counter gets to zero, exit the program.

So, what do we take away from this? Java Swing's event handling mechanism is fundamentally about polymorphism and inheritance (or at least the limited form of inheritance offered by interfaces). In fact, it is a prime example of multiple implementations of the same interface, because every time we need to define how to react to an event (like a window closing) we provide a different implementation of the listener interface.

A common pattern - extending JFrame

Let's do one last tweak ... a nice tweak though. Let's give each window its own id, and have the WindowDisposer keep track of the id's of the unclosed windows so that it not only recognizes that all windows are closed, but also reports after each closure which are the remaining unclosed windows. This means every JFrame in our program will be a normal JFrame with the added functionality of having an id that it knows and can report, and a mechanism for ensuring that whenever a new WindowDisposer gets added as a listener, the WindowDisposer is made aware of the JFrame's id. In other words, we need to derive a new class from JFrame, DisposableJFrame, that adds this extra functionality. For ease of presentation, I'm going to split the program into three classes at this point, rather than use nested classes.

So what's the takeaway here? Very often this is how we make GUIs in Java. We don't just use JFrame's like we do Strings and ints, we adapt and extend them to fit the application we have in mind. In any given application, almost all of the JFrame's behavior is simply inherited - i.e. almost everthing works like a generic JFrame - but some crucial parts will change, or some important new functionality will be added. The fact that we can derive new components, like DisposableJFrame, from existing ones is central to the Swing design.