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 you would otherwise have to write yourself.
This chapter discusses the low-level graphics and printing features of the AWT. Chapter 28 discusses the low-level input handling, while Chapters 29 and 30 discuss the higher-level portions of the AWT.
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 gets called. Your applet actually has three different methods that are used in redrawing the applet, as follows:
The Graphics class provides methods for drawing a number of graphical figures, including the following:
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
27.1 gives you an example of some coordinates.
FIG. 27.1
Unlike math coordinates, where y increases from bottom to top, the y coordinates
in Java increase from the top down.
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 27.1 uses the drawLine method to draw some lines. The output from this applet is shown in Figure 27.2.
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. 27.2
Line drawing is one of the most basic graphics operations.
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 abstract 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 27.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. 27.3
Java provides several flexible ways of drawing 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 is similar to drawRect and fillRect, except 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 catch 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 27.2 draws some raised and lowered rectangles, both filled and unfilled.
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 27.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. 27.4
The draw3DRect and fill3DRect methods use shading to produce
a 3-D effect.
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 27.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. 27.5
Java uses an ellipse to determine the amount of rounding.
The applet in Listing 27.3 draws a rounded rectangle and a filled, rounded rectangle. Figure 27.6 shows the output from this applet.
import java.awt.*; import java.applet.*; // Example 27.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. 27.6
Java's rounded rectangles are a pleasant alternative to sharp-cornered rectangles.
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
27.7 illustrates the concept of the enclosing rectangle.
FIG. 27.7
Circles and Ellipses are drawn within the bounds of an imaginary enclosing rectangle.
The applet in Listing 27.4 draws a circle and a filled ellipse. Figure 27.8 shows the output from this applet.
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. 27.8
Java doesn't know the difference between ellipses and circles; they're all just ovals.
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 27.5 draws a polygon using an array of points. Figure 27.9 shows the output from this applet.
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. 27.9
Java allows you to draw polygons of almost any shape you can imagine.
CAUTION:
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.
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 getBounds method to determine the area taken up by this polygon (the minimum and maximum x and y coordinates):
public Rectangle getBounds()
The Rectangle class returned by getBounds() 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 contains(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.contains(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 27.6 creates an instance of a polygon and draws a filled polygon. Figure 27.10 shows the output from this applet.
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. 27.10
Polygons created with the Polygon class look just like those created
from an array of points.
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:
CAUTION:
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.
NOTE: 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.
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 27.7 draws some characters from a character array and from a byte array.
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); } }
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. 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 640x480 screen resolution should be about the same height on your monitor as a 14-point font in 1024x768 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 1280x960 resolution would be twice as tall as a 14-point font in 640x480 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 27.8 uses getFontList to display the available fonts in a variety of styles. Figure 27.12 shows the results of Listing 27.8.
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. 27.12
Java provides a number of different fonts and font styles.
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)
You can also get the font metrics for the current font:
public FontMetrics getFontMetrics()
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()
The Graphics class has two different modes for drawing figures: paint and XOR. 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, 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 draws 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.
NOTE: When using XOR on a color system, 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.
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 27.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. 27.13
XOR drawing produces an inverse effect when objects collide.
The Graphics class provides a way to draw images with the drawImage method:
public abstract boolean drawImage(Image img, int x, int y, ImageObserver observer) public abstract boolean drawImage(Image img, int x, int y, int width, int height, ImageObserver observer) public abstract boolean drawImage(Image img, int x, int y, Color bg, ImageObserver ob) public abstract boolean drawImage(Image img, int x, int y, int width, int height, Color bg, ImageObserver ob) public abstract boolean drawImage(Image img, int dx1, int dy1, int dx2, int dy2, int sx1, int sy1, int sx2, int sy2, Color bg, ImageObserver ob) public abstract boolean drawImage(Image img, int dx1, int dy1, int dx2, int dy2, int sx1, int sy1, int sx2, int sy2, ImageObserver ob)
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. The bg parameter, if present, indicates the color of the background area of the rectangle into which the image is drawn. This is often used if the image has transparent pixels where the bg color indicates the color used for the transparent pixels.
The drawImage method can draw a portion of an image and scale it as it draws. The sx,sy parameters indicate the top-left and bottom-right corners of the region of the original image that is to be drawn. The dx,dy parameters indicate the top-left and bottom-right corners of the region where the image is to be drawn. If the size of the sx and ' rectangles is different, the image is scaled appropriately.
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 27.10 retrieves an image and draws it. Figure 27.14 shows the output from this applet.
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. 27.14
You can draw any GIF or JPEG in a Java applet with the drawImage method.
One problem you may face when 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 synchronized 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 27.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.
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 you have waited MediaTracker myTracker; // Tracks the loading of an image Image myImage; // The image you are loading public void init() { // Get the image you 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 you've been through this loop waitCount++; // If you'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 you 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; } }
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.
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)
You can also create an uninitialized point, or initialize a point using another Point object:
public Point() public Point(Point p)
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.
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
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 want x and y to be 0, you can create the rectangle using only the width and height:
public Rectangle(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)
You can use another Rectangle object as the source for the new rectangle's coordinates and size:
public Rectangle(Rectangle r)
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 setSize 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 setSize(int newWidth, int newHeight) public void grow(int widthChange, int heightChange)
The setBounds method changes the x, y, width, and height all in one method call:
public void setBounds(int newX, int newY, int newWidth, int newHeight)
The contains method returns true if a rectangle contains a specific x, y point:
public boolean contains(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 is contained by the rectangles:
public Rectangle union(Rectangle anotherRect)
The add method returns the smallest rectangle containing both the current rectangle and another point:
public void add(Point p) public void 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 void add(Rectangle anotherRect)
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.
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 27.1 indicates the red, green, and blue amounts for some common colors.
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 |
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 brighter 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.
Clipping is a technique in graphics systems that prevents one area from drawing over another. Basically, you draw in a 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 getClipBounds method:
public abstract Rectangle getClipBounds()
The applet in Listing 27.12 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 27.15 shows the output from this applet.
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. 27.15
The clipRect method reduces the drawing area and cuts off anything that
extends outside it.
The clipRect method will only reduce the current clipping region. Prior to Java 1.1, there was no way to expand the clipping region once you reduced it. Java 1.1 adds the setClip method that can either expand or reduce the clipping area:
public abstract void setClip(int x, int y, int width, int height)
In preparation for the possibility of non-rectangular clipping areas, Sun has added a Shape interface and a method to use a Shape object as a clipping region. The Shape interface currently has only one method:
public abstract Rectangle getBounds()
You can set the clipping region with any object that implements the Shape interface using this variation of setClip:
public abstract void setClip(Shape region)
Since the clipping region may one day be non-rectangular, the getClipBounds method will not be sufficient for retrieving the clipping region. The getClip method returns the current clipping region as a Shape object:
public abstract Shape getClip()
Although the Shape interface might allow you to create non-rectangular clipping regions, you cannot do it yet. The only method defined in the Shape interface returns a rectangular area. The Shape interface will need to be expanded to support non-rectangular regions.
You may have noticed a lot of screen flicker when you ran the ShapeManipulator 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); }
CAUTION
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.
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 27.13).
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()); // You've set the 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); }
The ability to send information to a printer was one of the most glaring omissions in the 1.0 release of Java. Fortunately, Java 1.1 addresses that problem with the PrintJob class.
The first thing you need to do in order to print something is to create an instance of a PrintJob object. You can do this with the getPrintJob method in java.awt.Toolkit:
public abstract PrintJob getPrintJob(Frame parent, String jobname, Properties props)
As you can see, a print job must be associated with a Frame object. If you are printing from an applet, you must first create a Frame object before calling getPrintJob. Once you have a PrintJob object, you print individual pages by calling getGraphics in the PrintJob object, which creates a Graphics object which you can then draw on:
public abstract Graphics getGraphics()
Every new instance of Graphics represents a separate print page. Once you have printed all the pages you want, you call the end method in PrintJob to complete the job:
public abstract void end()
The Graphics object returned by getGraphics is identical to the Graphics object passed to your paint method. You can use all the drawing methods normally available to your paint method. In fact, you can print an image of your current screen by manually calling your paint method with the Graphics object returned by getGraphics. Once you finish drawing on a Graphics object, you invoke its dispose method to complete the page.
When printing, you often want to know the resolution of the page, or how many pixels per inch are on the page. The getResolution method in a PrintJob object returns this information:
public abstract int getPageResolution()
The getPageDimension method returns the page width and height in pixels:
public abstract Dimension getPageDimension()
Some systems and some printers print the last page first. You can find out if you will be printing in last-page-first order by calling lastPageFirst:
public abstract boolean lastPageFirst()
Listing 27.14 shows the printing equivalent of the famous "Hello World" program.
import java.awt.*; import java.applet.*; public class PrintHelloWorld extends Applet { public void init() { // First create a frame to be associated with the print job Frame myFrame = new Frame(); // Start a new print job PrintJob job = Toolkit.getPrintJob(myFrame, "Hello", NULL); // Get a graphics object for drawing Graphics g = job.getGraphics(); // Print the famous message to the graphics object g.drawString("Hello World!", 50, 100); // Complete the printing of this page by disposing of the graphics object g.dispose(); // Complete the print job job.end(); } }
The drawing functions provided by the Graphics object are fairly primitive by modern standards. These functions will eventually be superseded by the Java 2D API which will provide a much more robust drawing model.