1.124 Lecture 13 10/29/1998

Adding Interactivity to a Graphical User Interface

Contents

1. Event-Driven Programming

Graphical programs require a very different programming model to the non-graphical programs we have encountered in the past.  A non-graphical program typically runs straight through from beginning to end.  By contrast, a graphical program should be capable of running indefinitely, accepting input through the graphical user interface (GUI) and responding accordingly.  This kind of programming is known as event-driven programming.  The general structure of a graphical program is as follows:

    layout_user_interface();

    while (true) {                                    // The event loop.
          Event e = get_next_event();          // Get the next event from the event queue.

          if (e.type == QUIT) {
                break;
          }
          else {
                process_event(e);                   // Respond to the event, according its type.
          }
     }

In C++, the programmer must often explicitly write an event loop similar to the one shown above.  This can involve a lot of work, so Java attempts to shield the programmer from the actual event loop, while still providing a flexible way to specify how events are processed.
 
 

2. The Java Event Model (JDK 1.1 and above)

(Ref. Java Tutorial: http://java.sun.com/docs/books/tutorial/ui/components/eventmodel.html,  Core Java Vol. 1 Ch. 8)

The Java event model is based on the notion of event sources and event listeners.

The event listener registration and notification process is done according to event category.  Any object wishing to listen to events of a particular category must implement the interface corresponding to that category.  The interface simply specifies a standard set of functions that our listener object must have.

Here is a partial list of event types, their event categories and the corresponding listener interfaces.
 
 
Event Event Category Listener Interface
Button click, menu selection, text field entry. ActionEvent ActionListener
Resize, move, show, hide. ComponentEvent ComponentListener
Mouse button down, mouse button up. MouseEvent MouseListener
Mouse move, mouse drag. MouseEvent MouseMotionListener
Key press, key release. KeyEvent KeyListener
Gain focus, lose focus. FocusEvent FocusListener
Window closing, window iconified, window deiconified. WindowEvent WindowListener
 

Suppose we want our user interface to have a single Button, which causes a message to be displayed every time it is clicked.  To receive button clicks, our event listener must be capable of listening to ActionEvents.  We must therefore make our event listener class implement the ActionListener interface.  The ActionListener interface requires us to implement a single method named actionPerformed().
 

MyListener.java

import java.awt.event.*;

public class MyListener implements ActionListener {
    // Respond to an ActionEvent.
    public void actionPerformed(ActionEvent e) {
        System.out.println("Clicked button.");
    }
}
 

We must now create the Button and register a MyListener object by calling the button's addActionListener() method.
 

MyFrame.java

import java.awt.*;

public class MyFrame extends Frame {
    Button mButton;                                     // The event source.
    MyListener mListener;                             // The event listener.

    // The constructor.
    public MyFrame() {
        mButton = new Button("Click me.");
        mListener = new MyListener();

        mButton.addActionListener(mListener);  // Register the listener object with the button.
        add(mButton);                                     // Position the button within the frame.
        setSize(300,400);                                  // Set the size of the frame.
    }

    // The main function.
    public static void main(String[] args) {
        MyFrame f = new MyFrame();
        f.show();
    }
}
 
 

3. Laying Out the User Interface

(Ref. Java Tutorial: http://java.sun.com/docs/books/tutorial/ui/layout/index.html - this uses Swing components instead of the basic AWT components, Core Java Vol. 1 Ch. 9)

Our previous example has only one interesting UI component: a Button.  What if we wanted to add a second Button and perhaps a TextArea, so that we can display the message through the UI?  We can control the layout of these components within the frame by using a layout manager.  Java comes with five layout managers:

It is also possible to use no layout manager and instead position components by specifying their absolute coordinates.

Suppose we wish to position our two Buttons side by side, with the TextArea positioned below them.  This is most easily done by embedding the Buttons within a Panel, and then positioning the Panel above the TextArea.  The Panel thus serves as a container for the two Buttons.  The following code illustrates this.
 

MyFrame.java

import java.awt.*;

public class MyFrame extends Frame {
    Button mButton1;                                                // The first event source.
    Button mButton2;                                                // The second event source.
    Panel mPanel;                                                    // A container for the buttons.
    TextArea mTextArea;                                           // An area for displaying messages.
    MyListener mListener1;                                       // An event listener for the first button.
    MyListener mListener2;                                       // An event listener for the second button.

    // The constructor.
    public MyFrame() {
        // Create two buttons.
        mButton1 = new Button("Button 1");
        mButton2 = new Button("Button 2");
 
        // Create a panel and set its layout manager.  In fact, FlowLayout
        // happens to be the default layout manager for a Panel.
        mPanel = new Panel();
        mPanel.setLayout(new FlowLayout());

        // Position the two buttons within the panel.
        mPanel.add(mButton1);
        mPanel.add(mButton2);

        // Create a text area for displaying messages.
        mTextArea = new TextArea();

        // Set the layout manager for the frame.  In fact, BorderLayout
        // happens to be the default layout manager for a Frame.
        setLayout(new BorderLayout());

        // Position the panel and the text area within the frame.
        add(mPanel, "North");
        add(mTextArea, "South");

        // Create the listener objects and register them with the buttons.
        mListener1 = new MyListener(1, mTextArea);
        mButton1.addActionListener(mListener1);
        mListener2 = new MyListener(2, mTextArea);
        mButton2.addActionListener(mListener2);

        // Set the size of the frame.
        setSize(300,400);
    }
 

    // The main function.
    public static void main(String[] args) {
        MyFrame f = new MyFrame();
        f.show();
    }
}
 

MyListener.java

import java.awt.*;
import java.awt.event.*;

public class MyListener implements ActionListener {
    int mButtonNumber;
    TextArea mTextArea;

    // The constructor.
    public MyListener(int i, TextArea t) {
        mButtonNumber = i;
        mTextArea = t;
    }

    // Respond to an ActionEvent.
    public void actionPerformed(ActionEvent e) {
        mTextArea.append("Clicked button " + mButtonNumber + "\n");
    }
}

4. A Graphics Example

Here is a more complex example that allows the user to interactively create 2D geometric objects using the mouse.

Geometry.java

// This is a Java graphics example that can be run either as an applet or as an application.
// Created by Kevin Amaratunga 10/17/1997.

import java.awt.*;
import java.awt.event.*;
import java.applet.Applet;
import java.util.*;

// In order to run as an applet, class Geometry must be declared as
// a public class.  Note that there cannot be more than one public
// class in a .java file.  Also, the public class must have the same
// same name as the .java file.
public class Geometry extends Applet {
    TextArea mTextArea;
    MyCanvas mCanvas;

    public Geometry() {
        // Choose a layout manager.  BorderLayout is a straightforward one to use.
        setLayout(new BorderLayout());

       // Create a drawing area and add it to the center of the applet.
       mCanvas = new MyCanvas(this);
       add("Center", mCanvas);

       // Create a read only text area to be used for displaying
       // information.  Add it to the bottom of the applet.
       mTextArea = new TextArea(5, 40);
       mTextArea.setEditable(false);
       add("South", mTextArea);
    }

    public TextArea getTextArea() {
        return mTextArea;
    }

    public static void main(String args[]) {
        // Create the applet object.
        Geometry mGeomApplet = new Geometry();
 
        // Create the main window with the applet embedded in it.
        new MainWindow(mGeomApplet, 600, 800);
    }
}
 

// When running as a Java application, we need to create a Frame within which
// to display the applet.
class MainWindow extends Frame implements WindowListener {
    // MainWindow constructor.
    MainWindow(Geometry geomApplet, int iWidth, int iHeight) {
        // Set the size of the frame, and its title.
        setSize(iWidth, iHeight);
        setTitle(geomApplet.getClass().getName());

       // Register the frame to start receiving window events.
       addWindowListener(this);

       // Add the applet to the center of the frame.
       add("Center", geomApplet);

       // Initialize the applet.
       geomApplet.init();

       // Make the frame visible.
       show();

       // Start the applet.
       geomApplet.start();
    }

    // The following methods are all required, since we implement the
    // WindowListener interface.  We only care about handling window
    // closing events, so this is the only method that does anything.
    public void windowClosing(WindowEvent e) {
        System.out.println("Window closing");
        System.exit(0);
    }

    public void windowClosed(WindowEvent e) {}

    public void windowOpened(WindowEvent e) {}

    public void windowIconified(WindowEvent e) {}
 
    public void windowDeiconified(WindowEvent e) {}

    public void windowActivated(WindowEvent e) {}
 
    public void windowDeactivated(WindowEvent e) {}
}
 

// We can't just instantiate Canvas, since its default implementation
// gives us nothing interesting to look at or do.  So here's a Canvas
// subclass that draws something slightly interesting.
class MyCanvas extends Canvas implements MouseListener {
    // Parent and child widgets.
    Geometry mGeomApplet;                     // The parent applet.
    PopupMenu mPopupMenu;                 // Popup menu for creating new objects.

    // Object lists.
    Vector mPointList;                             // List of all Point objects.
    Vector mLineList;                               // List of all Line objects.
    Vector mPolygonList;                         // List of all Polygon objects.

    // Constants that indicate which kind of object (if any) is currently
    // being created.
    static final int NO_OBJECT = 0;
    static final int POINT_OBJECT = 1;
    static final int LINE_OBJECT = 2;
    static final int POLYGON_OBJECT = 3;

    // Miscellaneous state variables.
    int miLastButton = 0;                        // Last button for which an event was received.
    int miAcceptingInput = 0;                  // Type of object (if any) that we are
    // currently creating.
    int miPointsEntered = 0;                   // Number of points entered for this object
    // so far.
    Object mCurrentObject = null;           // The object that we are currently creating.
 

    // MyCanvas constructor.
    MyCanvas(Geometry geomApplet) {
        MenuItem item;

        mGeomApplet = geomApplet;

        // Set the background color.
        setBackground(Color.white);

        // Register the canvas to start listening to mouse events.
        addMouseListener(this);

        // Create a popup menu and make it a child of the canvas, but don't show it just yet.
        mPopupMenu = new PopupMenu("New Object");
        mPopupMenu.add("Point");
        item = mPopupMenu.getItem(0);
        item.addActionListener(new PointActionListener(this));
        mPopupMenu.add("Line");
        item = mPopupMenu.getItem(1);
        item.addActionListener(new LineActionListener(this));
        mPopupMenu.add("Polygon");
        item = mPopupMenu.getItem(2);
        item.addActionListener(new PolygonActionListener(this));
        add(mPopupMenu);

        // Create the object lists with a reasonable initial capacity.
        mPointList = new Vector(10);
        mLineList = new Vector(10);
        mPolygonList = new Vector(10);
    }
 

    // The paint method.
    public void paint(Graphics g) {
        int i;

        // Draw all objects that are stored in the object lists.
        for (i = 0; i < mPointList.size(); i++) {
            Point point = (Point)mPointList.elementAt(i);
            g.fillRect(point.x-1, point.y-1, 3, 3);
        }

        for (i = 0; i < mLineList.size(); i++) {
            Line line = (Line)mLineList.elementAt(i);
            line.draw(g);
        }

        for (i = 0; i < mPolygonList.size(); i++) {
            Polygon polygon = (Polygon)mPolygonList.elementAt(i);
            int j;

            g.setColor(Color.red);
            g.drawPolygon(polygon);
            g.setColor(Color.black);
            for (j = 0; j < polygon.npoints; j++) {
                g.fillRect(polygon.xpoints[j], polygon.ypoints[j], 3, 3);
            }
        }

        // Draw as much of the current object as available.
        switch (miAcceptingInput) {
        case LINE_OBJECT:
            Line line = (Line)mCurrentObject;
            if (line.mb1 && !line.mb2)
                g.fillRect(line.mEnd1.x-1, line.mEnd1.y-1, 3, 3);
            break;

        case POLYGON_OBJECT:
            Polygon polygon = (Polygon)mCurrentObject;
            int j;
            g.setColor(Color.red);
            g.drawPolyline(polygon.xpoints, polygon.ypoints, polygon.npoints);
            g.setColor(Color.black);
            for (j = 0; j < polygon.npoints; j++) {
                g.fillRect(polygon.xpoints[j], polygon.ypoints[j], 3, 3);
            }
            break;

        default:
            break;
        }

        // Draw some text at the top of the canvas.
        int w = getSize().width;
        int h = getSize().height;
        g.drawRect(0, 0, w - 1, h - 1);
        g.setFont(new Font("Helvetica", Font.PLAIN, 15));
        g.drawString("Canvas", (w - g.getFontMetrics().stringWidth("Canvas"))/2, 10);
    }

 
    // The next five methods are required, since we implement the
    // MouseListener interface.  We are only interested in mouse pressed
    // events.
    public void mousePressed(MouseEvent e) {
        int iX = e.getX();  // The x and y coordinates of the
        int iY = e.getY();  // mouse event.
        int iModifier = e.getModifiers();

        // Workaround for bug in JDK 1.1.6
        if (iModifier < 4) {
            iModifier += 16;
        }

        if ((iModifier & InputEvent.BUTTON1_MASK) != 0) {
            miLastButton = 1;

            // If we are currently accepting input for a new object,
            // then add the current point to the object.
            if (miAcceptingInput != NO_OBJECT)
                addPointToObject(iX, iY);
        }
        else if ((iModifier & InputEvent.BUTTON2_MASK) != 0) {
            miLastButton = 2;

            // If current object is a polygon, finish it.
            if (miAcceptingInput == POLYGON_OBJECT) {
                mPolygonList.addElement(mCurrentObject);
                String str = "Finished creating polygon object.\n";
                mGeomApplet.getTextArea().append(str);
                repaint();
                miAcceptingInput = NO_OBJECT;
                miPointsEntered = 0;
                mCurrentObject = null;
            }
        }
        else if ((iModifier & InputEvent.BUTTON3_MASK) != 0) {
            miLastButton = 3;

            // Display the popup menu provided we are not accepting
            // any input for a new object.
            if (miAcceptingInput == NO_OBJECT)
                mPopupMenu.show(this, iX, iY);
        }
    }

    public void mouseClicked(MouseEvent e) {}

    public void mouseEntered(MouseEvent e) {}

    public void mouseExited(MouseEvent e) {}

    public void mouseReleased(MouseEvent e) {}

    public void getPointInput() {
        miAcceptingInput = POINT_OBJECT;
        mCurrentObject = (Object)new Point();
        mGeomApplet.getTextArea().append("New point object: enter point.\n");
    }

    public void getLineInput() {
        miAcceptingInput = LINE_OBJECT;
        mCurrentObject = (Object)new Line();
        mGeomApplet.getTextArea().append("New line: enter end points.\n");
    }

    public void getPolygonInput() {
        miAcceptingInput = POLYGON_OBJECT;
        mCurrentObject = (Object)new Polygon();
        mGeomApplet.getTextArea().append("New polygon: enter vertices ");
        mGeomApplet.getTextArea().append("(click middle button to finish).\n");
    }

    void addPointToObject(int iX, int iY) {
        String str;

        miPointsEntered++;
        switch (miAcceptingInput) {
        case POINT_OBJECT:
            str = "Point at (" + iX + "," + iY + ")\n";
            mGeomApplet.getTextArea().append(str);
            Point point = (Point)mCurrentObject;
            point.x = iX;
            point.y = iY;
            mPointList.addElement(mCurrentObject);
            str = "Finished creating point object.\n";
            mGeomApplet.getTextArea().append(str);
            repaint();
            miAcceptingInput = NO_OBJECT;
            miPointsEntered = 0;
            mCurrentObject = null;
            break;

        case LINE_OBJECT:
            if (miPointsEntered <= 2) {
                str = "End " + miPointsEntered + " at (" + iX + "," + iY + ")";
                str += "\n";
                mGeomApplet.getTextArea().append(str);
            }
            Line line = (Line)mCurrentObject;
            if (miPointsEntered == 1) {
                line.setEnd1(iX, iY);
                repaint();
            }
            else {
                if (miPointsEntered == 2) {
                    line.setEnd2(iX, iY);
                    mLineList.addElement(mCurrentObject);
                    str = "Finished creating line object.\n";
                    mGeomApplet.getTextArea().append(str);
                    repaint();
                }
                miAcceptingInput = NO_OBJECT;
                miPointsEntered = 0;
                mCurrentObject = null;
            }
            break;

        case POLYGON_OBJECT:
            str = "Vertex " + miPointsEntered + " at (" + iX + "," + iY + ")";
            str += "\n";
            mGeomApplet.getTextArea().append(str);
            Polygon polygon = (Polygon)mCurrentObject;
            polygon.addPoint(iX, iY);
            repaint();
            break;

        default:
            break;
        }                           // End switch.
    }
}
 

// Action listener to create a new Point object.
class PointActionListener implements ActionListener {
    MyCanvas mCanvas;

    PointActionListener(MyCanvas canvas) {
        mCanvas = canvas;
    }

    public void actionPerformed(ActionEvent e) {
        mCanvas.getPointInput();
    }
}
 

// Action listener to create a new Line object.
class LineActionListener implements ActionListener {
    MyCanvas mCanvas;

    LineActionListener(MyCanvas canvas) {
        mCanvas = canvas;
    }

    public void actionPerformed(ActionEvent e) {
        mCanvas.getLineInput();
    }
}
 

// Action listener to create a new Polygon object.
class PolygonActionListener implements ActionListener {
    MyCanvas mCanvas;

    PolygonActionListener(MyCanvas canvas) {
        mCanvas = canvas;
    }

    public void actionPerformed(ActionEvent e) {
        mCanvas.getPolygonInput();
    }
}
 

// A line class.
class Line {
    Point mEnd1, mEnd2;
    boolean mb1, mb2;

    Line() {
        mb1 = mb2 = false;
        mEnd1 = new Point();
        mEnd2 = new Point();
    }
 
    void setEnd1(int iX, int iY) {
        mEnd1.x = iX;
        mEnd1.y = iY;
        mb1 = true;
    }

    void setEnd2(int iX, int iY) {
        mEnd2.x = iX;
        mEnd2.y = iY;
        mb2 = true;
    }

    void draw(Graphics g) {
        g.fillRect(mEnd1.x-1, mEnd1.y-1, 3, 3);
        g.fillRect(mEnd2.x-1, mEnd2.y-1, 3, 3);
        g.setColor(Color.green);
        g.drawLine(mEnd1.x, mEnd1.y, mEnd2.x, mEnd2.y);
        g.setColor(Color.black);
    }
}