CS 124, Spring 2014
Lab 10: GUI Tutorial

In this lab you will create a simple GUI program that lets the user draw some colored rectangles in a drawing area. The program is not very exciting, but the lab introduces you to some fundamental ideas of GUI programming in tutorial form. You should carefully follow the steps for creating the program, and you should test your program frequently along the way. Almost all of the code from the program can be copy-and-pasted from this page into your program — but you should try to understand what you copy. However, you won't understand everything fully until you read Chapter 6.

Start by creating an Eclipse project named lab10. There are no files to copy into the project. You will create the necessary files from scratch.

Your work from this lab is due at the start of lab next Thursday. As a special dispensation for this tutorial lab, you do NOT need to add comments to the program.

Panels and Program

The visual building blocks of a Graphical User Interface (GUI) are called components in Java. Often they are referred to as "widgets." A fundamental component is the panel, represented by the Java class JPanel. A panel can be used in two ways: as a place where you can draw with paintComponent and as a container for other components. This program will use several panels. The main panel will be a container that will hold a smaller panel where the rectangles will be drawn. The main panel will also hold other components.

You need a class to represent the main panel. Create a new class in your project named RectPanel for this purpose. This class, like many GUI classes, needs to import the standard GUI classes. To do this, you can add the following lines to the top of the file:

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;

Alternatively, you could import individual classes one at a time as you need them. A RectPanel will be a kind of JPanel, that is, it extends the JPanel class. To make this true, change the first line of the class definition to read

