- Special Edition Using Java, 2nd Edition -

Chapter 28

Java.awt—Graphics, Mouse, and Keyboard


by Mark Wutka

The Abstract Windowing Toolkit (AWT) provides an Application Programming Interface (API) for common User Interface components such as buttons and menus.

One of the main goals of Java is to provide a platform-independent development environment. The area of Graphical User Interfaces has always been one of the stickiest parts of creating highly portable code. The Windows API is different from the OS/2 Presentation Manager API, which is different from the X-Windows API, which is different from the Mac API. The most common solution to this problem is to take a look at all the platforms you want to use, identify the components that are common to all of them (or would be easy to implement on all of them), and create a single API that you can use. On each different platform, the common API would interface with the platform’s native API. Applications using the common API would then have the same look and feel as applications using the native API.

The opposite of this approach is to create a single look and feel, and then implement that look and feel on each different platform. For Java, Sun chose the common API approach, which allows Java applications to blend in smoothly with their surroundings. Sun called this common API the Abstract Windowing Toolkit, or AWT for short.

The AWT addresses graphics from two different levels. At the lower level, it handles the raw graphics functions and the different input devices such as the mouse and keyboard. At the higher level, it provides a number of components like pushbuttons and scroll bars that you would otherwise have to write yourself.

This chapter discusses the low-level features of the AWT. Chapter 30 discusses the higher-level portions of the AWT.

Paint, Update, and Repaint

As you saw in the simple HelloWorld applet, Java applets can redraw themselves by overriding the paint method. Because your applet never explicitly calls the paint method, you may have wondered how it actually gets called. Your applet actually has three different methods that are used in redrawing the applet, as follows:

The Graphics Class

The Graphics class provides methods for drawing a number of graphical figures, including the following:

The Coordinate System

The coordinate system used in Java is a simple Cartesian (x, y) system where x is the number of screen pixels from the left-hand side, and y is the number of pixels from the top of the screen. The upper-left corner of the screen is represented by (0, 0). This is the coordinate system used in almost all graphics systems. Figure 28.1 gives you an example of some coordinates.


FIG. 28.1

Unlike math coordinates, where y increases from bottom to top, the y coordinates in Java increase from the top down.

Drawing Lines

The simplest figure you can draw with the Graphics class is a line. The drawLine method takes two pairs of coordinates—x1,y1 and x2,y2—and draws a line between them:

public abstract void drawLine(int x1, int y1, int x2, int y2)

The applet in listing 28.1 uses the drawLine method to draw some lines. The output from this applet is shown in figure 28.2.

Listing 28.1 Source Code for DrawLines.java

import java.awt.*;
import java.applet.*;
//
// This applet draws a pair of lines using the Graphics class
//
public class DrawLines extends Applet
{
public void paint(Graphics g)
{
// Draw a line from the upper-left corner to the point at (200, 100)
g.drawLine(0, 0, 200, 100);
// Draw a horizontal line from (20, 120) to (250, 120)
g.drawLine(20, 120, 250, 120);
}
}

FIG. 28.2

Line drawing is one of the most basic graphics operations.

Drawing Rectangles

Now that you know how to draw a line, you can progress to rectangles and filled rectangles. To draw a rectangle, you use the drawRect method and pass it the x and y coordinates of the upper-left corner of the rectangle, the width of the rectangle, and its height:

public void drawRect(int x, int y, int width, int height)

To draw a rectangle at (150, 100) that is 200 pixels wide and 120 pixels high, your call would be:

g.drawRect(150, 100, 200, 120);

The drawRect method draws only the outline of a box. If you want to draw a solid box, you can use the fillRect method, which takes the same parameters as drawRect:

public abstract void fillRect(int x, int y, int width, int height)

You may also clear out an area with the clearRect method, which also takes the same parameters as drawRect:

public abstract void clearRect(int x, int y, int width, int height)

Figure 28.3 shows you the difference between drawRect, fillRect, and clearRect. The rectangle on the left is drawn with drawRect, and the center one is drawn with fillRect. The rectangle on the right is drawn with fillRect, but the clearRect is used to make the empty area in the middle.


FIG. 28.3

Java provides several flexible ways of drawing rectangles.

Drawing 3-D Rectangles

The Graphics class also provides a way to draw "3-D" rectangles similar to buttons that you might find on a toolbar. Unfortunately, the Graphics class draws these buttons with very little height or depth, making the 3-D effect difficult to see. The syntax for the draw3DRect and fill3DRect are similar to drawRect and fillRect, except that they have an extra parameter at the end—a Boolean indicator as to whether the rectangle is raised or not:

public void draw3DRect(int x, int y, int width, int height, boolean raised)

public void fill3DRect(int x, int y, int width, int height, boolean raised)

The raising/lowering effect is produced by drawing light and dark lines around the borders of the rectangle.

Imagine a light coming from the upper-left corner of the screen. Any 3-D rectangle that is raised would catch light on its top and left sides, while the bottom and right sides would have a shadow. If the rectangle was lowered, the top and left sides would be in shadow, while the bottom and right sides caught the light. Both the draw3DRect and fill3DRect methods draw the top and left sides in a lighter color for raised rectangles while drawing the bottom and right sides in a darker color. They draw the top and left darker and the bottom and right lighter for lowered rectangles. In addition, the fill3DRect method will draw the entire button in a darker shade when it is lowered. The applet in listing 28.2 draws some raised and lowered rectangles, both filled and unfilled.

Listing 28.2 Source Code for Rect3d.java

import java.awt.*;
import java.applet.*;
//
// This applet draws four varieties of 3-d rectangles.
// It sets the drawing color to the same color as the
// background because this shows up well in HotJava and
// Netscape.
public class Rect3d extends Applet
{
public void paint(Graphics g)
{
// Make the drawing color the same as the background
g.setColor(getBackground());
// Draw a raised 3-d rectangle in the upper-left
g.draw3DRect(10, 10, 60, 40, true);
// Draw a lowered 3-d rectangle in the upper-right
g.draw3DRect(100, 10, 60, 40, false);
// Fill a raised 3-d rectangle in the lower-left
g.fill3DRect(10, 80, 60, 40, true);
// Fill a lowered 3-d rectangle in the lower-right
g.fill3DRect(100, 80, 60, 40, false);
}
}

