`ClockApplet' example program: an applet that displays a clock

ClockApplet.java

/*==================================================================
Implements a clock display in an applet

The display looks like this:


(If you aren't looking at the HTML version of this file, you won't be able to see the picture) This program demonstrates some important features of graphics programming. Specifically, it demonstrates some simple methods to speed the program up so that the screen is updated smoothly, without too much flicker. This is particularly important in Java applets, which never run particularly fast. The clock display is updated once every second. This involves redrawing the `hands' of the clock. However, to give the impression that the hands are `moving', we must first erase the hand from the old position. So every second the following actions occur: 1. Find out the current time (hours, minutes and seconds) 2. Calculate the position of the clock hands for the _previous_ time (NOT the current time) 3. Draw the clock hands in these positions, but with the colour set to white (i.e., erase the hands). 4. Draw the clock hands in the new position, with colour set to something visible (I have chosen blue and black) 5. Save the current time as the `previous' time, so that when we get back to step 1, it will erase the hands correctly. This process is the basis of smooth animation: calculate everything before drawing anything. If there is too much time between the erase operation and the draw operation, the screen will flicker every time it is updated, which looks very ugly. This applet creates a separate `thread of execution' for itself. This technique will be covered in week 10, so don't worry if you don't understand it yet. You should still be able to follow almost all of this program. Orignal version written by Kevin Boone, August 98 Modified for JDK1.2 July 1999 ==================================================================*/ import java.awt.*; import java.applet.Applet; import java.awt.event.*; import java.util.Calendar; public class ClockApplet extends Applet implements Runnable { /* Use a big font for the numeral display. the Font constructor takes three arguments: 1. The typeface name (e.g., `roman', `helvetica') 2. The `style' of the font (e.g., bold, italic, normal) 3. The point size. Normal book text is usually 10-12 points in size (technically there are 72 points to the British inch */ Font numeralFont = new Font ("roman", Font.BOLD, 14); /* The attributes lastMinutes, lastSeconds, lastHours store the minutes, seconds and hours at the time the display was last re-drawn. We need this information so that the hands can be erased before they are redraw. This is to give the impression of movement. Note that I am using the value of `-1' for lastMinutes to indicate that there was not previous time. This will be the case when the program starts. We need to ensure that we do not attempt to erase the hand display when there is nothing to erase. As soon as the display has been drawn once, lastMinutes, lastSeconds and lastHours will be set to real values. */ int lastMinutes = -1, lastSeconds, lastHours; /* The following constants are used as arguments to the operation `drawHand'. They control how the hand should be drawn. */ final int ERASE = 0, DRAW = 1; final int LINEHAND = 0, SHAPEHAND = 1; /* Following attributes describe the sizes of teh various parts of the display. In theory, all these can be determined from the fundamental one `overallRadius'. This itself is determined from the size of the applet itself. However, if we have to calculate all of these values every time a hand is redraw, it will slow the program down and lead to increasd flicker. So we save all these things as attributes. These attributes are all calculated by the operation calculateAndSetBasicSizeAttribtues(). All the values are in pixels. */ // The overall radius of the clock face (right up // the very edge. If the applet is tall and thin, // the overall radius will be based on the width. // If it is short and wide, it is based on the // height. This is because the clock display must // be a circle, whatever the shape of the applet's // screen area int overallRadius; // Width of the applet int width; // Height of the applet int height; // X and Y co-ordinates of the centre of the applet. // This is where all the clock hands start from int centreX; int centreY; /* effectiveFaceRadius is the radius of the circle which joins the centres of the clock figures. If the clock figures are drawn on a circle the same size as the overall radius, then some of the figures will be off the edge of the display. This attribute is calculated by examining the height and width of the text of the largest number (`12') that appears on the clock face. */ int effectiveFaceRadius; // The height of the clock figures, in pixels int maxTextWidth; // The width of the largest clock figure (`12'), in pixels int maxTextHeight; // The distance from the top-left corner to the bottom-right // corner of the figure `12'. This is used to calculate how // long the clock second hand should be int radiusLostByText; // Keep going is set to `true' initially, and the applet will // continue to update the display until it is set to false. // The value is set to false when the Browser calls the applet's // `stop' operation (to indicate that the applet should stop // running. boolean keepGoing = true; /*================================================================== ClockApplet constructor Doesn't do much in this program; just sets the background colour ==================================================================*/ public ClockApplet () { /// QUESTION: if I wanted to change the clock to have a /// red background, rather than a white one, changing /// the line below would not be enough. What other lines in /// the program should we change? How could it be made easier /// to change the background colour? setBackground(Color.white); } /*================================================================== drawHand Draws a clock hand in the specified position. The `position' argument takes a value from 0 (straight up) to 0.5 (straight down) and back to 1.0 (back to straight up. I.e., a complete circle is a variation from 0 to 1. This is to allow hour, minute and second hands to be drawn with the same operation, even though they take different values (e.g. hours are from 1 to 12, minutes are from 0 to 59). The constant `action' controls whether the hands are to be redrawn or erased. If action is `DRAW', then the hand is drawn in blue or black. If it is `ERASE' it is drawn in white. `Shape' controls whether to draw a second hand shape, or an hour/minute hand shape From a Java perspective, the trignometry of calculating the coordinates of the hands is not important; however, note that general maths functions like sine and cosine are in fact static member functions of the `Math' class. You should be aware that any moderately sophisticated graphical program is likely to involve a bit of mathematics. However, as this is not a maths course I don't propose to describe the calculations themselves. ==================================================================*/ protected void drawHand (double position, int radius, int action, int shape) { /// QUESTION: what does getGraphics() do? Why do we need it /// here? Graphics g = this.getGraphics(); // It is possible that this operation may be called while // the browser is removing the applet from the display. If // this happens, `getGraphics()' will return `null'. In // this case, don't attempt to redraw the screen, as the // program will crash if (g == null) return; // In most `real' clocks, the hands do not originate // exactly from the centre, but a little way beyond // the centre. I am going to call this distance the // `overshoot radius' (lacking a better term). In this // program I have set the overshoot radius 10 // percent on the size of the hand itself. This figure // is also used to control the `thickness' of the hour // and minute hands int overshootRadius = overallRadius / 10; // As the `position' argument lies between 0 and 1, we // must convert this into a `real' anglue in radians. // Note that the constact `pi' is Math.PI in Java double angle = position * 2 * Math.PI; /* We will be drawing the shapes of the hour hand and the minute hand using `drawPolygon' (to draw the outline) and `fillPolygon' to draw the inside. These operations (which are in the Graphics class) take as input two arrays of integers. These arrays are the X and Y co-ordinates of the points that make up the shape to draw. So we calculate both these arrays, and pass them to the appropriate operations. Again, as this is not a maths course I don't propose to explain how the co-ordinates are calculated. */ int x[] = { centreX - (int)(overshootRadius * Math.sin(angle)), centreX - (int)(overshootRadius / 2.0 * Math.sin(angle+Math.PI/2)), centreX + (int)(radius * Math.sin(angle)), centreX - (int)(overshootRadius / 2.0 * Math.sin(angle-Math.PI/2)), }; int y[] = { centreY + (int)(overshootRadius * Math.cos(angle)), centreY + (int)(overshootRadius / 2.0 * Math.cos(angle+Math.PI/2)), centreY - (int)(radius * Math.cos(angle)), centreY + (int)(overshootRadius / 2.0 * Math.cos(angle-Math.PI/2)), }; // Depending on whether `action' is `DRAW' or `ERASE', // draw the hands in colour or in white if (action == DRAW) { if (shape == SHAPEHAND) { /// QUESTION: what does `Colour(0, 100, 200)' mean? g.setColor (new Color(0, 100, 200)); g.fillPolygon (x, y, 4); g.setColor (Color.black); g.drawPolygon (x, y, 4); } else { g.setColor (Color.black); g.drawLine (x[0],y[0],x[2],y[2]); } } else { // Erase if (shape == SHAPEHAND) { g.setColor (Color.white); g.fillPolygon (x, y, 4); g.drawPolygon (x, y, 4); } else { g.setColor (Color.white); g.drawLine (x[0],y[0],x[2],y[2]); } } } /*================================================================== drawClockFace Draw the clock face, i.e., the set of figures round the edge of the display. We use the attribute `effectiveFaceRadius' to determine the size of the circle that the figures lie on ==================================================================*/ protected void drawClockFace(Graphics g) { g.setFont (numeralFont); for (int i = 0; i < 12; i ++) { double angle = (i + 1) / 12.0 * 2.0 * Math.PI; g.drawString (Integer.toString(i+1), centreX + (int)(effectiveFaceRadius * Math.sin(angle) - maxTextWidth / 4), centreY - (int)(effectiveFaceRadius * Math.cos(angle) - maxTextHeight / 2 - 2)); } } /*================================================================== calculateHandRadius Work out the size of the display that is available for drawing the clock hands, taking into account the numerals around the edge. This is very simple: we just subtrace the distance across the largest figure (this is the attribute `radiusLostByText') from the overall radius of the clock ==================================================================*/ int calculateLargestHandRadius () { int radius = overallRadius - radiusLostByText; return radius; } /*================================================================== paint `paint' is called automatically by the Java system, when it knows the program's display has to be redrawn. This will happen when the applet is constructed, and whenever it is redraw after, for example, the browser's window is uncovered. In this case we just draw the clock face (that is, the figures). The hands are drawn every second by updateTime() ==================================================================*/ public void paint (Graphics g) { drawClockFace(g); } /*================================================================== updateTime Draw the clock hands, but not the face. The face only needs to be drawn when the applet needs to be redrawn. Java (via the Web browser) tells it when to do this. updateTime is called once a second by `run' This is the main animation procedure in the applet. It tries to make the display drawing as smooth as possible, by doing all the calculations before drawing anything. Actually, it is not perfect because some calculations are done during each call to drawHand() ==================================================================*/ void updateTime () { // Get the current time (hours, minutes and seconds) Calendar now = Calendar.getInstance(); int hours = now.get(Calendar.HOUR); int minutes = now.get(Calendar.MINUTE); int seconds = now.get(Calendar.SECOND); int handRadius = calculateLargestHandRadius(); // Erase the hands in their previous positions // If lastMintes is -1, this indicates that there were // no previous positions, so there is not need to erase if (lastMinutes != -1) { drawHand (lastMinutes/60.0, (int)(0.85*handRadius), ERASE, SHAPEHAND); drawHand (lastHours/12.0 + lastMinutes/60.0/12.0, (int)(0.6 * handRadius), ERASE, SHAPEHAND); drawHand (lastSeconds/60.0, (int)(0.9 * handRadius), ERASE, LINEHAND); } // Now draw the hands in their new positions drawHand (minutes/60.0, (int)(0.85 * handRadius), DRAW, SHAPEHAND); drawHand (hours/12.0 + minutes/60.0/12.0, (int)(0.6 * handRadius), DRAW, SHAPEHAND); drawHand (seconds/60.0, (int)(0.9 * handRadius), DRAW, LINEHAND); /// QUESTION: what do these three lines do, and why? lastHours = hours; lastMinutes = minutes; lastSeconds = seconds; } /*================================================================== calculateAndSetBasicSizeAttributes This operation is called by `start' to set up the attributes that contain important size information. This could be done every time a hand is re-drawn, but this would be very slow. As applets don't normally change size, we only call this operation once when the program starts. However, if we wanted to convert this applet into an application, where the user could change the size of the window, we would need to calculate new sizes whenever the window size changed. So I have grouped these calculations into one operation to make this easier to implement if necessary ==================================================================*/ void calculateAndSetBasicSizeAttributes() { width = getSize().width; height = getSize().height; centreX = width/2; centreY = height/2; overallRadius = Math.min(width, height) / 2; Graphics g = getGraphics(); g.setFont (numeralFont); /* FontMetrics is a very important class in programs that display text. Its operations can be used to determine the sizes of characters and lines as they appear on the display (in pixels). */ FontMetrics fm = g.getFontMetrics(); maxTextWidth = fm.charsWidth ("12".toCharArray(), 0, 2); /// QUESTION: what is an `ascent' and a `descent'? maxTextHeight = fm.getMaxAscent() - fm.getMaxDescent(); radiusLostByText = (int)Math.sqrt(maxTextHeight * maxTextHeight + maxTextWidth * maxTextWidth); effectiveFaceRadius = overallRadius - radiusLostByText / 2 - 4; } /*================================================================== start Get things moving by creating a new thread for this program and running it by calling `start' Multi-threading will be covered later in the course; don't worry if this operation doesn't make sense at the moment ==================================================================*/ public void start () { calculateAndSetBasicSizeAttributes(); Thread thisThread = new Thread(this); // Calling start (Thread.start()) starts the // new thread, and causes `run' (runnable.run() // _and_ Thread.run()) to be executed. This starts the // process of drawing the clock hands thisThread.start(); } /*================================================================== stop The browser (or applet viewer) has decided that the user wants the applet to stop running. ==================================================================*/ /// QUESTION: in the program `Calculator.java' we did not need a /// `stop' operation. What is different about this program that means /// we have to process `stop'? public void stop () { keepGoing = false; } /*================================================================== run The `run' operation controls the work of the applet. It is started by `thisThread.start' in operation `start', and runs until the applet is terminated by the browser ==================================================================*/ public void run () { // Redraw once per second, as long as `keepGoing' is true. // This will be the case until `stop' is called, which // sets keepGoing to false // It simply calls `updateTime' once every second while (keepGoing) { // The operation `Thread.sleep()' causes the // the program to stop for a certain length of // time. The time is specified in // milliseconds, so a one-second wait is 1000 // milliseconds. try {Thread.currentThread().sleep(1000);} catch (InterruptedException e){} updateTime (); } } }

©1994-2003 Kevin Boone, all rights reserved