Introduction To Programming Using Java 09 PDF
Introduction To Programming Using Java 09 PDF
It’s possible to program a wide variety of GUI applications using only the techniques cov-
ered in Chapter 6. In many cases, the basic events, components, layouts, and graphics routines
covered in that chapter suffice. But the Swing graphical user interface library is far richer than
what we have seen so far, and it can be used to build highly sophisticated applications. This
chapter is a further introduction to Swing and other aspects of GUI programming. Although
the title of the chapter is “Advanced GUI Programming,” it is still just an introduction. Full
coverage of this topic would require at least another complete book.
611
612 CHAPTER 12. ADVANCED GUI PROGRAMMING
drawing surface. It is possible to draw to an offscreen image using the same Graphics class that
is used for drawing on the screen.
An Image of either type can be copied onto the screen (or onto an off-screen canvas)
using methods that are defined in the Graphics class. This is most commonly done in the
paintComponent() method of a JComponent. Suppose that g is the Graphics object that is
provided as a parameter to the paintComponent() method, and that img is of type Image.
Then the statement
g.drawImage(img, x, y, this);
will draw the image img in a rectangular area in the component. The integer-valued 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 Subsection 5.6.1 that refers to the JComponent itself. This parameter is
there for technical reasons having to do with the funny way Java treats image files. For most
applications, you don’t need to understand this, but here is how it works: g.drawImage() does
not actually draw the image in all cases. It is possible that the complete image is not available
when this method is called; this can happen, for example, if the image has to be read from
a file. In that case, g.drawImage() merely initiates the drawing of the image and returns
immediately. Pieces of the image are drawn later, asynchronously, as they become available.
The question is, how do they get drawn? That’s where the fourth parameter to the drawImage
method comes in. The fourth parameter is something called an ImageObserver. When a piece
of the image becomes available to be drawn, the system will inform the ImageObserver, and
that piece of the image will appear on the screen. Any JComponent object can act as an
ImageObserver. The drawImage method returns a boolean value to indicate whether the image
has actually been drawn or not when the method returns. When drawing an image that you
have created in the computer’s memory, or one that you are sure has already been completely
loaded, you can set the ImageObserver parameter to null.
There are a few useful variations of the drawImage() method. 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, imageObserver);
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, imageObserver);
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:
12.1. IMAGES AND RESOURCES 613
(This image is from the Gnome desktop project, http://www.gnome.org, and is shown here
much smaller than its actual size.) Now, only one Image object is needed. Drawing one card
means drawing a rectangular region from the image. This technique is used in a variation
of the sample program HighLowGUI.java from Subsection 6.7.6. In the original version, the
cards are represented by textual descriptions such as “King of Hearts.” In the new version,
HighLowWithImages.java, the cards are shown as images. An applet version of the program
can be found in the on-line version of this section.
In the program, the cards are drawn using the following method. The instance variable
cardImages is a variable of type Image that represents the image that is shown above, containing
52 cards, plus two Jokers and a face-down card. Each card is 79 by 123 pixels. These numbers
are used, together with the suit and value of the card, to compute the corners of the source
rectangle for the drawImage() command:
/**
* Draws a card in a 79x123 pixel rectangle with its
* upper left corner at a specified point (x,y). Drawing the card
* requires the image file "cards.png".
* @param g The graphics context used for drawing the card.
* @param card The card that is to be drawn. If the value is null, then a
* face-down card is drawn.
* @param x the x-coord of the upper left corner of the card
* @param y the y-coord of the upper left corner of the card
*/
public void drawCard(Graphics g, Card card, int x, int y) {
int cx; // x-coord of upper left corner of the card inside cardsImage
int cy; // y-coord of upper left corner of the card inside cardsImage
if (card == null) {
cy = 4*123; // coords for a face-down card.
cx = 2*79;
}
else {
614 CHAPTER 12. ADVANCED GUI PROGRAMMING
cx = (card.getValue()-1)*79;
switch (card.getSuit()) {
case Card.CLUBS:
cy = 0;
break;
case Card.DIAMONDS:
cy = 123;
break;
case Card.HEARTS:
cy = 2*123;
break;
default: // spades
cy = 3*123;
break;
}
}
g.drawImage(cardImages,x,y,x+79,y+123,cx,cy,cx+79,cy+123,this);
}
I will tell you later in this section how the image file, cards.png, can be loaded into the program.
∗ ∗ ∗
In addition to images loaded from files, it is possible to create images by drawing to an
off-screen canvas. An off-screen canvas can be represented by an object belonging to the class
BufferedImage, which is defined in the package java.awt.image. BufferedImage is a subclass of
Image, so that once you have a BufferedImage, you can copy it into a graphics context g using one
of the g.drawImage() methods, just as you would do with any other image. A BufferedImage
can be created using the constructor
public BufferedImage(int width, int height, int imageType)
where width and height specify the width and height of the image in pixels, and imageType
can be one of several constants that are defined in the BufferedImage. The image type
specifies how the color of each pixel is represented. The most likely value for imageType
is BufferedImage.TYPE INT RGB, which specifies that the color of each pixel is a usual
RGB color, with red, green and blue components in the range 0 to 255. The image type
BufferedImage.TYPE INT ARGB represents an RGB image with “transparency”; see the next
section for more information on this. The image type BufferedImage.TYPE BYTE GRAY can be
used to create a grayscale image in which the only possible colors are shades of gray.
To draw to a BufferedImage, you need a graphics context that is set up to do its drawing on
the image. If OSC is of type BufferedImage, then the method
OSC.getGraphics()
returns an object of type Graphics that can be used for drawing on the image.
There are several reasons why a programmer might want to draw to an off-screen canvas.
One is to simply keep a copy of an image that is shown on the screen. Remember that a picture
that is drawn on a component can be lost, for example when the component is covered by
another window. This means that you have to be able to redraw the picture on demand, and
that in turn means keeping enough information around to enable you to redraw the picture.
One way to do this is to keep a copy of the picture in an off-screen canvas. Whenever the on-
screen picture needs to be redrawn, you just have to copy the contents of the off-screen canvas
onto the screen. Essentially, the off-screen canvas allows you to save a copy of the color of every
12.1. IMAGES AND RESOURCES 615
the constructor, getWidth() and getHeight() will return the value zero and we won’t get an
off-screen image of the correct size. The approach that I take in PaintWithOffScreenCanvas
is to call createOSC() in the paintComponent() method, the first time the paintComponent()
method is called. At that time, the size of the panel has definitely been set, but the user has
not yet had a chance to draw anything. With this in mind you are ready to understand the
paintComponent() method:
public void paintComponent(Graphics g) {
/* First create the off-screen canvas, if it does not already exist. */
if (OSC == null)
createOSC();
/* Copy the off-screen canvas to the panel. Since we know that the
image is already completely available, the fourth "ImageObserver"
parameter to g.drawImage() can be null. Since the canvas completely
fills the panel, there is no need to call super.paintComponent(g). */
g.drawImage(OSC,0,0,null);
/* If the user is currently dragging the mouse to draw a line, oval,
or rectangle, draw the shape on top of the image from the off-screen
canvas, using the current drawing color. (This is not done if the
user is drawing a curve or using the smudge tool or the erase tool.) */
if (dragging && SHAPE TOOLS.contains(currentTool)) {
g.setColor(currentColor);
putCurrentShape(g);
}
}
Here, dragging is a boolean instance variable that is set to true while the user is dragging the
mouse, and currentTool tells which tool is currently in use. The possible tools are defined by
an enum named Tool, and SHAPE TOOLS is a variable of type EnumSet<Tool> that contains the
line, oval, rectangle, filled oval, and filled rectangle tools. (See Subsection 10.2.4.)
You might notice that there is a problem if the size of the panel is ever changed, since the
size of the off-screen canvas will not be changed to match. The PaintWithOffScreenCanvas
program does not allow the user to resize the program’s window, so this is not an issue in
that program. If we want to allow resizing, however, a new off-screen canvas must be created
whenever the size of the panel changes. One simple way to do this is to check the size of the
canvas in the paintComponent() method and to create a new canvas if the size of the canvas
does not match the size of the panel:
if (OSC == null || getWidth() != OSC.getWidth() || getHeight() != OSC.getHeight())
createOSC();
Of course, this will discard the picture that was contained in the old canvas unless some ar-
rangement is made to copy the picture from the old canvas to the new one before the old canvas
is discarded.
The other point in the program where the off-screen canvas is used is during a mouse-drag
operation, which is handled in the mousePressed(), mouseDragged(), and mouseReleased()
methods. The strategy that is implemented was discussed above. Shapes are drawn to the
off-screen canvas only at the end of the drag operation, in the mouseReleased() method.
12.1. IMAGES AND RESOURCES 617
However, as the user drags the mouse, the part of the image where the shape appears is
redrawn each time the mouse is moved; the shape that appears on the screen is drawn on top of
the canvas by the paintComponent() method. For the other tools, changes are made directly
to the canvas, and the region that was changed is repainted so that the change will appear on
the screen. (By the way, the program uses a version of the repaint() method that repaints
just a part of a component. The command repaint(x,y,width,height) tells the system to
repaint the rectangle with upper left corner (x,y) and with the specified width and height.
This can be substantially faster than repainting the entire component.) See the source code,
PaintWithOffScreenCanvas.java, if you want to see how it’s all done.
∗ ∗ ∗
One traditional use of off-screen canvasses is for double buffering . In double-buffering,
the off-screen image is an exact copy of the image that appears on screen. In double buffering,
whenever 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, the long drawing process takes
place off-screen and the completed image is then copied very quickly onto the screen. The
user doesn’t see all the steps involved in redrawing. This technique can be used to implement
smooth, flicker-free animation.
The term “double buffering” comes from the term “frame buffer,” which refers to the re-
gion 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.
In Java’s older AWT graphical API, it was up to the programmer to do double buffering by
hand. In the Swing graphical API, double buffering is applied automatically by the system, and
the programmer doesn’t have to worry about it. (It is possible to turn this automatic double
buffering off in Swing, but there is seldom a good reason to do so.)
One final historical note about off-screen canvasses: There is an alternative way to create
them. The Component class defines the following instance method, which can be used in any
GUI component object:
public Image createImage(int width, int height)
This method creates an Image with a specified width and height. You can use this image
as an off-screen canvas in the same way that you would a BufferedImage. In fact, you can
expect that in a modern version of Java, the image that is returned by this method is in fact
a BufferedImage. The createImage() method was part of Java from the beginning, before the
BufferedImage class was introduced.
• image.getRGB(x,y) — returns an int that encodes the color of the pixel at coordinates
(x,y) in the image. The values of the integers x and y must lie within the image. That is,
618 CHAPTER 12. ADVANCED GUI PROGRAMMING
it must be true that 0 <= x < image.getWidth() and 0 <= y < image.getHeight();
if not, then an exception is thrown.
• image.setRGB(x,y,rgb) — sets the color of the pixel at coordinates (x,y) to the color
encoded by rgb. Again, x and y must be in the valid range. The third parameter, rgb, is
an integer that encodes the color.
These methods use integer codes for colors. If c is of type Color, the integer code for the color
can be obtained by calling c.getRGB(). Conversely, if rgb is an integer that encodes a color,
the corresponding Color object can be obtained with the constructor call new Color(rgb).
This means that you can use
to get the color of a pixel as a value of type Color. And if c is of type Color, you can set a pixel
to that color with
image.setRGB( x, y, c.getRGB() );
The red, green, and blue components of a color are represented as 8-bit integers, in the
range 0 to 255. When a color is encoded as a single int, the blue component is contained in
the eight low-order bits of the int, the green component in the next lowest eight bits, and the
red component in the next eight bits. (The eight high order bits store the “alpha component”
of the color, which we’ll encounter in the next section.) It is easy to translate between the two
representations using the shift operators << and >> and the bitwise logical operators &
and |. (I have not covered these operators previously in this book. Briefly: If A and B are
integers, then A << B is the integer obtained by shifting each bit of A B bit positions to the left;
A >> B is the integer obtained by shifting each bit of A B bit positions to the right; A & B is the
integer obtained by applying the logical and operation to each pair of bits in A and B; and A | B
is obtained similarly, using the logical or operation. For example, using 8-bit binary numbers,
we have: 01100101 & 10100001 is 00100001, while 01100101 | 10100001 is 11100101.) You
don’t necessarily need to understand these operators. Here are incantations that you can use
to work with color codes:
∗ ∗ ∗
An example of using pixel colors in a BufferedImage is provided by the smudge tool in the
sample program PaintWithOffScreenCanvas.java. The purpose of this tool is to smear the
colors of an image, as if it were drawn in wet paint. For example, if you rub the middle of a
black rectangle with the smudge tool, you’ll get something like this:
12.1. IMAGES AND RESOURCES 619
This is an effect that can only be achieved by manipulating the colors of individual pixels! Here’s
how it works: when the user presses the mouse using the smudge tool, the color components
of a 7-by-7 block of pixels are copied from the off-screen canvas into arrays named smudgeRed,
smudgeGreen and smudgeBlue. This is done in the mousePressed() routine with the following
code:
int w = OSC.getWidth();
int h = OSC.getHeight();
int x = evt.getX();
int y = evt.getY();
for (int i = 0; i < 7; i++)
for (int j = 0; j < 7; j++) {
int r = y + j - 3;
int c = x + i - 3;
if (r < 0 || r >= h || c < 0 || c >= w) {
// A -1 in the smudgeRed array indicates that the
// corresponding pixel was outside the canvas.
smudgeRed[i][j] = -1;
}
else {
int color = OSC.getRGB(c,r);
smudgeRed[i][j] = (color >> 16) & 0xFF;
smudgeGreen[i][j] = (color >> 8) & 0xFF;
smudgeBlue[i][j] = color & 0xFF;
}
}
The arrays are of type double[ ][ ] because I am going to do some computations with them that
require real numbers. As the user moves the mouse, the colors in the array are blended with
the colors in the image, just as if you were mixing wet paint by smudging it with your finger.
That is, the colors at the new mouse position in the image are replaced with a weighted average
of the current colors in the image and the colors in the arrays. This has the effect of moving
some of the color from the previous mouse position to the new mouse position. At the same
time, the colors in the arrays are replaced by a weighted average of the colors in the arrays and
the colors from the image. This has the effect of moving some color from the image into the
arrays. This is done using the following code for each pixel position, (c,r), in a 7-by-7 block
around the new mouse location:
int curCol = OSC.getRGB(c,r);
int curRed = (curCol >> 16) & 0xFF;
int curGreen = (curCol >> 8) & 0xFF;
int curBlue = curCol & 0xFF;
int newRed = (int)(curRed*0.7 + smudgeRed[i][j]*0.3);
int newGreen = (int)(curGreen*0.7 + smudgeGreen[i][j]*0.3);
int newBlue = (int)(curBlue*0.7 + smudgeBlue[i][j]*0.3);
int newCol = newRed << 16 | newGreen << 8 | newBlue;
OSC.setRGB(c,r,newCol);
620 CHAPTER 12. ADVANCED GUI PROGRAMMING
12.1.3 Resources
Throughout this textbook, up until now, we have been thinking of a program as made up
entirely of Java code. However, programs often use other types of data, including images,
sounds, and text, as part of their basic structure. These data are referred to as resources. An
example is the image file, cards.png, that was used in the HighLowWithImages.java program
earlier in this section. This file is part of the program. The program needs it in order to run.
The user of the program doesn’t need to know that this file exists or where it is located; as far
as the user is concerned, it is just part of the program. The program of course, does need some
way of locating the resource file and loading its data.
Resources are ordinarily stored in files that are in the same locations as the compiled class
files for the program. Class files are located and loaded by something called a class loader ,
which is represented in Java by an object of type ClassLoader. A class loader has a list of
locations where it will look for class files. This list is called the class path . It includes the
location where Java’s standard classes are stored. It generally includes the current directory.
If the program is stored in a jar file, the jar file is included on the class path. In addition to
class files, a ClassLoader is capable of locating resource files that are located on the class path
or in subdirectories of locations that are on the class path.
The first step in using a resource is to obtain a ClassLoader and to use it to locate the
resource file. In the HighLowWithImages program, this is done with:
ClassLoader cl = HighLowWithImages.class.getClassLoader();
URL imageURL = cl.getResource("cards.png");
The idea of the first line is that in order to get a class loader, you have to ask a class that
was loaded by the class loader. Here, HighLowWithImages.class is a name for the object that
represents the actual class HighLowWithImages. In other programs, you would just substitute
for “HighLowWithImages” the name of the class that contains the call to getClassLoader().
The second line uses the class loader to locate the resource file named cards.png. The return
value of cl.getResource() is of type java.net.URL, and it represents the location of the
resource rather than the resource itself. If the resource file cannot be found, then the return
value is null. The class URL was discussed in Subsection 11.4.1.
Often, resources are stored not directly on the class path but in a subdirectory. In that
case, the parameter to getResource() must be a path name that includes the directory path
to the resource. For example, suppose that the image file “cards.png” were stored in a directory
named images inside a directory named resources, where resources is directly on the class
path. Then the path to the file is “resources/images/cards.png” and the command for locating
the resource would be
URL imageURL = cl.getResource("resources/images/cards.png");
Once you have a URL that represents the location of a resource file, you could use a URL-
Connection, as discussed in Subsection 11.4.1, to read the contents of that file. However, Java
provides more convenient methods for loading several types of resources. For loading image
resources, a convenient method is available in the class java.awt.Toolkit. It can be used as
in the following line from HighLowWithImages, where cardImages is an instance variable of
type Image and imageURL is the URL that represents the location of the image file:
12.1. IMAGES AND RESOURCES 621
cardImages = Toolkit.getDefaultToolkit().createImage(imageURL);
This still does not load the image completely—that will only be done later, for example when
cardImages is used in a drawImage command.
∗ ∗ ∗
The Applet and JApplet classes have an instance method that can be used to load an image
from a given URL:
public Image getImage(URL imageURL)
When you are writing an applet, this method can be used as an alternative to the
createImage() method in class Toolkit.
More interesting is the fact that Applet and JApplet contain a static method that can be
used to load sound resources:
public static AudioClip newAudioClip(URL soundURL)
Since this is a static method, it can be used in any program, simply by calling it as
Applet.newAudioClip(soundURL) or JApplet.newAudioClip(soundURL). (This seems to be
the only easy way to use sounds in a Java program; it’s not clear why this capability is only in
the applet classes.) The return value is of type java.applet.AudioClip. Once you have an
AudioClip, you can call its play() method to play the audio clip from the beginning.
Here is a method that puts all this together to load and play the sound from an audio
resource file:
private void playAudioResource(String audioResourceName) {
ClassLoader cl = SoundAndCursorDemo.class.getClassLoader();
URL resourceURL = cl.getResource(audioResourceName);
if (resourceURL != null) {
AudioClip sound = JApplet.newAudioClip(resourceURL);
sound.play();
}
}
This method is from a sample program SoundAndCursorDemo that will be discussed in the next
subsection. Of course, if you plan to reuse the sound often, it would be better to load the sound
once into an instance variable of type AudioClip, which could then be used to play the sound
any number of times, without the need to reload it each time.
The AudioClip class supports audio files in the common WAV, AIFF, and AU formats.
subsection. It could even be a BufferedImage that you create in your program. It should be
small, maybe 16-by-16 or 24-by-24 pixels. (Some platforms might only be able to handle certain
cursor sizes; see the documentation for Toolkit.getBestCursorSize() for more information.)
A custom cursor can be created by calling the static method createCustomCursor() in the
Toolkit class:
Cursor c = Toolkit.getDefaultToolkit().createCustomCursor(image,hotSpot,name);
where hotSpot is of type Point and name is a String that will act as a name for the cursor (and
which serves no real purpose that I know of).
Cursors are associated with GUI components. When the mouse moves over a component,
the cursor changes to whatever Cursor is associated with that component. To associate a Cursor
with a component, call the component’s instance method setCursor(cursor). For example,
to set the cursor for a JPanel, panel, to be the standard “wait” cursor:
panel.setCursor( Cursor.getPredefinedCursor(Cursor.WAIT CURSOR) );
To set the cursor to be an image that is defined in an image resource file named imageResource,
you might use:
ClassLoader cl = SoundAndCursorDemo.class.getClassLoader();
URL resourceURL = cl.getResource(imageResource);
if (resourceURL != null) {
Toolkit toolkit = Toolkit.getDefaultToolkit();
Image image = toolkit.createImage(resourceURL);
Point hotSpot = new Point(7,7);
Cursor cursor = toolkit.createCustomCursor(image, hotSpot, "mycursor");
panel.setCursor(cursor);
}
The sample program SoundAndCursorDemo.java shows how to use predefined and custom
cursors and how to play sounds from resource files. The program has several buttons that you
can click. Some of the buttons change the cursor that is associated with the main panel of the
program. Some of the buttons play sounds. When you play a sound, the cursor is reset to be
the default cursor. You can find an applet version of the program in the on-line version of this
section.
Another standard use of images in GUI interfaces is for icons. An icon is simply a small
picture. As we’ll see in Section 12.3, icons can be used on Java’s buttons, menu items, and
labels; in fact, for our purposes, an icon is simply an image that can be used in this way.
An icon is represented by an object of type Icon, which is actually an interface rather than
a class. The class ImageIcon, which implements the Icon interface, is used to create icons from
Images. If image is a (rather small) Image, then the constructor call new ImageIcon(image)
creates an ImageIcon whose picture is the specified image. Often, the image comes from a
resource file. We will see examples of this later in this chapter
PaintWithOffScreenCanvas, so that the users would be able to save their work and to open
and edit existing images. (See Exercise 12.1.)
There are many ways that the data for an image could be stored in a file. Many standard
formats have been created for doing this. Java supports at least three standard image formats:
PNG, JPEG, and GIF. (Individual implementations of Java might support more.) The JPEG
format is “lossy,” which means that the picture that you get when you read a JPEG file is
only an approximation of the picture that was saved. Some information in the picture has been
lost. Allowing some information to be lost makes it possible to compress the image into a lot
fewer bits than would otherwise be necessary. Usually, the approximation is quite good. It
works best for photographic images and worst for simple line drawings. The PNG format, on
the other hand is “lossless,” meaning that the picture in the file is an exact duplicate of the
picture that was saved. A PNG file is compressed, but not in a way that loses information. The
compression works best for images made up mostly of large blocks of uniform color; it works
worst for photographic images. GIF is an older format that is limited to just 256 colors in an
image; it has mostly been superseded by PNG.
Suppose that image is a BufferedImage. The image can be saved to a file simply by calling
ImageIO.write( image, format, file )
where format is a String that specifies the image format of the file and file is a File that
specifies the file that is to be written. (See Subsection 11.2.2 for information about the File
class.) The format string should ordinarily be either "PNG" or "JPEG", although other formats
might be supported.
ImageIO.write() is a static method in the ImageIO class. It returns a boolean value
that is false if the image format is not supported. That is, if the specified image format is
not supported, then the image is not saved, but no exception is thrown. This means that you
should always check the return value! For example:
boolean hasFormat = ImageIO.write(OSC,format,selectedFile);
if ( ! hasFormat )
throw new Exception(format + " format is not available.");
If the image format is recognized, it is still possible that an IOExcption might be thrown when
the attempt is made to send the data to the file.
Usually, the file to be used in ImageIO.write() will be selected by the user using a JFile-
Chooser, as discussed in Subsection 11.2.3. For example, here is a typical method for saving an
image:
/**
* Attempts to save an image to a file selected by the user.
* @param image the BufferedImage to be saved to the file
* @param format the format of the image, probably either "PNG" or "JPEG"
*/
private void doSaveFile(BufferedImage image, String format) {
if (fileDialog == null)
fileDialog = new JFileChooser();
fileDialog.setSelectedFile(new File("image." + format.toLowerCase()));
fileDialog.setDialogTitle("Select File to be Saved");
int option = fileDialog.showSaveDialog(null);
if (option != JFileChooser.APPROVE OPTION)
return; // User canceled or clicked the dialog’s close box.
File selectedFile = fileDialog.getSelectedFile();
624 CHAPTER 12. ADVANCED GUI PROGRAMMING
∗ ∗ ∗
The ImageIO class also has a static read() method for reading an image from a file into
a program. The method
ImageIO.read( inputFile )
takes a variable of type File as a parameter and returns a BufferedImage. The return value
is null if the file does not contain an image that is stored in a supported format. Again, no
exception is thrown in this case, so you should always be careful to check the return value. It
is also possible for an IOException to occur when the attempt is made to read the file. There
is another version of the read() method that takes an InputStream instead of a file as its
parameter, and a third version that takes a URL.
Earlier in this section, we encountered another method for reading an image from a URL,
the createImage() method from the Toolkit class. The difference is that ImageIO.read()
reads the image data completely and stores the result in a BufferedImage. On the other hand,
createImage() does not actually read the data; it really just stores the image location and
the data won’t be read until later, when the image is used. This has the advantage that the
createImage() method itself can complete very quickly. ImageIO.read(), on the other hand,
can take some time to execute.
For example, if image is of type BufferedImage, then you can get a Graphics2D for drawing on
the image using:
Graphics2D g2 = (Graphics2D)image.getGraphics();
Note that when you do this, g and g2 are just two variables that refer to the same object,
so they both draw to the same drawing surface; g2 just gives you access to methods that are
defined in Graphics2D but not in Graphics. When properties of g2, such as drawing color, are
changed, the changes also apply to g. By saying
Graphics2D g2 = (Graphics2D)g.create()
you can obtain a newly created graphics context. The object created by g.create() is a
graphics context that draws to the same drawing surface as g and that initially has all the
same properties as g. However, it is a separate object, so that changing properties in g2 has no
effect on g. This can be useful if you want to keep an unmodified copy of the original graphics
context around for some drawing operations.
A s c e n t
D e s c e n t
L e a d i n g
L i n e h e i g h t
626 CHAPTER 12. ADVANCED GUI PROGRAMMING
The dashed lines in the illustration are the baselines of the two lines of text. The baseline
of a string is the line on which the bases of the characters rest. 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 baseline, 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 . (The term comes from the time when lead blocks were used for printing. Characters
were formed on blocks of lead that were lined up to make up the text of a page, covered with
ink, and pressed onto paper to print the page. Extra, blank “leading” was used to separate the
lines of characters.) 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
total width of a string of characters.
Recall that a font in Java is represented by the class Font. A FontMetrics object is asso-
ciated with a given font and is used to measure characters and strings in that font. If font
is of type Font and g is a graphics context, you can get a FontMetrics object for the font by
calling g.getFontMetrics(font). Then, if fm is the 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 char-
acter, 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 when drawn in that font.
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) {
super.paintComponent(g);
centerX = getWidth() / 2;
centerY = getHeight() / 2;
You can change the font that is used for drawing strings as described in Subsection 6.3.3. For
the height of the string in this method, I use fm.getAscent(). If I were drawing “Goodbye
World” instead of “Hello World,” I would have used fm.getAscent() + fm.getDescent(),
where the descent is added to the height in order to take into account the tail on the “y” in
“Goodbye”. The value of baseX is computed to be the amount of space between the left edge
of the component and the start of the string. It is obtained by subtracting half the width of the
string from the horizontal center of the component. This will center the string horizontally in
the component. The next line computes the position of the top of the string in the same way.
However, to draw the string, we need the y-coordinate of the baseline, not the y-coordinate of
the top of the string. The baseline of the string is below the top of the string by an amount
equal to the ascent of the font.
There is an example of centering a two-line block of text in the sample program Transparen-
cyDemo.java, which is discussed in the next subsection.
12.2.2 Transparency
A color is represented by red, blue, and green components. In Java’s usual representation, each
component is an eight-bit number in the range 0 to 255. The three color components can be
packed into a 32-bit integer, but that only accounts for 24 bits in the integer. What about the
other eight bits? They don’t have to be wasted. They can be used as a fourth component of
the color, the alpha component . The alpha component can be used in several ways, but it
is most commonly associated with transparency . When you draw with a transparent color,
it’s like laying down a sheet of colored glass. It doesn’t completely obscure the part of the
image that is colored over. Instead, the background image is blended with the transparent
color that is used for drawing—as if you were looking at the background through colored glass.
This type of drawing is properly referred to as alpha blending , and it is not equivalent to true
transparency; nevertheless, most people refer to it as transparency.
The value of the alpha component determines how transparent that color is. Actually, the
alpha component gives the opaqueness of the color. Opaqueness is the opposite of trans-
parency. If something is fully opaque, you can’t see through it at all; if something is almost
fully opaque, then it is just a little transparent; and so on. When the alpha component of a
color has the maximum possible value, the color is fully opaque. When you draw with a fully
opaque color, that color simply replaces the color of the background over which you draw. This
is the only type of color that we have used up until now. If the alpha component of a color is
zero, then the color is perfectly transparent, and drawing with that color has no effect at all.
Intermediate values of the alpha component give partially opaque colors that will blend with
the background when they are used for drawing.
The sample program TransparencyDemo.java can help you to understand transparency.
When you run the program you will see a display area containing a triangle, an oval, a rect-
angle, and some text. Sliders at the bottom of the applet allow you to control the degree of
transparency of each shape. When a slider is moved all the way to the right, the corresponding
shape is fully opaque; all the way to the left, and the shape is fully transparent. An applet
version of the program can be found in the on-line version of this section.
∗ ∗ ∗
Colors with alpha components were introduced in Java along with Graphics2D, but they can
be used with ordinary Graphics objects as well. To specify the alpha component of a color, you
can create the Color object using one of the following constructors from the Color class:
628 CHAPTER 12. ADVANCED GUI PROGRAMMING
g2.dispose();
Point hotSpot = new Point(12,12);
Toolkit tk = Toolkit.getDefaultToolkit();
Cursor cursor = tk.createCustomCursor(image,hotSpot,"square");
setCursor(cursor);
}
12.2.3 Antialiasing
To draw a geometric figure such as a line or circle, you just have to color the pixels that are part
of the figure, right? Actually, there is a problem with this. Pixels are little squares. Geometric
figures, on the other hand, are made of geometric points that have no size at all. Think about
drawing a circle, and think about a pixel on the boundary of that circle. The infinitely thin
geometric boundary of the circle cuts through the pixel. Part of the pixel lies inside the circle,
part lies outside. So, when we are filling the circle with color, do we color that pixel or not?
A possible solution is to color the pixel if the geometric circle covers 50% or more of the pixel.
Following this procedure, however, leads to a visual defect known as aliasing . It is visible in
images as a jaggedness or “staircasing” effect along the borders of shapes. Lines that are not
horizontal or vertical also have a jagged, aliased appearance. (The term “aliasing” seems to
refer to the fact that many different geometric points map to the same pixel. If you think of
the real-number coordinates of a geometric point as a “name” for the pixel that contains that
point, then each pixel has many different names or “aliases.”)
It’s not possible to build a circle out of squares, but there is a technique that can eliminate
some of the jaggedness of aliased images. The technique is called antialiasing . Antialiasing is
based on transparency. The idea is simple: If 50% of a pixel is covered by the geometric figure
that you are trying to draw, then color that pixel with a color that is 50% transparent. If 25%
of the pixel is covered, use a color that is 75% transparent (25% opaque). If the entire pixel
is covered by the figure, of course, use a color that is 100% opaque—antialiasing only affects
pixels along the boundary of the shape.
In antialiasing, the color that you are drawing with is blended with the original color of the
pixel, and the amount of blending depends on the fraction of the pixel that is covered by the
geometric shape. (The fraction is difficult to compute exactly, so in practice, various methods
are used to approximate it.) Of course, you still don’t get a picture of the exact geometric
shape, but antialiased images do tend to look better than jagged, aliased images.
For an example, look at the image in the next subsection. Antialiasing is used to draw
the panels in the second and third row of the image, but it is not used in the top row. You
should note the jagged appearance of the lines and rectangles in the top row. (By the way,
when antialiasing is applied to a line, the line is treated as a geometric rectangle whose width
is equal to the size of one pixel.)
Antialiasing is supported in Graphics2D. By default, antialiasing is turned off. If g2 is a
graphics context of type Graphics2D, you can turn on antialiasing in g2 by saying:
g2.setRenderingHint(RenderingHints.KEY ANTIALIASING,
RenderingHints.VALUE ANTIALIAS ON);
As you can see, this is only a “hint” that you would like to use antialiasing, and it is even
possible that the hint will be ignored. However, it is likely that subsequent drawing operations
in g2 will be antialiased. If you want to turn antialiasing off in g2, you can just say:
630 CHAPTER 12. ADVANCED GUI PROGRAMMING
g2.setRenderingHint(RenderingHints.KEY ANTIALIASING,
RenderingHints.VALUE ANTIALIAS OFF);
This illustration shows the sample program StrokeDemo.java. (You can try an applet version
of the program in the on-line version of this section.) In this program, you can click and drag
in any of the small panels, and the lines in all the panels will be redrawn as you move the
mouse. In addition, if you right-click and drag, then rectangles will be drawn instead of lines;
this shows that strokes are used for drawing the outlines of shapes and not just for straight
lines. If you look at the corners of the rectangles that are drawn by the program, you’ll see
that there are several ways of drawing a corner where two wide line segments meet.
All the options that you want for a BasicStroke have to be specified in the constructor. Once
the stroke object is created, there is no way to change the options. There is one constructor
that lets you specify all possible options:
public BasicStroke( float width, int capType, int joinType, float miterlimit,
float[] dashPattern, float dashPhase )
I don’t want to cover all the options in detail, but here’s some basic info:
• width specifies the thickness of the line
• capType specifies how the ends of a line are “capped.” The possible values are
BasicStroke.CAP SQUARE, BasicStroke.CAP ROUND and BasicStroke.CAP BUTT. These
values are used, respectively, in the first, second, and third rows of the above picture. The
default is BasicStroke.CAP SQUARE.
• joinType specifies how two line segments are joined together at corners. Possible values
are BasicStroke.JOIN MITER, BasicStroke.JOIN ROUND, and BasicStroke.JOIN BEVEL.
Again, these are used in the three rows of panels in the sample program. The default is
BasicStroke.JOIN MITER.
• miterLimit is used only if the value of joinType is JOIN MITER; just use the default value,
10.0F.
• dashPattern is used to specify dotted and dashed lines. The values in the array specify
lengths in the dot/dash pattern. The numbers in the array represent the length of a solid
piece, followed by the length of a transparent piece, followed by the length of a solid piece,
632 CHAPTER 12. ADVANCED GUI PROGRAMMING
and so on. At the end of the array, the pattern wraps back to the beginning of the array.
If you want a solid line, use a different constructor that has fewer parameters.
• dashPhase tells the computer where to start in the dashPattern array, for the first seg-
ment of the line. Use 0 for this parameter in most cases.
For the third row in the above picture, the dashPattern is set to new float[] {5,5}. This
means that the lines are drawn starting with a solid segment of length 5, followed by a transpar-
ent section of length 5, and then repeating the same pattern. A simple dotted line would have
thickness 1 and dashPattern new float[] {1,1}. A pattern of short and long dashes could be
made by using new float[] {10,4,4,4}. For more information, see the Java documentation,
or try experimenting with the source code for the sample program.
∗ ∗ ∗
So now we can draw fancier lines. But any drawing operation is still restricted to drawing
with a single color. We can get around that restriction by using Paint. An object of type Paint
is used to assign color to each pixel that is “hit” by a drawing operation. Paint is an interface,
and the Color class implements the Paint interface. When a color is used for painting, it applies
the same color to every pixel that is hit. However, there are other types of paint where the color
that is applied to a pixel depends on the coordinates of that pixel. Standard Java includes two
classes that define paint with this property: GradientPaint and TexturePaint. In a gradient, the
color that is applied to pixels changes gradually from one color to a second color as you move
in a certain direction. In a texture, the pixel colors come from an image, which is repeated, if
necessary, like a wallpaper pattern to cover the entire xy-plane.
It will be helpful to look at some examples. This illustration shows a polygon filled with
two different textures. The polygon on the left uses a GradientPaint while the one on the right
uses a TexturePaint. Note that in this picture, the paint is used only for filling the polygon.
The outline of the polygon is drawn in a plain black color. However, Paint objects can be used
for drawing lines as well as for filling shapes. These pictures were made by the sample program
PaintDemo.java. In that program, you can select among several different paints, and you can
control certain properties of the paints. As usual, an applet version of the program is available
on line.
This constructs a gradient that has color c1 at the point with coordinates (x1,y1) and color
c2 at the point (x2,y2). As you move along the line between the two points, the color of the
gradient changes from c1 to c2; along lines perpendicular to this line, the color is constant.
The last parameter, cyclic, tells what happens if you move past the point (x2,y2) on the
line from (x1,y1) to (x2,y2). If cyclic is false, the color stops changing and any point
beyond (x2,y2) has color c2. If cyclic is true, then the colors continue to change in a cyclic
pattern after you move past (x2,y2). (It works the same way if you move past the other
endpoint, (x1,y1).) In most cases, you will set cyclic to true. Note that you can vary the
points (x1,y1) and (x2,y2) to change the width and direction of the gradient. For example,
to create a cyclic gradient that varies from black to light gray along the line from (0,0) to
(100,100), use:
new GradientPaint( 0, 0, Color.BLACK, 100, 100, Color.LIGHT GRAY, true)
To construct a TexturePaint, you need a BufferedImage that contains the image that will be
used for the texture. You also specify a rectangle in which the image will be drawn. The image
will be scaled, if necessary, to exactly fill the rectangle. Outside the specified rectangle, the
image will be repeated horizontally and vertically to fill the plane. You can vary the size and
position of the rectangle to change the scale of the texture and its positioning on the plane.
Ordinarily, however the upper left corner of the rectangle is placed at (0,0), and the size of
the rectangle is the same as the actual size of the image. The constructor for TexturePaint is
defined as
public TexturePaint( BufferedImage textureImage, Rectangle2D anchorRect)
The Rectangle2D is part of the Graphics2D framework and will be discussed at the end of this
section. Often, a call to the constructor takes the form:
new TexturePaint( image,
new Rectangle2D.Double(0,0,image.getWidth(),image.getHeight() )
Once you have a Paint object, you can use the setPaint() method of a Graphics2D object
to install the paint in a graphics context. For example, if g2 is of type Graphics2D, then the
command
g2.setPaint( new GradientPaint(0,0,Color.BLUE,100,100,Color.GREEN,true) );
sets up g2 to use a gradient paint. Subsequent drawing operations with g2 will draw using a
blue/green gradient.
12.2.5 Transforms
In the standard drawing coordinates on a component, the upper left corner of the component
has coordinates (0,0). Coordinates are integers, and the coordinates (x,y) refer to the point
that is x pixels over from the left edge of the component and y pixels down from the top. With
Graphics2D, however, you are not restricted to using these coordinates. In fact, you can can
set up a Graphics2D graphics context to use any system of coordinates that you like. You can
use this capability to select the coordinate system that is most appropriate for the things that
you want to draw. For example, if you are drawing architectural blueprints, you might use
coordinates in which one unit represents an actual distance of one foot.
Changes to a coordinate system are referred to as transforms. There are three basic
types of transform. A translate transform changes the position of the origin, (0,0). A scale
transform changes the scale, that is, the unit of distance. And a rotation transform applies a
634 CHAPTER 12. ADVANCED GUI PROGRAMMING
rotation about some point. You can make more complex transforms by combining transforms
of the three basic types. For example, you can apply a rotation, followed by a scale, followed by
a translation, followed by another rotation. When you apply several transforms in a row, their
effects are cumulative. It takes a fair amount of study to fully understand complex transforms.
I will limit myself here to discussing a few of the most simple cases, just to give you an idea of
what transforms can do.
Suppose that g2 is of type Graphics2D. Then g2.translate(x,y) moves the origin,
(0,0), to the point (x,y). This means that if you use coordinates (0,0) after saying
g2.translate(x,y), then you are referring to the point that used to be (x,y), before the
translation was applied. All other coordinate pairs are moved by the same amount. For exam-
ple saying
g.translate(x,y);
g.drawLine( 0, 0, 100, 200 );
draws the same line as
g.drawLine( x, y, 100+x, 200+y );
In the second case, you are just doing the same translation “by hand.” A translation (like all
transforms) affects all subsequent drawing operations. Instead of thinking in terms of coordinate
systems, you might find it clearer to think of what happens to the objects that are drawn. After
you say g2.translate(x,y), any objects that you draw are displaced x units vertically and y
units horizontally. Note that the parameters x and y can be real numbers.
As an example, perhaps you would prefer to have (0,0) at the center of a component, instead
of at its upper left corner. To do this, just use the following command in the paintComponent()
method of the component:
g2.translate( getWidth()/2, getHeight()/2 );
To apply a scale transform to a Graphics2D g2, use g2.scale(s,s), where s is the real
number that specifies the scaling factor. If s is greater than 1, everything is magnified by a
factor of s, while if s is between 0 and 1, everything is shrunk by a factor of s. The center of
scaling is (0,0). That is, the point (0,0) is unaffected by the scaling, and other points more
towards or away from (0,0) by a factor of s. Again, it can be clearer to think of the effect
on objects that are drawn after a scale transform is applied. Those objects will be magnified
or shrunk by a factor of s. Note that scaling affects everything, including thickness of lines
and size of fonts. By the way, it is possible to use scale factors that are less than 0. It is even
possible to use different scale factors in the horizontal and vertical direction with a command
of the form g2.scale(sx,sy), although that will distort the shapes of objects.
The third type of basic transform is rotation. The command g2.rotate(r) rotates all
subsequently drawn objects through an angle of r about the point (0,0). You can rotate
instead about the point (x,y) with the command g2.rotate(r,x,y). All the parameters can
be real numbers. Angles are measured in radians, where one radian is equal to 180 degrees. To
rotate through an angle of d degrees, use
g2.rotate( d * Math.PI / 180 );
Positive angles are clockwise rotations, while negative angles are counterclockwise (unless you
have already applied a negative scale factor, which reverses the orientation).
Rotation is not as common as translation or scaling, but there are a few things that you
can do with it that can’t be done any other way. For example, you can use it to draw an image
“on the slant.” Rotation also makes it possible to draw text that is rotated so that its baseline
12.2. FANCIER GRAPHICS 635
is slanted or even vertical. To draw the string “Hello World” with its basepoint at (x,y) and
rising at an angle of 30 degrees, use:
g2.rotate( -30 * Math.PI / 180, x, y );
g2.drawString( "Hello World", x, y );
To draw the message vertically, with the center of its baseline at the point (x,y), we can use
FontMetrics to measure the string, and say:
FontMetrics fm = g2.getFontMetrics( g2.getFont() );
int baselineLength = fm.stringWidth("Hello World");
g2.rotate( -90 * Math.PI / 180, x, y);
g2.drawString( "Hello World", x - baselineLength/2, y );
∗ ∗ ∗
The drawing operations in the Graphics class use integer coordinates only. Graphics2D makes
it possible to use real numbers as coordinates. This becomes particularly important once you
start using transforms, since after you apply a scale, a square of size one might cover many
pixels instead of just a single pixel. Unfortunately, the designers of Java couldn’t decide whether
to use numbers of type float or double as coordinates, and their indecision makes things a little
more complicated than they need to be. (My guess is that they really wanted to use float, since
values of type float have enough accuracy for graphics and are probably used in the underlying
graphical computations of the computer. However, in Java programming, it’s easier to use
double than float, so they wanted to make it possible to use double values too.)
To use real number coordinates, you have to use classes defined in the package
java.awt.geom. Among the classes in this package are classes that represent geometric shapes
such as lines and rectangles. For example, the class Line2D represents a line whose endpoints
are given as real number coordinates. The unfortunate thing is that Line2D is an abstract
class, which means that you can’t create objects of type Line2D directly. However, Line2D
has two concrete subclasses that can be used to create objects. One subclass uses coordinates
of type float, and one uses coordinates of type double. The most peculiar part is that these
subclasses are defined as static nested classes inside Line2D. Their names are Line2D.Float and
Line2D.Double. This means that Line2D objects can be created, for example, with:
Line2D line1 = new Line2D.Float( 0.17F, 1.3F, -2.7F, 5.21F );
Line2D line2 = new Line2D.Double( 0, 0, 1, 0);
Line2D line3 = new Line2D.Double( x1, y1, x2, y2 );
where x1, y1, x2, y2 are any numeric variables. In my own code, I generally use Line2D.Double
rather than Line2D.Float.
Other shape classes in java.awt.geom are similar. The class that represents rectangles is
Rectangle2D. To create a rectangle object, you have to use either Rectangle2D.Float or Rectan-
gle2D.Double. For example,
Rectangle2D rect = new Rectangle2D.Double( -0.5, -0.5, 1.0, 1.0 );
creates a rectangle with a corner at (-0.5,-0.5) and with width and height both equal to 1.
Other classes include Point2D, which represents a single point; Ellipse2D, which represents an
oval; and Arc2D, which represents an arc of a circle.
If g2 is of type Graphics2D and shape is an object belonging to one of the 2D shape classes,
then the command
g2.draw(shape);
636 CHAPTER 12. ADVANCED GUI PROGRAMMING
draws the shape. For a shape such as a rectangle or ellipse that has an interior, only the outline
is drawn. To fill in the interior of such a shape, use
g2.fill(shape)
For example, to draw a line from (x1,y1) to (x2,y2), use
g2.draw( new Line2D.Double(x1,y1,x2,y2) );
and to draw a filled rectangle with a corner at (3.5,7), with width 5, and with height 3, use
g2.fill( new Rectangle2D.Double(3.5, 7, 5, 3) );
The package java.awt.geom also has a very nice class GeneralPath that can be used to draw
polygons and curves defined by any number of points. See the Java documentation if you want
to find out how to use it. There is still a large part of the Graphics2D framework for you to
explore.
The parameter, "Clear", in the constructor of the AbstractAction is the name of the action.
Other properties can be set by calling the method setValue(key,value), which is part of the
Action interface. For example,
clearAction.setValue(Action.SHORT DESCRIPTION, "Clear the Display");
sets the SHORT DESCRIPTION property of the action to have the value “Clear the Display”. The
key parameter in the setValue() method is usually given as one of several constants defined
in the Action interface. As another example, you can change the name of an action by using
Action.NAME as the key in the setValue() method.
Once you have an Action, you can use it in the constructor of a button. For example, using
the action clearAction defined above, we can create the JButton
JButton clearButton = new JButton( clearAction );
The name of the action will be used as the text of the button, and some other properties of
the button will be taken from properties of the action. For example, if the SHORT DESCRIPTION
property of the action has a value, then that value is used as the tooltip text for the button.
(The tooltip text appears when the user hovers the mouse over the button.) Furthermore,
when you change a property of the action, the corresponding property of the button will also
be changed.
The Action interface defines a setEnabled() method that is used to enable and
disable the action. The clearAction action can be enabled and disabled by calling
clearAction.setEnabled(true) and clearAction.setEnabled(false). When you do this,
any button that has been created from the action is also enabled or disabled at the same time.
Now of course, the question is, why should you want to use Actions at all? One advantage
is that using actions can help you to organize your code better. You can create separate objects
that represent each of the actions that can be performed in your program. This represents a nice
division of responsibility. Of course, you could do the same thing with individual ActionListener
objects, but then you couldn’t associate descriptions and other properties with the actions.
More important is the fact that Actions can also be used in other places in the Java API.
You can use an Action to create a JMenuItem in the same way as for a JButton:
JMenuItem clearCommand = new JMenuItem( clearAction );
A JMenuItem, in fact, is a kind of button and shares many of the same properties that a JButton
can have. You can use the same Action to create both a button and a menu item (or even
several of each if you want). Whenever you enable or disable the action or change its name, the
button and the menu item will both be changed to match. If you change the NAME property
of the action, the text of both the menu item and the button will be set to the new name of
the action. You can think of the button and the menu items as being two presentations of the
Action, and you don’t have to keep track of the button or menu item after you create them.
You can do everything that you need to do by manipulating the Action object.
It is also possible to associate an Action with any key, so that the action will be performed
whenever the user presses that key. I won’t explain how to do it here, but you can look up the
documentation for the classes javax.swing.InputMap and javax.swing.ActionMap.
638 CHAPTER 12. ADVANCED GUI PROGRAMMING
By the way, if you want to add a menu item that is defined from an Action to a menu, you
don’t even need to create the JMenuItem yourself. You can add the action object directly to
the menu, and the menu item will be created from the properties of the action. For example, if
menu is a JMenu and clearAction is an Action, you can simply say menu.add(clearAction).
The icon for a button can be set by calling the button’s setIcon() method, or by passing the
icon object as a parameter to the constructor when the button is created. To create the button
shown above, I created an ImageIcon from a BufferedImage on which I drew the picture that
I wanted, and I constructed the JButton using a constructor that takes both the text and the
icon for the button as parameters. Here’s the code segment that does it:
BufferedImage image = new BufferedImage(24,24,BufferedImage.TYPE INT RGB);
Graphics2D g2 = (Graphics2D)image.getGraphics();
g2.setColor(Color.LIGHT GRAY); // Draw the image for the icon.
g2.fillRect(0,0,24,24);
g2.setStroke( new BasicStroke(3) ); // Use thick lines.
g2.setColor(Color.BLACK);
g2.drawLine(4,4,20,20); // Draw the "X".
g2.drawLine(4,20,20,4);
g2.dispose();
Icon clearIcon = new ImageIcon(image); // Create the icon.
JButton clearButton = new JButton("Clear the Display", clearIcon);
You can create a button with an icon but no text by using a constructor that takes just the
icon as parameter. Another alternative is for the button to get its icon from an Action. When
a button is constructed from an action, it takes its icon from the value of the action property
Action.SMALL ICON. For example, suppose that we want to use an action named clearAction
to create the button shown above. This could be done with:
clearAction.putValue( Action.SMALL ICON, clearIcon );
JButton clearButton = new JButton( clearAction );
The icon could also be associated with the action by passing it as a parameter to the constructor
of an AbstractAction:
Action clearAction = new AbstractAction("Clear the Display", clearIcon) {
public void actionPerformed(ActionEvent evt) {
.
. // Carry out the action.
.
}
}
JButton clearButton = new JButton( clearAction );
12.3. ACTIONS AND BUTTONS 639
The appearance of buttons can be tweaked in many ways. For example, you can change the
size of the gap between the button’s text and its icon. You can associate additional icons with
a button that are used when the button is in certain states, such as when it is pressed or when
it is disabled. It is even possible to change the positioning of the text with respect to the icon.
For example, to place the text centered below the icon on a button, you can say:
button.setHorizontalTextPosition(JButton.CENTER);
button.setVerticalTextPosition(JButton.BOTTOM);
These methods and many others are defined in the class AbstractButton. This class is a super-
class for JMenuItem, as well as for JButton and for the classes that define check boxes and radio
buttons. Note in particular that an icon can be shown in a menu by associating the icon with
a menu item or with the action that is used to create the menu item.
Finally, I will mention that it is possible to use icons on JLabels in much the same way that
they can be used on JButtons.
The individual buttons must still be added to a container if they are to appear on the screen.
If you want to respond immediately when the user clicks on one of the radio buttons, you can
register an ActionListener for each button. Just as for checkboxes, it is not always necessary
to register listeners for radio buttons. In some cases, you can simply check the state of each
button when you need to know it, using the button’s isSelected() method.
All this is demonstrated in the sample program RadioButtonDemo.java. The program shows
four radio buttons. When the user selects one of the radio buttons, the text and background
color of a label is changed. Here is a picture of the program, with the “Green” radio button
selected:
You can add the equivalent of a group of radio buttons to a menu by using the class
JRadioButtonMenuItem. To use this class, create several objects of this type, and create a But-
tonGroup to manage them. Add each JRadioButtonMenuItem to the ButtonGroup, and also add
them to a JMenu. If you want one of the items to be selected initially, call its setSelected()
method to set its selection state to true. You can add ActionListeners to each JRadioButton-
MenuItem if you need to take some action when the user selects the menu item; if not, you
can simply check the selected states of the buttons whenever you need to know them. As an
example, suppose that menu is a JMenu. Then you can add a group of buttons to menu as
follows:
JRadioButtonMenuItem selectRedItem, selectGreenItem, selectBlueItem;
// These might be defined as instance variables
ButtonGroup group = new ButtonGroup();
selectRedItem = new JRadioButtonMenuItem("Red");
group.add(selectRedItem);
menu.add(selectRedItem);
12.3. ACTIONS AND BUTTONS 641
∗ ∗ ∗
When it’s drawn on the screen, a JCheckBox includes a little box that is either checked or
unchecked to show the state of the box. That box is actually a pair of Icons. One icon is shown
when the check box is unselected; the other is shown when it is selected. You can change the
appearance of the check box by substituting different icons for the standard ones.
The icon that is shown when the check box is unselected is just the main icon for the
JCheckBox. You can provide a different unselected icon in the constructor or you can change
the icon using the setIcon() method of the JCheckBox object. To change the icon that is
shown when the check box is selected, use the setSelectedIcon() method of the JCheckBox.
All this applies equally to JRadioButton, JCheckBoxMenuItem, and JRadioButtonMenuItem.
An example of this can be found in the sample program ToolBarDemo.java, which is dis-
cussed in the next subsection. That program creates a set of radio buttons that use custom
icons. The buttons are created by the following method:
/**
* Create a JRadioButton and add it to a specified button group. The button
* is meant for selecting a drawing color in the display. The color is used to
* create two custom icons, one for the unselected state of the button and one
* for the selected state. These icons are used instead of the usual
* radio button icons.
* @param c the color of the button, and the color to be used for drawing.
* (Note that c has to be "final" since it is used in the anonymous inner
* class that defines the response to ActionEvents on the button.)
* @param grp the ButtonGroup to which the radio button will be added.
* @param selected if true, then the state of the button is set to selected.
* @return the radio button that was just created; sorry, but the button
is not as pretty as I would like!
*/
private JRadioButton makeColorRadioButton(final Color c,
ButtonGroup grp, boolean selected) {
/* Create an ImageIcon for the normal, unselected state of the button,
using a BufferedImage that is drawn here from scratch. */
BufferedImage image = new BufferedImage(30,30,BufferedImage.TYPE INT RGB);
Graphics g = image.getGraphics();
g.setColor(Color.LIGHT GRAY);
g.fillRect(0,0,30,30);
g.setColor(c);
g.fill3DRect(1, 1, 24, 24, true);
g.dispose();
Icon unselectedIcon = new ImageIcon(image);
/* Create an ImageIcon for the selected state of the button. */
image = new BufferedImage(30,30,BufferedImage.TYPE INT RGB);
g = image.getGraphics();
642 CHAPTER 12. ADVANCED GUI PROGRAMMING
g.setColor(Color.DARK GRAY);
g.fillRect(0,0,30,30);
g.setColor(c);
g.fill3DRect(3, 3, 24, 24, false);
g.dispose();
Icon selectedIcon = new ImageIcon(image);
/* Create and configure the button. */
JRadioButton button = new JRadioButton(unselectedIcon);
button.setSelectedIcon(selectedIcon);
button.addActionListener( new ActionListener() {
public void actionPerformed(ActionEvent e) {
// The action for this button sets the current drawing color
// in the display to c.
display.setCurrentColor(c);
}
});
grp.add(button);
if (selected)
button.setSelected(true);
return button;
} // end makeColorRadioButton
∗ ∗ ∗
It is possible to create radio buttons and check boxes from Actions. The button takes its
name, main icon, tooltip text, and enabled/disabled state from the action. However, in Java
5.0, an action has no property corresponding to the selected/unselected state. This means that
you can’t check or set the selection state through the action. In Java 6.0, the action API is
considerably improved, and among the changes is support for selection state.
12.3.4 Toolbars
It has become increasingly common for programs to have a row of small buttons along the top
or side of the program window that offer access to some of the commonly used features of the
program. The row of buttons is known as a tool bar . Typically, the buttons in a tool bar are
presented as small icons, with no text. Tool bars can also contain other components, such as
JTextFields and JLabels.
In Swing, tool bars are represented by the class JToolBar. A JToolBar is a container that can
hold other components. It is also itself a component, and so can be added to other containers.
In general, the parent component of the tool bar should use a BorderLayout. The tool bar
should occupy one of the edge positions—NORTH, SOUTH, EAST, or WEST—in the BorderLayout.
Furthermore, the other three edge positions should be empty. The reason for this is that it
might be possible (depending on the platform and configuration) for the user to drag the tool
bar from one edge position in the parent container to another. It might even be possible for
the user to drag the toolbar off its parent entirely, so that it becomes a separate window.
Here is a picture of a tool bar from the sample program ToolBarDemo.java.
12.3. ACTIONS AND BUTTONS 643
In this program, the user can draw colored curves in a large drawing area. The first three
buttons in the tool bar are a set of radio buttons that control the drawing color. The fourth
button is a push button that the user can click to clear the drawing.
Tool bars are easy to use. You just have to create the JToolBar object, add it to a container,
and add some buttons and possibly other components to the tool bar. One fine point is adding
space to a tool bar, such as the gap between the radio buttons and the push button in the
sample program. You can leave a gap by adding a separator to the tool bar. For example:
toolbar.addSeparator(new Dimension(20,20));
This adds an invisible 20-by-20 pixel block to the tool bar. This will appear as a 20 pixel gap
between components.
Here is the constructor from the ToolBarDemo program. It shows how to create the tool
bar and place it in a container. Note that class ToolBarDemo is a subclass of JPanel, and the
tool bar and display are added to the panel object that is being constructed:
public ToolBarDemo() {
setLayout(new BorderLayout(2,2));
setBackground(Color.GRAY);
setBorder(BorderFactory.createLineBorder(Color.GRAY,2));
display = new Display();
add(display, BorderLayout.CENTER);
JToolBar toolbar = new JToolBar();
add(toolbar, BorderLayout.NORTH);
ButtonGroup group = new ButtonGroup();
toolbar.add( makeColorRadioButton(Color.RED,group,true) );
toolbar.add( makeColorRadioButton(Color.GREEN,group,false) );
toolbar.add( makeColorRadioButton(Color.BLUE,group,false) );
toolbar.addSeparator(new Dimension(20,20));
toolbar.add( makeClearButton() );
}
Note that the gray outline of the tool bar comes from two sources: The line at the bottom
shows the background color of the main panel, which is visible because the BorderLayout that
is used on that panel has vertical and horizontal gaps of 2 pixels. The other three sides are
part of the border of the main panel.
If you want a vertical tool bar that can be placed in the EAST or WEST position of a Border-
Layout, you should specify the orientation in the tool bar’s constructor:
JToolBar toolbar = new JToolBar( JToolBar.VERTICAL );
The default orientation is JToolBar.HORIZONTAL. The orientation is adjusted automatically
when the user drags the toolbar into a new position. If you want to prevent the user from
dragging the toolbar, just say toolbar.setFloatable(false).
equivalent CONTROL-S, and the “Undo” command corresponds to CONTROL-Z. (Under Mac OS,
the keyboard equivalents for these commands would probably be META-C and META-Z, where
META refers to holding down the “apple” key.) The keyboard equivalents for menu commands
are referred to as accelerators.
The class javax.swing.KeyStroke is used to represent key strokes that the user can type
on the keyboard. A key stroke consists of pressing a key, possibly while holding down one or
more of the modifier keys control, shift, alt, and meta. The KeyStroke class has a static
method, getKeyStroke(String), that makes it easy to create key stroke objects. For example,
KeyStroke.getKeyStroke( "ctrl S" )
returns a KeyStroke that represents the action of pressing the “S” key while holding down the
control key. In addition to “ctrl”, you can use the modifiers “shift”, “alt”, and “meta” in the
string that describes the key stroke. You can even combine several modifiers, so that
KeyStroke.getKeyStroke( "ctrl shift Z" )
represents the action of pressing the “Z” key while holding down both the control and the shift
keys. When the key stroke involves pressing a character key, the character must appear in the
string in upper case form. You can also have key strokes that correspond to non-character keys.
The number keys can be referred to as “1”, “2”, etc., while certain special keys have names
such as “F1”, “ENTER”, and “LEFT” (for the left arrow key). The class KeyEvent defines
many constants such as VK ENTER, VK LEFT, and VK S. The names that are used for keys in the
keystroke description are just these constants with the leading “VK ” removed.
There are at least two ways to associate a keyboard accelerator with a menu item. One is
to use the setAccelerator() method of the menu item object:
JMenuItem saveCommand = new JMenuItem( "Save..." );
saveCommand.setAccelerator( KeyStroke.getKeyStroke("ctrl S") );
The other technique can be used if the menu item is created from an Action. The action property
Action.ACCELERATOR KEY can be used to associate a KeyStroke with an Action. When a menu
item is created from the action, the keyboard accelerator for the menu item is taken from the
value of this property. For example, if redoAction is an Action representing a “Redo” action,
then you might say:
redoAction.putValue( Action.ACCELERATOR KEY,
KeyStroke.getKeyStroke("ctrl shift Z") );
JMenuItem redoCommand = new JMenuItem( redoAction );
or, alternatively, you could simply add the action to a JMenu, editMenu, with
editMenu.add(redoAction). (Note, by the way, that accelerators apply only to menu items,
not to push buttons. When you create a JButton from an action, the ACCELERATOR KEY property
of the action is ignored.)
Note that you can use accelerators for JCheckBoxMenuItems and JRadioButtonMenuItems,
as well as for simple JMenuItems.
For an example of using keyboard accelerators, see the solution to Exercise 12.2.
∗ ∗ ∗
By the way, as noted above, in the Macintosh operating system, the meta (or apple) key
is usually used for keyboard accelerators instead of the control key. If you would like to make
your program more Mac-friendly, you can test whether your program is running under Mac OS
and, if so, adapt your accelerators to the Mac OS style. The recommended way to detect Mac
12.3. ACTIONS AND BUTTONS 645
If the string of text that is applied to a button starts with “<html>”, then the string is
interpreted as HTML. The string does not have to use strict HTML format; for example, you
don’t need a closing </html> at the end of the string. To get multi-line text, use <br> in the
string to represent line breaks. If you would like the lines of text to be center justified, include
the entire text (except for the <html>) between <center> and </center>. For example,
JButton button = new JButton(
"<html><center>This button has<br>two lines of text</center>" );
creates a button that displays two centered lines of text. You can apply italics to part of the
string by enclosing that part between <i> and </i>. Similarly, use <b>...</b> for bold text and
<u>...</u> for underlined text. For green text, enclose the text between <font color=green>
and </font >. You can, of course, use other colors in place of “green.” The “Java” button that
is shown above was created using:
646 CHAPTER 12. ADVANCED GUI PROGRAMMING
Other HTML features can also be used on buttons and labels—experiment to see what you can
get away with!
12.4.1 Model-View-Controller
One of the principles of object-oriented design is division of responsibilities. Ideally, an object
should have a single, clearly defined role, with a limited realm of responsibility. One application
of this principle to the design of graphical user interfaces is the MVC pattern. “MVC” stands
for “Model-View-Controller” and refers to three different realms of responsibility in the design
of a graphical user interface.
When the MVC pattern is applied to a component, the model consists of the data that
represents the current state of the component. The view is simply the visual presentation of
the component on the screen. And the controller is the aspect of the component that carries
out actions in response to events generated by the user. The idea is to assign responsibility for
the model, the view, and the controller to different objects.
The view is the easiest part of the MVC pattern to understand. It is often represented by
the component object itself, and its responsibility is to draw the component on the screen. In
doing this, of course, it has to consult the model, since the model represents the state of the
component, and that state can determine what appears on the screen. To get at the model
data—which is stored in a separate object according to the MVC pattern—the component
object needs to keep a reference to the model object. Furthermore, when the model changes,
the view might have to be redrawn to reflect the changed state. The component needs some way
of knowing when changes in the model occur. Typically, in Java, this is done with events and
listeners. The model object is set up to generate events when its data changes. The view object
12.4. COMPLEX COMPONENTS AND MVC 647
registers itself as a listener for those events. When the model changes, an event is generated,
the view is notified of that event, and the view responds by updating its appearance on the
screen.
When MVC is used for Swing components, the controller is generally not so well defined as
the model and view, and its responsibilities are often split among several objects. The controller
might include mouse and keyboard listeners that respond to user events on the view; Actions
that respond to menu commands or buttons; and listeners for other high-level events, such as
those from a slider, that affect the state of the component. Usually, the controller responds
to events by making modifications to the model, and the view is changed only indirectly, in
response to the changes in the model.
The MVC pattern is used in many places in the design of Swing. It is even used for buttons.
The state of a Swing button is stored in an object of type ButtonModel. The model stores such
information as whether the button is enabled, whether it is selected, and what ButtonGroup it
is part of, if any. If button is of type JButton (or one of the other subclasses of AbstractButton),
then its ButtonModel can be obtained by calling button.getModel(). In the case of buttons,
you might never need to use the model or even know that it exists. But for the list and table
components that we will look at next, knowledge of the model is essential.
Note that the scrollbar in this program is not part of the JList. To add a scrollbar to a list,
the list must be placed into a JScrollPane. See Subsection 6.6.4, where the use of JScrollPane
to hold a JTextArea was discussed. Scroll panes are used in the same way with lists and with
other components. In this case, the JList, iconList, was added to a scroll pane and the scroll
pane was added to a panel with the single command:
add( new JScrollPane(iconList), BorderLayout.EAST );
648 CHAPTER 12. ADVANCED GUI PROGRAMMING
One way to construct a JList is from an array that contains the objects that will appear
in the list. The items can be of any type, but only icons and strings can actually appear in
the list; an item that is not of type Icon or String is converted into a string by calling its
toString() method. (It’s possible to “teach” a JList to display other types of items; see the
setCellRenderer() method in the JList class.) In the SillyStamper program, the images for
the icons are read from resource files, the icons are placed into an array, and the array is used
to construct the list. This is done by the following method:
private JList createIconList() {
String[] iconNames = new String[] {
"icon5.png", "icon7.png", "icon8.png", "icon9.png", "icon10.png",
"icon11.png", "icon24.png", "icon25.png", "icon26.png", "icon31.png",
"icon33.png", "icon34.png"
}; // Array containing resource file names for the icon images.
iconImages = new Image[iconNames.length];
ClassLoader classLoader = getClass().getClassLoader();
Toolkit toolkit = Toolkit.getDefaultToolkit();
try { // Get the icon images from the resource files.
for (int i = 0; i < iconNames.length; i++) {
URL imageURL = classLoader.getResource("stamper icons/" + iconNames[i]);
if (imageURL == null)
throw new Exception();
iconImages[i] = toolkit.createImage(imageURL);
}
}
catch (Exception e) {
iconImages = null;
return null;
}
ImageIcon[] icons = new ImageIcon[iconImages.length];
for (int i = 0; i < iconImages.length; i++) // Create the icons.
icons[i] = new ImageIcon(iconImages[i]);
JList list = new JList(icons); // A list containing the image icons.
list.setSelectionMode(ListSelectionModel.SINGLE SELECTION);
list.setSelectedIndex(0); // First item in the list is currently selected.
return list;
}
By default, the user can select any number of items in a list. A single item is selected by
clicking on it. Multiple items can be selected by shift-clicking and by either control-clicking or
meta-clicking (depending on the platform). In the SillyStamper program, I wanted to restrict
the selection so that only one item can be selected at a time. This restriction is imposed by
calling
list.setSelectionMode(ListSelectionModel.SINGLE SELECTION);
With this selection mode, when the user selects an item, the previously selected item, if
any, is deselected. Note that the selection can be changed by the program by calling
list.setSelectedIndex(itemNum). Items are numbered starting from zero. To find out the
currently selected item in single selection mode, call list.getSelectedIndex(). This returns
12.4. COMPLEX COMPONENTS AND MVC 649
the item number of the selected item, or -1 if no item is currently selected. If multiple selec-
tions are allowed, you can call list.getSelectedIndices(), which returns an array of ints
that contains the item numbers of all selected items.
Now, the list that you see on the screen is only the view aspect of the list. The controller
consists of the listener objects that respond when the user clicks an item in the list. For its
model, a JList uses an object of type ListModel. This is the object that knows the actual list of
items. Now, a model is defined not only by the data that it contains but by the set of operations
that can be performed on the data. When a JList is constructed from an array of objects, the
model that is used is very simple. The model can tell you how many items it contains and what
those items are, but it can’t do much else. In particular, there is no way to add items to the
list or to delete items from the list. If you need that capability, you will have to use a different
list model.
The class DefaultListModel defines list models that support adding items to and removing
items from the list. (Note that the list model that you get when you create a JList from an
array is not of this type.) If dlmodel is of type DefaultListModel, the following methods, among
others, are defined:
• dlmodel.getSize() — returns the number of items.
• dlmodel.getElementAt(index) — returns the item at position index in the list.
• dlmodel.addElement(item) — Adds item to the end of the list; item can be any Object.
• dlmodel.insertElementAt(item, index) — inserts the specified item into the list at
the specified index; items that come after that position in the list are moved down to
make room for the new item.
• dlmodel.setElementAt(item, index) — Replaces the item that is currently at position
index in the list with item.
• dlmodel.remove(index) — removes the item at position index in the list.
• dlmodel.removeAllElements() — removes everything from the list, leaving it empty.
To use a modifiable JList, you should create a DefaultListModel, add any items to it that
should be in the list initially, and pass it to the JList constructor. For example:
DefaultListModel listModel; // Should probably be instance variables!
JList flavorList;
listModel = new DefaultListModel(); // Create the model object.
listModel.addElement("Chocolate"); // Add items to the model.
listModel.addElement("Vanilla");
listModel.addElement("Strawberry");
listModel.addElement("Rum Raisin");
flavorList = new JList(listModel); // Create the list component.
By keeping a reference to the model around in an instance variable, you will be able to add and
delete flavors as the program is running by calling the appropriate methods in listModel. Keep
in mind that changes that are made to the model will automatically be reflected in the view.
Behind the scenes, when a list model is modified, it generates an event of type ListDataEvent.
The JList registers itself with its model as a listener for these events, and it responds to an
event by redrawing itself to reflect the changes in the model. The programmer doesn’t have to
take any extra action, beyond changing the model.
650 CHAPTER 12. ADVANCED GUI PROGRAMMING
By the way, the model for a JList actually has another part in addition to the ListModel : An
object of type ListSelectionModel stores information about which items in the list are currently
selected. When the model is complex, it’s not uncommon to use several model objects to store
different aspects of the state.
}
The table will call this method and use the return value to decide how to display and edit
items in the table. For example, if a column is specified to hold Boolean values, the cells in that
column will be displayed and edited as check boxes. For numeric types, the table will not accept
illegal input when the user types in the value. (It is possible to change the way that a table
edits or displays items. See the methods setDefaultEditor() and setDefaultRenderer() in
the JTable class.)
As an alternative to using a subclass of DefaultTableModel, a custom table model can also
be defined using a subclass of AbstractTableModel. Whereas DefaultTableModel provides a lot
of predefined functionality, AbstractTableModel provides very little. However, using Abstract-
TableModel gives you the freedom to represent the table data any way you want. The sample
program ScatterPlotTableDemo.java uses a subclass of AbstractTableModel to define the model
for a JTable. In this program, the table has three columns. The first column holds a row
number and is not editable. The other columns hold values of type Double; these two columns
represent the x- and y-coordinates of points in the plane. The points themselves are graphed in
a “scatter plot” next to the table. Initially, the program fills in the first six points with random
values. Here is a picture of the program, with the x-coordinate in row 5 selected for editing:
Note, by the way, that in this program, the scatter plot can be considered to be a view of
the table model, in the same way that the table itself is. The scatter plot registers itself as a
listener with the model, so that it will receive notification whenever the model changes. When
that happens, the scatter plot redraws itself to reflect the new state of the model. It is an
important property of the MVC pattern that several views can share the same model, offering
alternative presentations of the same data. The views don’t have to know about each other
or communicate with each other except by sharing the model. Although I didn’t do it in this
program, it would even be possible to add a controller to the scatter plot view. This would let
the user drag a point in the scatter plot to change its coordinates. Since the scatter plot and
table share the same model, the values in the table would automatically change to match.
Here is the definition of the class that defines the model in the scatter plot program. All
the methods in this class must be defined in any subclass of AbstractTableModel except for
setValueAt(), which only has to be defined if the table is modifiable.
/**
12.4. COMPLEX COMPONENTS AND MVC 653
* This class defines the TableModel that is used for the JTable in this
* program. The table has three columns. Column 0 simply holds the
* row number of each row. Column 1 holds the x-coordinates of the
* points for the scatter plot, and Column 2 holds the y-coordinates.
* The table has 25 rows. No support is provided for adding more rows.
*/
private class CoordInputTableModel extends AbstractTableModel {
private Double[] xCoord = new Double[25]; // Data for Column 1.
private Double[] yCoord = new Double[25]; // Data for Column 2.
// Initially, all the values in the array are null, which means
// that all the cells are empty.
public int getColumnCount() { // Tells caller how many columns there are.
return 3;
}
public int getRowCount() { // Tells caller how many rows there are.
return xCoord.length;
}
public Object getValueAt(int row, int col) { // Get value from cell.
if (col == 0)
return (row+1); // Column 0 holds the row number.
else if (col == 1)
return xCoord[row]; // Column 1 holds the x-coordinates.
else
return yCoord[row]; // column 2 holds the y-coordinates.
}
public Class<?> getColumnClass(int col) { // Get data type of column.
if (col == 0)
return Integer.class;
else
return Double.class;
}
public String getColumnName(int col) { // Returns a name for column header.
if (col == 0)
return "Num";
else if (col == 1)
return "X";
else
return "Y";
}
public boolean isCellEditable(int row, int col) { // Can user edit cell?
return col > 0;
}
public void setValueAt(Object obj, int row, int col) {
// (This method is called by the system if the value of the cell
// needs to be changed because the user has edited the cell.
// It can also be called to change the value programmatically.
// In this case, only columns 1 and 2 can be modified, and the data
// type for obj must be Double. The method fireTableCellUpdated()
// has to be called to send an event to registered listeners to
654 CHAPTER 12. ADVANCED GUI PROGRAMMING
An HTML document can display links to other pages. When the user clicks on a link, the
web browser should go to the linked page. A JEditorPane does not do this automatically, but
it does generate an event of type HyperLinkEvent when the user clicks a link (provided that the
edit pane has been set to be non-editable by the user). A program can register a listener for
such events and respond by loading the new page.
There are a lot of web pages that a JEditorPane won’t be able to display correctly, but it
can be very useful in cases where you have control over the pages that will be displayed. A
nice application is to distribute HTML-format help and information files with a program. The
files can be stored as resource files in the jar file of the program, and a URL for a resource
file can be obtained in the usual way, using the getResource() method of a ClassLoader. (See
Subsection 12.1.3.)
It turns out, by the way, that SimpleWebBrowser.java is a little too simple. A modified
version, SimpleWebBrowserWithThread.java, improves on the original by using a thread to load
a page and by checking the content type of a page before trying to load it. It actually does
work as a simple web browser.
The model for a JTextComponent is an object of type Document. If you want to be notified
of changes in the model, you can add a listener to the model using
textComponent.getDocument().addDocumentListener(listener)
stopwatch function collected together neatly in one place. For more complicated components,
both of these considerations are very important.)
The StopWatchLabel class is not very hard to write. I need an instance variable to record
the time when the user starts the stopwatch. Times in Java are measured in milliseconds and
are stored in variables of type long (to allow for very large values). In the mousePressed()
method, I need to know whether the timer is being started or stopped, so I need a boolean
instance variable, running, to keep track of this aspect of the component’s state. There is
one more item of interest: How do I know what time the mouse was clicked? The method
System.currentTimeMillis() returns the current time. But there can be some delay between
the time the user clicks the mouse and the time when the mousePressed() routine is called.
To make my stopwatch as accurate as possible, I don’t want to know the current time. I want
to know the exact time when the mouse was pressed. When I wrote the StopWatchLabel class,
this need sent me on a search in the Java documentation. I found that if evt is an object of
type MouseEvent, then the function evt.getWhen() returns the time when the event occurred.
I call this function in the mousePressed() routine to determine the exact time when the user
clicked on the label. The complete StopWatch class is rather short:
import java.awt.event.*;
import javax.swing.*;
/**
* A custom component that acts as a simple stop-watch. When the user clicks
* on it, this component starts timing. When the user clicks again,
* it displays the time between the two clicks. Clicking a third time
* starts another timer, etc. While it is timing, the label just
* displays the message "Timing....".
*/
public class StopWatchLabel extends JLabel implements MouseListener {
private long startTime; // Start time of timer.
// (Time is measured in milliseconds.)
private boolean running; // True when the timer is running.
/**
* Constructor sets initial text on the label to
* "Click to start timer." and sets up a mouse listener
* so the label can respond to clicks.
*/
public StopWatchLabel() {
super(" Click to start timer. ", JLabel.CENTER);
addMouseListener(this);
}
/**
* Tells whether the timer is currently running.
*/
public boolean isRunning() {
return running;
}
/**
* React when the user presses the mouse by starting or stopping
12.4. COMPLEX COMPONENTS AND MVC 657
* the timer and changing the text that is shown on the label.
*/
public void mousePressed(MouseEvent evt) {
if (running == false) {
// Record the time and start the timer.
running = true;
startTime = evt.getWhen(); // Time when mouse was clicked.
setText("Timing....");
}
else {
// Stop the timer. Compute the elapsed time since the
// timer was started and display it.
running = false;
long endTime = evt.getWhen();
double seconds = (endTime - startTime) / 1000.0;
setText("Time: " + seconds + " sec.");
}
}
public void mouseReleased(MouseEvent evt) { }
public void mouseClicked(MouseEvent evt) { }
public void mouseEntered(MouseEvent evt) { }
public void mouseExited(MouseEvent evt) { }
}
Don’t forget that since StopWatchLabel is a subclass of JLabel, you can do anything with a
StopWatchLabel that you can do with a JLabel. You can add it to a container. You can set its
font, foreground color, and background color. You can set the text that it displays (although
this would interfere with its stopwatch function). You can even add a Border if you want.
Let’s look at one more example of defining a custom component. Suppose that—for no
good reason whatsoever—I want a component that acts like a JLabel except that it displays
its text in mirror-reversed form. Since no standard component does anything like this, the
MirrorText class is defined as a subclass of JPanel. It has a constructor that specifies the text to
be displayed and a setText() method that changes the displayed text. The paintComponent()
method draws the text mirror-reversed, in the center of the component. This uses techniques
discussed in Subsection 12.1.1 and Subsection 12.2.1. Information from a FontMetrics object
is used to center the text in the component. The reversal is achieved by using an off-screen
canvas. The text is drawn to the off-screen canvas, in the usual way. Then the image is copied
to the screen with the following command, where OSC is the variable that refers to the off-screen
canvas, and width and height give the size of both the component and the off-screen canvas:
g.drawImage(OSC, width, 0, 0, height, 0, 0, width, height, this);
This is the version of drawImage() that specifies corners of destination and source rect-
angles. The corner (0,0) in OSC is matched to the corner (width,0) on the screen, while
(width,height) is matched to (0,height). This reverses the image left-to-right. Here is the
complete class:
import java.awt.*;
import javax.swing.*;
import java.awt.image.BufferedImage;
/**
658 CHAPTER 12. ADVANCED GUI PROGRAMMING
As the loop is repeated, the point (x,y) changes. The question is, does (x,y) grow without
bound or is it trapped forever in a finite region of the plane? If (x,y) escapes to infinity (that
is, grows without bound), then the starting point (a,b) is not in the Mandelbrot set. If (x,y)
is trapped in a finite region, then (a,b) is in the Mandelbrot set. Now, it is known that if
x2 + y2 ever becomes strictly greater than 4, then (x,y) will escape to infinity. If x2 + y2
ever becomes bigger than 4 in the above loop, we can end the loop and say that (a,b) is not
in the Mandelbrot set. For a point (a,b) in the Mandelbrot set, this will never happen. When
we do this on a computer, of course, we don’t want to have a loop that runs forever, so we put
a limit on the number of times that the loop is executed:
x = a;
y = b;
12.5. FINISHING TOUCHES 661
count = 0;
while ( x*x + y*y < 4.1 ) {
count++;
if (count > maxIterations)
break;
double newX = x*x - y*y + a;
double newY = 2*x*y + b;
x = newY;
y = newY;
}
After this loop ends, if count is less than or equal to maxIterations, we can say that (a,b) is
not in the Mandelbrot set. If count is greater than maxIterations, then (a,b) might or might
not be in the Mandelbrot set (but the larger maxIterations is, the more likely that (a,b) is
actually in the set).
To make a picture from this procedure, use a rectangular grid of pixels to represent some
rectangle in the plane. Each pixel corresponds to some real number coordinates (a,b). (Use
the coordinates of the center of the pixel.) Run the above loop for each pixel. If the count goes
past maxIterations, color the pixel black; this is a point that is possibly in the Mandelbrot set.
Otherwise, base the color of the pixel on the value of count after the loop ends, using different
colors for different counts. In some sense, the higher the count, the closer the point is to the
Mandelbrot set, so the colors give some information about points outside the set and about
the shape of the set. However, it’s important to understand that the colors are arbitrary and
that colored points are not in the set. Here is a picture that was produced by the Mandelbrot
Viewer program using this computation. The black region is the Mandelbrot set:
When you use the program, you can “zoom in” on small regions of the plane. To do so, just
drag the mouse on the picture. This will draw a rectangle around part of the picture. When
you release the mouse, the part of the picture inside the rectangle will be zoomed to fill the
entire display. If you simply click a point in the picture, you will zoom in on the point where
you click by a magnification factor of two. (Shift-click or use the right mouse button to zoom
out instead of zooming in.) The interesting points are along the boundary of the Mandelbrot
662 CHAPTER 12. ADVANCED GUI PROGRAMMING
set. In fact, the boundary is infinitely complex. (Note that if you zoom in too far, you will
exceed the capabilities of the double data type; nothing is done in the program to prevent this.)
Use the “MaxIterations” menu to increase the maximum number of iterations in the loop.
Remember that black pixels might or might not be in the set; when you increase “MaxIter-
ations,” you might find that a black region becomes filled with color. The “Palette” menu
determines the set of colors that are used. Different palettes give very different visualizations
of the set. The “PaletteLength” menu determines how many different colors are used. In the
default setting, a different color is used for each possible value of count in the algorithm. Some-
times, you can get a much better picture by using a different number of colors. If the palette
length is less than maxIterations, the palette is repeated to cover all the possible values of
count; if the palette length is greater than maxIterations, only part of of the palette will be
used. (If the picture is of an almost uniform color, try decreasing the palette length, since that
makes the color vary more quickly as count changes. If you see what look like randomly colored
dots instead of bands of color, try increasing the palette length.)
If you run the Mandelbrot Viewer program as a stand-alone application, it will have a “File”
menu that can be used to save the picture as a PNG image file. You can also save a “param”
file which simply saves the settings that produced the current picture. A param file can be read
back into the program using the “Open” command.
The Mandelbrot set is named after Benoit Mandelbrot, who was the first person to note the
incredible complexity of the set. It is astonishing that such complexity and beauty can arise
out of such a simple algorithm.
One interesting point is that the contents of the menu bar are different, depending on
whether the program is being run as an applet or as a stand-alone application. Since applets
cannot access the file system, there is no “File” menu for an applet. Furthermore, accelerator
keys are generally not functional in an applet that is running on a web page, so accelerator
keys are only added to menu items if the program is being run in its own window. (See
Subsection 12.3.5 for information on accelerators.) To accomplish this, the constructor in the
Menus class has parameters that tell it whether the menu bar will be used by an applet and
whether it will be used in a frame; these parameters are consulted as the menu bar is being
built.
A third parameter to the constructor is the MandelbrotPanel that is being used in the
program. Many of the menu commands operate on this panel or on the MandelbrotDisplay that
it contains. In order to carry out these commands, the Menus object needs a reference to the
MandelbrotPanel. As for the MandelbrotDisplay, the panel has a method getDisplay() that
returns a reference to the display that it contains. So as long as the menu bar has a reference
to the panel, it can obtain a reference to the display. In previous examples, everything was
written as one large class file, so all the objects were directly available to all the code. When a
program is made up of multiple interacting files, getting access to the necessary objects can be
more of a problem.
MandelbrotPanel, MandelbrotDisplay, and Menus are the main classes that make up the
Mandelbrot Viewer program. MandelbrotFrame.java defines a simple subclass of JFrame that
runs the program in its own window. MandelbrotApplet.java defines an applet that runs the
program on a web page. (This applet version has an extra “Examples” menu that is discussed
in the source code file.) There are a few other classes that I will discuss below.
This brief discussion of the design of the Mandelbrot Viewer has shown that it uses a wide
variety of techniques that were covered earlier in this book. In the rest of this section, we’ll
look at a few new features of Java that were used in the program.
12.5.3 Internationalization
Internationalization refers to writing a program that is easy to adapt for running in different
parts of the world. Internationalization is often referred to as I18n, where 18 is the number of
letters between the “I” and the final “n” in “Internationalization.” The process of adapting the
program to a particular location is called localization, and the locations are called locales.
Locales differ in many ways, including the type of currency used and the format used for
numbers and dates, but the most obvious difference is language. Here, I will discuss how to
write a program so that it can be easily translated into other languages.
The key idea is that strings that will be presented to the user should not be coded into
the program source code. If they were, then a translator would have to search through the
entire source code, replacing every string with its translation. Then the program would have
to be recompiled. In a properly internationalized program, all the strings are stored together
in one or more files that are separate from the source code, where they can easily be found and
translated. And since the source code doesn’t have to be modified to do the translation, no
recompilation is necessary.
To implement this idea, the strings are stored in one or more properties files. A properties
file is just a list of key/value pairs. For translation purposes, the values are strings that will be
presented to the user; these are the strings that have to be translated. The keys are also strings,
but they don’t have to be translated because they will never be presented to the user. Since
they won’t have to be modified, the key strings can be used in the program source code. Each
12.5. FINISHING TOUCHES 665
key uniquely identifies one of the value strings. The program can use the key string to look up
the corresponding value string from the properties file. The program only needs to know the
key string; the user will only see the value string. When the properties file is translated, the
user of the program will see different value strings.
The format of a properties file is very simple. The key/value pairs take the form
key.string=value string
There are no spaces in the key string or before the equals sign. The value string can contain
spaces or any other characters. If the line ends with a backslash (“\”), the value string can be
continued on the next line; in this case, spaces at the beginning of that line are ignored. One
unfortunate detail is that a properties file can contain only plain ASCII characters. The ASCII
character set only supports the English alphabet. Nevertheless, a value string can include
arbitrary UNICODE characters. Non-ASCII characters just have to be specially encoded.
Sun Microsystems provides a program, native2ascii, that can convert files that use non-ASCII
characters into a form that is suitable for use as a properties file.
Suppose that the program wants to present a string to the user (as the name of a menu
command, for example). The properties file would contain a key/value pair such as
menu.saveimage=Save PNG Image...
where “Save PNG Image. . . ” is the string that will appear in the menu. The program would
use the key string, “menu.saveimage”, to look up the corresponding value string and would then
use the value string as the text of the menu item. In Java, the look up process is supported
by the ResourceBundle class, which knows how to retrieve and use properties files. Sometimes
a string that is presented to the user contains substrings that are not known until the time
when the program is running. A typical example is the name of a file. Suppose, for example,
that the program wants to tell the user, “Sorry, the file, filename, cannot be loaded”, where
filename is the name of a file that was selected by the user at run time. To handle cases like
this, value strings in properties files can include placeholders that will be replaced by strings
to be determined by the program at run time. The placeholders take the form “{0}”, “{1}”,
“{2}”, . . . . For the file error example, the properties file might contain:
error.cantLoad=Sorry, the file, {0}, cannot be loaded
The program would fetch the value string for the key error.cantLoad. It would then substitute
the actual file name for the placeholder, “{0}”. Note that when the string is translated, the
word order might be completely different. By using a placeholder for the file name, you can be
sure that the file name will be put in the correct grammatical position for the language that is
being used. Placeholder substitution is not handled by the ResourceBundle class, but Java has
another class, MessageFormat, that makes such substitutions easy.
For the Mandelbrot Viewer program, the properties file is strings.properties. (Any properties
file should have a name that ends in “.properties”.) Any string that you see when you run
the program comes from this file. For handling value string lookup, I wrote I18n.java. The
I18n class has a static method
public static tr( String key, Object... args )
that handles the whole process. Here, key is the key string that will be looked up in
strings.properties. Additional parameters, if any, will be substituted for placeholders in
the value string. (Recall that the formal parameter declaration “Object...” means that there
can be any number of actual parameters after key; see Subsection 7.2.6.) Typical uses would
include:
666 CHAPTER 12. ADVANCED GUI PROGRAMMING
The Mandelbrot Viewer program responds to mouse events on the display. These events
are generated by the display object, but the display class itself doesn’t care about mouse events
and doesn’t do anything in response to them. Mouse events are handled by a listener in
the MandelbrotPanel, which responds to them by zooming the display and by showing mouse
coordinates in the status bar.
The status bar also shows the new size of the display whenever that size is changed. To
handle this, events of type ComponentEvent are used. When the size of a component is changed,
a ComponentEvent is generated. In the Mandelbrot Viewer program, a ComponentListener in
the MandelbrotPanel class listens for size-change events in the display. When one occurs, the
listener responds by showing the new size in the status bar; the display knows nothing about
the status bar that shows the display’s size.
Component events are also used internally in the MandelbrotDisplay class in an interesting
way. When the user dynamically changes the size of the display, its size can change several
times each second. Normally, a change of display size would trigger the creation of a new off-
screen canvas and the start of a new asynchronous computation of the image. However, doing
this is a big deal, not something I want to do several times in a second. If you try resizing the
program’s window, you’ll notice that the image doesn’t change size dynamically as the window
size changes. The same image and off-screen canvas are used as long as the size is changing.
Only about one-third of a second after the size has stopped changing will a new, resized image
be produced. Here is how this works: The display sets up a ComponentEvent to listen for resize
events on itself. When a resize occurs, the listener starts a Timer that has a delay of 1/3 second.
(See Subsection 6.5.1.) While this timer is running, the paintComponent() method does not
resize the image; instead, it reuses the image that already exists. If the timer fires 1/3 second
later, the image will be resized at that time. However, if another resize event occurs while the
first timer is running, then the first timer will be stopped before it has a chance to fire, and a
new timer will be started with a delay of 1/3 second. The result is that the image does not get
resized until 1/3 second after the size of the window stops changing.
The Mandelbrot Viewer program also uses events of type WindowEvent, which are generated
by a window when it opens or closes (among other things). One example is in the file Launcher-
Applet.java. This file defines an applet that appears as a button on the web page. The button
is labeled “Launch Mandelbrot Viewer”. When the user clicks the button, a MandelbrotFrame
is opened on the screen, and the text on the button changes to “Close Mandelbrot Viewer”.
When the frame closes, the button changes back to “Launch Mandelbrot Viewer”, and the but-
ton can be used to open another window. The frame can be closed by clicking the button, but
it can also be closed using a “Close” command in the frame’s menu bar or by clicking the close
box in the frame’s title bar. The question is, how does the button’s text get changed when the
frame is closed by one of the latter two methods? One possibility would be to have the frame
call a method in the applet to tell the applet that it is closing, but that would tightly couple
the frame class to the applet class. In fact, it’s done with WindowEvents. A WindowListener in
the applet listens for close events from the frame. In response to a close event, the text of the
button is changed. Again, this can happen even though the frame class knows nothing about
the applet class. Window events are also used by Main.java to trigger an action that has to be
taken when the program is ending; this will be discussed below.
Perhaps the most interesting use of events in the Mandelbrot Viewer program is to enable
and disable menu commands based on the status of the display. For this, events of type Prop-
ertyChangeEvent are used. This event class is part of the “bean” framework that was discussed
briefly in Subsection 11.6.2, and class PropertyChangeEvent and related classes are defined in
668 CHAPTER 12. ADVANCED GUI PROGRAMMING
the package java.beans. The idea is that bean objects are defined by their “properties” (which
are just aspects of the state of the bean). When a bean property changes, the bean can emit
a PropertyChangeEvent to notify other objects of the change. Properties for which property
change events are emitted are known technically as bound properties. A bound property has
a name that identifies that particular property among all the properties of the bean. When
a property change event is generated, the event object includes the name of the property that
has changed, the previous value of the property, and the new value of the property.
The MandelbrotDisplay class has a bound property whose name is given by the constant
MandelbrotDisplay.STATUS PROPERTY. A display emits a property change event when its sta-
tus changes. The possible values of the status property are given by other constants, such
as MandelbrotDisplay.STATUS READY. The READY status indicates that the display is not cur-
rently running a computation and is ready to do another one. There are several menu commands
that should be enabled only when the status of the display is READY. To implement this, the
Menus class defines a PropertyChangeListener to listen for property change events from the dis-
play. When this listener hears an event, it responds by enabling or disabling menu commands
according to the new value of the status property.
All of Java’s GUI components are beans and are capable of emitting property change events.
In any subclass of Component, this can be done simply by calling the method
public void firePropertyChange(String propertyName,
Object oldValue, Object newValue)
For example, the MandelbrotDisplay class uses the following method for setting its current status:
private void setStatus(String status) {
if (status == this.status) {
// Note: Event should be fired only if status actually changes.
return;
}
String oldStatus = this.status;
this.status = status;
firePropertyChange(STATUS PROPERTY, oldStatus, status);
}
When writing bean classes from scratch, you have to add support for property change events,
if you need them. To make this easier, the java.beans package provides the PropertyChange-
Support class.
Dialog boxes can be either modal or modeless. When a modal dialog is put up on the
screen, the rest of the application is blocked until the dialog box is dismissed. This is the most
common case, and all the standard dialog boxes are modal. Modeless dialog boxes are more
like independent windows, since they can stay on the screen while the user interacts with other
windows. There are no modeless dialogs in the Mandelbrot Viewer program.
The Mandelbrot Viewer program uses two custom dialog boxes. They are used to implement
the “Set Image Size” and “Set Limits” commands and are defined by the files SetImageSizeDi-
alog.java and SetLimitsDialog.java. The “set image size” dialog lets the user enter a new width
and height for the Mandelbrot image. The “set limits” dialog lets the user input the minimum
and maximum values for x and y that are shown in the image. The two dialog classes are very
similar. In both classes, several JTextFields are used for user input. Two buttons named “OK”
and “Cancel” are added to the window, and listeners are set up for these buttons. If the user
clicks “OK”, the listener checks whether the inputs in the text fields are legal; if not, an error
message is displayed to the user and the dialog stays on the screen. If the input is legal when the
user clicks “OK”, the dialog is disposed. The dialog is also disposed if the user clicks “Cancel”
or clicks the dialog box’s close box. The net effect is that the dialog box stays on the screen
until the user either cancels the dialog or enters legal values for the inputs and clicks “OK”.
The user can find out which of these occurred by calling a method named getInput() in the
dialog object after showing the dialog. This method returns null if the dialog was canceled;
otherwise it returns the user input.
To make my custom dialog boxes easy to use, I added a static showDialog() method to
each dialog class. When this function is called, it shows the dialog, waits for it to be dismissed,
and then returns the value of the getInput() method. This makes it possible to use my custom
dialog boxes in much the same way as Java’s standard dialog boxes are used.
Custom dialog boxes are not difficult to create and to use, if you already know about frames.
I will not discuss them further here, but you can look at the source code file SetImageSizeDia-
log.java as a model.
12.5.6 Preferences
Most serious programs allow the user to set preferences. A preference is really just a piece of
the program’s state that is saved between runs of the program. In order to make preferences
persistent from one run of the program to the next, the preferences could simply be saved to a
file in the user’s home directory. However, there would then be the problem of locating the file.
There would be the problem of naming the file in a way that avoids conflicts with file names
used by other programs. And there would be the problem of cluttering up the user’s home
directory with files that the user shouldn’t even have to know about.
To deal with these problems, Java has a standard means of handling preferences. It is
defined by the package java.util.prefs. In general, the only thing that you need from this
package is Preferences.
In the Mandelbrot Viewer program, the file Main.java has an example of using Preferences.
Main.java runs the stand-alone application version of the program, and its use of preferences
applies only when the program is run in that way.
In most programs, the user sets preferences in a custom dialog box. However, the Man-
delbrot program doesn’t have any preferences that are appropriate for that type of treatment.
Instead, as an example, I automatically save a few aspects of the program’s state as preferences.
Every time the program starts up, it reads the preferences, if any are available. Every time
the program terminates, it saves the preferences. (Saving the preferences poses an interesting
670 CHAPTER 12. ADVANCED GUI PROGRAMMING
problem because the program ends when the MandelbrotFrame window closes, not when the
main() routine ends. In fact, the main() routine ends as soon as the window appears on the
screen. So, it won’t work to save the preferences at the end of the main program. The solution
is to use events: A listener listens for WindowEvents from the frame. When a window-closed
event is received, indicating that the program is ending, the listener saves the preferences.)
Preferences for Java programs are stored in some platform-dependent form in some platform-
dependent location. As a Java programmer, you don’t have to worry about it; the Java pref-
erences system knows where to store the data. There is still the problem of identifying the
preferences for one program among all the possible Java programs that might be running on a
computer. Java solves this problem in the same way that it solves the package naming prob-
lem. In fact, by convention, the preferences for a program are identified by the package name of
the program, with a slight change in notation. For example, the Mandelbrot Viewer program
is defined in the package edu.hws.eck.mdb, and its preferences are identified by the string
“/edu/hws/eck/mdb”. (The periods have been changed to “/”, and an extra “/” has been
added at the beginning.)
The preferences for a program are stored in something called a “node.” The user preferences
node for a given program identifier can be accessed as follows:
Preferences root = Preferences.userRoot();
Preferences node = root.node(pathName);
where pathname is the string, such as “/edu/hws/eck/mdb”, that identifies the node. The
node itself consists of a simple list of key/value pairs, where both the key and the value are
strings. You can store any strings you want in preferences nodes—they are really just a way of
storing some persistent data between program runs. In general, though, the key string identifies
some particular preference item, and the associated value string is the value of that preference.
A Preferences object, node, contains methods node.get(key) for retrieving the value string
associated with a given key and node.put(key,value) for setting the value string for a given
key.
In Main.java, I use preferences to store the shape and position of the program’s window.
This makes the size and shape of the window persistent between runs of the program; when
you run the program, the window will be right where you left it the last time you ran it. I also
store the name of the directory that is currently selected in the file dialog box that is used by
the program for the Save and Open commands. This is particularly satisfying, since the default
behavior for a file dialog box is to start in the user’s home directory, which is hardly ever the
place where the user wants to keep a program’s files. With the preferences feature, I can switch
to the right directory the first time I use the program, and from then on I’ll automatically be
back in that directory when I use the program. You can look at the source code in Main.java
for the details.
∗ ∗ ∗
And that’s it. . . . There’s a lot more that I could say about Java and about programming
in general, but this book is only “An Introduction to Programming with Java,” and it’s time
for our journey to end. I hope that it has been a pleasant journey for you, and I hope that I
have helped you establish a foundation that you can use as a basis for further exploration.
Exercises 671
2. For this exercise, you should continue to work on the program from the previous exercise.
Add a “StrokeWidth” menu that allows the user to draw lines of varying thicknesses.
Make it possible to use different colors for the interior of a filled shape and for the outline
of that shape. To do this, change the “Color” menu to “StrokeColor” and add a “Fill-
Color” menu. (My solution adds two new tools, “Stroked Filled Rectangle” and “Stroked
Filled Oval”, to represent filled shapes that are outlined with the current stroke.) Add
support for filling shapes with transparent color. A simple approach to this is to use a
JCheckboxMenuItem to select either fully opaque or 50% opaque fill. (Don’t try to apply
transparency to stokes—it’s difficult to make transparency work correctly for the Curve
tool, and in any case, shape outlines look better if they are opaque.) Finally, make the
menus more user friendly by adding some keyboard accelerators to some commands and
by using JRadioButtonMenuItems where appropriate, such as in the color and tool menus.
This exercise takes quite a bit of work to get it all right, so you should tackle the problem
in pieces.
3. The StopWatchLabel component from Subsection 12.4.5 displays the text “Timing. . . ”
when the stop watch is running. It would be nice if it displayed the elapsed time since
the stop watch was started. For that, you need to create a Timer. (See Subsection 6.5.1.)
Add a Timer to the original source code, StopWatchLabel.java, to drive the display of the
elapsed time in seconds. Create the timer in the mousePressed() routine when the stop
watch is started. Stop the timer in the mousePressed() routine when the stop watch
is stopped. The elapsed time won’t be very accurate anyway, so just show the integral
number of seconds. You only need to set the text a few times per second. For my Timer
method, I use a delay of 200 milliseconds for the timer.
4. The custom component example MirrorText.java, from Subsection 12.4.5, uses an off-
screen canvas to show mirror-reversed text in a JPanel. An alternative approach would
be to draw the text after applying a transform to the graphics context that is used for
drawing. (See Subsection 12.2.5.) With this approach, the custom component can be
defined as a subclass of JLabel in which the paintComponent() method is overridden.
Write a version of the MirrorText component that takes this approach. The solution is
very short, but tricky. Note that the scale transform g2.scale(-1,1) does a left-right
reflection through the left edge of the component.
5. The sample program PhoneDirectoryFileDemo.java from Subsection 11.3.2 keeps data for
a “phone directory” in a file in the user’s home directory. Exercise 11.5 asked you to
672 CHAPTER 12. ADVANCED GUI PROGRAMMING
revise that program to use an XML format for the data. Both programs have a simple
command-line user interface. For this exercise, you should provide a GUI interface for the
phone directory data. You can base your program either on the original sample program
or on the modified version from the exercise. Use a JTable to hold the data. The user
should be able to edit all the entries in the table. Also, the user should be able to add and
delete rows. Include either buttons or menu commands that can be used to perform these
actions. The delete command should delete the selected row, if any. New rows should be
added at the end of the table. For this program, you can use a standard DefaultTableModel.
Your program should load data from the file when it starts and save data to the file
when it ends, just as the two previous programs do. For a GUI program, you can’t simply
save the data at the end of the main() routine, since main() terminates as soon as the
window shows up on the screen. You want to save the data when the user closes the window
and ends the program. There are several approaches. One is to use a WindowListener to
detect the event that occurs when the window closes. Another is to use a “Quit” command
to end the program; when the user quits, you can save the data and close the window (by
calling its dispose() method), and end the program. If you use the “Quit” command
approach, you don’t want the user to be able to end the program simply by closing the
window. To accomplish this, you should call
frame.setDefaultCloseOperation(JFrame.DO NOTHING ON CLOSE);
where frame refers to the JFrame that you have created for the program’s user interface.
When using a WindowListener, you want the close box on the window to close the window,
not end the program. For this, you need
frame.setDefaultCloseOperation(JFrame.DISPOSE ON CLOSE);
When the listener is notified of a window closed event, it can save the data and end the
program.
Most of the JTable and DefaultTableModel methods that you need for this exercise are
discussed in Subsection 12.4.3, but there are a few more that you need to know about. To
determine which row is selected in a JTable, call table.getSelectedRow(). This method
returns the row number of the selected row, or returns -1 if no row is selected. To specify
which cell is currently being edited, you can use:
table.setRowSelectionInterval(rowNum, rowNum); // Selects row number rowNum.
table.editCellAt( rowNum, colNum ); // Edit cell at position (rowNum,colNum).
phoneTable.getEditorComponent().requestFocus(); // Put input cursor in cell.
One particularly troublesome point is that the data that is in the cell that is currently
being edited is not in the table model. The value in the edit cell is not put into the table
model until after the editing is finished. This means that even though the user sees the
data in the cell, it’s not really part of the table data yet. If you lose that data, the user
would be justified in complaining. To make sure that you get the right data when you
save the data at the end of the program, you have to turn off editing before retrieving the
data from the model. This can be done with the following method:
private void stopEditing() {
if (table.getCellEditor() != null)
table.getCellEditor().stopCellEditing();
}
This method must also be called before modifying the table by adding or deleting rows; if
such modifications are made while editing is in progress, the effect can be very strange.
Quiz 673
Quiz on Chapter 12
1. Describe the object that is created by the following statement, and give an example of
how it might be used in a program:
BufferedImage OSC = new BufferedImage(32,32,BufferedImage.TYPE INT RGB);
2. Many programs depend on resource files. What is meant by a resource in this sense? Give
an example.
5. What is antialiasing?
7. What does the acronym MVC stand for, and how does it apply to the JTable class?
10. Suppose that the class that you are writing has an instance method doOpen() (with no
parameters) that is meant to be used to open a file selected by the user. Write a code
segment that creates an Action that represents the action of opening a file. Then show
how to create a button and a menu item from that action.
674 CHAPTER 12. ADVANCED GUI PROGRAMMING
Appendix: Source Files
This section contains a list of the examples appearing in the free, on-line textbook In-
troduction to Programming Using Java, Version 5.1. You should be able to compile these files
and use them. Note however that some of these examples depend on other source files, such as
TextIO.java and MosaicPanel.java, that are not built into Java. These are classes that I have
written. Links to all necessary non-standard source code files are provided below.
To use examples that depend on my classes, you will need to compile the source code for the
required classes and place the compiled classes in the same directory with the main class file.
If you are using an integrated development environment such as Eclipse, you can simply add
any required source code files to your project.
Most of the solutions to end-of-chapter exercises are not listed in this section. Each end-
of-chapter exercise has its own Web page, which discusses its solution. The source code of a
sample solution of each exercise is given in full on the solution page for that exercise. If you
want to compile the solution, you should be able to cut-and-paste the solution out of a Web
browser window and into a text editing program. (You can’t cut-and-paste from the HTML
source of the solution page, since it contains extra HTML markup commands that the Java
compiler won’t understand; the HTML markup does not appear when the page is displayed in
a Web browser.)
Note that many of these examples require Java version 5.0 or later. Some of them were
written for older versions, but will still work with current versions. When you compile some of
these older programs with current versions of Java, you might get warnings about “deprecated”
methods. These warnings are not errors. When a method is deprecated, it means that it should
not be used in new code, but it has not yet been removed from the language. It is possible that
deprecated methods might be removed from the language at some future time, but for now you
just get a warning about using them.
675
676 Source Code Listing
• ReverseWithDynamicArray.java, from Section 7.3, reads numbers from the user then
prints them out in reverse order. It does this using the class DynamicArrayOfInt.java
as an example of using dynamic arrays.
• LengthConverter2.java, from Section 8.2, converts measurements input by the user to
inches, feet, yards, and miles. This improvement on LengthConverter.java allows inputs
combining several measurements, such as “3 feet 7 inches,” and it detects illegal inputs.
• LengthConverter3.java, from Section 8.3, a revision of the previous example that uses
exceptions to handle errors in the user’s input.
• ThreadTest1.java, from Section 8.5, runs several threads all computing the same task to
test whether there is any speedup when more than one thread is used.
• ThreadTest2.java, from Section 8.5, divides a task among several threads and combines the
results from all the threads. Also shows how to wait for all threads to finish. And Thread-
Test3.java from the same section performs the same task but uses the producer/consumer
pattern for communication between threads.
• TowersOfHanoi.java, from Section 9.2, prints out the steps in a solution to the Towers of
Hanoi problem; an example of recursion.
• StringList.java, from Section 9.2, implements a linked list of strings. The program List-
Demo.java tests this class.
• PostfixEval.java, from Section 9.3, evaluates postfix expressions using a stack. Depends
on the StackOfDouble class defined in StackOfDouble.java.
• SortTreeDemo.java, from Section 9.4, demonstrates a binary sort tree of strings.
• SimpleParser1.java, from Section 9.5, evaluates fully parenthesized expressions input by
the user.
• SimpleParser2.java, from Section 9.5, evaluates ordinary infix expressions input by the
user.
• SimpleParser3.java, from Section 9.5, reads infix expressions input by the user and con-
structs expression trees that represent those expressions.
• WordListWithTreeSet.java, from Section 10.2, makes an alphabetical list of words from a
file. A TreeSet is used to eliminate duplicates and sort the words.
• SimpleInterpreter.java, from Section 10.4, demonstrates the use of a HashMap as a symbol
table in a program that interprets simple commands from the user.
• WordCount.java, from Section 10.4, counts the number of occurrences of each word in a
file. The program uses several features from the Java Collection Framework.
• ReverseFile.java, from Section 11.2, shows how to read and write files in a simple
command-line application; uses the non-standard class TextReader.java.
• DirectoryList.java, from Section 11.2, lists the contents of a directory specified by the user;
demonstrates the use of the File class.
• CopyFile.java, from Section 11.3, is a program that makes a copy of a file, using file names
that are given as command-line arguments.
• PhoneDirectoryFileDemo.java, from Section 11.3, demonstrates the use of a file for storing
data between runs of a program.
• ShowMyNetwork.java, mentioned in Section 11.4, is a short program that prints informa-
tion about each network interface on the computer where it is run, including IP addresses
associated with each interface.
678 Source Code Listing
• DateClient.java and DateServer.java, from Section 11.4, are very simple first examples of
network client and server programs.
• CLChatClient.java and CLChatServer.java, from Section 11.4, demonstrate two-way com-
munication over a network by letting users send messages back and forth; however, no
threading is used and the messages must strictly alternate.
• CLMandelbrotMaster.java, CLMandelbrotWorker.java, and CLMandelbrotTask.java, from
Section 11.5, are a demonstration of distributed computing in which pieces of a large
computation are sent over a network to be computed by “worker” programs.
• RandomStringsApplet.java, from Section 6.3, shows 25 copies of the string “Java!” (or
some other string specified in an applet param) in random colors and fonts. The applet
uses RandomStringsPanel.java for its content pane, and there is a stand-alone application
RandomStringsApp.java that uses the same panel class.
• ClickableRandomStringsApp.java, from Section 6.4, is similar to RandomStringsApp.java
except that the window is repainted when the user clicks the window. This is a first exam-
ple of using a mouse listener. The applet version is ClickableRandomStringsApplet.java.
• SimpleStamper.java, from Section 6.4, lets the user place rectangles and ovals on a drawing
area by clicking with the mouse. The applet version is SimpleStamperApplet.java. Both
versions use SimpleStamperPanel.java for their content panes.
• SimpleTrackMouse.java, from Section 6.4, shows information about mouse events. The
applet version is SimpleTrackMouseApplet.java. Both versions use SimpleTrackMousePa-
nel.java for their content panes.
• SimplePaint.java, from Section 6.4, lets the user draw curves in a drawing area and select
the drawing color from a palette. The class SimplePaint can be used either as as applet or
as a stand-alone application.
• RandomArtPanel.java, from Section 6.5, shows a new random “artwork” every four sec-
onds. This is an example of using a Timer. Used in an applet version of the program,
RandomArtApplet.java, and a stand-alone application version, RandomArt.java.
• KeyboardAndFocusDemo.java, from Section 6.5, shows how to use keyboard and focus
events. This class can be run either has an applet or as a stand-alone application.
• SubKillerPanel.java, from Section 6.5, lets the user play a simple game. Uses a timer as
well as keyboard and focus events. The applet version is SubKillerApplet.java, and the
stand-alone application version is SubKiller.java.
• SliderDemo.java, a simple demo from Section 6.6, is an applet that shows three JSliders.
• TextAreaDemo.java, from Section 6.6, is an applet that demonstrates a JTextArea in a
JScrollPane.
• BorderDemo.java, from Section 6.7, a very simple applet that demonstrates six types of
border.
• SliderAndComboBoxDemo.java, from Section 6.7, shows how to create several components
and lay them out in a GridLayout. Can be used either as an applet or as a stand-alone
application.
• SimpleCalc.java, from Section 6.7, lets the user add, subtract, multiply, or divide two num-
bers input by the user. A demo of text fields, buttons, and layout with nested subpanels.
Can be used either as an applet or as a stand-alone application.
• NullLayoutDemo.java, from Section 6.7, shows how to lay out the components in a con-
tainer for which the layout manager has been set to null. Can be used either as an applet
or as a stand-alone application.
• HighLowGUI.java, from Section 6.7, is a GUI version of HighLow.java, a game where the
user sees a playing card and guesses whether the next card will be higher or lower in value.
This program also requires Card.java, Hand.java, and Deck.java. Can be used as a stand-
alone application and also contains a nested class HighLowGUI.Applet that represents the
applet version of the program
• MosaicDrawController.java, from Section 6.8, demonstrates menus and a color chooser
dialog. This is used in a program where the user colors the squares of a mosaic by
680 Source Code Listing
clicking-and-dragging the mouse. It uses MosaicPanel.java to define the mosaic panel it-
self. MosaicDrawController is used in the stand-alone application MosaicDrawFrame.java,
in the applet MosaicDrawApplet.java, and in the applet MosaicDrawLauncherApplet.java.
The latter applet appears as a button on a web page; clicking the button opens a Mosaic-
DrawFrame.
• SimpleDialogDemo.java, from Section 6.8, is an applet that demonstrates JColorChooser
and some dialogs from JOptionPane.
• RandomStringsWithArray.java, from Section 7.2, shows 25 copies of a message in random
colors, sizes, and positions. This is an improved version of RandomStringsPanel.java that
uses an array to keep track of the data, so that the same picture can be redrawn whenever
necessary. (Written only as an applet.)
• SimpleDrawRects.java, from Section 7.3, lets the user place colored rectangles in a drawing
area. Illustrates the use of an ArrayList. An applet version is SimpleDrawRectsApplet.java.
This program uses and depends on RainbowPalette.java.
• SimplePaint2.java, from Section 7.3, lets the user draw colored curves and stores the data
needed for repainting the drawing surface in a list of type ArrayList<CurveData>.
• Checkers.java, from Section 7.5, lets two users play a game of checkers against each other.
Illustrates the use of a two-dimensional array. (This is the longest program in the book
so far, at over 750 lines!)
• Blobs.java, from Section 9.1, recursively counts groups of colored squares in a grid.
• DepthBreadth.java, from Section 9.3, demonstrates stacks and queues.
• TrivialEdit.java, from Section 11.3, lets the user edit short text files. This program demon-
strates reading and writing files and using file dialogs.
• SimplePaintWithFiles.java, from Section 11.3, demonstrates saving data from a program
to a file in both binary and character form. The program is a simple sketching program
based on SimplePaint2.java.
• ChatSimulation.java, from Section 11.5 (on-line version only), is an applet that simulates
a network chat. There is no networking in this applet. The only point is to demonstrate
how a thread could be used to process (simulated) incoming messages.
• GUIChat.java, from Section 11.5, is a simple GUI program for chatting between two people
over a network.
• BuddyChat.java, BuddyChatServer.java, BuddyChatWindow.java, and BuddyChatServer-
Shutdown.java, from Section 11.5, represent a multithreaded server and a client program
for the service that it offers. The BuddyChatServer program is a non-GUI server that
keeps a list of connected clients. These clients are available to chat to other clients. The
client program is BuddyChat. Each client connects to the server and gets a list of other
clients that are connected. A BuddyChat user can request a connection to one of the
other clients in the list; when a connection is made, a pair of BuddyChatWindows is used
for chatting between the two clients. (The server has no part in the chat connections.)
BuddyChatServerShutdown can be used to shut down the server cleanly. This example
is not scalable; that is, it should only be used for fairly small numbers of clients. There
is absolutely no defense against denial of service attacks, such as someone opening a very
large number of connections. It is just an example of basic client/server programming
using threads.
Source Code Listing 681
• XMLDemo.java, from Section 11.6, is a simple program that demonstrates basic parsing
of an XML document and traversal of the Document Object Model representation of
the document. The user enters the XML to be parsed in a text area. The nested class
XMLDemo.XMLDemoApplet runs the program as an applet.
• SimplePaintWithXML.java and SimplePaintWithXMLEncoder.java, from Section 11.6,
demonstrate saving data from a program to a file in XML format. The first program uses
an invented XML language, while the second uses an XMLEncoder for writing files and
an XMLDecoder for reading files. These programs are modifications of SimplePaintWith-
Files.java.
• HighLowWithImages.java, from Section 12.1, is a variation of HighLowGUI.java that takes
playing card images from an image file. Requires the image file cards.png and depends on
Card.java, Deck.java, and Hand.java.
• PaintWithOffScreenCanvas.java, from Section 12.1, is a little paint program that illus-
trates the use of a BufferedImage as an off-screen canvas.
• SoundAndCursorDemo.java, from Section 12.1, lets the user play a few sounds and change
the cursor by clicking some buttons. This demonstrates using audio resource files and using
an image resource to create a custom cursor. Requires the resource files in the directory
snc resources.
• TransparencyDemo.java, from Section 12.2, demonstrates using the alpha component of
colors. It is also an example of using FontMetrics.
• StrokeDemo.java, from Section 12.2, demonstrates the use of various BasicStrokes for draw-
ing lines and rectangles. Also demonstrates antialiasing.
• PaintDemo.java, from Section 12.2, demonstrates using a GradientPaint and using a Tex-
turePaint to fill a polygon. Uses the image resource files TinySmiley.png and QueenOf-
Hearts.png.
• RadioButtonDemo.java, from Section 12.3, does what its name indicates.
• ToolBarDemo.java, from Section 12.3, uses a JToolBar that holds a group of 3 radio
buttons and a push button. All the buttons use custom icons, and the push button is
created from an Action.
• SillyStamper.java, from Section 12.4, demonstrates using a JList of Icons. The user can
“stamp” images of a selected icon onto a drawing area. This program uses the icon images
in the directory stamper icons as resources.
• StatesAndCapitalsTableDemo.java, from Section 12.4, is a completely trivial demo of a
JTable.
• ScatterPlotTableDemo.java, from Section 12.4, uses a TableModel to customize a JTable.
The table is a list of xy-points that the user can edit. A scatter plot of the points is
displayed.
• SimpleWebBrowser.java and SimpleWebBrowserWithThread.java, from Section 12.4, im-
plement a simple web browser using JEditorPane (which is ridiculously easy). The dif-
ference between the programs is that the first loads documents synchronously, which can
hang the program in an unpleasant way, while the second uses a thread to load documents
asynchronously.
• SimpleRTFEdit.java, mentioned but just barely discussed in Section 12.4, lets the user
edit RTF files, which are text files in a format that include style information such as bold
and italics. This is mainly a simple demo of using Actions defined by “editor kits.”
682 Source Code Listing
• StopWatchLabel.java and MirrorText.java, from Section 12.4, are classes that implement
custom components. CustomComponentTest.java is a program that tests them.
• The Mandelbrot program from Section 12.5, which computes and displays visualizations
of the Mandelbrot set, is defined by several classes in the package edu.hws.eck.mdb. The
source code files can be found in the directory edu/hws/eck/mdb.
1. Moire.java, an animated design, shown at the end of Section 1.7. You can use applet
parameters to control various aspects of this applet’s behavior. Also note that you can
click on the applet and drag the pattern around by hand. See the source code for details.
2. JavaPops.java, from Section 2.6 is a simple animation that shows copies of the string
“Java!” in various sizes and colors appearing and disappearing. This is an old applet that
depends on an old animation framework named SimpleAnimationApplet.java
3. MovingRects.java shows a simple animation of rectangles continuously shrinking towards
the center of the applet. This is also a programming example in Section 3.8. It depends
on SimpleAnimationApplet2.java.
4. RandomBrighten.java, showing a grid of colored squares that get more and more red as a
wandering disturbance visits them, from the end of Section 4.7. Depends on MosaicCan-
vas.java
5. SymmetricBrighten.java, a subclass of the previous example that makes a symmetric
pattern, from the end of Section 5.7. Depends on MosaicCanvas.java and Random-
Brighten.java.
6. TrackLines.java, an applet with lines that track the mouse, from Section 6.8.
7. KaleidoAnimate.java, from Section 7.5, shows moving kaleidoscopic images.
8. SimpleCA.java, a Cellular Automaton applet, from the end of Section 8.4. This ap-
plet depends on the file CACanvas.java For more information on cellular automata see
http://math.hws.edu/xJava/CA/.
9. TowersOfHanoiGUI.java, from Section 9.5, graphically shows the solution to the Towers
of Hanoi problem with 10 disks.
10. LittlePentominosApplet.java, from Section 10.5, solves pentominos puzzles using a simple
recursive backtracking algorithm. This applet depends on MosaicPanel.java. For a much
more complex Pentominos applet, see http://math.hws.edu/xJava/PentominosSolver/.
11. Maze.java, an applet that creates a random maze and solves it, from Section 11.6.
12. The applet at the end of Section 12.5 is the same Mandelbrot program that is discussed
as an example in that section, with source files in the directory edu/hws/eck/mdb.
Source Code Listing 683