Section 6.3
Graphics and Painting
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.
An applet
is an example of a GUI component. The term component refers to
a visual element in a GUI, including buttons, menus, text-input boxes,
scroll bars, check boxes, and so on. In Java, GUI components are represented
by objects belonging to subclasses of the class java.awt.Component.
Most components in the Swing GUI -- although not top-level components like JApplet --
belong to subclasses of the class javax.swing.JComponent. Every
component is responsible for drawing itself. For example, if you want to use a
standard component, you only have to add it to your applet. You don't have to worry about
painting it on the screen. That will happen automatically.
Sometimes, however, you do want to draw on a component. You will have to do
this whenever you want to display something that is not included among the
standard, pre-defined component classes. When you want to do this, you have to
define your own component class and provide a method in that class for drawing
the component.
As we have seen in Section 6.1 and in
Section 3.7, when painting on a plain,
non-Swing Applet, the drawing is done in a paint() method.
To do custom drawing, you have to define a subclass of Applet
and include a paint() method to do the drawing.
However, when it comes to Swing and JApplets, things are a little more
complicated. You should not draw directly on JApplets or on other top-level
Swing components. Instead, you should make
a separate component to use as a drawing surface,
and you should add that component to the JApplet.
You will have to write a class to represent the drawing surface,
so programming a JApplet that does custom drawing will always
involve writing at least two classes: a class for the applet itself
and a class for the drawing surface. Typically, the class for the
drawing surface will be defined as a subclass of javax.swing.JPanel,
which by default is nothing but a blank area on the screen.
A JPanel, like any JComponent, draws its content in the method
public void paintComponent(Graphics g)
To create a drawing surface, you should define a subclass of JPanel
and provide a custom paintComponent() method.
Create an object belonging to your new class, and add it to your JApplet.
When the time comes for your component to be drawn on the screen, the system
will call its paintComponent() to do the drawing.
All this is not really as complicated as it might sound.
We will go over this in more detail when the time comes.
Note that the paintComponent() method has a parameter of
type Graphics. The Graphics object will be provided by the system
when it calls your method. You need this object to do the actual drawing.
To do any drawing at all in Java, you need a graphics
context. A graphics context is an object belonging to the
class, java.awt.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 a GUI component belonging to some subclass of JComponent.
The Graphics class is an abstract class, which means that it
is impossible to create a graphics context directly, with a constructor.
There are actually two ways to get a graphics context for drawing on a component:
First of all, of course, when the paintComponent() method of a component
is called by the system, the parameter to
that method is a graphics context for drawing on the component.
Second, each component has an instance method called getGraphics().
This method is a function that returns a graphics context
that can be used for drawing on the component outside its
paintComponent() method. The official line is that you
should not do this, and I will avoid it for the most part.
But I have found it convenient to use getGraphics()
in some cases, since it can mean better performance for certain
types of drawing. (Anyway, if the people who designed Java really didn't
want us to use it, they shouldn't have made the getGraphics() method
public!)
Most components do, in fact, do all drawing operations in their
paintComponent() methods. What happens if,
in the middle of some other method, you realize that the content
of the component needs to be changed? You should not
call paintComponent() directly to make the change; this method
is meant to be called only by the system. Instead, you have to
inform the system that the component needs to be redrawn, and let
the system do its job by calling paintComponent(). You do
this by calling the repaint() method. The method
public void repaint();
is defined in the Component class, and so can be used
with any component. You should call repaint() to inform
the system that the component needs to be redrawn. The repaint()
method returns immediately, without doing any painting itself. The system
will call the component's paintComponent() method later, as
soon as it gets a chance to do so, after processing other pending
events if there are any.
Note that the
system can also call paintComponent() for other reasons.
It is called when the component first appears on the screen.
It will also be called if the component is covered up by another
window and then uncovered. The system does not save a copy of
the component's contents when it is covered. When it is uncovered,
the component is responsible for redrawing itself. (As you will
see, some of our early examples will not be able to do this
correctly.)
This means that, to work properly, the paintComponent() method
must be smart enough to correctly redraw the component at any time.
To make this possible, a program should store data about the state
of the component in its instance variables. These variables should
contain all the information necessary to redraw the component completely.
The paintComponent() method should use the data in these variables
to decide what to draw. When the program wants to change the content
of the component, it should not simply draw the new content.
It should change the values of the relevant variables and
call repaint(). When the system calls paintComponent(),
this method will use the new values of the variables and will draw the component
with the desired modifications. This might seem a roundabout way of doing
things. Why not just draw the modifications directly? There are at
least two reasons. First of all, it really does turn out to be easier to get
things right if all drawing is done in one method. Second, even if you did
make modifications directly, you would still have to make the paintComponent()
method aware of them in some way so that it will be able to redraw the component
correctly when it is covered and uncovered.
You will see how all this works in practice as we work through
examples in the rest of this chapter. For now,
we will spend the rest of this section looking at how to
get some actual drawing done.
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,
java.awt.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 paintComponent() method that looks like:
public void paintComponent(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, java.awt.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 the number of parameters or the parameter types 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) before
doing the drawing. 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.
Generally, the component is filled with the background color before
anything else is drawn (although some components are "transparent,"
meaning that the background color is ignored).
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.
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 the original 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 java.awt.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 tail 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 parameters 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 a
JApplet. Since we will be drawing on the applet, we will
need to create a drawing surface. The drawing surface will be a
JComponent belonging to a subclass of the JPanel class. We will
define this class as a nested class inside the main applet class. Nested
classes were introduced in Section 5.6.
All the
drawing is done in the paintComponent() method of the drawing
surface class. I will
use nested classes consistently to define drawing surfaces, although
it is perfectly legal to use an independent class instead of a nested
class to define the drawing surface.
A nested class can be either static or non-static. In general, a non-static
class must be used if it needs access to instance variables or instance
methods that are defined in the main class. This will be the case in
most of my examples.
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 drawing surface's
paintComponent() 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 paintComponent() 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 would be to compute the contents of the picture
elsewhere, outside the paintComponent() method. Information about the
picture should be stored in instance variables, and the paintComponent()
method should use that information to draw the picture. If paintComponent()
is called twice, it should draw the same
picture twice, unless the data has changed in the meantime.
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 represent
different sizes and styles of text. These variables are initialized in
the applet's init() method and are used in the drawing surface's paintComponent()
method. I also use the init()
method to create the drawing surface, add it to the applet, and
set its background color to black.
The paintComponent() method for the drawing surface
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.
The drawing surface class, which is named Display, defines
the paintComponent() method that draws all the strings that
appear in the applet. The drawing surface is created in the applet's
init() method as an object of type Display.
This object is set to be the "content pane" of the applet.
A JApplet's content pane fills the entire applet, except for an
optional menu bar. An applet comes with a default content pane,
and you can add components to that content pane. However,
any JComponent can be a content pane, and in a case like this
where a single component fills the applet, it makes sense to
replace the content pane with the setContentPane() method.
The paintComponent() method in the Display class
begins with a call to super.paintComponent(g). The special
variable super was discussed in Section 5.5.
The command super.paintComponent(g) simply calls the paintComponent()
method that is defined in the superclass, JPanel. The effect
of this is to fill the component with its background color. Most
paintComponent() methods begin with a call to super.paintComponent(g),
but this is not necessary if the drawing commands in the method cover the
background of the component completely.
Here is the complete source code for the RandomStrings applet:
/* 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.
Note: This applet uses bad style, because every time
the paintComponent() method is called, new random values are
used. This means that a different picture will be drawn each
time. This is particularly bad if only part of the applet
needs to be redrawn, since then the applet will contain
cut-off pieces of messages.
When this file is compiled, it produces two classes,
RandomStrings.class and RandomStrings$Display.class. Both
classes are required to use the applet.
*/
import java.awt.*;
import javax.swing.*;
public class RandomStrings extends JApplet {
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.
Display drawingSurface; // This is the component on which the
// drawing will actually be done. It
// is defined by a nested class that
// can be found below.
public void init() {
// Called by the system to initialize the applet.
message = getParameter("message");
if (message == null)
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);
drawingSurface = new Display(); // Create the drawing surface.
drawingSurface.setBackground(Color.black);
setContentPane(drawingSurface); // Since drawingSurface will fill
// the entire applet, we simply
// replace the applet's content
// pane with drawingSurface.
} // end init()
class Display extends JPanel {
// This nested class defines a JPanel that is used
// for displaying the content of the applet. An
// object of this class is used as the content pane
// of the applet. Note that since this is a nested
// non-static class, it has access to the instance
// variables of the main class such as message and font1.
public void paintComponent(Graphics g) {
super.paintComponent(g); // Call the paintComponent method from
// the superclass, JPanel. This simply
// fills the entire component with the
// component's background color.
int width = getSize().width; // Get this component's width.
int height = getSize().height; // Get this component's 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 a bright, saturated color, with 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 paintComponent()
} // end nested class Display
} // end class RandomStrings