Figure 28.4 shows the output from the Rect3d applet. Notice that the raised rectangles appear the same for the filled and unfilled. This is only because the drawing color is the same color as the background. If the drawing color were different, the filled button would be filled with the drawing color, while the unfilled button would still show the background color.


FIG. 28.4

The draw3DRect and fill3DRect methods use shading to produce a 3D effect.

Drawing Rounded Rectangles

In addition to the regular and 3-D rectangles, you can also draw rectangles with rounded corners. The drawRoundRect and fillRoundRect methods are similar to drawRect and fillRect except that they take two extra parameters:

public abstract void drawRoundRect(int x, int y, int width, int height,
int arcWidth, int arcHeight)
public abstract void fillRoundRect(int x, int y, int width, int height,
int arcWidth, int arcHeight)

The arcWidth and arcHeight parameters indicate how much of the corners will be rounded. For instance, an arcWidth of 10 tells the Graphics class to round off the left-most five pixels and the right-most five pixels of the corners of the rectangle. An arcHeight of 8 tells the class to round off the top-most and bottom-most four pixels of the rectangle's corners.

Figure 28.5 shows the corner of a rounded rectangle. The arcWidth for the figure is 30, while the arcHeight is 10. The figure shows an imaginary ellipse with a width of 30 and a height of 29 to help illustrate how the rounding is done.


FIG. 28.5

Java uses an ellipse to determine the amount of rounding.

The applet in listing 28.3 draws a rounded rectangle and a filled, rounded rectangle. Figure 28.6 shows the output from this applet.

Listing 28.3 Source Code for RoundRect.java

import java.awt.*;
import java.applet.*;
// Example 28.3—RoundRect Applet
//
// This applet draws a rounded rectangle and then a
// filled, rounded rectangle.
public class RoundRect extends Applet
{
public void paint(Graphics g)
{
// Draw a rounded rectangle with an arcWidth of 20, and an arcHeight of 20
g.drawRoundRect(10, 10, 40, 50, 20, 20);
// Fill a rounded rectangle with an arcWidth of 10, and an arcHeight of 8
g.fillRoundRect(10, 80, 40, 50, 10, 6);
}
}

FIG. 28.6

Java’s rounded rectangles are a pleasant alternative to sharp cornered rectangles.

Drawing Circles and Ellipses

If you are bored with square shapes, you can try your hand at circles. The Graphics class does not distinguish between a circle and an ellipse, so there is no drawCircle method. Instead, you use the drawOval and fillOval methods:

public abstract void drawOval(int x, int y, int width, int height)

public abstract void fillOval(int x, int y, int width, int height)

To draw a circle or an ellipse, first imagine that the figure is surrounded by a rectangle that just barely touches the edges. You pass drawOval the coordinates of the upper-left corner of this rectangle. You also pass the width and height of the oval. If the width and height are the same, you are drawing a circle. Figure 28.7 illustrates the concept of the enclosing rectangle.


FIG. 28.7

Circles and Ellipses are drawn within the bounds of an imaginary enclosing rectangle.

The applet in listing 28.4 draws a circle and a filled ellipse. Figure 28.8 shows the output from this applet.

Listing 28.4 Source Code for Ovals.java

import java.awt.*;
import java.applet.*;
//
// This applet draws an unfilled circle and a filled ellipse
public class Ovals extends Applet
{
public void paint(Graphics g)
{
// Draw a circle with a diameter of 30 (width=30, height=30)
// With the enclosing rectangle's upper-left corner at (0, 0)
g.drawOval(0, 0, 30, 30);
// Fill an ellipse with a width of 40 and a height of 20
// The upper-left corner of the enclosing rectangle is at (0, 60)
g.fillOval(0, 60, 40, 20);
}
}

FIG. 28.8

Java doesn’t know the difference between ellipses and circles, they’re all just ovals.

Drawing Polygons

You can also draw polygons and filled polygons using the Graphics class. You have two options when drawing polygons. You can either pass two arrays containing the x and y coordinates of the points in the polygon, or you can pass an instance of a Polygon class:

public abstract void drawPolygon(int[] xPoints, int[] yPoints, int numPoints)

public void drawPolygon(Polygon p)

The applet in listing 28.5 draws a polygon using an array of points. Figure 28.9 shows the output from this applet.

Listing 28.5 Source Code for DrawPoly.java

import java.applet.*;
import java.awt.*;
//
// This applet draws a polygon using an array of points
public class DrawPoly extends Applet
{
// Define an array of X coordinates for the polygon
int xCoords[] = { 10, 40, 60, 30, 10 };
// Define an array of Y coordinates for the polygon
int yCoords[] = { 20, 0, 10, 60, 40 };
public void paint(Graphics g)
{
g.drawPolygon(xCoords, yCoords, 5); // 5 points in polygon
}
}

FIG. 28.9

Java allows you to draw polygons of almost any shape you can imagine.

Notice that in this example, the polygon is not "closed off." In other words, there is no line between the last point in the polygon and the first one. If you want the polygon to be closed, you must repeat the first point at the end of the array.
 

(d)The Polygon Class

The Polygon class provides a more flexible way to define polygons. You can create a Polygon by passing it an array of x points and an array of y points:

public Polygon(int[] xPoints, int[] yPoints, int numPoints)

You can also create an empty polygon and add points to it one at a time:

public Polygon()

public void addPoint(int x, int y)

Once you have created an instance of a Polygon class, you can use the getBoundingBox method to determine the area taken up by this polygon (the minimum and maximum x and y coordinates):

public Rectangle getBoundingBox()

The Rectangle class returned by getBoundingBox contains variables indicating the x and y coordinates of the rectangle and its width and height. You can also determine whether or not a point is contained within the polygon or outside it by calling inside with the x and y coordinates of the point:

public boolean inside(int x, int y)

For example, you can check to see if the point (5,10) is contained within myPolygon using the following code fragment:

if (myPolygon.inside(5, 10))
{
// the point (5, 10) is inside this polygon
}

You can use this Polygon class in place of the array of points for either the drawPolygon or fillPolygon methods. The applet in listing 28.6 creates an instance of a polygon and draws a filled polygon. Figure 28.10 shows the output from this applet.

Listing 28.6 Source Code for Polygons.java

