ANU The Australian National University



____________________________________________________

[ANU] [DCS] [COMP2100/2500] [Description] [Schedule] [Lectures] [Labs] [Homework] [Assignments] [COMP2500] [Assessment] [PSP] [Java] [Reading] [Help]

____________________________________________________

COMP2100/2500
Lecture 16: GUI III

Recapitulate on GUI


Swing GUI Elements and Event Handlers

General Category Generated Events Event Handler Interface
Mouse MouseEvent (dragging, moving)
MouseEvent (clicking, selecting, releasing)
MouseMotionListener
MouseListener
Mouse wheel Mouse wheel event MouseWheelListener
Keyboard KeyEvent (click, release) KeyListener
Selecting (item, checkbox etc) ItemEvent (Selection) ItemListener
Text Input TextEvent (new line entered) TextListener
Scrolling AdjustmentEvent (SB slider moved) AdjustmentListener
Buttons, Menu etc ActionEvent ActionListener
Window change WindowEvent (open, close etc) WindowListener
Keyboard focus change FocusEvent (component must have focus) FocusListener
Component change ComponentEvent (resizing, hiding etc) ComponentListener
Container change ContainerEvent (adding-removing component) ContainerListener

Every component can associate (listeners can register with the component) with itself a XxxListener via using addXxxListener(XxxListener) method. RealHandler implements XxxListener interface via implementing some (one-to-all) methonds declared in the XxxListener to achieve the behaviour required by the program.


Swing Lightweight Control Elements

GUI Category Control Element Swing Class
Basic controls Button
Combo box
List
Menu
Slider
Toolbar
Text Fields

JButton, JCheckBox
JJRadioButton
JComboBox
JList
JMenu, JMenuBar, JMenuItem
JSlider
JToolbar
JTextField, JPasswordField,
JTextArea, JFormatTextField
Uneditable displays Label
Tooltip
Progress bar
JLabel
JToolTip
JProgressBar
Editable displays Table
Text
Tree
ColorChooser
FileChooser
ValueChooser
JTable
JTextPane, JTextArea, JEditorPane
JTree
JColorChooser
JFileChooser
JSpinner
Space-Saving containers Scroll pane
Split pane
Tabbed pane
JScrollPane, JScrollBar
JSplitPane
JTabbedPane
Top-level container Frame
Applet
Dialog
JFrame
JApplet
JDialog, JOptionPane
Other containers Panel
Internal frame
Layered pane
Root pane
JPanel
JInternalFrame
JLayeredPane
JRootPane

Once again on anonymous inner classes

Inside a class...

    // ge is a GuiElement object which can generate 
    // XxxEvent and with which we register an event handler,
    // which implements XxxListener interface by implementing
    // the method handleXxxEvent(XxxEvent e), through creating 
    // the event handler without explicitly naming and declaring it
            .......     .........
    ge.addXxxListener(new XxxListener() {
                    public void handleXxxEvent(XxxEvent e) {
                        ...  do something ...
                    }
    }  ); //end of anonymous inner class
            .......     .........

What is being achieved through the inner classes technique?

Using the anonymous inner class techniques a good practice only if the event handler is small (few lines of code). If it takes half a screen or more, it's better to make it in proper outside class of its own.


This lecture proper

Discussion and demonstration of the construction of a simple graphical clock application.

Aims


Introduction

Here are two versions of the simple Java clock:

  1. clock1.jar

  2. clock3.jar

The initial version of the clock is not very nicely structured, with all the code in one class. The next version splits the code into Model, View and Controller parts. The model and view use the standard Java version of the Observer Pattern: that is, Model extends the standard abstract class Observable and View implements the Observer interface. This version is also more efficient in that it only redraws the clock face when it actually changes.


Clock version 1

The whole thing is coded into a single class ClockPanel, which inherits from JPanel (as its name suggests). The main() method just creates a JFrame, creates a ClockPanel and puts it into the frame, and then sets it going. Nothing new here.

The constructor is more interesting. It sets up a Timer that generates events 10 times every second, and links it with a listener. Every time the timer activates the listener, it asks the system for the exact time using a Calendar object, stores the hour, minute and second in fields, and then calls repaint() on the panel. Here is the code:

    public ClockPanel() {
        setPreferredSize(new Dimension(200, 200));
        setBackground(Color.white);

        ActionListener listener = new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                Calendar date = new  GregorianCalendar();
                hour = date.get(Calendar.HOUR);
                minute = date.get(Calendar.MINUTE);
                second = date.get(Calendar.SECOND);
                //System.out.println(hour + ":" + minute + ":" + second);
                repaint();
            }
        };

        Timer t = new Timer(100, listener);
        t.start();
    }

