The purpose of this lab is to introduce the Swing graphical toolset. The classes and methods shown in this lab may be applicable to the graphical user interface of Problem Set 6, but its main goal is to prepare you for the Gizmoball final project.
In the beginning... The first version of Java included a graphics system called AWT (the Abstract Window Toolkit). Each AWT object is represented and drawn by the host operating system's analogue for that object. For example, creating an AWT dialog box simply creates a native dialog box that is presented to the user.
The fundamental problem with this approach was that MacOS, Windows and UNIX all have dialog boxes that look very different from each other. This made it difficult to create platform-independent applications with platform-independent user interfaces.
Java2 introduced the Swing toolset, which is a layer on top of AWT. Swing essentially works by creating a blank canvas, and then painting all user interface elements inside that window. Because it only depends on the operating system for canvas creation, the majority of an application can be designed to look the same on any platform.
The division between Swing and AWT is fairly blurry. The entire Swing API is implemented by calling AWT methods to draw using the operating system's graphic primitives. Also, many components in Swing inherit features or specs from AWT components, so you will need to refer to AWT documentation on occasion.
All Swing classes are in the package javax.swing and its subpackages, and all AWT classes are in java.awt.
Copy that Lab 8 code to your directory:
athena% mkdir ~/6.170/lab8 athena% cp /mit/6.170/www/labs/lab8/*.java ~/6.170/lab8/
What most people consider a "window" is referred to as a JFrame (javax.swing.JFrame) in Swing. There is a similar AWT class called java.awt.Frame. JFrames use Frames, which in turn call the operating system's native windowing methods.
Consider this code, given in ExampleJFrame.java:
package lab8;
import javax.swing.*;
public class showJFrame {
public static void main(String[] args) {
JFrame frame = new JFrame();
frame.setSize(200,200);
frame.show();
}
}
Most of your code will need to import javax.swing.* and/or java.awt.*. The code simply creates a window and displays it.
Compile and run ExampleJFrame.java
What happens when you click on the close button (or "Destroy" it with certain window managers for Unix)? The program continues running, and you're not returned to the command prompt. This is by design, because an application can own several windows (for instance, a word processor with multiple open documents), and closing just one of them should not quit the entire application. Consider ExampleJFrame2.java:
package lab8;
import javax.swing.*;
import java.awt.event.*;
public class showJFrame2 {
public static void main(String[] args) {
JFrame frame = new JFrame();
frame.addWindowListener(new WindowAdapter () {
public void windowClosing (WindowEvent e) {
System.exit(0);
}
});
frame.setSize(200,200);
frame.show();
}
}
Compile and run ExampleJFrame2.java
There are three important things to note about showJFrame2.
First, we imported java.awt.event.* for the WindowAdaptor class. This is just the beginning of reliance on AWT to write Swing applications.
Second, we added a listener on the JFrame. Listeners are frequently used in event-driven programs such as a GUI applications, where the events are things like mouse clicks and key presses. With a listener, you don't need to constantly keep checking "was the `y' key pressed" every 100 milliseconds. Instead, you can just register a listener, and then when a key is pressed, a method of the listener is automatically called.
In showJFrame2, we register a window listener. When the user clicks on the JFrame's close button, the listener's windowClosing() method automatically gets called, and the program exits. Your Gizmoball program will use listener objects to listen for key presses.
Third, we've taken a shortcut that might be confusing if you're not familiar with it. The listener that we're registering is not quite a WindowAdapter. We've created what's known as an anonymous class. The notation
new Foo() {
...
method declaration
...
}
creates a subclass of Foo, overrides some methods, and
then creates an instance of that subclass. Anonymous classes simply
allow the convenience of not having to name the intermediate class.
This anonymous class notation is desugared, at compile time, into
class ANONCLASS1 extends Foo {
...
method declaration
...
}
new ANONCLASS1()
(Actually, the class is named something like
"ExampleJFrame2$1" to indicate that it's the first anonymous
class defined in ExampleJFrame2.java. The name of the class
generally never matters, but this explains why you have a file called
ExampleJFrame2$1.class in your directory after compiling showJFrame2.java.)
If you have any questions at this point, please ask an LA for help.
Another type of listener is, as mentioned above, a key listener. ExampleKeyPress.java adds another listener to the JFrame:
frame.addKeyListener( new KeyListener() {
public void keyPressed(KeyEvent e) {
System.out.println("PRESSED " + e);
}
public void keyReleased(KeyEvent e) {
System.out.println("RELEASED " + e);
}
public void keyTyped(KeyEvent e) {
System.out.println("TYPED " + e);
}
} );
Compile and run ExampleKeyPress.java
Using a callback like this is very much akin to the difference between "push" and "pull". In a "pull"-based event model, we'd need to continually ask Swing if any keys were pressed, and if so, what are they. In a "push"-based system, we define a set of actions, and if the event happens, Swing pushes the event along to our event handlers.
This listener just prints out the key information as you press keys in the window. Because KeyListener is an interface, the anonymous class must implement all of the methods declared in the interface. However, if you have no need for release information, you can extend KeyAdapter and override only the methods you need. (Like WindowAdapter, KeyAdapter has empty methods to satisfy the compiler.)
In more advanced applications, you must be aware of where the keyboard focus is. "Focus" tells the program where to enter text if you have 5 text fields in a window. Usually, one of the fields has an insertion cursor to indicate that it has the focus. In the keyPress program, the entire window has the focus and receives any key events. A more complex application must carefully manage where focus is. This problem usually manifests itself when you have registered a key listener in a component, but the listener isn't receiving any events. You must find a way to grab the focus for the intended component, or prevent the other component from stealing the focus in the first place.
Now we will finally draw something. Suppose we want something like:
create a new JFrame window
draw a line
draw a square
draw a circle
ExamplePaintFrame.java modifies ExampleFrame2.java by adding:
Graphics g = frame.getGraphics();
g.drawLine( 0, 50, 20, 50);
g.drawRect(40, 50, 20, 20);
g.drawOval(80, 50, 20, 20);
Compile and run ExamplePaintFrame.java
The getGraphics method returns a graphics context of type Graphics that allows you to draw on the part of the screen that the Swing object controls.
Compile and run ExamplePaintFrame. Does it draw the line, square, and circle? Try move the window on your screen. Also try resizing the window. What happens?
When we drew onto the JFrame, it was essentially a one-time operation. Swing doesn't remember that you drew those shapes, so when it needs to redraw the JFrame for any reason (such as because the window changed sizes), the shapes are lost. We need a way to make the shapes persistent by having Swing redraw them every time it tries to redraw the JFrame. We can do this by overriding the JFrame's paint() method.
public void paint(Graphics g) {
super.paint(g);
g.drawLine( 0, 20, 20, 50);
g.drawRect(40, 20, 20, 20);
g.drawOval(80, 20, 20, 20);
}
Compile and run ExamplePaintFrame2.java
ExamplePaintFrame2 keeps the objects drawn (actually, it redraws them), even through resizes and repaints. In general, it's not a good idea to override a JFrame's paint() method, but we have done so here for illustration. What you'd usually do is add some component (suchas JComponent or Canvas.
) to the JFrame, and have that object's paint method be overridden to draw the figures.Each JFrame has a content pane, which is where all user interface elements are put. The content pane is a JPanel, which is a grouping construct that Swing uses to compose multiple components into a single box-shaped unit. JPanels can contain other JPanels, as well as check boxes, text fields, etc.
Instead of painting directly onto a JFrame as in the last section, we might implement the same functionality by adding something to the JFrame's content pane which draws a line, square, and circle.
JFrame frame = new JFrame();
JPanel panel = frame.getContentPane();
panel.add(something that can paint shapes);
ExampleJPanel.java shows some very crude nesting of JPanels inside each other. Of course, just having JPanels isn't too useful, but we'll learn about other interface components soon. What you should understand from this sections is that JPanels help you group elements in your window for positioning, and can themselves be grouped.
Compile and run ExampleJPanel.java
The previous section only used the JPanel.add() method to add elements to the JPanel, without giving much control over how to place it in the parent container.
Swing gives you several ways of positioning elements, called layout managers. Each JPanel can have a layout manager assigned to it, which then interprets your add() commands and places the object appropriately in the window. The main managers are:
Look at the API documentation for descriptions of each of these layout managers. For most of the managers, you supply an additional parameter to the add() method specifying where the JPanel should add the new element.
Compile and run ExampleLayout.java
ExampleLayout.java creates 5 JFrames showing examples of each of these. Compare the source code to the examples to understand how the placement is happening. (The windows have been rearranged for the figure.)
We'll now learn about Swing's interface elements -- the components you put into JPanels. When designing an interface, first determine what sort of layout manager will give you optimal flexibility. Then cut up the frame's content pane into JPanels. Finally, place the interface elements where appropriate.
A JLabel is a text string that is placed onto your pane. They're often useful to place beside something else as a label.
JLabel label = new JLabel("a label");
panel.add(label);
Compile and run ExampleJLabel.java
Buttons are self-explanatory. But how do you specify what should happen when the user clicks on the button? For this, we introduce a new type of listener called an ActionListener. We'll call JButton.addActionListener() with a class that extends ActionListener.
Each ActionListener has an actionPerformed() method. Each time the button is pressed, the actionPerformed() method is called with an event object that represents information about the click.
Compile and run ExampleJButton.java
ExampleJButton.java constructs a grid of buttons, each of which has an action listener that prints the information to the terminal.
There can also be a many-to-one relationship between JButtons and ActionListeners. ExampleJButton2.java shows a single ActionListener which listens for button presses from all four buttons. How does it differentiate the button that was pressed? Look in java.swing.AbstractButton to see if you can figure out how to set an alternate string as the action command, which does not need to match the button text.
Compile and run ExampleJButton2.java
JTextFields are small boxes meant to hold a line of text. You can create them with a specified width and/or initial content. (See the API documentation for more information.)
You can register an action listener for the text field, which will be called whenever the user presses the enter key. Since you might want to get information from the text field without an explicit enter keypress, sometimes an "Enter" button is provided that collects the data from the field and performs some action on it.
Compile and run ExampleJTextField.java
ExampleJTextField.java behaves exactly like this. Compile and run it, and see how it behaves. When text is sent to the terminal, it's prefaced by what caused it to be printed -- either a button press or the text field's action handler.
ExampleJTextField.java also takes advantage of the DocumentListener interface, which is described in the API documentation as well. Each Swing text component has a backing Document class, and a document listener can be registered for notifications of changes to the backing Document. So in the example class, entering any text causes an immediate document change. Pressing the enter key in the field causes the action listener to be called. Finally pressing the button causes the button's action listener to be called.
JTextAreas have similar semantics to JTextFields, since both are text components. You can register listeners, as well as get and set the text of the area. There is the method append() for convenience, or you can modify the backing Document object directly.
JCheckBoxes are a type of button which can toggle between an off state and an on state. For example:
final JCheckBox check = new JCheckBox("Check");
check.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent evt) {
if(check.isSelected()) {
System.out.println("is selected");
} else {
System.out.println("not selected");
}
}
});
An interesting note is that we needed to declare check as final. The anonymous inner class refers to that variable, so it needs a fixed handle on the object it's going to send a message to. An alternative way of writing the class is:
JCheckBox check = new JCheckBox("Check");
check.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent evt) {
JCheckBox s = (JCheckBox)evt.getSource();
if(s.isSelected()) {
System.out.println("is selected");
} else {
System.out.println("not selected");
}
}
});
ActionEvent.getSource returns an object of compile-time type Objects; you will need to cast it to a different type.
Using the starter code Exercise1.java, create an interface as shown on this page.
The user can enter a number into the text field, and click on the "Draw" button. The right side of the window then draws a polygon with that many sides (including the degenerate "2-gon" line). Each polygon "starts" at the far right for its first point.
To finish this exercise, you will need to:
Using your solution from Exercise 1, add a checkbox below the "Draw!" button labeled "Dynamic". If it's checked, the user should no longer need to press enter or click on the button to cause an update.
You will need to: