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); }
[ Next Section | Previous Chapter | Chapter Index | Main Index ]