If you want to know more about the calendar class, you can look it up in the API documentation. For timers, there are also a couple of articles on the Sun website that might be worth looking at. It turns out that there are two Timer classes in the standard API. I'm using a javax.swing.Timer but there is also java.util.Timer. The article Using Timers in Swing Applications gives a good introduction and explains the difference between the two timer classes. From a quick look it seems that I might have been better off using the other one. (Exercise: Read this in detail and if I'm right, recode the clock using java.util.Timer.) There's also How to use Swing timers in the Swing Tutorial.

The rest of the code is all in the paintComponent() method (inherited from JComponent; the method paintComponents() is availbale from Component which is a part of AWT. Can be confusing at times, isn't?). Notice the name. It is not called paint(). Apparently this is important. You mustn't override paint() or repaint() or all sorts of things will go wrong.

What is this argument Graphics g that gets passed. This is called a “graphics context”. It keeps track of all sorts of details of how we're doing graphics right now: the background colour, the foreground colour, the line thickness, the current font and so on. In the code that follows we sometimes change the state of the graphics object. When we actually draw something, the graphics object is the target, not the component we're drawing onto. This seems to me to be quite confusing.

Let's go through line by line.

When you override paintComponent(), the first thing you should do is to call the overridden method from the superclass.

        super.paintComponent(g);

otherwise (as API warns) "you will likely see visual artifacts". Next we need to get the dimensions of the area we're drawing the clock face onto.

        Rectangle bounds = getBounds();

This next bit is a bit confusing too. For backwards compatibility with the earliest versions of Java, the argument of paintComponent() is of class Graphics. But what we really get is an object belonging to its subclass Graphics2D, which has enhanced capabilities. Have a look at the API documentation for those two classes, or read the 2D Graphics trail in the Java Tutorial. (The Swing Tutorial is one “trail”; this is another.) If we want to access those extra features, we need to do a cast:

        Graphics2D gg = (Graphics2D) g;

From now on, we work with gg. The next step is to locate the co-ordinates of the centre of the panel.

        int x0 = bounds.width / 2;
        int y0 = bounds.height / 2;

We want to draw the biggest clock face that will fit inside this window. The variable size will represent the radius of the largest circle that will fit.

        int size = Math.min(x0, y0);

One of the cute things that Graphics2D objects can do is to peform co-ordinate transformations. Instead of having to write out the code for all sorts of complicated geometry (see below), we could just translate and scale everything so that the centre of the clock face was the origin and the radius was 1. Wouldn't that be nice? Unfortunately it doesn't work for drawing text, only for lines and shapes. So we could use this for the hands and the tick marks, but not for the numbers. So in the end I didn't use co-ordinate transformations at all. I thought it would be even more confusing having two different sets of co-ordinates in use at the same time. But it's a useful thing to know about, and it might come in handy for something else some time.

        //gg.translate(x0, y0);
        //gg.scale(size, size);
        //gg.setStroke(new BasicStroke(0));

This next bit sets the line width for drawing. This gives you a thin line.

        gg.setStroke(new BasicStroke(1));

Now the code to draw the tick marks around the outside of the clock. There's a bit of co-ordinate geometry here, but you should be able to understand it. The loop takes n from 0 to 59, so there's one iteration for each of the 60 points around the clock face. Each of those points is 6 degrees around from the last one. Angles in mathematics are measured anti-clockwise from the positive x-axis, so the angle around the circle, in degrees, of the nth tick mark should be (90 - 6n). Java deals with angles in radians, so that has to be multiplied by 180/pi. The small tick marks go from 0.7 to 0.75 of the size. The long tick marks go from 0.65 to 0.75 of the size. Finally, Java's co-ordinates have the x-axis as you would expect from mathematics, increasing to the right, but the y-axis is the opposite, it increases down the screen, rather than up. That's why the calculation for the y co-ordinates of the endpoints of the tick marks involves y0 - rsin(theta) where in maths it would be +.

        double radius = 0;
        double theta = 0;

        // Draw the tick marks around the outside
        for (int n = 0; n < 60; n++) {
            theta = (90 - n * 6) / (180 / Math.PI);
            if (n % 5 == 0) {
                radius = 0.65 * size;
            } else {
                radius = 0.7 * size;
            }
            double x1 = x0 + radius * Math.cos(theta);
            double y1 = y0 - radius * Math.sin(theta);
            radius = 0.75 * size;
            double x2 = x0 + radius * Math.cos(theta);
            double y2 = y0 - radius * Math.sin(theta);
            gg.draw(new Line2D.Double(x1, y1, x2, y2));
        }

For drawing the numbers there's another new thing. We have to specify the font. Without going into too much detail, I choose a simple sans-serif font here, with the size increasing in proportion to the size of the window. The tricky thing here is drawing a string with its centre at a particular point. For convenience of writing text on the screen, strings have a reference point which is on the left edge at the baseline (the imaginary line that characters sit on, and that some characters like ‘g’ descend below). To be able to put the centre of a string at a particular point (0.9 of the size out from the centre of the window) we need to get all the dimensions of our string and do a translation.

        // Draw the numbers
        Font font = new Font("SansSerif", Font.PLAIN, size / 5);
        gg.setFont(font);
        for (int n = 1; n <= 12; n++) {
            theta = (90 - n * 30) / (180 / Math.PI);
            radius = 0.9 * size;
            double x1 = x0 + radius * Math.cos(theta);
            double y1 = y0 - radius * Math.sin(theta);
            String s = "" + n;
            // To centre the numbers on their places, we need to get
            // the exact dimensions of the box
            FontRenderContext context = gg.getFontRenderContext();
            Rectangle2D msgbounds = font.getStringBounds(s, context);
            double ascent = -msgbounds.getY();
            double descent = msgbounds.getHeight() + msgbounds.getY();
            double height = msgbounds.getHeight();
            double width = msgbounds.getWidth();

            gg.drawString(s, (new Float(x1 - width/2)).floatValue(),
                          (new Float(y1 + height/2 - descent)).floatValue());
        }

The rest is just more of the same sort of thing. I tried using a float argument to BasicStroke() to get finer control over the thickness of lines, and it lets you do that, but it doesn't have much effect. Those numbers are basically numbers of pixels, and it's hard to draw a line 1.5 pixels wide on the screen. Setting the thickness to zero doesn't give you an invisible line, it gives you the thinnest line the system can draw, which is (unsurprisingly) one pixel wide.

        // Draw the hour hand
        gg.setStroke(new BasicStroke(2.0f));
        theta = (90 - (hour + minute / 60.0) * 30) / (180 / Math.PI);
        radius = 0.5 * size;
        double x1 = x0 + radius * Math.cos(theta);
        double y1 = y0 - radius * Math.sin(theta);
        gg.draw(new Line2D.Double(x0, y0, x1, y1));

        // Draw the minute hand
        gg.setStroke(new BasicStroke(1.1f));
        theta = (90 - (minute + second / 60.0) * 6) / (180 / Math.PI);
        radius = 0.75 * size;
        x1 = x0 + radius * Math.cos(theta);
        y1 = y0 - radius * Math.sin(theta);
        gg.draw(new Line2D.Double(x0, y0, x1, y1));

        // Draw the second hand
        gg.setColor(Color.red);
        gg.setStroke(new BasicStroke(0));
        theta = (90 - second * 6) / (180 / Math.PI);
        x1 = x0 + radius * Math.cos(theta);
        y1 = y0 - radius * Math.sin(theta);
        gg.draw(new Line2D.Double(x0, y0, x1, y1));

That's it for the first version of the clock. If you haven't already, download the Jar file and try it. You can run it directly from the Jar file by typing java -jar clock1.jar at the command line. You can also extract the Jar file with jar xvf clock1.jar and play with modifying the code.


Clock version 3

What happened to version 2? It's my first refactoring of the clock using Model-View-Controller. Then I discovered that the Java API has its own classes for the Observer pattern, which is essentially a simplified version of M-V-C. So I redid it using the Java Observer and Observable, and the result is version 3.

Unpack the Jar file and read through the code as you read this explanation.

In this version of the program, the code is divided into five classes. One each for Model, View and Controller, a main class called Clock that just creates one object of each, and a ClockPanel class that contains the drawing code.

Class Model inherits from (extends) the standard Observable class. Observables maintain a list of their observers, and notify them if anything has changed.

Class View implements the Observer interface. This means it must have a method called update() that gets called whenever the Observable it is observing changes. In our case all this does is repaint() the panel.

Class Controller needs links to the Model and View it is controlling. Here is where the timer and listener are defined. Now when the timer returns, all that happens is it calls update() on the Model, which causes it to find out the new time. If anything has changed since last time, the Model calls notifyObservers() which calls update() on View, which calls repaint() on the panel. Get it?

The guts of class ClockPanel are pretty much the same as in the first version.

For a small application like this, dividing it up into five classes seems a bit like overkill. Certainly for beginners, version 1 is easier to understand. The point is that the architecture in version 1 doesn't scale well. As the size and complexity of the application grows, having it all in one class becomes unmanageable. Dividing the parts up using the Model-View-Controller architecture splits the code into more reasonable parts with a clear division of responsibilities and a clean simple interface between them. That's the essence of good design for large applications.

You'll be modifying this version of the clock in Lab 4.

____________________________________________________

[ANU] [DCS] [COMP2100/2500] [Description] [Schedule] [Lectures] [Labs] [Homework] [Assignments] [COMP2500] [Assessment] [PSP] [Java] [Reading] [Help]

____________________________________________________

Copyright © 2006, Ian Barnes and Alexei Khorev, The Australian National University
Version 2006.3, Thursday, 6 April 2006, 12:09:54 +1000
Feedback & Queries to comp2100@cs.anu.edu.au