There are a few basic ideas we should consider with GUIs:
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).
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. The little blip on the line below is all you get. The top part is the "title bar" of the window, the gray part is the bottom border.
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.
import javax.swing.*;
public class Ex0 {
public static void main(String[] args) {
JFrame f = new JFrame();
f.setTitle("IC211 GUI Ex0");
f.setSize(300, 400);
f.setLocation(100, 100);
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
f.setVisible(true);
}
}
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.
import javax.swing.*;
public class Ex1 {
public static void main(String[] args) {
JFrame[] F = new JFrame[5];
for( int i = 0; i < 5; i++ ) {
JFrame f = new JFrame();
f.setTitle("IC211 GUI Ex1 " + i);
f.setSize(300, 400);
f.setLocation(100 + 10 * i, 100 + 10 * i);
// f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
F[i] = f;
}
System.out.print("Enter x to continue: ");
System.console().readLine();
for( int i = 0; i < 5; i++ ) {
F[i].setVisible(true);
}
}
}
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 weirdness 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.
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!
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 every time a window closes, just to show that we can react to these events.
import javax.swing.*;
import java.awt.event.*;
public class Ex2 {
static class WindowDisposer implements WindowListener {
public void windowActivated(WindowEvent e) {}
public void windowClosed(WindowEvent e) {}
public void windowClosing(WindowEvent e) {
System.out.println("Closed a window!");
}
public void windowDeactivated(WindowEvent e) {}
public void windowDeiconified(WindowEvent e) {}
public void windowIconified(WindowEvent e) {}
public void windowOpened(WindowEvent e) {}
}
public static void main(String[] args) {
WindowDisposer wd = new WindowDisposer();
JFrame[] F = new JFrame[5];
for( int i = 0; i < 5; i++ ) {
JFrame f = new JFrame();
f.setTitle("IC211 GUI Ex2 " + i);
f.setSize(300, 400);
f.setLocation(100 + 10 * i, 100 + 10 * i);
f.addWindowListener(wd);
F[i] = f;
}
System.out.print("Enter x to continue: ");
System.console().readLine();
for( int i = 0; i < 5; i++ ) {
F[i].setVisible(true);
}
}
}
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.
import javax.swing.*;
import java.awt.event.*;
public class Ex3 {
static class WindowDisposer extends WindowAdapter {
public void windowClosing(WindowEvent e) {
System.out.println("Closed a window!");
}
}
public static void main(String[] args) {
WindowDisposer wd = new WindowDisposer();
JFrame[] F = new JFrame[5];
for( int i = 0; i < 5; i++ ) {
JFrame f = new JFrame();
f.setTitle("IC211 GUI Ex3 " + i);
f.setSize(300, 400);
f.setLocation(100 + 10 * i, 100 + 10 * i);
f.addWindowListener(wd);
F[i] = f;
}
System.out.print("Enter x to continue: ");
System.console().readLine();
for( int i = 0; i < 5; i++ ) {
F[i].setVisible(true);
}
}
}
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.
import javax.swing.*;
import java.awt.event.*;
public class Ex4 {
static class WindowDisposer extends WindowAdapter {
int count;
public WindowDisposer(int i) {
count = i;
}
public void windowClosing(WindowEvent e) {
System.out.println("Closed a window!");
if (--count == 0) {
System.exit(0);
}
}
}
public static void main(String[] args) {
WindowDisposer wd = new WindowDisposer(5);
JFrame[] F = new JFrame[5];
for( int i = 0; i < 5; i++ ) {
JFrame f = new JFrame();
f.setTitle("IC211 GUI Ex4 " + i);
f.setSize(300, 400);
f.setLocation(100 + 10 * i, 100 + 10 * i);
f.addWindowListener(wd);
F[i] = f;
}
System.out.print("Enter x to continue: ");
System.console().readLine();
for( int i = 0; i < 5; i++ ) {
F[i].setVisible(true);
}
}
}
Questions: Why must we make WindowAdapter a class? Why couldn't we make it an interface? If you are defining a class "Foo" that you want to add as a WindowListener to a JFrame, what limitations are forced on you if you decide to extend the class WindowAdapter rather than implement the interface WindowListener?
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.
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.
import javax.swing.*;
import java.awt.event.*;
public class Ex5 {
public static void main(String[] args) {
WindowDisposer wd = new WindowDisposer();
JFrame[] F = new JFrame[5];
for( int i = 0; i < 5; i++ ) {
F[i] = new DisposableJFrame(wd);
}
System.out.print("Enter x to continue: ");
System.console().readLine();
for( int i = 0; i < 5; i++ ) {
F[i].setVisible(true);
}
}
}
import javax.swing.*;
import java.awt.event.*;
class DisposableJFrame extends JFrame {
private static int nextId = 0;
private int id;
public DisposableJFrame(WindowDisposer wd) {
id = nextId++;
this.setTitle("IC211 GUI Ex0 " + id);
this.setSize(300, 400);
this.setLocation(100 + 10 * id, 100 + 10 * id);
this.addWindowListener(wd);
}
public int getId() {
return id;
}
public void addWindowListener(WindowDisposer wd) {
wd.add(id);
super.addWindowListener(wd);
}
}
import java.util.*;
import javax.swing.*;
import java.awt.event.*;
class WindowDisposer extends WindowAdapter {
private ArrayList<Integer> A = new ArrayList<Integer>();
public void add(int i) {
A.add(i);
}
public void windowClosing(WindowEvent e) {
int id = ((DisposableJFrame)(e.getWindow())).getId();
System.out.println("Closed a window " + id);
A.remove(new Integer(id));
if (A.size() == 0) {
System.exit(0);
}
System.out.print("Windows remaining are:");
for( Integer i : A ) {
System.out.print(" " + i);
}
System.out.println();
}
}
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 everything 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.