public class RectPanel extends JPanel {

Your program will need several other classes. It is useful to have a class to represent rectangles that have been drawn on the screen. You also need a class to represent the panel where the drawing is done; that class will extend JPanel, just like the main panel class. It would be possible to put these classes in separate files, but we will take advantage of the fact that a class definition can contain nested classes. To define the two classes as nested classes, add the following code inside the definition of the class RectPanel. You will not have to make any changes to these classes for the rest of the lab:

private class Rect {
    int x1,y1;   // coordinates of one corner of the rect
    int x2,y2;   // coordinates of the other corner
    Color color; // the color of the rectangle
}

private class Display extends JPanel {
    protected void paintComponent(Graphics g) {
        super.paintComponent(g); // fill with background color
        for (int i = 0; i < rectCount; i++) {
               // draw the i-th rect, with a black border
            Rect r = rects[i];
            int x = Math.min( r.x1, r.x2 );
            int y = Math.min( r.y1, r.y2 );
            int w = Math.max( r.x1, r.x2 ) - x;
            int h = Math.max( r.y1, r.y2 ) - y;
            g.setColor( r.color );
            g.fillRect( x,y,w,h );
            g.setColor(Color.BLACK);
            g.drawRect( x,y,w,h );
        }
    }
}

You should also add the following instance variables to the RectPanel class. Note that both the Rect and the Display classes are used to define instance variables.:

private Rect[] rects;       // Contains the rects that the user has drawn
private int rectCount;      // number of rects that have been drawn
private Color currentColor; // currently selected color for new Rects
private Display display;    // the drawing area

Finally, for now, you need a constructor for the RectPanel class. Add the following constructor to the class definition:

public RectPanel() {
    rects = new Rect[1000];
    rectCount = 0;
    currentColor = Color.RED;
    display = new Display();
    display.setPreferredSize( new Dimension(640,480) );
    display.setBackground(Color.WHITE);
    this.setLayout(new BorderLayout());
    this.add(display, BorderLayout.CENTER);
}

This constructor initializes all the instance variables (which could also be done as part of the declarations of those variables). It configures the display panel by setting its "preferred size," the size that it wants to be, and by setting its background color. The new feature here is that the display panel is "added" to the RectPanel that is being constructed. The add() method is used to add components to a JPanel that is being used as a container. A panel uses a "layout" to set the positions and sizes of the components that it contains. The default layout just lines up the components in a row, but here I want to use a "border layout," so I set the layout for this panel to be a new object of type BordeLayout. A border layout has a large "center" component bordered, optionally, by components to the "north", "south", "east", and "west" of the center. Now I can add the display to this panel. The second parameter to add says that we are placing the display in the center of the layout. Later, we will add another component in the south position. (Note that the use of "this" in the last two commands is optional, but I want to emphasize that I am calling a method in the RectPanel that is being constructed.)


Even a GUI program needs a main routine. When you run a program, it's the main routine that is executed. For a GUI program, the main routine typically just creates and configures a window and shows it on the screen. The window itself is represented by an object of type JFrame. A JFrame comes with a title bar and the controls that the title bar contains. It can hold a "content pane," usually a JPanel, that fills most of the window. It can also hold a menu bar, represented by an object of type JMenuBar.

Although the main routine can go in the RectPanel class, it really doesn't belong there. It makes more sense to put it in its own separate class. So, create a new class to contain the following main routine:

public static void main(String[] args) {
    JFrame window = new JFrame("Draw Some Rectangles!");
    RectPanel mainPanel = new RectPanel();
    window.setContentPane( mainPanel );
    window.pack();
    window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    window.setLocation(60,60);
    window.setVisible(true);
}

When you want to run your program, remember that you have to run the class that contains the main program. Most of the code in the main routine should be reasonably easy to figure out, but note that the command window.pack() sets the size of the window based on the preferred sizes of the components that it contains.

A this point, you should be able to run the program and see a window containing a large, blank, white area.

Mouse Drag

For the next step, we add mouse interaction to the program. When the user interacts with a GUI program, it generates "events." The program can "listen" for these events, which just means that the program can specify subroutines to be called when the events occur. These subroutines allow the program to respond to the user's events.

When the user clicks-and-drags with the mouse on a component, a series of events is generated. The action starts with a "mousePressed" event when the user first presses a button on the mouse while the mouse is over the component. This is followed by a series of "mouseDragged" events as the user moves the mouse. Finally, there is a "mouseReleased" event when the use releases the mouse button. To program responses to these events, we need an object to act as a listener. For this application, we use another nested class for this purpose. Here is a general-purpose nested class definition that can be used to define listeners for mouse events. Add it to the inside of class RectPanel:

private class MouseHandler extends MouseAdapter {
    boolean dragging; // is the user dragging the mouse?
    public void mousePressed(MouseEvent evt) {
        if (dragging) { // don't start a new drag if already dragging
            return;
        }
        dragging = true;
    }
    public void mouseDragged(MouseEvent evt) {
        if (!dragging) { // don't do anything if not dragging
            return;
        }
    }
    public void mouseReleased(MouseEvent evt) {
        if (!dragging) { // don't do anything if not dragging
            return;
        }
        dragging = false;
    }
}

To set up listening, you have to add the following code to the RectPanel constructor:

MouseHandler mouser = new MouseHandler();
display.addMouseListener(mouser);
display.addMouseMotionListener(mouser);

The MouseHandler object is added as a listener to the display panel, which means that it will respond to mouse events on that panel. It is added as a "mouseListener," which means it will process the mousePressed and mouseReleased events. And it is added as a "mouseMotionListener," which means that it will process the mouseDragged events.

It only remains to program the methods in the MouseHandler class. When the user presses the mouse, you want to create a new Rect and add it to the array of Rects. Both corners of the new rectangle are placed at the mouse position, given by evt.getX() and evt.getY(). Since both corners are at the same point, this rectangle starts out with size zero and is not visible. The color of the rectangle is copied from the instance variable currentColor, which for now is red. To do all this, add the following code to the mousePressed() method:

Rect r = new Rect();
r.x1 = r.x2 = evt.getX();
r.y1 = r.y2 = evt.getY();
r.color = currentColor;
rects[rectCount] = r;
rectCount = rectCount + 1;

Note that since we have added 1 to rectCount, the rectangle that is currently being drawn is rects[rectCount-1]. As the user drags the mouse, the second corner of this rectangle must be set to the new mouse position, and the display must be repainted to show the change. To accomplish this, add the following code to the mouseDragged() method:

Rect r = rects[rectCount-1];
r.x2 = evt.getX();
r.y2 = evt.getY();
display.repaint();

For this program, no changes are necessary in mouseReleased().

You should now be able to run the program and draw rectangles by dragging the mouse on the display.

Control Panel with Buttons

It's time to add another component to the RectPanel. We will add another small panel in the "south" position of the RectPanel. This panel will contain a couple of buttons. A button is represented by an object of type JButton. So, we create a JPanel, add it to the main panel, then create a JButton and add it to the new panel. To do this, add the following code to the RectPanel constructor:

JPanel buttonBar = new JPanel();
this.add(buttonBar, BorderLayout.SOUTH);
JButton clearButton = new JButton("Clear");
buttonBar.add(clearButton);

The parameter to the JButton constructor is the text that will appear on the button. You can run the program now to see the button, but clicking on it won't do anything yet. When the user clicks a button, an event of type ActionEvent is generated. To program a response, you need to add an "action listener" object to the button. Then, when the user clicks the button, the system will call an actionPerformed() method in the action listener object. To define the action listener, use another nested class inside RectPanel:

private class ActionHandler implements ActionListener {
    public void actionPerformed(ActionEvent evt) {
        String cmd = evt.getActionCommand(); // the text from the button
        if (cmd.equals("Clear")) {
            rectCount = 0;  // effectively delete all rects from the data
            display.repaint();  // repaint the display to show the change
        }
    }
}

Back in the RectPanel constructor, you have to create an object of type ActionHandler and add it as an action listener for the button. To do this, add the following two lines at the end of the RectPanel constructor, and then run the program to see that it works::

ActionHandler actor = new ActionHandler();
clearButton.addActionListener(actor);

Assignment: Add an "Undo" button to the buttonBar panel, and program it by adding some code to the ActionHandler. When the user clicks the Undo button, the most recently added rectangle should be removed from the drawing. You can do this simply by subtracting 1 from rectCount — but you should be careful not to do that if rectCount is zero.

Menu Bar

Commands in GUI programs are often given using menus rather than buttons. As an example, we will use a menu for setting the current drawing color. Menu items are similar to buttons: When the user selects a menu command an action event is generated, and you can use an action listener to respond to the command. However, a menu item must be in a menu, which must in turn be in a menu bar, and that menu bar must be added to a window — which sounds more complicated that it is. A problem for us is that the window is created in the main program, but the menu bar has to be created and programmed in the RectPanel class. There are a few steps to make this possible...

First, write a method in the RectPanel class to create the menu bar that will be used in the program. Here is the code that adds just one item to the color menu:

public JMenuBar makeMenuBar() {
    JMenuBar menubar = new JMenuBar(); // create the menu bar
    JMenu colorMenu = new JMenu("Color"); // create a menu
    menubar.add(colorMenu); // add the menu to the menu bar
    ActionHandler menuHandler = new ActionHandler(); // for responding to menu commands

    JMenuItem item; // item to be added to the color menu
    
    item = new JMenuItem("Blue");  // create the item
    item.addActionListener(menuHandler);  // set menuHandler to handle this command
    colorMenu.add(item);  // add the item to the colorMenu

    return menubar;
}

You also have to program the action listener to respond to the command from the menu item. Note that we are using the same action listener class that we used for the buttons (which is not especially good style). Setting the current drawing color to blue just means setting the value of the instance variable currentColor to Color.BLUE, so add the following case to the actionPerformed() method in the ActionHandler class:

else if (cmd.equals("Blue")) {
    currentColor = Color.BLUE;
}

Finally, to get the menu to appear in the window, you have to add the menu bar to the JFrame that represents the window. To do that, go to the main() routine and add the following line:

window.setJMenuBar( mainPanel.makeMenuBar() );

You should now be able to run the program and use the menu to change the drawing color to blue.

Assignment: Add at least three more colors to the "Color" menu, including red (and make them work).

Dialog Box

It's boring to have only a few drawing colors to choose from. To fix that, we can make it possible for the user to select a custom color of their choice. Java has a standard method for putting up a "color chooser" dialog box where the user can select any color.

To implement custom colors, first add another item to the "Color" menu. The item should read "Custom...". The "..." is a standard way of indicating that the command will call up a dialog box. Then add the following case to the actionPerformed() method in the ActionHandler class:

else if (cmd.equals("Custom...")) {
    Color c;
    c = JColorChooser.showDialog(display,"Select Drawing Color",currentColor);
    if (c != null) {  // (if c is null, the user canceled the dialog)
        currentColor = c;
    }
}    

And that's it! Try drawing with a custom color!

Bored?

I won't give any extra credit for adding to this program, and it's certainly not required, but if you want to do some more work on the program, here are some ideas. Add a "Background" menu to let the user change the background color of the display. (To set the background color, you just need to call display.setBackground(color).) Disable the "Clear" and "Undo" button when it doesn't make sense to use them. (You will have to look up how to do this, and you will have to make the variables for those buttons into instance variables instead of local variables in the constructor.) Add a "File" menu with commands "Save...", "Open...", and "Quit". For Save and Open, you can use TextIO.writeUserSelectedFile() and TextIO.readUserSelectedFile(). (Just write some kind of textual description of all the rectangles to the file that you save.) You might also add a "Tool" menu that lets the user draw ovals and roundrects as well as rects.