import java.applet.*;
import java.awt.*;
//
// This applet creates an instance of a Polygon class and then
// uses fillPoly to draw the Polygon as a filled polygon.
public class Polygons extends Applet
{
// Define an array of X coordinates for the polygon
int xCoords[] = { 10, 40, 60, 30, 10 };
// Define an array of Y coordinates for the polygon
int yCoords[] = { 20, 0, 10, 60, 40 };
public void paint(Graphics g)
{
// Create a new instance of a polygon with 5 points
Polygon drawingPoly = new Polygon(xCoords, yCoords, 5);
// Draw a filled polygon
g.fillPolygon(drawingPoly);
}
}

FIG. 28.10

Polygons created with the Polygon class look just like those created from an array of points.

(d)Drawing Text

The Graphics class also contains methods to draw text characters and strings. As you have seen in the "Hello World" applet, you can use the drawString method to draw a text string on the screen. Before plunging into the various aspects of drawing text, you should be familiar with some common terms for fonts and text, as follows:

The term ascent in Java is slightly different from the same term in the publishing world. The publishing term ascent refers to the distance from the top of the letter x to the top of a character, where the Java term ascent refers to the distance from the baseline to the top of a character.
 

Figure 28.11 illustrates the relationship between the descent, ascent, baseline, and leading.


FIG. 28.11

Java’s font terminology originated in the publishing field, but some of the meanings have been changed.

You may also hear the terms proportional and fixed associated with fonts. In a fixed font, every character takes up the same amount of space. Typewriters (if you actually remember those) wrote in a fixed font. Characters in a proportional font only take up as much space as they need. You can use this book as an example.
 
The text of the book is in a proportional font, which is much easier on the eyes. Look at some of the words and notice how the letters only take up as much space as necessary. (Compare the letters i and m, for example.) The code examples in this book, however, are written in a fixed font (this preserves the original spacing). Notice how each letter takes up exactly the same amount of space.
 

