Simple Graphics

Contents:
Graphics
Point
Dimension
Shape
Rectangle
Polygon
Image
MediaTracker

This chapter digs into the meat of the AWT classes. After completing this chapter, you will be able to draw strings, images, and shapes via the Graphics class in your Java programs. We discuss geometry-related classes--Polygon, Rectangle, Point, and Dimension, and the Shape interface--you will see these throughout the remaining AWT objects. You will also learn several ways to do smooth animation by using double buffering and the MediaTracker.

After reading this chapter, you should be able to do simple animation and image manipulation with AWT. For most applications, this should be sufficient. If you want to look at AWT's more advanced graphics capabilities, be sure to take a look at Image Processing.

Graphics

The Graphics class is an abstract class that provides the means to access different graphics devices. It is the class that lets you draw on the screen, display images, and so forth. Graphics is an abstract class because working with graphics requires detailed knowledge of the platform on which the program runs. The actual work is done by concrete classes that are closely tied to a particular platform. Your Java Virtual Machine vendor provides the necessary concrete classes for your environment. You never need to worry about the platform-specific classes; once you have a Graphics object, you can call all the methods of the Graphics class, confident that the platform-specific classes will work correctly wherever your program runs.

You rarely need to create a Graphics object yourself; its constructor is protected and is only called by the subclasses that extend Graphics. How then do you get a Graphics object to work with? The sole parameter of the Component.paint() and Component.update() methods is the current graphics context. Therefore, a Graphics object is always available when you override a component's paint() and update() methods. You can ask for the graphics context of a Component by calling Component.getGraphics(). However, many components do not have a drawable graphics context. Canvas and Container objects return a valid Graphics object; whether or not any other component has a drawable graphics context depends on the run-time environment. (The latest versions of Netscape Navigator provide a drawable graphics context for any component, but you shouldn't get used to writing platform-specific code.) This restriction isn't as harsh as it sounds. For most components, a drawable graphics context doesn't make much sense; for example, why would you want to draw on a List? If you want to draw on a component, you probably can't. The notable exception is Button, and that may be fixed in future versions of AWT.

Graphics Methods

Constructors

The abstract methods of the Graphics class are implemented by some windowing system-specific class. You rarely need to know which subclass of Graphics you are using, but the classes you actually get (if you are using the JDK) are sun.awt.win32.Win32Graphics ( JDK1.0), sun.awt.window.WGraphics ( JDK1.1), sun.awt.motif.X11Graphics, or sun.awt.macos.MacGraphics. Pseudo-constructors

In addition to using the graphics contexts given to you by getGraphics() or in Component.paint(), you can get a Graphics object by creating a copy of another Graphics object. Creating new graphics contexts has resource implications. Certain platforms have a limited number of graphics contexts that can be active. For instance, on Windows you cannot have more than four in use at one time. Therefore, it's a good idea to call dispose() as soon as you are done with a Graphics object. Do not rely on the garbage collector to clean up for you.

Drawing strings

These methods let you draw text strings on the screen. The coordinates refer to the left end of the text's baseline.

For more information about Font and FontMetrics, see Fonts and Colors. Painting

Painting mode

There are two painting or drawing modes for the Graphics class: paint (the default) and XOR mode. In paint mode, anything you draw replaces whatever is already on the screen. If you draw a red square, you get a red square, no matter what was underneath; this is what most developers have learned to expect.

The behavior of XOR mode is rather strange, at least to people accustomed to modern developing environments. XOR mode is short for eXclusive-OR mode. The idea behind XOR mode is that drawing the same object twice returns the screen to its original state. This technique was commonly used for simple animations prior to the development of more sophisticated methods and cheaper hardware.

The side effect of XOR mode is that painting operations don't necessarily get the color you request. Instead of replacing the original pixel with the new value, XOR mode merges the original color, the painting color, and an XOR color (usually the background color) to form a new color. The new color is chosen so that if you repaint the pixel with the same color, you get the original pixel back. For example, if you paint a red square in XOR mode, you get a square of some other color on the screen. Painting the same red square again returns the screen to its original state.

import java.awt.*;
public class xor extends java.applet.Applet {
 public void init () {
 setBackground (Color.red);
}
public void paint (Graphics g) {
 g.setColor (Color.green); g.setXORMode (Color.blue); g.fillRect (10, 10, 100, 100); g.fillRect (10, 60, 100, 100);
}
} 

Figure 2.2: Drawing in XOR mode

[Graphic: Figure 2-2]

Although it's hard to visualize what color XOR mode will pick, there is one important special case. Let's say that there are only two colors: a background color (the XOR color) and a foreground color (the painting color). Each pixel must be in one color or the other. Painting "flips" each pixel to the other color. Foreground pixels become background, and vice versa.

Drawing shapes

Most of the drawing methods require you to specify a bounding rectangle for the object you want to draw: the location of the object's upper left corner, plus its width and height. The two exceptions are lines and polygons. For lines, you supply two endpoints; for polygons, you provide a set of points.

Versions 1.0.2 and 1.1 of AWT always draw solid lines that are one pixel wide; there is no support for line width or fill patterns. A future version should support lines with variable widths and patterns.

Figure 2.7: Filled and unfilled ovals

