Section 7.1
More About Graphics
IN THIS SECTION, we'll look at some additional aspects
of graphics in Java. Most of the section deals with Images, which
are pictures stored in files or in the computer's memory. But we'll also consider
a few other techniques that can be used to draw better or more efficiently.
Images
To a computer, an image is just a set of numbers. The numbers specify
the color of each pixel in the image. The numbers that represent the
image on the computer's screen are stored in a part of memory called a
frame buffer. Many times each second,
the computer's video card reads the data in the frame buffer and
colors each pixel on the screen according to that data. Whenever the
computer needs to make some change to the screen, it writes some new
numbers to the frame buffer, and the change appears on the screen
a fraction of a second later, the next time the screen is redrawn
by the video card.
Since it's just a set of numbers, the data for an image doesn't have
to be stored in a frame buffer. It can be stored elsewhere in the
computer's memory. It can be stored in a file on the computer's
hard disk. Just like any other data file, an image file can be
downloaded over the Internet. Java includes standard classes
and subroutines that can be used to copy image data from one
part of memory to another and to get data from an image file
and use it to display the image on the screen.
The standard class java.awt.Image is used to represent
images. A particular object of type Image contains information
about some particular image. There are actually two kinds of Image
objects. One kind represents an image in an image data file. The
second kind represents an image in the computer's memory. Either type
of image can be displayed on the screen. The second kind of Image
can also be modified while it is in memory. We'll look at this
second kind of Image below.
Every image is coded as a set of numbers, but there are various
ways in which the coding can be done. For images in files, there
are two main coding schemes which are used in Java and on the Internet.
One is used for GIF images, which are usually stored in files that have names
ending in ".gif". The other is used for JPEG images, which
are stored in files that have names ending in ".jpg" or
".jpeg". Both GIF and JPEG images are
compressed. That is, redundancies in the
data are exploited to reduce the number of numbers needed to represent the
data. In general, the compression method used for GIF images works
well for line drawings and other images with large patches of
uniform color. JPEG compression generally works well for photographs.
The Applet class defines a method, getImage, that can be used for
loading images stored in GIF and JPEG files. (As we will see later, stand-alone applications
use a different technique for loading image files.) For example,
suppose that the image of an ace of clubs, shown at the right, is contained
in a file named "ace.gif". And suppose that img is
a variable of type Image. Then the following command could be used
in the source code of your applet:
img = getImage( getCodeBase(), "ace.gif" );
This would create an Image object to represent the ace. The second parameter is
the name of the file that contains the image. The first parameter specifies the
directory that contains the image file. The value "getCodeBase()"
specifies that the image file is in the code base directory for the applet.
Assuming that the applet is in the default package, as usual, that just means
that the image file is in the same directory as the compiled class file
of the applet.
Once you have an object of type Image, however you obtain it,
you can draw the image in any graphics context. Most commonly, this will be
done in the paintComponent() method of a JPanel (or some
other JComponent.) If g is the Graphics object
that is provided as a parameter to the paintComponent() method, then the
command:
g.drawImage(img, x, y, this);
will draw the image img in a rectangular area in the component. The parameters x
and y give the position of the upper-left corner of the rectangle in which
the image is displayed, and the rectangle is just large enough to hold the image.
The fourth parameter, this, is the special variable from Section 5.5
that refers to the component itself. This parameter is there for technical reasons
having to do with the funny way Java treats image files. (Although you don't
really need to know this, here is how it works: When you use getImage() to create an
Image object from an image file, the file is not downloaded immediately. The
Image object simply remembers where the file is. The file will be downloaded the
first time you draw the image. However, when the image needs to be downloaded,
the drawImage() method only initiates the downloading. It doesn't wait for
the data to arrive. So, after drawImage() has finished executing, it's quite
possible that the image has not actually been drawn! But then, when does it get drawn?
That's where the fourth parameter to the drawImage() command comes in.
The fourth parameter is something called an ImageObserver. After the
image has been downloaded, the system will inform the ImageObserver that
the image is available, and the ImageObserver will actually draw the
image at that time. For large images, it's even possible that the image will
be drawn in several parts as it is downloaded. Any JComponent object
can act as an ImageObserver. If you are sure that the image that you
are drawing has already been downloaded, you can set the fourth parameter of
drawImage() to null.)
There are a few useful variations of the drawImage() command.
For example, it is possible to scale the image as it is drawn to a specified
width and height. This is done with the command
g.drawImage(img, x, y, width, height, this);
The parameters width and height give the size of the
rectangle in which the image is displayed.
Another version makes it possible to draw just part of the image. In the
command:
g.drawImage(img, dest_x1, dest_y1, dest_x2, dest_y2,
source_x1, source_y1, source_x2, source_y2, this);
the integers source_x1, source_y1, source_x2, and source_y2
specify the top-left and bottom-right corners of a rectangular region in the source image.
The integers dest_x1, dest_y1, dest_x2, and dest_y2
specify the corners of a region in the destination graphics context. The specified
rectangle in the image is drawn, with scaling if necessary, to the specified rectangle
in the graphics context. For an example in which this is useful, consider a card
game that needs to display 52 different cards. Dealing with 52 image files can be
cumbersome and inefficient, especially for downloading over the Internet. So, all
the cards might be put into a single image:
Now, only one Image object is needed. Drawing one card means drawing
a rectangular region from the image. This technique is used in the following
version of the HighLow card game
from Section 6.6:
In this applet, the cards are drawn by the following method. The variable,
cardImages, is a variable of type Image that represents the image
of 52 cards that is shown above. Each card is 40 by 60 pixels. These numbers are
used, together with the suit and value of the card, to compute the corners of
the source and destination rectangles for the drawImage() command:
void drawCard(Graphics g, Card card, int x, int y) {
// Draws a card as a 40 by 60 rectangle with
// upper left corner at (x,y). The card is drawn
// in the graphics context g. If card is null, then
// a face-down card is drawn. The cards are taken
// from an Image object that loads the image from
// the file smallcards.gif.
if (card == null) {
// Draw a face-down card
g.setColor(Color.blue);
g.fillRect(x,y,40,60);
g.setColor(Color.white);
g.drawRect(x+3,y+3,33,53);
g.drawRect(x+4,y+4,31,51);
}
else {
int row = 0; // Which of the four rows contains this card?
switch (card.getSuit()) {
case Card.CLUBS: row = 0; break;
case Card.HEARTS: row = 1; break;
case Card.SPADES: row = 2; break;
case Card.DIAMONDS: row = 3; break;
}
int sx, sy; // Coords of upper left corner in the source image.
sx = 40*(card.getValue() - 1);
sy = 60*row;
g.drawImage(cardImages, x, y, x+40, y+60,
sx, sy, sx+40, sy+60, this);
}
} // end drawCard()
The variable cardImages is defined as an instance variable in the
applet, and the image object is created in the init() method of the
applet with the command:
cardImages = getImage( getCodeBase(), "smallcards.gif" );
The complete source code for this applet can be found in HighLowGUI2.java.
Off-screen Images and Double Buffering
In addition to images in image files, objects of type Image can be used
to represent images stored in the computer's memory. What makes such images particularly
useful is that it is possible to draw to an Image in the computer's memory.
This drawing is not visible to the user. Later, however, the image can be copied very
quickly to the screen. In fact, this technique is used automatically in Swing to
draw the components that you see on the screen. When the on-screen picture needs
to be redrawn, the new picture is drawn step-by-step to an off-screen image.
This can take some time. If all this drawing were done on screen,
the user would see the image flicker as it is drawn. Instead, a complete new image replaces the old
one on the screen almost instantaneously. The user doesn't see all the steps involved in
redrawing. This technique makes smooth, flicker-free animation and dragging easy in
Swing. (It is not at all easy or automatic when using the older AWT GUI components. This
is one big advantage of Swing.)
The technique of drawing an off-screen image and then quickly copying the image to the screen is
called double buffering. The name comes from the term
"frame buffer," which refers to the region in memory that holds the image on the screen.
(In fact, true double buffering uses two frame buffers. The video card can display either
frame buffer on the screen and can switch instantaneously from one frame buffer to the other.
One frame buffer is used to draw a new image for the screen.
Then the video card is told to switch from one frame buffer to the other. No copying
of memory is involved. Double-buffering as it is implemented in Java does require copying,
which takes some time and is not perfectly flicker-free.)
It's possible to turn off double buffering in Swing (although there is little reason to do so).
To help you understand the effect of double buffering,
here are two applets that are identical, except that one uses double buffering and
one does not. You can drag the red squares around the applets. I've added
a lot of lines in the background to increase the time it takes to redraw the
applet. You should notice an annoying flicker in the non-double-buffered
applet on the left:
Swing's double buffering uses an off-screen image. Sometimes, it's useful to
create your own off-screen images for other purposes.
An off-screen Image object
can be created by calling the instance method createImage().
This method is defined in the Component class, and so can be used just
about anywhere in an applet's source code.
The createImage() method takes two parameters to specify the
width and height of the image to be created. For example,
Image offScreenImage = createImage(width, height);
Drawing to an off-screen image is done in the same way as any other drawing in Java,
by using a graphics context. The Image class defines an instance method
getGraphics() that returns a Graphics object that can be used for
drawing on the off-screen image. (This works only for off-screen images.
If you try to do this with an Image from
a file, an error will occur.) That is, if offScreenImage is a variable
of type Image that refers to an off-screen image, you can say
Graphics offscreenGraphics = offScreenImage.getGraphics();
Then, any drawing operations performed with the graphics context offscreenGraphics
are applied to the off-screen image. For example, "offscreenGraphics.drawRect(10,10,50,100);"
will draw a 50-by-100-pixel rectangle on the off-screen image. Once a picture has been
drawn on the off-screen image,
the picture can be copied into another graphics context,
using the graphics context's drawImage() method. For example:
g.drawImage(offScreenImage,0,0,null). For an off-screen image, the file parameter
to drawImage() can be null. (Since the image is already in memory, there
is no need for an "ImageObserver" to wait for the image to be loaded from a file.)
Off-screen images can be used to solve one problem that we have seen in many of
our sample applets. In many cases, we have had no convenient way of remembering
what was drawn on an applet, so that we were unable restore the drawing when necessary.
For example, in the paint applet in
Section 6.6, the user's sketch will disappear if the applet
is covered up and then uncovered. An off-screen image can be used to solve this
problem. The idea is simple: Keep a copy of the drawing in an off-screen image.
When the component needs to be redrawn, copy the off-screen image onto the screen.
This method is used in the improved paint program at the end of this section.
When used in this way, the off-screen image should always contain a copy of the
picture on the screen. The paintComponent() method
copies this off-screen image to the screen. This will refresh the picture
when it is covered and uncovered. The actual drawing of the picture should take
place elsewhere. (Occasionally, it makes sense to draw some extra stuff on
the screen, on top of the image from the off-screen image. For example,
a hilite or a shape that is being dragged might be treated in this way. These
things are not permanently part of the image. The permanent image is
safe in the off-screen image, and it can be used to restore the on-screen
image when the hilite is removed or the shape is dragged to a different location.
We will use this technique in the next example.)
There are two approaches to keeping the image on the screen synchronized with the
image in the off-screen image. In the first approach, in order to change the
image, you make the change to the off-screen image and then call repaint()
to copy the modified image to the screen. This is safe and easy, but not always efficient.
The second approach is to make every change twice, once to the off-screen image and
once to the screen. This keeps the two images the same, but it requires some care to
make sure that exactly the same drawing is done in both (and it violates the
rule about doing drawing operations only inside paintComponent() methods).
When using an off-screen image as a backup for the picture displayed on a component,
the size of the off-screen image should be the same as the size of the component.
This raises the problem of where in the program the image should be created. If the
off-screen image is to fill an entire applet, then the image can be created in the
applet's init() method with the command:
offScreenImage = createImage(getSize().width,getSize().height);
However, components other than applets do not have convenient init() methods
for initialization. They have constructors, but the size of a component is not known
when its constructor is executed, so the above command will not work in a constructor.
An alternative is to create the off-screen image on demand, when it is needed.
We can even allow for changes in size of a component if we make a new off-screen
image whenever the size changes. Here is some sample code that implements this idea.
A method named checkOffScreenImage() will create the off-screen image when
necessary. This method should always be called before using the off-screen image.
For example, it is called in the paintComponent() method before copying
the image to the screen.
/* Some variables used for double-buffering. */
Image OSI; // The off-screen image (created in paintComponent()).
int widthOfOSI, heightOfOSI; // Current width and height of OSI.
// These are checked against the size
// of the component, to detect any change
// in the component's size. If the size
// has changed, a new OSI is created.
// The picture in the off-screen image
// is lost when that happens.
void checkOffScreenImage() {
// This method will create the off-screen image if it has not
// already been created or if the component's size has changed.
// It should always be called before using the off-screen
// image in any way.
if (OSI == null || widthOfOSI != getSize().width
|| heightOfOSI != getSize().height) {
// OSI doesn't yet exist, or else it exists but has a
// different size from the component's current size.
// Create a new OSI, and fill it with the component's
// background color.
OSI = null; // If OSI already exists, this frees up the memory.
widthOfOSI = getSize().width;
heightOfOSI = getSize().height;
OSI = createImage(widthOfOSI, heightOfOSI);
Graphics OSGr = OSC.getGraphics();
OSGr.setColor(getBackground());
OSGr.fillRect(0, 0, widthOfOSC, heightOfOSC);
OSGr.dispose(); // Free operating system resources.
}
}
public void paintComponent(Graphics g) {
// Paint the component by copying the off-screen image onto
// the screen. First, call checkOffScreenImage() to make
// sure that the off-screen image is ready.
// (Note that since the image fills the entire component,
// it is not necessary to call super.paintComponent(g).)
checkOffScreenImage();
g.drawImage(OSI, 0, 0, null); // Copy OSI onto the screen.
// Note: At this point, we could draw hiliting or other extra
// stuff on top of the picture in the off-screen image.
}
Note that the contents of the off-screen image are lost if the size changes.
If this is a problem, you can consider copying the contents of the old off-screen
image to the new one before discarding the old image. You can do this with
drawImage(), and you can even scale the image to fit the new size if
you want. However, the results of scaling are not always attractive.
Here is an applet that demonstrates some of these ideas. Draw red lines
by clicking and dragging on the applet. Draw blue rectangles by
right-clicking and dragging. Hold down the shift key and click to clear
the applet. Notice that as you drag the mouse, the figure that you are
drawing stretches between the current mouse position and the point where
you started dragging. This effect is sometimes called a rubber
band cursor:
In this applet, a copy of the picture that you've drawn is kept in an
off-screen image. If you cover the applet and uncover it, the picture
is restored by copying this backup image onto the screen. When you drag
the mouse, the figure that you are drawing is not added to the off-screen
image. The paintComponent() method simply draws the new figure
on top of the backup image. The backup image is not changed, and as you
move the mouse around, you can see that it is still there, "underneath" the
figure you are sketching. The new figure is only added to the off-screen
image when you release the mouse button. To see how all this works in
detail, check the source code, RubberBand.java.
There is one other point of interest in the above applet. To draw a rectangle in Java,
you need to know the coordinates of the upper left corner, the width, and
the height. However, when a rectangle is drawn in this applet, the available data consists of two
corners of the rectangle: the starting position of the mouse and its current position.
From these two corners, the left edge, the top edge, the width, and the
height of the rectangle have to be computed. This can be
done as follows:
void drawRectUsingCorners(Graphics g, int x1, int y1, int x2, int y2) {
// Draw a rectangle with corners at (x1,y1) and (x2,y2).
int x,y; // Coordinates of the top-left corner.
int w,h; // Width and height of rectangle.
if (x1 < x2) { // x1 is the left edge
x = x1;
w = x2 - x1;
}
else { // x2 is the left edge
x = x2;
w = x1 - x2;
}
if (y1 < y2) { // y1 is the top edge
y = y1;
h = y2 - y1;
}
else { // y2 is the top edge
y = y2;
h = y1 - y2;
}
g.drawRect(x, y, w, h); // Draw the rect.
}
Rectangles, Clipping, and Repainting
The example we've just looked at has one glaring inefficiency:
Every time the user drags the mouse, the entire applet is repainted,
even though only a small part of the picture might need to be changed.
It's possible to improve on this by repainting only a part of the
applet. There is a second version of the repaint()
command that makes this possible. If comp is a variable
that refers to some component, then
comp.repaint( x, y, width, height );
tells the system that a rectangular area in the component needs
to be repainted. The first two parameters, x and y,
specify the upper left corner of the rectangle and the next two
parameters give the width and height of the rectangle.
In response to this, the system will call paintComponent()
as usual, but the graphics context will be set up for drawing
only in the specified region. This is done by setting the
clip region of the graphics context.
The clip region of a graphics context specifies the area
where drawing can occur. Any attempt to use the graphics
context to draw outside the clip region is ignored. (If part of
a shape lies outside the clip region, that part is "clipped off"
before the shape is drawn on the screen.) Only the pixels inside
the clip region need to have their color set, and this can be
much more efficient than setting the color of every pixel in
the component. When an off-screen image is copied onto the
component, only the part that lies within the clip region is
actually copied.
The techniques covered in this section can be used to improve the
simple painting program from Section 6.6.
The new version uses an off-screen image to save a copy of the
user's work, and it uses the version of repaint() discussed
above. As before, the user can draw a free-hand sketch. However,
in this version, the user can also choose to draw several shapes by selecting
from the pop-up menu in the upper right. Try it out! Check that when you cover up
the applet with another window, your drawing is still there when you uncover it.
The source code for this improved paint applet is in the file SimplePaint3.java.
It uses an off-screen image pretty much in the way described above. The paintComponent()
method copies the off-screen image to the screen, and as the user drags the mouse,
clipping is used to restrict the drawing to the region that actually needs to be changed.
In this applet, curves are handled differently from the other shapes.
Suppose that the user is sketching a curve and that the user moves the
mouse from the point (prevX,prevY) to the point (mouseX,mouseY).
The applet responds to this by drawing a line segment in the off-screen image
from (prevX,prevY) to (mouseX,mouseY). To make this change
appear on the screen, a rectangle that contains these two points must be
copied from the off-screen image onto the screen. This is accomplished in
the applet by calling repaint(x,y,w,h) with appropriate values for
the parameters.
When the user is sketching one of the other shapes in the applet, the
rubber band cursor technique is used. That is, while the user is dragging
the mouse, the shape is drawn by the paintComponent() method
on top of the picture from the off-screen image. Let's say, for example, that the user
is drawing a rectangle. Suppose that the user starts by pressing the mouse
at the point (startX,startY). Consider what happens later, when the
user drags the mouse from the point (prevX,prevY) to the point
(mouseX,mouseY). At the beginning of this motion, a rectangle
is shown on the screen with corners at (startX,startY) and
(prevX,prevY). In response to the motion, this rectangle must be
removed and a new one with corners at (startX,startY) and
(mouseX,mouseY) should appear.
This can be accomplished by changing the values of the variables that
tell paintComponent() where to draw the rectangle and by
calling repaint(x,y,w,h) twice: once to repaint the area occupied
by the old rectangle and once to repaint the area that will be occupied
by the new rectangle. (The system will actually combine the two operations
into a single call to paintComponent().)
This version of "SimplePaint" is not really all that simple. There are a lot
of details to take care of. I urge you to look at the source code
to see how it's done.
FontMetrics
In the rest of this section, we turn from Images to look briefly at
another aspect of Java graphics.
Often, when drawing a string, it's important to know how big the image of the
string will be. You need this information if you want to center a string on
an applet. Or if you want to know how much space to leave between two lines
of text, when you draw them one above the other. Or if the user is typing the
string and you want to position a cursor at the end of the string. In Java, questions
about the size of a string are answered by an object belonging to the
standard class java.awt.FontMetrics.
There are several lengths associated with any given font. Some of them
are shown in this illustration:
The red lines in the
illustration are the baselines
of the two lines of text. The suggested distance between two
baselines, for single-spaced text, is known as the lineheight
of the font. The ascent is the distance that tall characters
can rise above the baselines, and the descent is the distance
that tails like the one on the letter g can descend below the baseline. The ascent and
descent do not add up to the lineheight, because there should be some extra space between
the tops of characters in one line and the tails of characters on the line above.
The extra space is called leading. All these quantities
can be determined by calling instance methods in a FontMetrics object.
There are also methods for determining the width of a character and the width of
a string.
If F is a font and g is a graphics context, you can get a
FontMetrics object for the font F by calling g.getFontMetrics(F).
If fm is a variable that refers to the FontMetrics object, then
the ascent, descent, leading, and lineheight of the font can be obtained by calling
fm.getAscent(), fm.getDescent(), fm.getLeading(), and fm.getHeight().
If ch is a character, then fm.charWidth(ch) is the width of the character
when it is drawn in that font. If str is a string, then fm.stringWidth(str)
is the width of the string. For example, here is a paintComponent() method that shows the message
"Hello World" in the exact center of the component:
public void paintComponent(Graphics g) {
int width, height; // Width and height of the string.
int x, y; // Starting point of baseline of string.
Font F = g.getFont(); // What font will g draw in?
FontMetrics fm = g.getFontMetrics(F);
width = fm.stringWidth("Hello World");
height = fm.getAscent(); // Note: There are no tails on
// any of the chars in the string!
x = getSize().width / 2 - width / 2; // Go to center and back up
// half the width of the
// string.
y = getSize().height / 2 + height / 2; // Go to center, then move
// down half the height of
// the string.
g.drawString("Hello World", x, y);
}