Now, to draw a string using the Graphics class, you simply call drawString and give it the string you want to draw and the x and y coordinates for the beginning of the baseline (that's why you needed the terminology briefing):

public abstract void drawString(String str, int x, int y)

You may recall the "Hello World" applet used this same method to draw its famous message:

public void paint(Graphics g)
{
g.drawString("Hello World", 10, 30);
}

You can also draw characters from an array of characters or an array of bytes. The format for drawChars and drawBytes is:

void drawChars(char charArray[], int offset, int numChars, int x, int y)
void drawBytes(byte byteArray[], int offset, int numChars, int x, int y)

The offset parameter refers to the position of the first character or byte in the array to draw. This will most often be zero because you will usually want to draw from the beginning of the array. The applet in listing 28.8 draws some characters from a character array and from a byte array.

Listing 28.8 Source Code for DrawChars.java.

import java.awt.*;
import java.applet.*;
//
// This applet draws a character array and a byte array
public class DrawChars extends Applet
{
char[] charsToDraw = { 'H', 'i', ' ', 'T', 'h', 'e', 'r', 'e', '!' };
byte[] bytesToDraw = { 65, 66, 67, 68, 69, 70, 71 }; // "ABCDEFG"
public void paint(Graphics g)
{
g.drawChars(charsToDraw, 0, charsToDraw.length, 10, 20);
g.drawBytes(bytesToDraw, 0, bytesToDraw.length, 10, 50);
}
}

The Font Class

You may find that the default font for your applet is not very interesting. Fortunately, you can select from a number of different fonts. These fonts have the potential to vary from system to system, which may lead to portability issues in the future; but for the moment, HotJava and Netscape support the same set of fonts.

In addition to selecting between multiple fonts, you may also select a number of font styles: Font.PLAIN, Font.BOLD, and Font.ITALIC. These styles can be added together, so you can use a bold italic font with Font.BOLD + Font.ITALIC.

When choosing a font, you must also give the point size of the font. The point size is a printing term that relates to the size of the font. There are 100 points to an inch when printing on a printer, but this does not necessarily apply to screen fonts. Microsoft Windows defines the point size as being about the same height in all different screen resolutions. In other words, a letter in a 14-point font in the 640 [ts] 480 screen resolution should be about the same height on your monitor as a 14-point font in 1024 [ts] 768 resolution on the same monitor. Java does not conform to this notion, however. In Java, a font’s height varies directly with the number of pixels. A 14-point font in 1280 [ts] 960 resolution would be twice as tall as a 14-point font in 640 [ts] 480 mode. The point sizing is done this way in Java because many applets use absolute screen coordinates, especially when drawing raw graphics. When you draw lines and squares, they are always a fixed number of pixels high. If you are drawing text along with these figures, you always want the text to have a fixed height, also.

You create an instance of a font by using the font name, the font style, and the point size:

public Font(String fontName, int style, int size)

The following declaration creates the Times Roman font that is both bold and italic and has a point size of 12:

Font myFont = new Font("TimesRoman", Font.BOLD + Font.ITALIC, 12);

You can also retrieve fonts that are described in the system properties using the getFont methods:

public static Font getFont(String propertyName)

Returns an instance of Font described by the system property named propertyName. If the property name is not set, it will return null.

public static Font getFont(String propertyName, Font defaultValue)

Returns an instance of Font described by the system property named propertyName. If the property name is not set, it will return defaultValue.

The getFont method allows the fonts described in the system properties to have a style and a point size associated with them in addition to the font name. The format for describing a font in the system properties is:

font-style-pointsize

The style parameter can be bold, italic, bolditalic, or not present. If the style parameter is not present, the format of the string is:

font-pointsize

You might describe a bold 16-point TimesRoman font in the system properties as:

TimesRoman-bold-16

This mechanism is used for setting specific kinds of fonts. For instance, you might write a Java VT-100 terminal emulator that used the system property defaultVT100Font to find out what font to use for displaying text. You could set such a property on the command line:

java -DdefaultVT100Font=courier-14 emulators.vt100

You can get information about a font using the following methods:

public String getFamily()

The family of a font is a platform-specific name for the font. It will often be the same as the font’s name.

public String getName()

public int getSize()

public int getStyle()

You can also examine the font’s style by checking for bold, italic, and plain individually:

public boolean isBold()

public boolean isItalic()

public boolean isPlain()

The getFontList method in the Toolkit class returns an array containing the names of the available fonts:

public abstract String[] getFontList()

You can use the getDefaultToolkit method in the Toolkit class to get a reference to the current toolkit:

public static synchronized ToolKit getDefaultToolkit()

The applet in listing 28.9 uses getFontList to display the available fonts in a variety of styles.

Listing 28.9 Source Code for ShowFonts.java.

import java.awt.*;
import java.applet.*;
//
// This applet uses the Toolkit class to get a list
// of available fonts, then displays each font in
// PLAIN, BOLD, and ITALIC style.
public class ShowFonts extends Applet
{
public void paint(Graphics g)
{
String fontList[];
int i;
int startY;
// Get a list of all available fonts
fontList = getToolkit().getFontList();
startY = 15;
for (i=0; i < fontList.length; i++)
{
// Set the font to the PLAIN version
g.setFont(new Font(fontList[i], Font.PLAIN, 12));
// Draw an example
g.drawString("This is the "+
fontList[i]+" font.", 5, startY);
// Move down a little on the screen
startY += 15;
// Set the font to the BOLD version
g.setFont(new Font(fontList[i], Font.BOLD, 12));
// Draw an example
g.drawString("This is the bold "+
fontList[i]+" font.", 5, startY);
// Move down a little on the screen
startY += 15;
// Set the font to the ITALIC version
g.setFont(new Font(fontList[i], Font.ITALIC, 12));
// Draw an example
g.drawString("This is the italic "+
fontList[i]+" font.", 5, startY);
// Move down a little on the screen with some extra spacing
startY += 20;
}
}
}

FIG. 28.12

Java provides a number of different fonts and font styles.

The FontMetrics Class

The FontMetrics class lets you examine the various character measurements for a particular font. The getFontMetrics method in the Graphics class returns an instance of FontMetrics for a particular font:

public abstract FontMetrics getFontMetrics(Font f)

An instance of FontMetrics is always associated with a particular font. To find out what font an instance of FontMetrics refers to, use the getFont method:

public Font getFont()

The getAscent, getDescent, getLeading, and getHeight methods return the various height aspects of a font:

public int getAscent()

returns the typical ascent for characters in the font. It is possible for certain characters in this font to extend beyond this ascent.

public int getDescent()

returns the typical descent for characters in the font. It is possible for certain characters in this font to extend below this descent.

public int getLeading()

returns the leading value for this font.

public int getHeight()

returns the total font height, calculated as ascent + descent + leading.

Because some characters may extend past the normal ascent and descent, you can get the absolute limits with getMaxAscent and getMaxDescent:

public int getMaxAscent()

public int getMaxDescent()

The width of a character is usually given in terms of its “advance”. The advance is the amount of space the character itself takes up plus the amount of whitespace that comes after the character. The width of a string as printed on the screen is the sum of the advances of all its characters. The charWidth method returns the advance for a particular character:

public int charWidth(char ch)

public int charWidth(int ch)

You can also get the maximum advance for any character in the font with the getMaxAdvance method:

public int getMaxAdvance()

One of the most common uses of the FontMetrics class is to get the width, or advance, of a string of characters. The stringWidth method returns the advance of a string:

public int stringWidth(String str)

You can also get the width for an array of characters or an array of bytes:

public int charsWidth(char[] data, int offset, int len)

returns the width for len characters stored in data starting at position offset.

public int bytesWidth(char[] data, int offset, int len)

returns the width for len bytes stored in data starting at position offset.

The getWidths method returns an array of widths for the first 256 characters in a font:

public int[] getWidths()

(d)Drawing Modes

The Graphics class has two different modes for drawing figures: paint mode and XOR mode. Paint mode means that when a figure is drawn, all the points in that figure overwrite the points that were underneath it. In other words, if you draw a straight line in blue, every point along that line will be blue. You probably just assumed that would happen anyway, but it doesn't have to. There is another drawing mode called XOR mode, short for eXclusive-OR.

The XOR drawing mode dates back several decades. You can visualize how the XOR mode works by forgetting for a moment that you are dealing with colors and imagining that you are drawing in white on a black background. Drawing in XOR involves the combination of the pixel you are trying to draw and the pixel that is on the screen where you want to draw. If you try to draw a white pixel where there is currently a black pixel, you will draw a white pixel. If you try to draw a white pixel where there is already a white pixel, you will instead draw a black pixel.

This may sound strange, but it was once very common to do animation using XOR. To understand why, you should first realize that if you draw a shape in XOR mode and then draw the shape again in XOR mode, you erase whatever you did in the first draw. If you were moving a figure in XOR mode, you would draw it once; then to move it, you'd draw it again in its old position, erasing it, then XOR draw it in its new position. Whenever two objects overlapped, the overlapping areas looked like a negative: black was white and white was black. You probably won't have to use this technique for animation, but at least you have some idea where it came from.

When using XOR on a color system, you think of the current drawing color as the white from the above example and identify another color as the XOR color—or the black. Because there are more than two colors, the XOR mode makes interesting combinations with other colors, but you can still erase any shape by drawing it again.
 

To change the drawing mode to XOR mode, just call the setXORMode and pass it the color you want to use as the XOR color. The applet in listing 28.7 shows a simple animation that uses XOR mode to move a ball past a square.

Listing 28.7 Source Code for BallAnim.java.

import java.awt.*;
import java.applet.*;
import java.lang.*;
//
// The BallAnim applet uses XOR mode to draw a rectangle
// and a moving ball. It implements the Runnable interface
// because it is performing animation.
public class BallAnim extends Applet implements Runnable
{
Thread animThread;
int ballX = 0; // X coordinate of ball
int ballDirection = 0; // 0 if going left-to-right, 1 otherwise
// Start is called when the applet first cranks up. It creates a thread for
// doing animation and starts up the thread.
public void start()
{
if (animThread == null)
{
animThread = new Thread(this);
animThread.start();
}
}
// Stop is called when the applet is terminated. It halts the animation
// thread and gets rid of it.
public void stop()
{
animThread.stop();
animThread = null;
}
// The run method is the main loop of the applet. It moves the ball, then
// sleeps for 1/10th of a second and then moves the ball again.
public void run()
{
Thread.currentThread().setPriority(Thread.NORM_PRIORITY);
while (true)
{
moveBall();
try {
Thread.sleep(100); // sleep 0.1 seconds
} catch (Exception sleepProblem) {
// This applet ignores any exceptions if it has a problem sleeping.
// Maybe it should take Sominex
}
}
}
private void moveBall()
{
// If moving the ball left-to-right, add 1 to the x coord
if (ballDirection == 0)
{
ballX++;
// Make the ball head back the other way once the x coord hits 100
if (ballX > 100)
{
ballDirection = 1;
ballX = 100;
}
}
else
{
// If moving the ball right-to-left, subtract 1 from the x coord
ballX--;
// Make the ball head back the other way once the x coord hits 0
if (ballX <= 0)
{
ballDirection = 0;
ballX = 0;
}
}
repaint();
}
public void paint(Graphics g)
{
g.setXORMode(getBackground());
g.fillRect(40, 10, 40, 40);
g.fillOval(ballX, 0, 30, 30);
}
}

Figure 28.13 is a snapshot of the BallAnim applet in action. Notice that the ball changes color as it passes over the square. This is due to the way the XOR mode works.


FIG. 28.13

XOR drawing produces an inverse effect when objects collide.

(d)Drawing Images

The Graphics class provides a way to draw images with the drawImage method:

boolean drawImage(Image img, int x, int y, ImageObserver observer)
boolean drawImage(Image img, int x, int y, int width, int height,
ImageObserver observer)

The observer parameter in the drawImage method is an object that is in charge of watching to see when the image is actually ready to draw. If you are calling drawImage from within your applet, you can pass this as the observer because the Applet class implements the ImageObserver interface.

To draw an image, however, you need to get the image first. That is not provided by the Graphics class. Fortunately, the Applet class provides a getImage method that you can use to retrieve images. The applet in listing 28.10 retrieves an image and draws it. Figure 28.14 shows the output from this applet.

Listing 28.10 Source Code for DrawImage.java.

import java.awt.*;
import java.applet.*;
//
// This applet uses getImage to retrieve an image
// and then draws it using drawImage
public class DrawImage extends Applet
{
private Image samImage;
public void init()
{
samImage = getImage(getDocumentBase(), "samantha.gif");
}
public void paint(Graphics g)
{
g.drawImage(samImage, 0, 0, this);
}
}

FIG. 28.14

You can draw any GIF or JPEG in a Java applet with the drawImage method.

(d)The MediaTracker Class

One problem you may face in trying to display images is that the images may be coming over a slow network link (for instance, a 14.4K modem). When you begin to draw the image, it may not have arrived completely. You can use a helper class called the MediaTracker to determine whether an image is ready for display.

To use the MediaTracker, you must first create one for your applet:

public MediaTracker(Component comp)

creates a new media tracker for a specific AWT component. The comp parameter is typically the applet using the media tracker.

For example, to create a media tracker within an applet:

MediaTracker myTracker = new MediaTracker(this); // "this" refers to the applet

Next, try to retrieve the image you want to display:

Image myImage = getImage("samantha.gif");

Now you tell the MediaTracker to keep an eye on the image. When you add an image to the MediaTracker, you also give it a numeric id:

public void addImage(Image image, int id)

The id value can be used for multiple images so when you want to see if an entire group of images is ready for display, you can check it with a single id. If you intend to scale an image before displaying it, you should specify the intended width and height in the addImage call:

public void addImage(Image image, int id, int width, int height)

As a simple case, you can just give an image an id of zero:

myTracker.addImage(myImage, 0); // Track the image, give an id of 0

Once you have started tracking an image, you can load it and wait for it to be ready by using the waitForID method:

public void waitForID(int id)

waits for all images with an id number of id.

public void waitForID(int id, long ms)

waits up to a maximum of ms milliseconds for all images with an ID number of id.

You can also wait for all images using the waitForAll method:

public void waitForAll()

As with the waitForID method, you can give a maximum number of milliseconds to wait:

public void waitForAll(long ms)

You may not want to take the time to load an image before starting your applet. You can use the statusID method to initiate a load, but not wait for it. When you call statusID, you pass the id you want to status and a boolean flag to indicate whether it should start loading the image. If you pass it true, it will start loading the image:

public int statusID(int id, boolean startLoading)

A companion to statusID is statusAll, which checks the status of all images in the MediaTracker:

public int statusAll(boolean startLoading)

The statusID and statusAll methods return an integer that is made up of the following flags:

You can also use checkID and checkAll to see if an image has been successfully loaded. All the variations of checkAll and checkID return a boolean value that is true if all the images checked have been loaded.

public boolean checkID(int id)

returns true if all images with a specific id have been loaded. It does not start loading the images if they are not loading already.

public synchronized boolean checkID(int id, boolean startLoading)

returns true if all images with a specific id have been loaded. If startLoading is true, it will initiate the loading of any images that are not already being loaded.

public boolean checkAll()

returns true if all images being tracked by this MediaTracker have been loaded, but does not initiate loading if an image is not being loaded.

public synchronized boolean checkAll(boolean startLoading)

returns true if all images being tracked by this MediaTracker have been loaded. If startLoading is true, it will initiate the loading of any images that have not started loading yet.

The applet in listing 28.11 uses the MediaTracker to watch for an image to complete loading. It will draw text in place of the image until the image is complete; then it will draw the image.

Listing 28.11 Source Code for ImageTracker.java.

import java.awt.*;
import java.applet.*;
import java.lang.*;
//
// The ImageTracker applet uses the media tracker to see if an
// image is ready to be displayed. In order to simulate a
// situation where the image takes a long time to display, this
// applet waits 10 seconds before starting to load the image.
// While the image is not ready, it displays the message:
// "Image goes here" where the image will be displayed.
public class ImageTracker extends Applet implements Runnable
{
Thread animThread; // Thread for doing animation
int waitCount; // Count number of seconds we have waited
MediaTracker myTracker; // Tracks the loading of an image
Image myImage; // The image we are loading
public void init()
{
// Get the image we want to show
myImage = getImage(getDocumentBase(), "samantha.gif");
// Create a media tracker to track the image
myTracker = new MediaTracker(this);
// Tell the media tracker to track this image
myTracker.addImage(myImage, 0);
}

public void run()
{
Thread.currentThread().setPriority(Thread.NORM_PRIORITY);
while (true)
{
// Count how many times we've been through this loop
waitCount++;
// If we've been through 10 times, call checkID and tell it to start
// loading the image
if (waitCount == 10)
{
myTracker.checkID(0, true);
}

repaint();
try {
// Sleep 1 second (1000 milliseconds)
Thread.sleep(1000); // sleep 1 second
} catch (Exception sleepProblem) {
}
}
}
public void paint(Graphics g)
{
if (myTracker.checkID(0))
{
// If the image is ready to display, display it
g.drawImage(myImage, 0, 0, this);
}
else
{
// Otherwise, draw a message where we will put the image
g.drawString("Image goes here", 0, 30);
}
}
public void start()
{
animThread = new Thread(this);
animThread.start();
}
public void stop()
{
animThread.stop();
animThread = null;
}
}

Graphics Utility Classes

The AWT contains several utility classes that do not perform any drawing, but represent various aspects of geometric figures. The Polygon class introduced earlier is one of these. The others are Point, Dimension, and Rectangle.

The Point Class

A Point represents an x-y point in the Java coordinate space. Several AWT methods return instances of Point. You can also create your own instance of point by passing the x and y coordinates to the constructor:

public Point(int x, int y)

The x and y coordinates of a Point object are public instance variables:

public int x

public int y

This means you may manipulate the x and y values of a Point object directly. You can also change the x and y values using either the move or translate methods:

public void move(int newX, int newY)

sets the point’s x and y coordinates to newX and newY.

public void translate(int xChange, yChange)

adds xChange to the current x coordinate, and yChange to the current y.

The Dimension Class

A dimension represents a width and height, but not at a fixed point. In other words, two rectangles can have identical dimensions without being located at the same coordinates. The empty constructor creates a dimension with a width and height of 0:

public Dimension()

You can also specify the width and height in the constructor:

public Dimension(int width, int height)

If you want to make a copy of an existing Dimension object, you can pass that object to the Dimension constructor:

public Dimension(Dimension oldDimension)

The width and height of a dimension are public instance variables, so you can manipulate them directly:

public int width

public int height

The Rectangle Class

A rectangle represents the combination of a Point and a Dimension. The Point represents the upper left corner of the rectangle, while the Dimension represents the rectangle’s width and height. You can create an instance of a Rectangle by passing a Point and a Dimension to the constructor:

public Rectangle(Point p, Dimension d)

Rather than creating a Point and a Dimension, you can pass the x and y coordinates of the point and the width and height of the dimension:

public Rectangle(int x, int y, int width, int height)

If you pass only a Point to the constructor, the width and height are set to 0:

public Rectangle(Point p)

Similarly, if you pass only a Dimension, the x and y are set to 0:

public Rectangle(Dimension d)

If you use the empty constructor, the x, y, width, and height are all set to 0:

public Rectangle()

The x, y, width, and height variables are all public instance variables, so you can manipulate them directly:

public int x

public int y

public int width

public int height

Like the Point class, the Rectangle class contains move and translate methods which modify the upper-left corner of the rectangle:

public void move(int newX, int newY)

public void translate(int xChange, yChange)

The resize and grow methods change the rectangle’s dimensions in much the same way that move and translate change the upper left corner point:

public void resize(int newWidth, int newHeight)

public void grow(int widthChange, int heightChange)

The reshape method changes the x, y, width, and height all in one method call:

public void reshape(int newX, int newY, int newWidth, int newHeight)

The inside method returns true if a rectangle contains a specific x, y point:

public boolean inside(int x, int y)

The intersection method returns a rectangle representing the area contained by both the current rectangle and another rectangle:

public Rectangle intersection(Rectangle anotherRect)

You can determine if two rectangles intersect at all using the intersects method:

public boolean intersects(Rectangle anotherRect)

The union method is similar to the intersection, except that instead of returning the area in common to the two rectangles, it returns the smallest rectangle that contains by the rectangles:

public Rectangle union(Rectangle anotherRect)

The add method returns the smallest rectangle containing both the current rectangle and another point:

public Rectangle add(Point p)

public Rectangle add(int x, int y)

If the point is contained in the current rectangle, the add method will return the current rectangle. The add method will also take a rectangle as a parameter, in which case it is identical to the union method:

public Rectangle add(Rectangle anotherRect)

The Color Class

You may recall learning about the primary colors when you were younger. There are actually two kinds of primary colors. When you are drawing with a crayon, you are actually dealing with pigments. The primary pigments are red, yellow, and blue. You probably know some of the typical mixtures, such as red + yellow = orange, yellow + blue = green, and blue + red = purple. Black is formed from mixing all the pigments together, while white indicates the absence of pigment.

Dealing with the primary colors of light is slightly different. The primary colors of light are red, green, and blue. Some common combinations are red + green = brown (or yellow, depending on how bright it is), green + blue = cyan (light blue), and red + blue = magenta (purple). For colors of light, the concept of black and white are the reverse of the pigments. Black is formed by the absence of all light, while white is formed by the combination of all the primary colors. In other words, red + blue + green (in equal amounts) = white. Java uses a color model called the RGB color model. This means that colors are described by giving the amount of Red, Green, and Blue light in the color.

You define a color in the RGB color model by indicating how much red light, green light, and blue light is in the color. You can do this either by using numbers between zero and 255 or by using floating point numbers between 0.0 and 1.0. Table 28.1 indicates the red, green, and blue amounts for some common colors:

Table 28.1 Common Colors and their RGB Values

Color Name Red Value Green Value Blue Value
White 255 255 255
Light Gray 192 192 192
Gray 128 128 128
Dark Gray 64 64 64
Black 0 0 0
Red 255 0 0
Pink 255 175 175
Orange 255 200 0
Yellow 255 255 0
Green 0 255 0
Magenta 255 0 255
Cyan 0 255 255
Blue 0 0 255

You can create a custom color three ways:

Color(int red, int green, int blue)

creates a color using red, green, and blue values between zero and 255.

Color(int rgb)

creates a color using red, green, and blue values between 0 and 255, but all combined into a single integer. Bits 16–23 hold the red value, 8–15 hold the green value, and 0–7 hold the blue value. These values are usually written in hexadecimal notation, so you can easily see the color values. For instance, 0x123456 would give a red value of 0x12 (18 decimal), a green value of 34 (52 decimal), and a blue value of 56 (96 decimal). Notice how each color takes exactly 2 digits in hexadecimal.

Color(float red, float green, float blue)

creates a color using red, green, and blue values between 0.0 and 1.0.

Once you have created a color, you can change the drawing color using the setColor method in the Graphics class:

public abstract void setColor(Color c)

For instance, suppose you wanted to draw in pink. A nice value for pink is 255 red, 192 green, and 192 blue. The following paint method sets the color to pink and draws a circle:

public void paint(Graphics g)
{
Color pinkColor = new Color(255, 192, 192);
g.setColor(pinkColor);
g.drawOval(5, 5, 50, 50);
}

You don't always have to create colors manually. The Color class provides a number of pre-defined colors:

Given a color, you can find out its red, green, and blue values by using the getRed, getGreen, and getBlue methods:

public int getRed()

public int getGreen()

public int getBlue()

The following code fragment creates a color and then extracts the red, green, and blue values from it:

int redAmount, greenAmount, blueAmount;
Color someColor = new Color(0x345678); // red=0x34, green = 0x56, blue = 0x78
redAmount = someColor.getRed(); // redAmount now equals 0x34
greenAmount = someColor.getGreen(); // greenAmount now equals 0x56
blueAmount = someColor.getBlue(); // blueAmount now equals 0x78

You can darken or lighten a color using the darker and brighten methods:

public Color darker()

public Color brighter()

These methods return a new Color instance that contains the darker or lighter version of the original color. The original color is left untouched.

Keyboard and Mouse Events

Your applet can receive information about the keyboard and mouse. You can be notified when a key is pressed and when it is released; when the mouse enters the applet window and when it leaves the applet window; when the mouse button is pressed and when it is released; and when the mouse moves and when it is dragged (moved with the button held down).

Keyboard Events

The keyDown method is called whenever a key is pressed. Its companion method, keyUp, is called whenever a key is released. You will normally just be concerned with a key being pressed, so you can usually ignore the keyUp method. The format for keyDown and keyUp is the following:

public boolean keyDown(Event event, int keyCode)
public boolean keyUp(Event event, int keyCode)

where event is an Event object that contains specific information about the keyboard event (the key press or the key release), and keyCode is the key that was pressed.

All of your event handling methods, such as keyDown and keyUp, should return a value of true if they actually handle the event, or false to pass the event up to their parent container.

For regular ASCII characters, the keyCode is the ASCII value of the character pressed. For instance, if you press the g key, the keyCode would be 107. You could also cast the keyCode to a character value, in which case it would be the character ‘g’. If you were to hold down shift and press g, the keyCode would be 71, representing the character value G. If you hold down control and press g, the keyCode would be 7.

You can also determine if the shift, control, or alt (sometimes called meta) keys have been pressed by checking the shiftDown, controlDown, and metaDown methods in the event class. For example:

public boolean keyDown(Event event, int keyCode)
{
if (event.shiftDown())
{
// someone pressed shift
}
if (event.controlDown())
{
// someone pressed control
}
if (event.metaDown())
{
// someone pressed meta (or alt)
}
return true;
}

Because the codes for certain keys vary from system to system, Java defines a number of key codes that can be used on all systems. These key codes are as follows:

Key Codes Key
Event.F1Event.F12 Function keys F1–F12
Event.LEFT left-arrow key
Event.RIGHT right-arrow key
Event.UP up-arrow key
Event.DOWN down-arrow key
Event.END End key
Event.HOME Home key
Event.PGDN Page Down key
Event.PGUP Page Up key

Mouse Events

You can receive information about the mouse through a number of different methods. The mouseDown event is called whenever the mouse button is pressed:

public boolean mouseDown(Event event, int x, int y)

where event is the Event class containing information about the event, and x and y are the coordinates where the mouse button was pressed.

You may also want to know when the mouse button is released. You can use the mouseUp method, which takes the same arguments as mouseDown:

public boolean mouseUp(Event event, int x, int y)

The mouseEnter and mouseExit methods are called whenever the mouse enters the applet area or leaves it. These methods also take the same arguments as mouseDown:

public boolean mouseEnter(Event event, int x, int y)
public boolean mouseExit(Event event, int x, int y)

You can also track the movement of the mouse with mouseMove and mouseDrag. mouseMove is called whenever the mouse is moved while the button is up; mouseDrag is called when the mouse is moved while the button is down. These methods also take the same arguments as mouseDown:

public boolean mouseMove(Event event, int x, int y)
public boolean mouseDrag(Event event, int x, int y)

The applet in listing 28.12 uses keyboard events and mouse events to manipulate shapes. The applet in listing 28.13 makes use of a utility class called Shape, which extends the Polygon class to enable a polygon to be moved around the screen easily.

Listing 28.12 Source Code for Shape.java.

import java.awt.*;
//
// The Shape class is an extension of the Polygon class that adds
// a method for moving the Polygon to a different location. It makes
// a copy of the original coordinates, then when you move it to a new
// location, it just adds the new position to each coordinate. In other words,
// if you moved the shape to (100,100), moveShape would add 100 to each x
// coordinate and each y coordinate. You should give the coordinates relative
// to 0, 0.
public class Shape extends Polygon
{
private int[] originalXpoints;
private int[] originalYpoints;
public int x;
public int y;
public Shape(int x[], int y[], int n)
{
super(x, y, n);
// Make a copy of the x coordinates
originalXpoints = new int[n];
System.arraycopy(x, 0, originalXpoints, 0, n);
// Make a copy of the x coordinates
originalYpoints = new int[n];
System.arraycopy(y, 0, originalYpoints, 0, n);
}
public void moveShape(int newX, int newY)
{
int i;
// Add the new X and new Y values to the original coordinates, and make that
// the new position of this shape.
for (i=0; i < npoints; i++)
{
xpoints[i] = originalXpoints[i] + newX;
ypoints[i] = originalYpoints[i] + newY;
}
}
}

Listing 28.13 Source Code for ShapeManipulator.java

import java.awt.*;
import java.applet.*;
//
// The ShapeManipulator applet lets you drag a shape
// around the screen by holding down the left mouse
// button. It uses three different shapes: a triangle,
// a square, and a pentagon. You can switch between these
// by hitting 't', 's', and 'p' respectively.
//
// This applet makes use of the Shape class, which extends
// the functionality of Polygon to enable the polygon to be
// moved to a new location with a single method call.
public class ShapeManipulator extends Applet
{
private int squareXCoords[] = { 0, 40, 40, 0 };
private int squareYCoords[] = { 0, 0, 40, 40 };
private int triangleXCoords[] = { 0, 20, 40 };
private int triangleYCoords[] = { 40, 0, 40 };
private int pentXCoords[] = { 0, 20, 40, 30, 10 };
private int pentYCoords[] = { 15, 0, 15, 40, 40 };
private int shapeX; // the X and Y of the current shape
private int shapeY;
private Shape currentShape; // What shape we are dragging
private Shape triangle;
private Shape square;
private Shape pentagon;
public void init()
{
shapeX = 0;
shapeY = 0;
triangle = new Shape(triangleXCoords, triangleYCoords, 3);
square = new Shape(squareXCoords, squareYCoords, 4);
pentagon = new Shape(pentXCoords, pentYCoords, 5);
currentShape = triangle; // Start with a triangle
}
public void paint(Graphics g)
{
g.fillPolygon(currentShape); // Draw the current shape
}
public boolean mouseDrag(Event event, int mouseX, int mouseY)
{
shapeX = mouseX; // make shape coordinates = mouse coordinates
shapeY = mouseY;
// Now move the shape to its new coordinates
currentShape.moveShape(shapeX, shapeY);
// Even though the shape is moved, we still need to call repaint to update
// the display.
repaint();
return true; // always do this in event handlers
}
public boolean keyDown(Event event, int keyCode)
{
// Check the keyCode to see if it is a t, an s, or a p
if ((char)keyCode == 't')
{
currentShape = triangle;
}
else if ((char)keyCode == 's')
{
currentShape = square;
}
else if ((char)keyCode == 'p')
{
currentShape = pentagon;
}
// because we may have changed the shape, make sure the current shape
// is moved to the current shape X and Y
currentShape.moveShape(shapeX, shapeY);
// Make sure the screen shows the current shape
repaint();
return true;
}
}

Clipping

Clipping is a technique in graphics systems that prevents one area from drawing over another. Basically, you draw in an rectangular area, and everything you try to draw outside the area gets "clipped off." Normally, your applet is clipped at the edges. In other words, you cannot draw beyond the bounds of the applet window. You cannot increase the clipping area; that is, you cannot draw outside the applet window, but you can further limit where you can draw inside the applet window. To set the boundaries of your clipping area, use the clipRect method in the Graphics class:

public abstract void clipRect(int x, int y, int width, int height)

You can query the current clipping area of a Graphics object with the getClipRect method:

public abstract Rectangle getClipRect()

The applet in listing 28.14 reduces its drawing area to a rectangle whose upper-left corner is at (10, 10) and is 60 pixels wide and 40 pixels high, and then tries to draw a circle. Figure 28.15 shows the output from this applet.

Listing 28.14 Source Code for Clipper.java.

import java.applet.*;
import java.awt.*;
//
// This applet demonstrates the clipRect method by setting
// up a clipping area and trying to draw a circle that partially
// extends outside the clipping area.
// I want you to go out there and win just one for the Clipper...
public class Clipper extends Applet
{
public void paint(Graphics g)
{
// Set up a clipping region
g.clipRect(10, 10, 60, 40);
// Draw a circle
g.fillOval(5, 5, 50, 50);
}
}

FIG. 28.15

The clipRect method reduces the drawing area and cuts off anything that extends outside it.

Within a paint method, once you change your clipping area, you cannot restore the old clipping area. In other words, you can only reduce your drawing area; you can never expand it. Even the clipping area is clipped, so if part of your new clipping area extends outside the old clipping area, only the portion of the new clipping area that falls within the old clipping area will be used. The clipping area lasts for the rest of your paint method; but the next time your paint method is called, the clipping area will be reset.

Animation Techniques

You may have noticed a lot of screen flicker when you ran the Shape Manipulator applet. It was intentionally written to not eliminate any flicker so you could see just how bad flicker can be. What causes this flicker? One major cause is that the shape is redrawn on the screen right in front of you. The constant redrawing catches your eye and makes things appear to flicker. A common solution to this problem is a technique called double-buffering.

The idea behind double-buffering is that you create an off-screen image, and do all your drawing to that off-screen image. Once you are finished drawing, you copy the off-screen image to your drawing area in one quick call so the drawing area updates immediately.

The other major cause of flicker is the update method. The default update method for an applet clears the drawing area, then calls your paint method. You can eliminate the flicker caused by the screen clearing by overriding update to simply call the paint method:

public void update(Graphics g)
{
paint(g);
}

There is a danger with changing update this way. Your applet must be aware that the screen has not been cleared. If you are using the double-buffering technique, this should not be a problem because you are replacing the entire drawing area with your off-screen image anyway.
 

The ShapeManipulator applet can be modified easily to support double-buffering and eliminate the screen-clear. In the declarations at the top of the class, you add an Image that will be the off-screen drawing area:

private Image offScreenImage;

Next, you add a line to the init method to initialize the off-screen image:

offScreenImage = createImage(size().width, size().height);

Finally, you create an update method that does not clear the real drawing area, but makes your paint method draw to the off-screen area and then copies the off-screen area to the screen (see listing 28.15).

Listing 28.15 An update method to support double-buffering.

public void update(Graphics g)
{
// This update method helps reduce flicker by supporting off-screen drawing
// and by not clearing the drawing area first. It enables you to leave
// the original paint method alone.
// Get the graphics context for the off-screen image
Graphics offScreenGraphics = offScreenImage.getGraphics();
// Now, go ahead and clear the off-screen image. It is O.K. to clear the
// off-screen image, because it is not being displayed on the screen.
// This way, your paint method can still expect a clear area, but the
// screen won't flicker because of it.
offScreenGraphics.setColor(getBackground());
// We've set our drawing color to the applet's background color, now
// fill the entire area with that color (i.e. clear it)
offScreenGraphics.fillRect(0, 0, size().width,
size().height);
// Now, because the paint method probably doesn't set its drawing color,
// set the drawing color back to what was in the original graphics context.
offScreenGraphics.setColor(g.getColor());
// Call the original paint method
paint(offScreenGraphics);
// Now, copy the off-screen image to the screen
g.drawImage(offScreenImage, 0, 0, this);
}


Previous Page TOC Next Page

| Previous Chapter | Next Chapter |

| Table of Contents | Book Home Page |

| Que Home Page | Digital Bookshelf | Disclaimer |


To order books from QUE, call us at 800-716-0044 or 317-361-5400.

For comments or technical support for our books and software, select Talk to Us.

© 1996, QUE Corporation, an imprint of Macmillan Publishing USA, a Simon and Schuster Company