[Graphic: Figure 2-7]

Filling polygons is a complex topic. It is not as easy as filling rectangles or ovals because a polygon may not be closed and its edges may cross. AWT uses an even-odd rule to fill polygons. This algorithm works by counting the number of times each scan line crosses an edge of the polygon. If the total number of crossings to the left of the current point is odd, the point is colored. If it is even, the point is left alone. Figure 2.9 demonstrates this algorithm for a single scan line that intersects the polygon at x values of 25, 75, 125, 175, 225, and 275.

Figure 2.9: Polygon fill algorithm

[Graphic: Figure 2-9]

The scan line starts at the left edge of the screen; at this point there haven't been any crossings, so the pixels are left untouched. The scan line reaches the first crossing when x equals 25. Here, the total number of crossings to the left is one, so the scan line is inside the polygon, and the pixels are colored. At 75, the scan line crosses again; the total number of crossings is two, so coloring stops.

Figure 2.10 shows several polygons created by the following code, containing different versions of drawPolygon() and fillPolygon():

int[] xPoints[] = {{50, 25, 25, 75, 75}, {50, 25, 25, 75, 75}, {100, 100, 150, 100, 150, 150, 125, 100, 150}, {100, 100, 150, 100, 150, 150, 125, 100, 150}};
 int[] yPoints[] = {{10, 35, 85, 85, 35, 10}, {110, 135, 185, 185, 135}, {85, 35, 35, 85, 85, 35, 10, 35, 85}, {185, 135, 135, 185, 185, 135, 110, 135, 185}};
 int nPoints[] = {5, 5, 9, 9};
 g.drawPolygon (xPoints[0], yPoints[0], nPoints[0]); g.fillPolygon (xPoints[1], yPoints[1], nPoints[1]); g.drawPolygon (new Polygon(xPoints[2], yPoints[2], nPoints[2])); g.fillPolygon (new Polygon(xPoints[3], yPoints[3], nPoints[3])); 

Figure 2.10: Filled and unfilled polygons

[Graphic: Figure 2-10]Drawing images

An Image is a displayable object maintained in memory. To get an image on the screen, you must draw it onto a graphics context, using the drawImage() method of the Graphics class. For example, within a paint() method, you would call g.drawImage(image, ... , this) to display some image on the screen. In other situations, you might use the createImage() method to generate an offscreen Graphics object, then use drawImage() to draw an image onto this object, for display later.

This begs the question: where do images come from? We will have more to say about the Image class later in this chapter. For now, it's enough to say that you can call getImage() to load an image from disk or across the Net. There are versions of getImage() in the Applet and Toolkit classes; the latter is for use in applications. You can also call createImage(), a method of the Component class, to generate an image in memory.

What about the last argument to drawImage()? What is this for? The last argument of drawImage() is always an image observer--that is, an object that implements the ImageObserver interface. This interface is discussed in detail in Image Processing. For the time being, it's enough to say that the call to drawImage() starts a new thread that loads the requested image. An image observer monitors the process of loading an image; the thread that is loading the image notifies the image observer whenever new data has arrived. The Component class implements the ImageObserver interface; when you're writing a paint() method, you're almost certainly overriding some component's paint() method; therefore, it's safe to use this as the image observer in a call to drawImage(). More simply, we could say that any component can serve as an image observer for images that are drawn on it.

The following code generated the images in Figure 2.11. The images on the left come from a standard JPEG file. The images on the right come from a file in GIF89a format, in which the white pixel is "transparent." Therefore, the gray background shows through this pair of images.

import java.awt.*; import java.applet.*;
public class drawingImages extends Applet {
 Image i, j;
public void init () {
 i = getImage (getDocumentBase(), "rosey.jpg"); j = getImage (getDocumentBase(), "rosey.gif");
}
public void paint (Graphics g) {
 g.drawImage (i, 10, 10, this); g.drawImage (i, 10, 85, 150, 200, this); g.drawImage (j, 270, 10, Color.lightGray, this); g.drawImage (j, 270, 85, 150, 200, Color.lightGray, this);
}
} 

Figure 2.11: Scaled and unscaled images

[Graphic: Figure 2-11]

The following code demonstrates the new drawImage() methods in Java 1.1. They allow you to scale, flip, and crop images without the use of image filters. The results are shown in Figure 2.12.

// Java 1.1 only import java.awt.*; import java.applet.*;
public class drawingImages11 extends Applet {
 Image i, j;
public void init () {
 i = getImage (getDocumentBase(), "rosey.gif");
}
public void paint (Graphics g) {
 g.drawImage (i, 10, 10, this); g.drawImage (i, 10, 85, i.getWidth(this)+10, i.getHeight(this)+85, i.getWidth(this), i.getHeight(this), 0, 0, this); g.drawImage (i, 270, 10, i.getWidth(this)+270, i.getHeight(this)*2+10, 0, 0, i.getWidth(this), i.getHeight(this), Color.gray, this); g.drawImage (i, 10, 170, i.getWidth(this)*2+10, i.getHeight(this)+170, 0, i.getHeight(this)/2, i.getWidth(this)/2, 0, this);
}
} 

Figure 2.12: Flipped, mirrored, and cropped images

[Graphic: Figure 2-12]Miscellaneous methods