New to Java? We'll help you get started with our revised beginner's tutorial, or our free online textbook.
|
Get the latest Java books |
|
h t t p : / /w w w . j a v a c o f f e e b r e a k . c
o m /
|
Section 6.3
Graphics and the Paint Method
EVERYTHING YOU SEE ON A COMPUTER SCREEN has to be drawn there, even the text. The Java API includes a range of classes and methods that are devoted to drawing. In this section, I'll look at some of the most basic of these. Note that all the classes mentioned in this section are defined in the package java.awt.
For most of this chapter, we'll be drawing directly on applets, usually in the applets' paint() methods. In Section 6, we'll encounter anther class of GUI component, Canvas, that exists only to be drawn on. In many cases, a program does all its drawing in a Canvas which is just one of several components contained in the applet. The Canvas class, the Applet class, and in fact all of the classes that represent GUI components are subclasses of another class, named Component. The Component class represents the general idea of a Graphical User Interface component that can appear on the screen. Many of the methods used in applets, including paint() and repaint(), are actually inherited from Component. Most of what I will say about graphics applies to any Component, not just to Applets and Canvases. Whenever I talk about GUI components, I am referring to all objects that belong to subclasses of the Component class.
To do any drawing at all in Java, you need a graphics context. A graphics context is an object belonging to the class, Graphics. Instance methods are provided in this class for drawing shapes, text, and images. Any given Graphics object can draw to only one location. In this chapter, that location will always be one of Java's GUI components, such as an applet. The Graphics class is an abstract class, which means that it is impossible to create a graphics context directly, with a constructor. There are two ways to get a graphics context for drawing on a component: First of all, of course, when a component's paint() method is called, the parameter to that method is a graphics context for drawing on the component. Second, to make it possible to draw on a component from outside its paint() method, every component has an instance method called getGraphics(). This method is a function that returns a graphics context for the component. (The official line is that all drawing in a component should be done in that component's paint() method, but I have found that this is not always practical and does not always give acceptable performance. Anyway, if the people who designed Java really meant it, they wouldn't have made the getGraphics() method public.)
The instance method, getGraphics(), is defined in the Component class. It returns a graphics context that can be used for drawing to a particular component. That is, if comp is any component object and if g is a variable of type Graphics, then you can say
g = comp.getGraphics();
After this assignment, g can be used for drawing to the rectangular area of the screen that represents the component, comp. When you are writing your own applet or other component class, you need to call the (inherited) getGraphics() method in the same class. So, you would say simply "g = getGraphics()". This gives you a graphics context for drawing in the component you are writing.
If g is a graphics context that you've obtained with the getGraphics() methods, it is a good idea to call the method g.dispose() after you have finished using it. This method frees any system resources that are used by the graphics context. This is a good idea because on many systems, such resources are limited. However, you should never call dispose() for the graphics context provided in the paint() method. And you should never try to use a graphics context that has been disposed.
Paint, Repaint, and Update
Most components do, in fact, do all drawing operations in their paint() methods. The paint() method should be smart enough to correctly redraw the component at any time, using data stored in instance variables that record the state of the component. If in the middle of some other method you realize that the appearance of the component should change, then you should change the values of the instance variables and call the component's repaint() method. This tells the system that it should redraw the component as soon as it gets a chance (by calling the component's paint() method). This approach is satisfactory in most cases. The alternative approach -- drawing directly to the applet with a graphics context obtained through getGraphics() -- should be used only when repaint() doesn't give satisfactory results.
Now, as it happens, the system does not actually call the paint() method directly. There is another method called update() which is the one actually called directly by the system. The built-in update procedure first fills in the entire component with a background color. Then it calls the paint() method. The paint() method draws on a rectangular area that has already been filled with the background color. Usually, this is the right thing to do. However, if the paint method itself fills in the entire rectangle, so that none of the background color is visible, then filling in the background was a wasted step. In that case, you can override update() to read simply:
public void update(Graphics g) { paint(g); // Don't fill with background color; just call paint. }It is possible to set the background color of a component, using the component's setBackground() instance method that I will discuss below.
Coordinates
The screen of a computer is a grid of little squares called pixels. The color of each pixel can be set individually, and drawing on the screen just means setting the colors of individual pixels.
A graphics context draws in a rectangle made up of pixels. A position in the rectangle is specified by a pair of integer coordinates, (x,y). The upper left corner has coordinates (0,0). The x coordinate increases from left to right, and the y coordinate increases from top to bottom. The illustration on the right shows a 12-by-8 pixel component (with very large pixels). A small line, rectangle, and oval are shown as they would be drawn by coloring individual pixels. (Note that, properly speaking, the coordinates don't belong to the pixels but to the grid lines between them.)
For any component, you can find out the size of the rectangle that it occupies by calling the instance method getSize(). This method returns an object that belongs to the class, Dimension. A Dimension object has two integer instance variables, width and height. The width of the component is getSize().width pixels, and its height is getSize().height pixels.
When you are writing an applet, you don't necessarily know the applet's size. The size of an applet is usually specified in an <APPLET> tag in the source code of a Web page, and it's easy for the Web-page author to change the specified size. In some cases, when the applet is displayed in some other kind of window instead of on a Web page, the applet can even be resized while it is running. So, it's not good form to depend on the size of the applet being set to some particular value. For other components, you have even less chance of knowing the component's size in advance. This means that it's good form to check the size of a component before doing any drawing on that component. For example, you can use a paint() method that looks like:
public void paint(Graphics g) { int width = getSize().width; // Find out the width of component. int height = getSize().height; // Find out its height. . . . // Draw the contents of the component. }Of course, your drawing commands will have to take the size into account. That is, they will have to use (x,y) coordinates that are calculated based on the actual height and width of the applet.
Colors
Java is designed to work with the RGB color system. An RGB color is specified by three numbers that give the level of red, green, and blue, respectively, in the color. A color in Java is an object of the class, Color. You can construct a new color by specifying its red, blue, and green components. For example,
myColor = new Color(r,g,b);
There are two constructors that you can call in this way. In the one that I almost always use, r, g, and b are integers in the range 0 to 255. In the other, they are numbers of type float in the range 0.0F to 1.0F. (You might recall that a literal of type float is written with an "F" to distinguish it from a double number.) Often, you can avoid constructing new colors altogether, since the Color class defines several named constants representing common colors: Color.white, Color.black, Color.red, Color.green, Color.blue, Color.cyan, Color.magenta, Color.yellow, Color.pink, Color.orange, Color.lightGray, Color.gray, and Color.darkGray.
An alternative to RGB is the HSB color system. In the HSB system, a color is specified by three numbers called the hue, the saturation, and the brightness. The hue is the basic color, ranging from red through orange through all the other colors of the rainbow. The brightness is pretty much what it sounds like. A fully saturated color is a pure color tone. Decreasing the saturation is like mixing white or gray paint into the pure color. In Java, the hue, saturation and brightness are always specified by values of type float in the range from 0.0F to 1.0F. The Color class has a static member function named getHSBColor for creating HSB colors. To create the color with HSB values given by h, s, and b, you can say:
myColor = Color.getHSBColor(h,s,b);
For example, you could make a random color that is as bright and as saturated as possible with
myColor = Color.getHSBColor( (float)Math.random(), 1.0F, 1.0F );
The type cast is necessary because the value returned by Math.random() is of type double, and Color.getHSBColor() requires values of type float. (By the way, you might ask why RGB colors are created using a constructor while HSB colors are created using a static member function. The problem is that we would need two different constructors, both of them with three parameters of type float. Unfortunately, this is impossible. You can only have two constructors if their numbers or type of parameters differ.)
The RGB system and the HSB system are just different ways of describing the same set of colors. It is possible to translate between one system and the other. The best way to understand the color systems is to experiment with them. In the following applet, you can use the scroll bars to control the RGB and HSB values of a color. A sample of the color is shown on the right side of the applet. Computer monitors differ as to the number of different colors they can display, so you might not get to see the full range of colors in this applet.
One of the instance variables in a Graphics object is the current drawing color, which is used for all drawing of shapes and text. If g is a graphics context, you can change the current drawing color for g using the method g.setColor(c), where c is a Color. For example, if you want to draw in green, you would just say g.setColor(Color.green). The graphics context continues to use the color until you explicitly change it with another setColor() command. If you want to know what the current drawing color is, you can call the function g.getColor(), which returns an object of type Color. This can be useful if you want to change to another drawing color temporarily and then restore the previous drawing color.
Every component has an associated foreground color and background color. When the component is filled by the update() method, it is filled with the background color. When a new graphics context is created for a component, the current drawing color is set to the foreground color. Note that the foreground color and background color are properties of the component, not of a graphics context.
The foreground and background colors can be set by instance methods setForeground(c) and setBackground(c), which are defined in the Component class and therefore are available for use with any component. These methods are commonly used to set the foreground and background colors of an applet in the applet's init() method.
Fonts
A font represents a particular size and style of text. The same character will appear different in different fonts. In Java, a font is characterized by a font name, a style, and a size. The available font names are system dependent, but you can always use the following four strings as font names: "Serif", "SansSerif", "Monospaced", and "Dialog". In Java 1.0, the font names were "TimesRoman", "Helvetica", and "Courier". You can still use the older names if you want. (A "serif" is a little decoration on a character, such as a short horizontal line at the bottom of the letter i. "SansSerif" means "without serifs." "Monospaced" means that all the characters in the font have the same width. The "Dialog" font is the one that is typically used in dialog boxes.)
The style of a font is specified using named constants that are defined in the Font class. You can specify the style as one of the four values:
- Font.PLAIN,
- Font.ITALIC,
- Font.BOLD, or
- Font.BOLD + Font.ITALIC.
The size of a font is an integer. Size typically ranges from about 10 to 36, although larger sizes can also be used. The size of a font is usually about equal to the height of the largest characters in the font, in pixels, but this is not a definite rule. The size of the default font is 12.
Java uses the class named Font for representing fonts. You can construct a new font by specifying its font name, style, and size in a constructor:
Font plainFont = new Font("Serif", Font.PLAIN, 12); Font bigBoldFont = new Font("SansSerif", Font.BOLD, 24);Every graphics context has a current font, which is used for drawing text. You can change the current font with the setFont() method. For example, if g is a graphics context and bigBoldFont is a font, then the command g.setFont(bigBoldFont) will set the current font of g to bigBoldFont. You can find out the current font of g by calling the method g.getFont(), which returns an object of type Font.
Every component has an associated font. It can be set with the instance method setFont(font), which is defined in the Component class. When a graphics context is created for drawing on a component, the graphic context's current font is set equal to the font of the component.
Shapes
The Graphics class includes a large number of instance methods for drawing various shapes, such as lines, rectangles, and ovals. The shapes are specified using the (x,y) coordinate system described above. They are drawn in the current drawing color of the graphics context. The current drawing color is set to the foreground color of the component when the graphics context is created, but it can be changed at any time using the setColor() method.
Here is a list of some of the most important drawing methods. With all these commands, any drawing that is done outside the boundaries of the component is ignored. Note that all these methods are in the Graphics class, so they all must be called through an object of type Graphics.
drawString(String str, int x, int y) -- Draws the text given by the string str. The string is drawn using the current color and font of the graphics context. x specifies the position of the left end of the string. y is the y-coordinate of the baseline of the string. The baseline is a horizontal line on which the characters rest. Some parts of the characters, such as the tails on a y or g, extend below the baseline.
drawLine(int x1, int y1, int x2, int y2) -- Draws a line from the point (x1,y1) to the point (x2,y2). The line is drawn as if with a pen that hangs one pixel to the right and one pixel down from the (x,y) point where the pen is located. For example, if g refers to an object of type Graphics, then the command g.drawLine(x,y,x,y), which corresponds to putting the pen down at a point, draws the single pixel located at the point (x,y).
drawRect(int x, int y, int width, int height) -- Draws the outline of a rectangle. The upper left corner is at (x,y), and the width and height of the rectangle are as specified. If width equals height, then the rectangle is a square. If the width or the height is negative, then nothing is drawn. The rectangle is drawn with the same pen that is used for drawLine(). This means that the actual width of the rectangle as drawn is width+1, and similarly for the height. There is an extra pixel along the right edge and the bottom edge. For example, if you want to draw a rectangle around the edges of the component, you can say "g.drawRect(0, 0, getSize().width-1, getSize().height-1);", where g is a graphics context for the component.
drawOval(int x, int y, int width, int height) -- Draws the outline of an oval. The oval is one that just fits inside the rectangle specified by x, y, width, and height. If width equals height, the oval is a circle.
drawRoundRect(int x, int y, int width, int height, int xdiam, int ydiam) -- Draws the outline of a rectangle with rounded corners. The basic rectangle is specified by x, y, width, and height, but the corners are rounded. The degree of rounding is given by xdiam and ydiam. The corners are arcs of an ellipse with horizontal diameter xdiam and vertical diameter ydiam. A typical value for xdiam and ydiam is 16. But the value used should really depend on how big the rectangle is.
draw3DRect(int x, int y, int width, int height, boolean raised) -- Draws the outline of a rectangle that is supposed to have a three-dimensional effect, as if it is raised from the screen or pushed into the screen. The basic rectangle is specified by x, y, width, and height. The raised parameter tells whether the rectangle seems to be raised from the screen or pushed into it. The 3D effect is achieved by using brighter and darker versions of the drawing color for different edges of the rectangle. The documentation recommends setting the drawing color equal to the background color before using this method. The effect won't work well for some colors.
drawArc(int x, int y, int width, int height, int startAngle, int arcAngle) -- Draws part of the oval that just fits inside the rectangle specified by x, y, width, and height. The part drawn is an arc that extends arcAngle degrees from a starting angle at startAngle degrees. Angles are measured with 0 degrees at the 3 o'clock position (the positive direction of the horizontal axis). Positive angles are measured counterclockwise from zero, and negative angles are measured clockwise. To get an arc of a circle, make sure that width is equal to height.
fillRect(int x, int y, int width, int height) -- Draws a filled-in rectangle. This fills in the interior of the rectangle that would be drawn by drawRect(x,y,width,height). The extra pixel along the bottom and right edges is not included. The width and height parameter give the exact width and height of the rectangle. For example, if you wanted to fill in the entire component, you could say "g.fillRect(0, 0, getSize().width, getSize().height);"
fillOval(int x, int y, int width, int height) -- Draws a filled-in oval.
fillRoundRect(int x, int y, int width, int height, int xdiam, int ydiam) -- Draws a filled-in rounded rectangle.
fill3DRect(int x, int y, int width, int height, boolean raised) -- Draws a filled-in three-dimensional rectangle.
fillArc(int x, int y, int width, int height, int startAngle, int arcAngle) -- Draw a filled-in arc. This looks like a wedge of pie, whose crust is the arc that would be drawn by the drawArc method.
Let's use some of the material covered in this section to write an applet. The applet will draw multiple copies of a message on a black background. Each copy of the message is in a random color. Five different fonts are used, with different sizes and styles. The displayed message is the string "Java!", but a different message can be specified in an applet param. (Applet params were discussed at the end of the previous section.) The applet works OK no matter what size is specified for the applet in the <applet> tag. Here's the applet:
The applet does have a problem. When the paint() method is called, it chooses random colors, fonts, and locations for the messages. The information about which colors, fonts, and locations are used is not stored anywhere. The next time paint() is called, it will make different random choices and will draw a different picture. For this particular applet, the problem only really appears when the applet is partially covered and then uncovered. Only the part that was covered will be redrawn, and in the part that's not redrawn, the old picture will remain. Try it. You'll see partial messages, cut off by the dividing line between the new picture and the old. (Actually, in some browsers, the entire applet might be repainted, even if only part of it was covered.) A better approach is to compute the contents of the picture elsewhere, outside the paint() method. Information about the picture should be stored in instance variables, and the paint() method should use that information to draw the picture. If paint() is called twice before the data is recomputed, it should draw the same picture twice. Unfortunately, to store the data for the picture in this applet, we would need to use either arrays, which will not be covered until Chapter 8, or off-screen images, which will not be covered until Section 7.1. Other applets in this chapter will suffer from the same problem.
The source for the applet is shown below. I use an instance variable called message to hold the message that the applet will display. There are five instance variables of type Font that hold represents different sizes and styles of text. These variables are initialized in the applet's init() method. I also use the init() method to set the background color of the applet to black.
The paint method simply draws 25 copies of the message. For each copy, it chooses one of the five fonts at random, and it calls g.setFont() to select that font for drawing the text. It creates a random HSB color and uses g.setColor() to select that color for drawing. It then chooses random (x,y) coordinates for the location of the message. The x coordinate gives the horizontal position of the left end of the string. The formula used for the x coordinate, "-50 + (int)(Math.random()*(width+40)" gives a random integer in the range from -50 to width-10. This makes it possible for the string to extend beyond the left edge or the right edge of the applet. Similarly, the formula for y allows the string to extend beyond the top and bottom of the applet.
Here is the complete source code:
/* This applet displays 25 copies of a message. The color and position of each message is selected at random. The font of each message is randomly chosen from among five possible fonts. The messages are displayed on a black background. */ import java.awt.*; import java.applet.*; public class RandomStrings extends Applet { String message; // The message to be displayed. This can be set // in an applet param with name "message". If no // value is provided in the applet tag, then // the string "Java!" is used as the default. Font font1, font2, font3, font4, font5; // The five fonts. public void init() { message = getParameter("message"); // Look for message in an // applet param named // "message". if (message == null) // If no message is found, use "Java!". message = "Java!"; font1 = new Font("Serif", Font.BOLD, 14); font2 = new Font("SansSerif", Font.BOLD + Font.ITALIC, 24); font3 = new Font("Monospaced", Font.PLAIN, 20); font4 = new Font("Dialog", Font.PLAIN, 30); font5 = new Font("Serif", Font.ITALIC, 36); setBackground(Color.black); } // end init() public void paint(Graphics g) { int width = getSize().width; // Get applet's width and height. int height = getSize().height; for (int i = 0; i < 25; i++) { // Draw one string. First, set the font to be one // of the five available fonts, at random. int fontNum = (int)(5*Math.random()) + 1; switch (fontNum) { case 1: g.setFont(font1); break; case 2: g.setFont(font2); break; case 3: g.setFont(font3); break; case 4: g.setFont(font4); break; case 5: g.setFont(font5); break; } // end switch // Set the color to be a bright, saturated color, // with a random hue. float hue = (float)Math.random(); g.setColor( Color.getHSBColor(hue, 1.0F, 1.0F) ); // Select the position of the string, at random. int x,y; x = -50 + (int)(Math.random()*(width+40)); y = (int)(Math.random()*(height+20)); // Draw the message. g.drawString(message,x,y); } // end for } // end paint() } // end class RandomStrings
[ Next Section | Previous Section | Chapter Index | Main Index ]