java_gui
--------------------------------------------------------------------------------
Positioning a Frame
The JFrame class itself has only a few methods for changing how frames look. Of course, through the magic of inheritance, most of the methods for working with the size and position of a frame come from the various superclasses of JFrame. Among the most important methods are the following ones:
The dispose method that closes the window and reclaims any system resources used in creating it;
The setIconImage method, which takes an Image object to use as the icon when the window is minimized (often called iconized in Java terminology);
The setTitle method for changing the text in the title bar;
The setResizable method, which takes a boolean to determine if a frame will be resizeable by the user.
Figure 7-5 illustrates the inheritance hierarchy for the JFrame class.
Figure 7-5. Inheritance hierarchy for the JFrame and JPanel classes
As the API notes indicate, the Component class (which is the ancestor of all GUI objects) and the Window class (which is the superclass of the Frame class) are where you need to look to find the methods to resize and reshape frames. For example, the setLocation method in the Component class is one way to reposition a component. If you make the call
setLocation(x, y)
the top-left corner is located x pixels across and y pixels down, where (0, 0) is the top-left corner of the screen. Similarly, the setBounds method in Component lets you resize and relocate a component (in particular, a JFrame) in one step, as
setBounds(x, y, width, height)
NOTE
For a frame, the coordinates of the setLocation and setBounds are taken relative to the whole screen. As you will see in Chapter 9, for other components inside a container, the measurements are taken relative to the container.
Remember: if you don't explicitly size a frame, all frames will default to being 0 by 0 pixels. To keep our example programs simple, we resize the frames to a size that we hope works acceptably on most displays. However, in a professional application, you should check the resolution of the user's screen and write code that resizes the frames accordingly: a window that looks nice on a laptop screen will look like a postage stamp on a high-resolution screen. As you will soon see, you can obtain the screen dimensions in pixels on the user's system. You can then use this information to compute the optimal window size for your program.
TIP
The API notes for this section give what we think are the most important methods for giving frames the proper look and feel. Some of these methods are defined in the JFrame class. Others come from the various superclasses of JFrame. At some point, you may need to search the API docs to see if there are methods for some special purpose. Unfortunately, that is a bit tedious to do with the JDK documentation. For subclasses, the API documentation only explains overridden methods. For example, the toFront method is applicable to objects of type JFrame, but because it is simply inherited from the Window class, the JFrame documentation doesn't explain it. If you feel that there should be a method to do something and it isn't explained in the documentation for the class you are working with, try looking at the API documentation for the methods of the superclasses of that class. The top of each API page has hyperlinks to the superclasses, and inherited methods are listed below the method summary for the new and overridden methods.
To give you an idea of what you can do with a window, we end this section by showing you a sample program that positions one of our closable frames so that
Its area is one-fourth that of the whole screen;
It is centered in the middle of the screen.
For example, if the screen was 800 x 600 pixels, we need a frame that is 400 x 300 pixels and we need to move it so the top left-hand corner is at (200,150).
To find out the screen size, use the following steps. Call the static getdefaultToolkit method of the Toolkit class to get the Toolkit object. (The Toolkit class is a dumping ground for a variety of methods that interface with the native windowing system.) Then call the getScreenSize method, which returns the screen size as a Dimension object. A Dimension object simultaneously stores a width and a height, in public (!) instance variables width and height. Here is the code:
Toolkit kit = Toolkit.getDefaultToolkit();
Dimension screenSize = kit.getScreenSize();
int screenWidth = screenSize.width;
int screenHeight = screenSize.height;
We also supply an icon. Because the representation of images is also system dependent, we again use the toolkit to load an image. Then, we set the image as the icon for the frame.
Image img = kit.getImage("icon.gif");
setIconImage(img);
Depending on your operating system, you can see the icon in various places. For example, in Windows, the icon is displayed in the top-left corner of the window, and you can see it in the list of active tasks when you press ALT+TAB.
Example 7-2 is the complete program. When you run the program, pay attention to the "Core Java" icon.
TIP
It is quite common to set the main frame of a program to the maximum size. As of JDK 1.4, you can simply maximize a frame by calling
frame.setExtendedState(Frame.MAXIMIZED_BOTH);
NOTE
If you write an application that takes advantage of multiple display screens, you should use the GraphicsEnvironment and GraphicsDevice classes to find the dimensions of the display screens. As of JDK 1.4, the GraphicsDevice class also lets you execute your application in full-screen mode.
Example 7-2. CenteredFrameTest.java
1. /**import java.awt.*;
2. import java.awt.event.*;
3. import javax.swing.*;
4.
5. public class CenteredFrameTest
6. {
7. public static void main(String[] args)
8. {
9. CenteredFrame frame = new CenteredFrame();
10. frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
11. frame.setVisible(true);
12. }
13. }
14.
15. class CenteredFrame extends JFrame
16. {
17. public CenteredFrame()
18. {
19. // get screen dimensions
20.
21. Toolkit kit = Toolkit.getDefaultToolkit();
22. Dimension screenSize = kit.getScreenSize();
23. int screenHeight = screenSize.height;
24. int screenWidth = screenSize.width;
25
26. // center frame in screen
27.
28. setSize(screenWidth / 2, screenHeight / 2);
29. setLocation(screenWidth / 4, screenHeight / 4);
30.
31. // set frame icon and title
32.
33. Image img = kit.getImage("icon.gif");
34. setIconImage(img);
35. setTitle("CenteredFrame");
36. }
37. }
java.awt.Component 1.0
boolean isVisible()
checks whether this component is set to be visible. Components are initially visible, with the exception of top-level components such as JFrame.
void setVisible(boolean b)
shows or hides the component depending on whether b is TRue or false.
boolean isShowing()
checks whether this component is showing on the screen. For this, it must be visible and be inside a container that is showing.
boolean isEnabled()
checks whether this component is enabled. An enabled component can receive keyboard input. Components are initially enabled.
void setEnabled(boolean b)
enables or disables a component.
Point getLocation() 1.1
returns the location of the top-left corner of this component, relative to the top-left corner of the surrounding container. (A Point object p encapsulates an x- and a y-coordinate which are accessible by p.x and p.y.)
Point getLocationOnScreen() 1.1
returns the location of the top-left corner of this component, using the screen's coordinates.
void setBounds(int x, int y, int width, int height) 1.1
moves and resizes this component. The location of the top-left corner is given by x and y, and the new size is given by the width and height parameters.
void setLocation(int x, int y) 1.1
void setLocation(Point p) 1.1
move the component to a new location. The x- and y-coordinates (or p.x and p.y) use the coordinates of the container if the component is not a top-level component, or the coordinates of the screen if the component is top level (for example, a JFrame).
Dimension getSize() 1.1
gets the current size of this component.
void setSize(int width, int height) 1.1
void setSize(Dimension d) 1.1
resize the component to the specified width and height.
java.awt.Window 1.0
void toFront()
shows this window on top of any other windows.
void toBack()
moves this window to the back of the stack of windows on the desktop and rearranges all other visible windows accordingly.
java.awt.Frame 1.0
void setResizable(boolean b)
determines whether the user can resize the frame.
void setTitle(String s)
sets the text in the title bar for the frame to the string s.
void setIconImage(Image image)
Parameters:
image
The image you want to appear as the icon for the frame
void setUndecorated(boolean b) 1.4
removes the frame decorations if b is TRue.
boolean isUndecorated() 1.4
returns true if this frame is undecorated.
int getExtendedState() 1.4
void setExtendedState(int state) 1.4
get or set the window state. The state is one of
Frame.NORMAL
Frame.ICONIFIED
Frame.MAXIMIZED_HORIZ
Frame.MAXIMIZED_VERT
Frame.MAXIMIZED_BOTH
java.awt.Toolkit 1.0
static Toolkit getDefaultToolkit()
returns the default toolkit.
Dimension getScreenSize()
gets the size of the user's screen.
Image getImage(String filename)
loads an image from the file with name filename.
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
Displaying Information in a Panel
In this section, we show you how to display information inside a frame. For example, rather than displaying "Not a Hello, World program" in text mode in a console window as we did in Chapter 3, we display the message in a frame, as shown in Figure 7-6.
Figure 7-6. A simple graphical program
You could draw the message string directly onto a frame, but that is not considered good programming practice. In Java, frames are really designed to be containers for components such as a menu bar and other user interface elements. You normally draw on another component, called a panel, which you add to the frame.
The structure of a JFrame is surprisingly complex. Look at Figure 7-7, which shows the makeup of a JFrame. As you can see, four panes are layered in a JFrame. The root pane, layered pane, and glass pane are of no interest to us; they are required to organize the menu bar and content pane and to implement the look and feel. The part that most concerns Swing programmers is the content pane. When designing a frame, you add components into the content pane, using code such as the following:
Container contentPane = frame.getContentPane();
Component c = . . .;
contentPane.add(c);
Figure 7-7. Internal structure of a JFrame
Up to JDK 1.4, the add method of the JFrame class was defined to throw an exception with the message "Do not use JFrame.add(). Use JFrame.getContentPane().add() instead". As of JDK 5.0, the JFrame.add method has given up trying to reeducate programmers, and it simply calls add on the content pane.
Thus, as of JDK 5.0, you can simply use the call
frame.add(c);
In our case, we want to add a single panel to the frame onto which we will draw our message. Panels are implemented by the JPanel class. They are user interface elements with two useful properties:
They have a surface onto which you can draw.
They themselves are containers.
Thus, they can hold other user interface components such as buttons, sliders, and so on.
To make a panel more interesting, you use inheritance to create a new class, and then override or add methods to get the extra functionality you want.
In particular, to draw on a panel, you
Define a class that extends JPanel; and
Override the paintComponent method in that class.
The paintComponent method is actually in JComponent-the superclass for all nonwindow Swing components. It takes one parameter of type Graphics. A Graphics object remembers a collection of settings for drawing images and text, such as the font you set or the current color. All drawing in Java must go through a Graphics object. It has methods that draw patterns, images, and text.
NOTE
The Graphics parameter is similar to a device context in Windows or a graphics context in X11 programming.
Here's how to make a panel onto which you can draw:
class MyPanel extends JPanel
{
public void paintComponent(Graphics g)
{
. . . // code for drawing will go here
}
}
Each time a window needs to be redrawn, no matter what the reason, the event handler notifies the component. This causes the paintComponent methods of all components to be executed.
Never call the paintComponent method yourself. It is called automatically whenever a part of your application needs to be redrawn, and you should not interfere with this automatic process.
What sorts of actions trigger this automatic response? For example, painting occurs because the user increased the size of the window or minimized and then restored the window. If the user popped up another window and it covered an existing window and then made the overlaid window disappear, the application window that was covered is now corrupted and will need to be repainted. (The graphics system does not save the pixels underneath.) And, of course, when the window is displayed for the first time, it needs to process the code that specifies how and where it should draw the initial elements.
TIP
If you need to force repainting of the screen, call the repaint method instead of paintComponent. The repaint method will cause paintComponent to be called for all components, with a properly configured Graphics object.
As you saw in the code fragment above, the paintComponent method takes a single parameter of type Graphics. Measurement on a Graphics object for screen display is done in pixels. The (0, 0) coordinate denotes the top-left corner of the component on whose surface you are drawing.
Displaying text is considered a special kind of drawing. The Graphics class has a drawString method that has the following syntax:
g.drawString(text, x, y)
In our case, we want to draw the string "Not a Hello, World Program" in our original window, roughly one-quarter of the way across and halfway down. Although we don't yet know how to measure the size of the string, we'll start the string at coordinates (75, 100). This means the first character in the string will start at a position 75 pixels to the right and 100 pixels down. (Actually, it is the baseline for the text that is 100 pixels down-see below for more on how text is measured.) Thus, our paintComponent method looks like this:
class NotHelloWorldPanel extends JPanel
{
public void paintComponent(Graphics g)
{
. . . // see below
g.drawString("Not a Hello, World program", MESSAGE_X, MESSAGE_Y);
}
public static final int MESSAGE_X = 75;
public static final int MESSAGE_Y = 100;
}
However, this paintComponent method is not complete. The NotHelloWorldPanel class extends the JPanel class, which has its own idea of how to paint the panel, namely, to fill it with the background color. To make sure that the superclass does its part of the job, we must call super.paintComponent before doing any painting on our own.
class NotHelloWorldPanel extends JPanel
{
public void paintComponent(Graphics g)
{
super.paintComponent(g);
. . . // code for drawing will go here
}
}
Example 7-3 shows the complete code. If you use JDK 1.4 or below, remember to change the call add(panel) to getContentPane().add(panel).
Example 7-3. NotHelloWorld.java
1. import javax.swing.*;
2. import java.awt.*;
3.
4. public class NotHelloWorld
5. {
6. public static void main(String[] args)
7. {
8. NotHelloWorldFrame frame = new NotHelloWorldFrame();
9. frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
10. frame.setVisible(true);
11. }
12.}
13.
14. /**
15. A frame that contains a message panel
16. */
17. class NotHelloWorldFrame extends JFrame
18. {
19. public NotHelloWorldFrame()
20. {
21. setTitle("NotHelloWorld");
22. setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
23.
24. // add panel to frame
25.
26. NotHelloWorldPanel panel = new NotHelloWorldPanel();
27. add(panel);
28. }
29.
30. public static final int DEFAULT_WIDTH = 300;
31. public static final int DEFAULT_HEIGHT = 200;
32. }
33.
34. /**
35. A panel that displays a message.
36. */
37. class NotHelloWorldPanel extends JPanel
38. {
39. public void paintComponent(Graphics g)
40. {
41. super.paintComponent(g);
42.
43. g.drawString("Not a Hello, World program", MESSAGE_X, MESSAGE_Y);
44. }
45.
46. public static final int MESSAGE_X = 75;
47. public static final int MESSAGE_Y = 100;
48. }
javax.swing.JFrame 1.2
Container getContentPane()
returns the content pane object for this JFrame.
void add(Component c)
adds the given component to the content pane of this frame. (Before JDK 5.0, this method threw an exception.)
java.awt.Component 1.0
void repaint()
causes a repaint of the component "as soon as possible."
public void repaint(int x, int y, int width, int height)
causes a repaint of a part of the component "as soon as possible."
javax.swing.JComponent 1.2
void paintComponent(Graphics g)
override this method to describe how your component needs to be painted.
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
Using Color
The setPaint method of the Graphics2D class lets you select a color that is used for all subsequent drawing operations on the graphics context. To draw in multiple colors, you select a color, draw, then select another color, and draw again.
You define colors with the Color class. The java.awt.Color class offers predefined constants for the 13 standard colors listed in Table 7-1.
Table 7-1. Standard Colors BLACK
GREEN
RED
BLUE
LIGHT_GRAY
WHITE
CYAN
MAGENTA
YELLOW
DARK_GRAY
ORANGE
GRAY
PINK
For example,
g2.setPaint(Color.RED);
g2.drawString("Warning!", 100, 100);
NOTE
Before JDK 1.4, color constant names were lower case, such as Color.red. This is odd because the standard coding convention is to write constants in upper case. Starting with JDK 1.4, you can write the standard color names in upper case or, for backward compatibility, in lower case.
You can specify a custom color by creating a Color object by its red, green, and blue components. Using a scale of 0-255 (that is, one byte) for the redness, blueness, and greenness, call the Color constructor like this:
Color(int redness, int greenness, int blueness)
Here is an example of setting a custom color:
g2.setPaint(new Color(0, 128, 128)); // a dull blue-green
g2.drawString("Welcome!", 75, 125);
NOTE
In addition to solid colors, you can select more complex "paint" settings, such as varying hues or images. See the Advanced AWT chapter in Volume 2 for more details. If you use a Graphics object instead of a Graphics2D object, you need to use the setColor method to set colors.
To set the background color, you use the setBackground method of the Component class, an ancestor of JPanel.
MyPanel p = new MyPanel();
p.setBackground(Color.PINK);
There is also a setForeground method. It specifies the default color that is used for drawing on the component.
TIP
The brighter() and darker() methods of the Color class produce, as their names suggest, either brighter or darker versions of the current color. Using the brighter method is also a good way to highlight an item. Actually, brighter() is just a little bit brighter. To make a color really stand out, apply it three times: c.brighter().brighter().brighter().
Java gives you predefined names for many more colors in its SystemColor class. The constants in this class encapsulate the colors used for various elements of the user's system. For example,
p.setBackground(SystemColor.window)
sets the background color of the panel to the default used by all windows on the user's desktop. (The background is filled in whenever the window is repainted.) Using the colors in the SystemColor class is particularly useful when you want to draw user interface elements so that the colors match those already found on the user's desktop. Table 7-2 lists the system color names and their meanings.
Color(int r, int g, int b)
creates a color object.
Table 7-2. System Colors desktop
Background color of desktop
activeCaption
Background color for captions
activeCaptionText
Text color for captions
activeCaptionBorder
Border color for caption text
inactiveCaption
Background color for inactive captions
inactiveCaptionText
Text color for inactive captions
inactiveCaptionBorder
Border color for inactive captions
window
Background for windows
windowBorder
Color of window border frame
windowText
Text color inside windows
menu
Background for menus
menuText
Text color for menus
text
Background color for text
textText
Text color for text
textInactiveText
Text color for inactive controls
textHighlight
Background color for highlighted text
textHighlightText
Text color for highlighted text
control
Background color for controls
controlText
Text color for controls
controlLtHighlight
Light highlight color for controls
controlHighlight
Highlight color for controls
controlShadow
Shadow color for controls
controlDkShadow
Dark shadow color for controls
scrollbar
Background color for scrollbars
info
Background color for spot-help text
infoText
Text color for spot-help text
java.awt.Color 1.0
Parameters:
r
The red value (0-255)
g
The green value (0-255)
b
The blue value (0-255)
java.awt.Graphics 1.0
void setColor(Color c)
changes the current color. All subsequent graphics operations will use the new color.
Parameters:
c
The new color
java.awt.Graphics2D 1.2
void setPaint(Paint p)
sets the paint attribute of this graphics context. The Color class implements the Paint interface. Therefore, you can use this method to set the paint attribute to a solid color.
java.awt.Component 1.0
void setBackground(Color c)
sets the background color.
Parameters:
c
The new background color
void setForeground(Color c)
sets the foreground color.
Parameters:
c
The new foreground color
Filling Shapes
You can fill the interiors of closed shapes (such as rectangles or ellipses) with a color (or, more generally, the current paint setting). Simply call fill instead of draw:
Rectangle2D rect = . . .;
g2.setPaint(Color.RED);
g2.fill(rect); // fills rect with red color
The program in Example 7-5 fills a rectangle in red, then an ellipse with the same boundary in a dull green (see Figure 7-12).
Example 7-5. FillTest.java
1. import java.awt.*;
2. import java.awt.geom.*;
3. import javax.swing.*;
4.
5. public class FillTest
6. {
7. public static void main(String[] args)
8. {
9. FillFrame frame = new FillFrame();
10. frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
11. frame.setVisible(true);
12. }
13. }
14.
15. /**
16. A frame that contains a panel with drawings
17. */
18. class FillFrame extends JFrame
19. {
20. public FillFrame()
21. {
22. setTitle("FillTest");
23. setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
24.
25. // add panel to frame
26.
27. FillPanel panel = new FillPanel();
28. add(panel);
29. }
30.
31. public static final int DEFAULT_WIDTH = 400;
32. public static final int DEFAULT_HEIGHT = 400;
33. }
34.
35. /**
36. A panel that displays filled rectangles and ellipses
37. */
38. class FillPanel extends JPanel
39. {
40. public void paintComponent(Graphics g)
41. {
42. super.paintComponent(g);
43. Graphics2D g2 = (Graphics2D) g;
44.
45. // draw a rectangle
46.
47. double leftX = 100;
48. double topY = 100;
49. double width = 200;
50. double height = 150;
51.
52. Rectangle2D rect = new Rectangle2D.Double(leftX, topY, width, height);
53. g2.setPaint(Color.RED);
54. g2.fill(rect);
55.
56. // draw the enclosed ellipse
57.
58. Ellipse2D ellipse = new Ellipse2D.Double();
59. ellipse.setFrame(rect);
60. g2.setPaint(new Color(0, 128, 128)); // a dull blue-green
61. g2.fill(ellipse);
62. }
63. }
Figure 7-12. Filled rectangles and ellipses
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
Using Special Fonts for Text
The "Not a Hello, World" program at the beginning of this chapter displayed a string in the default font. Often, you want to show text in a different font. You specify a font by its font face name. A font face name is composed of a font family name, such as "Helvetica," and an optional suffix such as "Bold." For example, the font faces "Helvetica" and "Helvetica Bold" are both considered to be part of the family named "Helvetica."
To find out which fonts are available on a particular computer, call the getAvailableFontFamilyNames method of the GraphicsEnvironment class. The method returns an array of strings that contains the names of all available fonts. To obtain an instance of the GraphicsEnvironment class that describes the graphics environment of the user's system, use the static getLocalGraphicsEnvironment method. Thus, the following program prints the names of all fonts on your system:
import java.awt.*;
public class ListFonts
{
public static void main(String[] args)
{
String[] fontNames = GraphicsEnvironment
.getLocalGraphicsEnvironment()
.getAvailableFontFamilyNames();
for (String fontName : fontNames)
System.out.println(fontName);
}
}
On one system, the list starts out like this:
Abadi MT Condensed Light
Arial
Arial Black
Arial Narrow
Arioso
Baskerville
Binner Gothic
. . .
and goes on for another 70 fonts or so.
NOTE
The JDK documentation claims that suffixes such as "heavy," "medium," "oblique," or "gothic" are variations inside a single family. In our experience, that is not the case. The "Bold," "Italic," and "Bold Italic" suffixes are recognized as family variations, but other suffixes aren't.
Unfortunately, there is no absolute way of knowing whether a user has a font with a particular "look" installed. Font face names can be trademarked, and font designs can be copyrighted in some jurisdictions. Thus, the distribution of fonts often involves royalty payments to a font foundry. Of course, just as there are inexpensive imitations of famous perfumes, there are lookalikes for name-brand fonts. For example, the Helvetica imitation that is shipped with Windows is called Arial.
To establish a common baseline, the AWT defines five logical font names:
SansSerif
Serif
Monospaced
Dialog
DialogInput
These names are always mapped to fonts that actually exist on the client machine. For example, on a Windows system, SansSerif is mapped to Arial.
NOTE
The font mapping is defined in the fontconfig.properties file in the jre/lib subdirectory of the Java installation. See http://java.sun.com/j2se/5.0/docs/guide/intl/fontconfig.html for information on this file. Earlier versions of the JDK used a font.properties file that is now obsolete.
To draw characters in a font, you must first create an object of the class Font. You specify the font face name, the font style, and the point size. Here is an example of how you construct a Font object:
Font helvb14 = new Font("Helvetica", Font.BOLD, 14);
The third argument is the point size. Points are commonly used in typography to indicate the size of a font. There are 72 points per inch. This sentence uses a 9-point font.
You can use a logical font name in the place of a font face name in the Font constructor. You specify the style (plain, bold, italic, or bold italic) by setting the second Font constructor argument to one of the following values:
Font.PLAIN
Font.BOLD
Font.ITALIC
Font.BOLD + Font.ITALIC
Here is an example:
Font sansbold14 = new Font("SansSerif", Font.BOLD, 14)
NOTE
Prior versions of Java used the names Helvetica, TimesRoman, Courier, and ZapfDingbats as logical font names. For backward compatibility, these font names are still treated as logical font names even though Helvetica is really a font face name and TimesRoman and ZapfDingbats are not font names at all-the actual font face names are "Times Roman" and "Zapf Dingbats."
TIP
Starting with JDK version 1.3, you can read TrueType fonts. You need an input stream for the font-typically from a disk file or URL. (See Chapter 12 for more information on streams.) Then call the static Font.createFont method:
URL url = new URL("http://www.fonts.com/Wingbats.ttf");
InputStream in = url.openStream();
Font f = Font.createFont(Font.TRUETYPE_FONT, in);
The font is plain with a font size of 1 point. Use the deriveFont method to get a font of the desired size:
Font df = f.deriveFont(14.0F);
CAUTION
There are two overloaded versions of the deriveFont method. One of them (with a float parameter) sets the font size, the other (with an int parameter) sets the font style. Thus, f.deriveFont(14) sets the style and not the size! (The result is an italic font because it happens that the binary representation of 14 sets the ITALIC bit but not the BOLD bit.)
The Java fonts contain the usual ASCII characters as well as symbols. For example, if you print the character '\u2297' in the Dialog font, then you get a character. Only those symbols that are defined in the Unicode character set are available.
Here's the code that displays the string "Hello, World!" in the standard sans serif font on your system, using 14-point bold type:
Font sansbold14 = new Font("SansSerif", Font.BOLD, 14);
g2.setFont(sansbold14);
String message = "Hello, World!";
g2.drawString(message, 75, 100);
Next, let's center the string in its panel rather than drawing it at an arbitrary position. We need to know the width and height of the string in pixels. These dimensions depend on three factors:
The font used (in our case, sans serif, bold, 14 point);
The string (in our case, "Hello, World!"); and
The device on which the font is drawn (in our case, the user's screen).
To obtain an object that represents the font characteristics of the screen device, you call the getFontRenderContext method of the Graphics2D class. It returns an object of the FontRenderContext class. You simply pass that object to the getStringBounds method of the Font class:
FontRenderContext context = g2.getFontRenderContext();
Rectangle2D bounds = f.getStringBounds(message, context);
The getStringBounds method returns a rectangle that encloses the string.
To interpret the dimensions of that rectangle, you should know some basic typesetting terms (see Figure 7-13). The baseline is the imaginary line where, for example, the bottom of a character like "e" rests. The ascent is the distance from the baseline to the top of an ascender, which is the upper part of a letter like "b" or "k," or an uppercase character. The descent is the distance from the baseline to a descender, which is the lower portion of a letter like "p" or "g."
Figure 7-13. Typesetting terms illustrated
Leading is the space between the descent of one line and the ascent of the next line. (The term has its origin from the strips of lead that typesetters used to separate lines.) The height of a font is the distance between successive baselines, which is the same as descent + leading + ascent.
The width of the rectangle that the getStringBounds method returns is the horizontal extent of the string. The height of the rectangle is the sum of ascent, descent, and leading. The rectangle has its origin at the baseline of the string. The top y-coordinate of the rectangle is negative. Thus, you can obtain string width, height, and ascent as follows:
double stringWidth = bounds.getWidth();
double stringHeight = bounds.getHeight();
double ascent = -bounds.getY();
If you need to know the descent or leading, you need to use the getLineMetrics method of the Font class. That method returns an object of the LineMetrics class, which has methods to obtain the descent and leading:
LineMetrics metrics = f.getLineMetrics(message, context);
float descent = metrics.getDescent();
float leading = metrics.getLeading();
The following code uses all this information to center a string in its surrounding panel:
FontRenderContext context = g2.getFontRenderContext();
Rectangle2D bounds = f.getStringBounds(message, context);
// (x,y) = top left corner of text
double x = (getWidth() - bounds.getWidth()) / 2;
double y = (getHeight() - bounds.getHeight()) / 2;
// add ascent to y to reach the baseline
double ascent = -bounds.getY();
double baseY = y + ascent;
g2.drawString(message, (int) x, (int) baseY);
To understand the centering, consider that getWidth() returns the width of the panel. A portion of that width, namely, bounds.getWidth(), is occupied by the message string. The remainder should be equally distributed on both sides. Therefore, the blank space on each side is half the difference. The same reasoning applies to the height.
Finally, the program draws the baseline and the bounding rectangle.
Figure 7-14 shows the screen display; Example 7-6 is the program listing.
Example 7-6. FontTest.java
1. import java.awt.*;
2. import java.awt.font.*;
3. import java.awt.geom.*;
4. import javax.swing.*;
5.
6. public class FontTest
7. {
8. public static void main(String[] args)
9. {
10. FontFrame frame = new FontFrame();
11. frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
12. frame.setVisible(true);
13. }
14. }
15.
16. /**
17. A frame with a text message panel
18. */
19. class FontFrame extends JFrame
20. {
21. public FontFrame()
22. {
23. setTitle("FontTest");
24. setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
25.
26. // add panel to frame
27.
28. FontPanel panel = new FontPanel();
29. add(panel);
30. }
31.
32. public static final int DEFAULT_WIDTH = 300;
33. public static final int DEFAULT_HEIGHT = 200;
34. }
35.
36. /**
37. A panel that shows a centered message in a box.
38. */
39. class FontPanel extends JPanel
40. {
41. public void paintComponent(Graphics g)
42. {
43. super.paintComponent(g);
44. Graphics2D g2 = (Graphics2D) g;
45.
46. String message = "Hello, World!";
47.
48. Font f = new Font("Serif", Font.BOLD, 36);
49. g2.setFont(f);
50.
51. // measure the size of the message
52.
53. FontRenderContext context = g2.getFontRenderContext();
54. Rectangle2D bounds = f.getStringBounds(message, context);
55.
56. // set (x,y) = top left corner of text
57.
58. double x = (getWidth() - bounds.getWidth()) / 2;
59. double y = (getHeight() - bounds.getHeight()) / 2;
60.
61. // add ascent to y to reach the baseline
62.
63. double ascent = -bounds.getY();
64. double baseY = y + ascent;
65.
66. // draw the message
67.
68. g2.drawString(message, (int) x, (int) baseY);
69.
70. g2.setPaint(Color.GRAY);
71.
72. // draw the baseline
73.
74. g2.draw(new Line2D.Double(x, baseY, x + bounds.getWidth(), baseY));
75.
76. // draw the enclosing rectangle
77.
78. Rectangle2D rect = new Rectangle2D.Double(x, y, bounds.getWidth(), bounds
.getHeight());
79. g2.draw(rect);
80. }
81. }
Figure 7-14. Drawing the baseline and string bounds
java.awt.Font 1.0
Font(String name, int style, int size)
creates a new font object.
Parameters:
name
The font name. This is either a font face name (such as "Helvetica Bold") or a logical font name (such as "Serif", "SansSerif")
style
The style (Font.PLAIN, Font.BOLD, Font.ITALIC, or Font.BOLD + Font.ITALIC)
size
The point size (for example, 12)
String getFontName()
gets the font face name (such as "Helvetica Bold").
String getFamily()
gets the font family name (such as "Helvetica").
String getName()
gets the logical name (such as "SansSerif") if the font was created with a logical font name; otherwise, gets the font face name.
Rectangle2D getStringBounds(String s, FontRenderContext context) 1.2
returns a rectangle that encloses the string. The origin of the rectangle falls on the baseline. The top y-coordinate of the rectangle equals the negative of the ascent. The height of the rectangle equals the sum of ascent, descent, and leading. The width equals the string width.
LineMetrics getLineMetrics(String s, FontRenderContext context) 1.2
returns a line metrics object to determine the extent of the string.
Font deriveFont(int style) 1.2
Font deriveFont(float size) 1.2
Font deriveFont(int style, float size) 1.2
return a new font that equals this font, except that it has the given size and style.
java.awt.font.LineMetrics 1.2
float getAscent()
gets the font ascent-the distance from the baseline to the tops of uppercase characters.
float getDescent()
gets the font descent-the distance from the baseline to the bottoms of descenders.
float getLeading()
gets the font leading-the space between the bottom of one line of text and the top of the next line.
float getHeight()
gets the total height of the font-the distance between the two baselines of text (descent + leading + ascent).
java.awt.Graphics 1.0
void setFont(Font font)
selects a font for the graphics context. That font will be used for subsequent text-drawing operations.
Parameters:
font
A font
void drawString(String str, int x, int y)
draws a string in the current font and color.
Parameters:
str
The string to be drawn
x
The x-coordinate of the start of the string
y
The y-coordinate of the baseline of the string
java.awt.Graphics2D 1.2
FontRenderContext getFontRenderContext()
gets a font render context that specifies font characteristics in this graphics context.
void drawString(String str, float x, float y)
draws a string in the current font and color.
Parameters:
str
The string to be drawn
x
The x-coordinate of the start of the string
y
The y-coordinate of the baseline of the string
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
Doing More with Images
You have already seen how to build up simple images by drawing lines and shapes. Complex images, such as photographs, are usually generated externally, for example, with a scanner or special image-manipulation software. (As you will see in Volume 2, it is also possible to produce an image, pixel by pixel, and store the result in an array. This procedure is common for fractal images, for example.)
Once images are stored in local files or someplace on the Internet, you can read them into a Java application and display them on Graphics objects. As of JDK 1.4, reading an image is very simple. If the image is stored in a local file, call
String filename = "...";
Image image = ImageIO.read(new File(filename));
Otherwise, you can supply a URL:
String urlname = "...";
Image image = ImageIO.read(new URL(urlname));
The read method throws an IOException if the image is not available. We discuss the general topic of exception handling in Chapter 11. For now, our sample program just catches that exception and prints a stack trace if it occurs.
Now the variable image contains a reference to an object that encapsulates the image data. You can display the image with the drawImage method of the Graphics class.
public void paintComponent(Graphics g)
{
. . .
g.drawImage(image, x, y, null);
}
Example 7-7 takes this a little bit further and tiles the window with the graphics image. The result looks like the screen shown in Figure 7-15. We do the tiling in the paintComponent method. We first draw one copy of the image in the top-left corner and then use the copyArea call to copy it into the entire window:
for (int i = 0; i * imageWidth <= getWidth(); i++)
for (int j = 0; j * imageHeight <= getHeight(); j++)
if (i + j > 0)
g.copyArea(0, 0, imageWidth, imageHeight, i * imageWidth, j * imageHeight);
Figure 7-15. Window with tiled graphics image
NOTE
To load an image with JDK 1.3 or earlier, you should use the MediaTracker class instead. A media tracker can track the acquisition of one or more images. (The name "media" suggests that the class should also be able to track audio files or other media. While such an extension may have been envisioned for the future, the current implementation tracks images only.)
You add an image to a tracker object with the following command:
MediaTracker tracker = new MediaTracker(component);
Image img = Toolkit.getDefaultToolkit().getImage(name);
int id = 1; // the ID used to track the image loading process
tracker.addImage(img, id);
You can add as many images as you like to a single media tracker. Each of the images should have a different ID number, but you can choose any numbering that is convenient. To wait for an image to be loaded completely, you use code like this:
try { tracker.waitForID(id); }
catch (InterruptedException e) {}
If you want to acquire multiple images, then you can add them all to the media tracker object and wait until they are all loaded. You can achieve this with the following code:
try { tracker.waitForAll(); }
catch (InterruptedException e) {}
Example 7-7 shows the full source code of the image display program. This concludes our introduction to Java graphics programming. For more advanced techniques, you can turn to the discussion about 2D graphics and image manipulation in Volume 2.
Example 7-7. ImageTest.java
1. import java.awt.*;
2. import java.awt.event.*;
3. import java.io.*;
4. import javax.imageio.*;
5. import javax.swing.*;
6.
7. public class ImageTest
8. {
9. public static void main(String[] args)
10. {
11. ImageFrame frame = new ImageFrame();
12. frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
13. frame.setVisible(true);
14. }
15. }
16.
17. /**
18. A frame with an image panel
19. */
20. class ImageFrame extends JFrame
21. {
22. public ImageFrame()
23. {
24. setTitle("ImageTest");
25. setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
26.
27. // add panel to frame
28.
29. ImagePanel panel = new ImagePanel();
30. add(panel);
31. }
32.
33. public static final int DEFAULT_WIDTH = 300;
34. public static final int DEFAULT_HEIGHT = 200;
35. }
36.
37. /**
38. A panel that displays a tiled image
39. */
40. class ImagePanel extends JPanel
41. {
42. public ImagePanel()
43. {
44. // acquire the image
45. try
46. {
47. image = ImageIO.read(new File("blue-ball.gif"));
48. }
49. catch (IOException e)
50. {
51. e.printStackTrace();
52. }
53. }
54.
55. public void paintComponent(Graphics g)
56. {
57. super.paintComponent(g);
58. if (image == null) return;
59.
60. int imageWidth = image.getWidth(this);
61. int imageHeight = image.getHeight(this);
62.
63. // draw the image in the upper-left corner
64.
65. g.drawImage(image, 0, 0, null);
66. // tile the image across the panel
67.
68. for (int i = 0; i * imageWidth <= getWidth(); i++)
69. for (int j = 0; j * imageHeight <= getHeight(); j++)
70. if (i + j > 0)
71. g.copyArea(0, 0, imageWidth, imageHeight,
72. i * imageWidth, j * imageHeight);
73. }
74.
75. private Image image;
76. }
javax.swing.ImageIO 1.4
static BufferedImage read(File f)
static BufferedImage read(URL u)
read an image from the given file or URL.
java.awt.Image 1.0
Graphics getGraphics()
gets a graphics context to draw into this image buffer.
void flush()
releases all resources held by this image object.
java.awt.Graphics 1.0
boolean drawImage(Image img, int x, int y, ImageObserver observer)
draws an unscaled image. Note: This call may return before the image is drawn.
Parameters:
img
The image to be drawn
x
The x-coordinate of the upper-left corner
y
The y-coordinate of the upper-left corner
observer
The object to notify of the progress of the rendering process (may be null)
boolean drawImage(Image img, int x, int y, int width, int height, ImageObserver observer)
draws a scaled image. The system scales the image to fit into a region with the given width and height. Note: This call may return before the image is drawn.
Parameters:
img
The image to be drawn
x
The x-coordinate of the upper-left corner
y
The y-coordinate of the upper-left corner
width
The desired width of image
height
The desired height of image
observer
The object to notify of the progress of the rendering process (may be null)
void copyArea(int x, int y, int width, int height, int dx, int dy)
copies an area of the screen.
Parameters:
x
The x-coordinate of the upper-left corner of the source area
y
The y-coordinate of the upper-left corner of the source area
width
The width of the source area
height
The height of the source area
dx
The horizontal distance from the source area to the target area
dy
The vertical distance from the source area to the target area
void dispose()
disposes of this graphics context and releases operating system resources. You should always dispose of the graphics contexts that you receive from calls to methods such as Image.getGraphics, but not the ones handed to you by paintComponent.
java.awt.Component 1.0
Image createImage(int width, int height)
creates an off-screen image buffer to be used for double buffering.
Parameters:
width
The width of the image
height
The height of the image
java.awt.MediaTracker 1.0
MediaTracker(Component c)
tracks images that are displayed in the given component.
void addImage(Image image, int id)
adds an image to the list of images being tracked. When the image is added, the image loading process is started.
Parameters:
image
The image to be tracked
id
The identifier used to later refer to this image
void waitForID(int id)
waits until all images with the specified ID are loaded.
void waitForAll()
waits until all images that are being tracked are loaded.
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
Basics of Event Handling
Any operating environment that supports GUIs constantly monitors events such as keystrokes or mouse clicks. The operating environment reports these events to the programs that are running. Each program then decides what, if anything, to do in response to these events. In languages like Visual Basic, the correspondence between events and code is obvious. One writes code for each specific event of interest and places the code in what is usually called an event procedure. For example, a Visual Basic button named HelpButton would have a HelpButton_Click event procedure associated with it. The code in this procedure executes whenever that button is clicked. Each Visual Basic GUI component responds to a fixed set of events, and it is impossible to change the events to which a Visual Basic component responds.
On the other hand, if you use a language like raw C to do event-driven programming, you need to write the code that constantly checks the event queue for what the operating environment is reporting. (You usually do this by encasing your code in a giant loop with a massive switch statement!) This technique is obviously rather ugly, and, in any case, it is much more difficult to code. The advantage is that the events you can respond to are not as limited as in languages, like Visual Basic, that go to great lengths to hide the event queue from the programmer.
The Java programming environment takes an approach somewhat between the Visual Basic approach and the raw C approach in terms of power and, therefore, in resulting complexity. Within the limits of the events that the AWT knows about, you completely control how events are transmitted from the event sources (such as buttons or scrollbars) to event listeners. You can designate any object to be an event listener-in practice, you pick an object that can conveniently carry out the desired response to the event. This event delegation model gives you much more flexibility than is possible with Visual Basic, in which the listener is predetermined, but it requires more code and is more difficult to untangle (at least until you get used to it).
Event sources have methods that allow you to register event listeners with them. When an event happens to the source, the source sends a notification of that event to all the listener objects that were registered for that event.
As one would expect in an object-oriented language like Java, the information about the event is encapsulated in an event object. In Java, all event objects ultimately derive from the class java.util.EventObject. Of course, there are subclasses for each event type, such as ActionEvent and WindowEvent.
Different event sources can produce different kinds of events. For example, a button can send ActionEvent objects, whereas a window can send WindowEvent objects.
To sum up, here's an overview of how event handling in the AWT works.
A listener object is an instance of a class that implements a special interface called (naturally enough) a listener interface.
An event source is an object that can register listener objects and send them event objects.
The event source sends out event objects to all registered listeners when that event occurs.
The listener objects will then use the information in the event object to determine their reaction to the event.
You register the listener object with the source object by using lines of code that follow the model
eventSourceObject.addEventListener(eventListenerObject);
Here is an example:
ActionListener listener = . . .;
JButton button = new JButton("Ok");
button.addActionListener(listener);
Now the listener object is notified whenever an "action event" occurs in the button. For buttons, as you might expect, an action event is a button click.
Code like the above requires that the class to which the listener object belongs implements the appropriate interface (which in this case is the ActionListener interface). As with all interfaces in Java, implementing an interface means supplying methods with the right signatures. To implement the ActionListener interface, the listener class must have a method called actionPerformed that receives an ActionEvent object as a parameter.
class MyListener implements ActionListener
{
. . .
public void actionPerformed(ActionEvent event)
{
// reaction to button click goes here
. . .
}
}
Whenever the user clicks the button, the JButton object creates an ActionEvent object and calls listener.actionPerformed(event), passing that event object. It is possible for multiple objects to be added as listeners to an event source such as a button. In that case, the button calls the actionPerformed methods of all listeners whenever the user clicks the button.
Figure 8-1 shows the interaction between the event source, event listener, and event object.
Figure 8-1. Event notification
NOTE
In this chapter, we put particular emphasis on event handling for user interface events such as button clicks and mouse moves. However, the basic event handling architecture is not limited to user interfaces. As you will see in the next chapter, objects that are not user interface components can also send event notifications to listeners-usually to tell them that a property of the object has changed.
Example: Handling a Button Click
As a way of getting comfortable with the event delegation model, let's work through all details needed for the simple example of responding to a button click. For this example, we will want
A panel populated with three buttons; and
Three listener objects that are added as action listeners to the buttons.
With this scenario, each time a user clicks on any of the buttons on the panel, the associated listener object then receives an ActionEvent that indicates a button click. In our sample program, the listener object will then change the background color of the panel.
Before we can show you the program that listens to button clicks, we first need to explain how to create buttons and how to add them to a panel. (For more on GUI elements, see Chapter 9.)
You create a button by specifying a label string, an icon, or both in the button constructor. Here are two examples:
JButton yellowButton = new JButton("Yellow");
JButton blueButton = new JButton(new ImageIcon("blue-ball.gif"));
Adding buttons to a panel occurs through a call to a method named (quite mnemonically) add. The add method takes as a parameter the specific component to be added to the container. For example,
class ButtonPanel extends JPanel
{
public ButtonPanel()
{
JButton yellowButton = new JButton("Yellow");
JButton blueButton = new JButton("Blue");
JButton redButton = new JButton("Red");
add(yellowButton);
add(blueButton);
add(redButton);
}
}
Figure 8-2 shows the result.
Figure 8-2. A panel filled with buttons
Now that you know how to add buttons to a panel, you'll need to add code that lets the panel listen to these buttons. This requires classes that implement the ActionListener interface, which, as we just mentioned, has one method: actionPerformed, whose signature looks like this:
public void actionPerformed(ActionEvent event)
NOTE
The ActionListener interface we used in the button example is not restricted to button clicks. It is used in many separate situations:
When an item is selected from a list box with a double click;
When a menu item is selected;
When the ENTER key is clicked in a text field;
When a certain amount of time has elapsed for a Timer component.
You will see more details in this chapter and the next.
The way to use the ActionListener interface is the same in all situations: the actionPerformed method (which is the only method in ActionListener) takes an object of type ActionEvent as a parameter. This event object gives you information about the event that happened.
When a button is clicked, then we want to set the background color of the panel to a particular color. We store the desired color in our listener class.
class ColorAction implements ActionListener
{
public ColorAction(Color c)
{
backgroundColor = c;
}
public void actionPerformed(ActionEvent event)
{
// set panel background color
. . .
}
private Color backgroundColor;
}
We then construct one object for each color and set the objects as the button listeners.
ColorAction yellowAction = new ColorAction(Color.YELLOW);
ColorAction blueAction = new ColorAction(Color.BLUE);
ColorAction redAction = new ColorAction(Color.RED);
yellowButton.addActionListener(yellowAction);
blueButton.addActionListener(blueAction);
redButton.addActionListener(redAction);
For example, if a user clicks on the button marked "Yellow," then the actionPerformed method of the yellowAction object is called. Its backgroundColor instance field is set to Color.YELLOW, and it can now proceed to set the panel's background color.
Just one issue remains. The ColorAction object doesn't have access to the panel variable. You can solve this problem in two ways. You can store the panel in the ColorAction object and set it in the ColorAction constructor. Or, more conveniently, you can make ColorAction into an inner class of the ButtonPanel class. Its methods can then access the outer panel automatically. (For more information on inner classes, see Chapter 6.)
We follow the latter approach. Here is how you place the ColorAction class inside the ButtonPanel class.
class ButtonPanel extends JPanel
{
. . .
private class ColorAction implements ActionListener
{
. . .
public void actionPerformed(ActionEvent event)
{
setBackground(backgroundColor);
// i.e., outer.setBackground(...)
}
private Color backgroundColor;
}
}
Look closely at the actionPerformed method. The ColorAction class doesn't have a setBackground method. But the outer ButtonPanel class does. The methods are invoked on the ButtonPanel object that constructed the inner class objects. (Note again that outer is not a keyword in the Java programming language. We just use it as an intuitive symbol for the invisible outer class reference in the inner class object.)
This situation is very common. Event listener objects usually need to carry out some action that affects other objects. You can often strategically place the listener class inside the class whose state the listener should modify.
Example 8-1 contains the complete program. Whenever you click one of the buttons, the appropriate action listener changes the background color of the panel.
Example 8-1. ButtonTest.java
1. import java.awt.*;
2. import java.awt.event.*;
3. import javax.swing.*;
4.
5. public class ButtonTest
6. {
7. public static void main(String[] args)
8. {
9. ButtonFrame frame = new ButtonFrame();
10. frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
11. frame.setVisible(true);
12. }
13. }
14.
15. /**
16. A frame with a button panel
17. */
18. class ButtonFrame extends JFrame
19. {
20. public ButtonFrame()
21. {
22. setTitle("ButtonTest");
23. setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
24.
25. // add panel to frame
26.
27. ButtonPanel panel = new ButtonPanel();
28. add(panel);
29. }
30.
31. public static final int DEFAULT_WIDTH = 300;
32. public static final int DEFAULT_HEIGHT = 200;
33. }
34.
35. /**
36. A panel with three buttons.
37. */
38. class ButtonPanel extends JPanel
39. {
40. public ButtonPanel()
41. {
42. // create buttons
43.
44. JButton yellowButton = new JButton("Yellow");
45. JButton blueButton = new JButton("Blue");
46. JButton redButton = new JButton("Red");
47.
48. // add buttons to panel
49.
50. add(yellowButton);
51. add(blueButton);
52. add(redButton);
53.
54. // create button actions
55.
56. ColorAction yellowAction = new ColorAction(Color.YELLOW);
57. ColorAction blueAction = new ColorAction(Color.BLUE);
58. ColorAction redAction = new ColorAction(Color.RED);
59.
60. // associate actions with buttons
61.
62. yellowButton.addActionListener(yellowAction);
63. blueButton.addActionListener(blueAction);
64. redButton.addActionListener(redAction);
65. }
66.
67. /**
68. An action listener that sets the panel's background color.
69. */
70. private class ColorAction implements ActionListener
71. {
72. public ColorAction(Color c)
73. {
74. backgroundColor = c;
75. }
76.
77. public void actionPerformed(ActionEvent event)
78. {
79. setBackground(backgroundColor);
80. }
81.
82. private Color backgroundColor;
83. }
84. }
javax.swing.JButton 1.2
JButton(String label)
constructs a button. The label string can be plain text, or, starting with JDK 1.3, HTML; for example, "<html>Ok</html>".
Parameters:
label
The text you want on the face of the button
JButton(Icon icon)
constructs a button.
Parameters:
icon
The icon you want on the face of the button
JButton(String label, Icon icon)
constructs a button.
Parameters:
label
The text you want on the face of the button
icon
The icon you want on the face of the button
java.awt.Container 1.0
Component add(Component c)
adds the component c to this container.
javax.swing.ImageIcon 1.2
ImageIcon(String filename)
constructs an icon whose image is stored in a file. The image is automatically loaded with a media tracker (see Chapter 7).
Becoming Comfortable with Inner Classes
Some people dislike inner classes because they feel that a proliferation of classes and objects makes their programs slower. Let's have a look at that claim. You don't need a new class for every user interface component. In our example, all three buttons share the same listener class. Of course, each of them has a separate listener object. But these objects aren't large. They each contain a color value and a reference to the panel. And the traditional solution, with if . . . else statements, also references the same color objects that the action listeners store, just as local variables and not as instance fields.
We believe the time has come to get used to inner classes. We recommend that you use dedicated inner classes for event handlers rather than turning existing classes into listeners. We think that even anonymous inner classes have their place.
Here is a good example of how anonymous inner classes can actually simplify your code. If you look at the code of Example 8-1, you will note that each button requires the same treatment:
1. Construct the button with a label string.
2. Add the button to the panel.
3. Construct an action listener with the appropriate color.
4. Add that action listener.
Let's implement a helper method to simplify these tasks:
void makeButton(String name, Color backgroundColor)
{
JButton button = new JButton(name);
add(button);
ColorAction action = new ColorAction(backgroundColor);
button.addActionListener(action);
}
Then the ButtonPanel constructor simply becomes
public ButtonPanel()
{
makeButton("yellow", Color.YELLOW);
makeButton("blue", Color.BLUE);
makeButton("red", Color.RED);
}
Now you can make a further simplification. Note that the ColorAction class is only needed once: in the makeButton method. Therefore, you can make it into an anonymous class:
void makeButton(String name, final Color backgroundColor)
{
JButton button = new JButton(name);
add(button);
button.addActionListener(new
ActionListener()
{
public void actionPerformed(ActionEvent event)
{
setBackground(backgroundColor);
}
});
}
The action listener code has become quite a bit simpler. The actionPerformed method simply refers to the parameter variable backgroundColor. (As with all local variables that are accessed in the inner class, the parameter needs to be declared as final.)
No explicit constructor is needed. As you saw in Chapter 6, the inner class mechanism automatically generates a constructor that stores all local final variables that are used in one of the methods of the inner class.
TIP
Anonymous inner classes can look confusing. But you can get used to deciphering them if you train your eyes to glaze over the routine code, like this:
button.addActionListener(new
ActionListener()
{
public void actionPerformed(ActionEvent event)
{
setBackground(backgroundColor);
}
});
That is, the button action sets the background color. As long as the event handler consists of just a few statements, we think this can be quite readable, particularly if you don't worry about the inner class mechanics.
TIP
JDK 1.4 introduces a mechanism that lets you specify simple event listeners without programming inner classes. For example, suppose you have a button labeled Load whose event handler contains a single method call:
frame.loadData();
Of course, you can use an anonymous inner class:
loadButton.addActionListener(new
ActionListener()
{
public void actionPerformed(ActionEvent event)
{
frame.loadData();
}
});
But the EventHandler class can create such a listener automatically, with the call
EventHandler.create(ActionListener.class, frame, "loadData")
Of course, you still need to install the handler:
loadButton.addActionListener((ActionListener)
EventHandler.create(ActionListener.class, frame, "loadData"));
The cast is necessary because the create method returns an Object. Perhaps a future version of the JDK will make use of generic types to make this method even more convenient.
If the event listener calls a method with a single parameter that is derived from the event handler, then you can use another form of the create method. For example, the call
EventHandler.create(ActionListener.class, frame, "loadData", "source.text")
is equivalent to
new ActionListener()
{
public void actionPerformed(ActionEvent event)
{
frame.loadData(((JTextField) event.getSource()).getText());
}
}
Note that the event handler turns the names of the properties source and text into method calls getSource and getText, using the JavaBeans convention. (For more information on properties and JavaBeans components, please turn to Volume 2.)
However, in practice, this situation is not all that common, and there is no mechanism for supplying parameters that aren't derived from the event object.
Turning Components into Event Listeners
You are completely free to designate any object of a class that implements the ActionListener interface as a button listener. We prefer to use objects of a new class that was expressly created for carrying out the desired button actions. However, some programmers are not comfortable with inner classes and choose a different strategy. They locate the component that changes as a result of the event, make that component implement the ActionListener interface, and add an actionPerformed method. In our example, you can turn the ButtonPanel into an action listener:
class ButtonPanel extends JPanel implements ActionListener
{
. . .
public void actionPerformed(ActionEvent event)
{
// set background color
. . .
}
}
Then the panel sets itself as the listener to all three buttons:
yellowButton.addActionListener(this);
blueButton.addActionListener(this);
redButton.addActionListener(this);
Note that now the three buttons no longer have individual listeners. They share a single listener object, namely, the button panel. Therefore, the actionPerformed method must figure out which button was clicked.
The getSource method of the EventObject class, the superclass of all other event classes, will tell you the source of every event. The event source is the object that generated the event and notified the listener:
Object source = event.getSource();
The actionPerformed method can then check which of the buttons was the source:
if (source == yellowButton) . . .
else if (source == blueButton) . . .
else if (source == redButton ) . . .
Of course, this approach requires that you keep references to the buttons as instance fields in the surrounding panel.
As you can see, turning the button panel into the action listener isn't really any simpler than defining an inner class. It also becomes really messy when the panel contains multiple user interface elements.
CAUTION
Some programmers use a different way to find out the event source in a listener object that is shared among multiple sources. The ActionEvent class has a getActionCommand method that returns the command string associated with this action. For buttons, it turns out that the command string defaults to being the button label. If you take this approach, an actionPerformed method contains code like this:
String command = event.getActionCommand();
if (command.equals("Yellow")) . . .;
else if (command.equals("Blue")) . . .;
else if (command.equals("Red")) . . .;
We suggest that you do not follow this approach. Relying on the button strings is dangerous. It is an easy mistake to label a button "Gray" and then spell the string slightly differently in the test:
if (command.equals("Grey")) . . .
Button strings give you grief when the time comes to internationalize your application. To make the German version with button labels "Gelb," "Blau," and "Rot," you have to change both the button labels and the strings in the actionPerformed method.
java.util.EventObject 1.1
Object getSource()
returns a reference to the object where the event occurred.
java.awt.event.ActionEvent 1.1
String getActionCommand()
returns the command string associated with this action event. If the action event originated from a button, the command string equals the button label, unless it has been changed with the setActionCommand method.
java.beans.EventHandler 1.4
static Object create(Class listenerInterface, Object target, String action)
static Object create(Class listenerInterface, Object target, String action, String eventProperty)
static Object create(Class listenerInterface, Object target, String action, String eventProperty, String listenerMethod)
construct an object of a proxy class that implements the given interface. Either the named method or all methods of the interface carry out the given action on the target object.
The action can be a method name or a property of the target. If it is a property, its setter method is executed. For example, an action "text" is turned into a call of the setText method.
The event property consists of one or more dot-separated property names. The first property is read from the parameter of the listener method. The second property is read from the resulting object, and so on. The final result becomes the parameter of the action. For example, the property "source.text" is turned into calls to the getSource and getText methods.
Example: Changing the Look and Feel
By default, Swing programs use the Metal look and feel. There are two ways to change to a different look and feel. The first way is to supply a file swing.properties in the jre/lib subdirectory of your Java installation. In that file, set the property swing.defaultlaf to the class name of the look and feel that you want. For example,
swing.defaultlaf=com.sun.java.swing.plaf.motif.MotifLookAndFeel
Note that the Metal look and feel is located in the javax.swing package. The other look-and-feel packages are located in the com.sun.java package and need not be present in every Java implementation. Currently, for copyright reasons, the Windows and Mac look-and-feel packages are only shipped with the Windows and Mac versions of the Java runtime environment.
TIP
Here is a useful tip for testing. Because lines starting with a # character are ignored in property files, you can supply several look and feel selections in the swing.properties file and move around the # to select one of them:
#swing.defaultlaf=javax.swing.plaf.metal.MetalLookAndFeel
swing.defaultlaf=com.sun.java.swing.plaf.motif.MotifLookAndFeel
#swing.defaultlaf=com.sun.java.swing.plaf.windows.WindowsLookAndFeel
You must restart your program to switch the look and feel in this way. A Swing program reads the swing.properties file only once, at startup.
The second way is to change the look and feel dynamically. Call the static UIManager.setLookAndFeel method and give it the name of the look-and-feel class that you want. Then call the static method SwingUtilities.updateComponentTreeUI to refresh the entire set of components. You need to supply one component to that method; it will find all others. The UIManager.setLookAndFeel method may throw a number of exceptions when it can't find the look and feel that you request, or when there is an error loading it. As always, we ask you to gloss over the exception handling code and wait until Chapter 11 for a full explanation.
Here is an example showing how you can switch to the Motif look and feel in your program:
String plaf = "com.sun.java.swing.plaf.motif.MotifLookAndFeel";
try
{
UIManager.setLookAndFeel(plaf);
SwingUtilities.updateComponentTreeUI(panel);
}
catch(Exception e) { e.printStackTrace(); }
To enumerate all installed look and feel implementations, call
UIManager.LookAndFeelInfo[] infos = UIManager.getInstalledLookAndFeels();
Then you can get the name and class name for each look and feel as
String name = infos[i].getName();
String className = infos[i].getClassName();
Example 8-2 is a complete program that demonstrates how to switch the look and feel (see Figure 8-3). The program is similar to Example 8-1. Following the advice of the preceding section, we use a helper method makeButton and an anonymous inner class to specify the button action, namely, to switch the look and feel.
Figure 8-3. Switching the look and feel
There is one fine point to this program. The actionPerformed method of the inner action listener class needs to pass the this reference of the outer PlafPanel class to the updateComponentTreeUI method. Recall from Chapter 6 that the outer object's this pointer must be prefixed by the outer class name:
SwingUtilities.updateComponentTreeUI(PlafPanel.this);
Example 8-2. PlafTest.java
1. import java.awt.*;
2. import java.awt.event.*;
3. import javax.swing.*;
4.
5. public class PlafTest
6. {
7. public static void main(String[] args)
8. {
9. PlafFrame frame = new PlafFrame();
10. frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
11. frame.setVisible(true);
12. }
13. }
14.
15. /**
16. A frame with a button panel for changing look and feel
17. */
18. class PlafFrame extends JFrame
19. {
20. public PlafFrame()
21. {
22. setTitle("PlafTest");
23. setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
24.
25. // add panel to frame
26.
27. PlafPanel panel = new PlafPanel();
28. add(panel);
29. }
30.
31. public static final int DEFAULT_WIDTH = 300;
32. public static final int DEFAULT_HEIGHT = 200;
33. }
34.
35. /**
36. A panel with buttons to change the pluggable look and feel
37. */
38. class PlafPanel extends JPanel
39. {
40. public PlafPanel()
41. {
42. UIManager.LookAndFeelInfo[] infos = UIManager.getInstalledLookAndFeels();
43. for (UIManager.LookAndFeelInfo info : infos)
44. makeButton(info.getName(), info.getClassName());
45. }
46.
47. /**
48. Makes a button to change the pluggable look and feel.
49. @param name the button name
50. @param plafName the name of the look and feel class
51. */
52. void makeButton(String name, final String plafName)
53. {
54. // add button to panel
55.
56. JButton button = new JButton(name);
57. add(button);
58.
59. // set button action
60.
61. button.addActionListener(new
62. ActionListener()
63. {
64. public void actionPerformed(ActionEvent event)
65. {
66. // button action: switch to the new look and feel
67. try
68. {
69. UIManager.setLookAndFeel(plafName);
70. SwingUtilities.updateComponentTreeUI(PlafPanel.this);
71. }
72. catch(Exception e) { e.printStackTrace(); }
73. }
74. });
75. }
76. }
javax.swing.UIManager 1.2
static UIManager.LookAndFeelInfo[] getInstalledLookAndFeels()
gets an array of objects that describe the installed look-and-feel implementations.
static setLookAndFeel(String className)
sets the current look and feel.
Parameters:
className
the name of the look-and-feel implementation class
javax.swing.UIManager.LookAndFeelInfo 1.2
String getName()
returns the display name for the look and feel.
String getClassName()
returns the name of the implementation class for the look and feel.
Example: Capturing Window Events
Not all events are as simple to handle as button clicks. Here is an example of a more complex case that we already briefly noted in Chapter 7. Before the appearance of the EXIT_ON_CLOSE option in the JDK 1.3, programmers had to manually exit the program when the main frame was closed. In a non-toy program, you will want to do that as well because you want to close the program only after you check that the user won't lose work. For example, when the user closes the frame, you may want to put up a dialog to warn the user if unsaved work is about to be lost and to exit the program only when the user agrees.
When the program user tries to close a frame window, the JFrame object is the source of a WindowEvent. If you want to catch that event, you must have an appropriate listener object and add it to the list of window listeners.
WindowListener listener = . . .;
frame.addWindowListener(listener);
The window listener must be an object of a class that implements the WindowListener interface. There are actually seven methods in the WindowListener interface. The frame calls them as the responses to seven distinct events that could happen to a window. The names are self-explanatory, except that "iconified" is usually called "minimized" under Windows. Here is the complete WindowListener interface:
public interface WindowListener
{
void windowOpened(WindowEvent e);
void windowClosing(WindowEvent e);
void windowClosed(WindowEvent e);
void windowIconified(WindowEvent e);
void windowDeiconified(WindowEvent e);
void windowActivated(WindowEvent e);
void windowDeactivated(WindowEvent e);
}
NOTE
To find out whether a window has been maximized, install a WindowStateListener. See the following API notes on page 302 for details.
As is always the case in Java, any class that implements an interface must implement all its methods; in this case, that means implementing seven methods. Recall that we are only interested in one of these seven methods, namely, the windowClosing method.
Of course, we can define a class that implements the interface, add a call to System.exit(0) in the windowClosing method, and write do-nothing functions for the other six methods:
class Terminator implements WindowListener
{
public void windowClosing(WindowEvent e)
{
System.exit(0);
}
public void windowOpened(WindowEvent e) {}
public void windowClosed(WindowEvent e) {}
public void windowIconified(WindowEvent e) {}
public void windowDeiconified(WindowEvent e) {}
public void windowActivated(WindowEvent e) {}
public void windowDeactivated(WindowEvent e) {}
}
Adapter Classes
Typing code for six methods that don't do anything is the kind of tedious busywork that nobody likes. To simplify this task, each of the AWT listener interfaces that has more than one method comes with a companion adapter class that implements all the methods in the interface but does nothing with them. For example, the WindowAdapter class has seven do-nothing methods. This means the adapter class automatically satisfies the technical requirements that Java imposes for implementing the associated listener interface. You can extend the adapter class to specify the desired reactions to some, but not all, of the event types in the interface. (An interface such as ActionListener that has only a single method does not need an adapter class.)
Let us make use of the window adapter. We can extend the WindowAdapter class, inherit six of the do-nothing methods, and override the windowClosing method:
class Terminator extends WindowAdapter
{
public void windowClosing(WindowEvent e)
{
System.exit(0);
}
}
Now you can register an object of type Terminator as the event listener:
WindowListener listener = new Terminator();
frame.addWindowListener(listener);
Whenever the frame generates a window event, it passes it to the listener object by calling one of its seven methods (see Figure 8-4). Six of those methods do nothing; the windowClosing method calls System.exit(0), terminating the application.
Figure 8-4. A window listener
CAUTION
If you misspell the name of a method when extending an adapter class, then the compiler won't catch your error. For example, if you define a method windowIsClosing in a WindowAdapter class, then you get a class with eight methods, and the windowClosing method does nothing.
Creating a listener class that extends the WindowAdapter is an improvement, but we can go even further. There is no need to give a name to the listener object. Simply write
frame.addWindowListener(new Terminator());
But why stop there? We can make the listener class into an anonymous inner class of the frame.
frame.addWindowListener(new
WindowAdapter()
{
public void windowClosing(WindowEvent e)
{
System.exit(0);
}
});
This code does the following:
Defines a class without a name that extends the WindowAdapter class;
Adds a windowClosing method to that anonymous class (as before, this method exits the program);
Inherits the remaining six do-nothing methods from WindowAdapter;
Creates an object of this class; that object does not have a name, either;
Passes that object to the addWindowListener method.
We say again that the syntax for using anonymous inner classes takes some getting used to. The payoff is that the resulting code is as short as possible.
java.awt.event.WindowListener 1.1
void windowOpened(WindowEvent e)
is called after the window has been opened.
void windowClosing(WindowEvent e)
is called when the user has issued a window manager command to close the window. Note that the window will close only if its hide or dispose method is called.
void windowClosed(WindowEvent e)
is called after the window has closed.
void windowIconified(WindowEvent e)
is called after the window has been iconified.
void windowDeiconified(WindowEvent e)
is called after the window has been deiconified.
void windowActivated(WindowEvent e)
is called after the window has become active. Only a frame or dialog can be active. Typically, the window manager decorates the active window, for example, by highlighting the title bar.
void windowDeactivated(WindowEvent e)
is called after the window has become deactivated.
java.awt.event.WindowStateListener 1.4
void windowStateChanged(WindowEvent event)
is called after the window has been maximized, iconified, or restored to its normal size.
java.awt.event.WindowEvent 1.1
int getNewState() 1.4
int getOldState() 1.4
return the new and old state of a window in a window state change event. The returned integer is one of the following values:
Frame.NORMAL
Frame.ICONIFIED
Frame.MAXIMIZED_HORIZ
Frame.MAXIMIZED_VERT
Frame.MAXIMIZED_BOTH
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
The AWT Event Hierarchy
Having given you a taste of how event handling works, we want to turn to a more general discussion of event handling in Java. As we briefly mentioned earlier, event handling in Java is object oriented, with all events descending from the EventObject class in the java.util package. (The common superclass is not called Event because that is the name of the event class in the old event model. Although the old model is now deprecated, its classes are still a part of the Java library.)
The EventObject class has a subclass AWTEvent, which is the parent of all AWT event classes. Figure 8-5 shows the inheritance diagram of the AWT events.
Figure 8-5. Inheritance diagram of AWT event classes
[View full size image]
Some of the Swing components generate event objects of yet more event types; these directly extend EventObject, not AWTEvent.
The event objects encapsulate information about the event that the event source communicates to its listeners. When necessary, you can then analyze the event objects that were passed to the listener object, as we did in the button example with the getSource and getActionCommand methods.
Some of the AWT event classes are of no practical use for the Java programmer. For example, the AWT inserts PaintEvent objects into the event queue, but these objects are not delivered to listeners. Java programmers don't listen to paint events; they override the paintComponent method to control repainting. The AWT also generates a number of events that are needed only by system programmers, to provide input systems for ideographic languages, automated testing robots, and so on. We do not discuss these specialized event types. Finally, we omit events that are associated with obsolete AWT components.
Here is a list of the commonly used AWT event types.
ActionEvent KeyEvent
AdjustmentEvent MouseEvent
FocusEvent MouseWheelEvent
ItemEvent WindowEvent
You will see examples of these event types in this chapter and the next.
The javax.swing.event package contains additional events that are specific to Swing components. We cover some of them in the next chapter.
The following interfaces listen to these events.
ActionListener MouseMotionListener
AdjustmentListener MouseWheelListener
FocusListener WindowListener
ItemListener WindowFocusListener
KeyListener WindowStateListener
MouseListener
You have already seen the ActionListener and WindowListener interface.
Although the javax.swing.event package contains many more listener interfaces that are specific to Swing user interface components, it still uses the basic AWT listener interfaces extensively for general event processing.
Several of the AWT listener interfaces, namely, those that have more than one method, come with a companion adapter class that implements all the methods in the interface to do nothing. (The other interfaces have only a single method each, so there is no benefit in having adapter classes for these interfaces.) Here are the commonly used adapter classes:
FocusAdapter MouseMotionAdapter
KeyAdapter WindowAdapter
MouseAdapter
Obviously, there are a lot of classes and interfaces to keep track of-it can all be a bit overwhelming. Fortunately, the principle is simple. A class that is interested in receiving events must implement a listener interface. It registers itself with the event source. It then gets the events that it asked for and processes them through the methods of the listener interface.
C++ NOTE
People coming from a C/C++ background may be wondering: Why the proliferation of objects, methods, and interfaces needed for event handling? C++ programmers are accustomed to doing GUI programming by writing callbacks with generic pointers or handles. This won't work in Java. The Java event model is strongly typed: the compiler watches out that events are sent only to objects that are capable of handling them.
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
Semantic and Low-Level Events in the AWT
The AWT makes a useful distinction between low-level and semantic events. A semantic event is one that expresses what the user is doing, such as "clicking that button"; hence, an ActionEvent is a semantic event. Low-level events are those events that make this possible. In the case of a button click, this is a mouse down, a series of mouse moves, and a mouse up (but only if the mouse up is inside the button area). Or it might be a keystroke, which happens if the user selects the button with the TAB key and then activates it with the space bar. Similarly, adjusting a scrollbar is a semantic event, but dragging the mouse is a low-level event.
Here are the most commonly used semantic event classes in the java.awt.event package:
ActionEvent (for a button click, a menu selection, selecting a list item, or ENTER typed in a text field)
AdjustmentEvent (the user adjusted a scrollbar)
ItemEvent (the user made a selection from a set of checkbox or list items)
Five low-level event classes are commonly used:
KeyEvent (a key was pressed or released)
MouseEvent (the mouse button was pressed, released, moved, or dragged)
MouseWheelEvent (the mouse wheel was rotated)
FocusEvent (a component got focus, or lost focus). See page 321 for more information about the focus concept.
WindowEvent (the window state changed)
Table 8-1 shows the most important AWT listener interfaces, events, and event sources.
Table 8-1. Event Handling Summary Interface
Methods
Parameter/Accessors
Events Generated By
ActionListener
actionperformed
ActionEvent
getActionCommand
getModifiers
AbstractButton
JComboBox
JTextField
Timer
AdjustmentListener
adjustmentvaluechanged
AdjustmentEvent
getAdjustable
getAdjustmentType
getValue
JScrollbar
ItemListener
itemstatechanged
ItemEvent
getItem
getItemSelectable
getStateChange
AbstractButton
JComboBox
FocusListener
focusgained
focuslost
FocusEvent
isTemporary
Component
KeyListener
keypressed
keyreleased
keytyped
KeyEvent
getKeyChar
getKeyCode
getKeyModifiersText
getKeyText
isActionKey
Component
MouseListener
mousepressed
mousereleased
mouseentered
mouseexited
mouseclicked
MouseEvent
getClickCount
getX
getY
getPoint
TRanslatePoint
Component
MouseMotionListener
mousedragged
mousemoved
MouseEvent
Component
MouseWheelListener
mousewheelmoved
MouseWheelEvent
getWheelRotation
getScrollAmount
Component
WindowListener
windowclosing
windowopened
windowiconified
windowdeiconified
windowclosed
windowactivated
windowdeactivated
WindowEvent
getWindow
Window
WindowFocusListener
windowgainedfocus
windowlostfocus
WindowEvent
getOppositeWindow
Window
WindowStateListener
WindowStateChanged
WindowEvent
getOldState
getNewState
Window
Event Handling Summary
Let's go over the event delegation mechanism one more time to make sure that you understand the relationship between event classes, listener interfaces, and adapter classes.
Event sources are user interface components, windows, and menus. The operating system notifies an event source about interesting activities, such as mouse moves and keystrokes. The event source describes the nature of the event in an event object. It also keeps a set of listeners-objects that want to be called when the event happens (see Figure 8-6). The event source then calls the appropriate method of the listener interface to deliver information about the event to the various listeners. The source does this by passing the appropriate event object to the method in the listener class. The listener analyzes the event object to find out more about the event. For example, you can use the getSource method to find out the source, or the getX and getY methods of the MouseEvent class to find out the current location of the mouse.
Figure 8-6. Relationship between event sources and listeners
[View full size image]
Note that there are separate MouseListener and MouseMotionListener interfaces. This is done for efficiency-there are a lot of mouse events as the user moves the mouse around, and a listener that just cares about mouse clicks will not be bothered with unwanted mouse moves.
All low-level events inherit from ComponentEvent. This class has a method, called getComponent, which reports the component that originated the event; you can use getComponent instead of getSource. The getComponent method returns the same value as getSource, but already cast as a Component. For example, if a key event was fired because of an input into a text field, then getComponent returns a reference to that text field.
java.awt.event.ComponentEvent 1.0
Component getComponent()
returns a reference to the component that is the source for the event. This is the same as (Component) getSource().
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
Low-Level Event Types
In the sections that follow, we discuss in more detail the events that are not linked to specific user interface components, in particular, events related to keystrokes and mouse activity. You can find a detailed discussion of semantic events generated by user interface components in the next chapter.
Keyboard Events
When the user pushes a key, a KeyEvent with ID KEY_PRESSED is generated. When the user releases the key, a KEY_RELEASED KeyEvent is triggered. You trap these events in the keyPressed and keyReleased methods of any class that implements the KeyListener interface. Use these methods to trap raw keystrokes. A third method, keyTyped, combines the two: it reports on the characters that were generated by the user's keystrokes.
The best way to see what happens is with an example. But before we can do that, we have to add a little more terminology. Java makes a distinction between characters and virtual key codes. Virtual key codes are indicated with a prefix of VK_, such as VK_A or VK_SHIFT. Virtual key codes correspond to keys on the keyboard. For example, VK_A denotes the key marked A. There is no separate lowercase virtual key code-the keyboard does not have separate lowercase keys.
NOTE
Virtual key codes are related to the "scan codes" that the keyboard sends to the computer whenever a physical key is pressed or released.
So, suppose that the user types an uppercase "A" in the usual way, by pressing the SHIFT key along with the A key. Java reports five events in response to this user action. Here are the actions and the associated events:
1. Pressed the SHIFT key (keyPressed called for VK_SHIFT)
2. Pressed the A key (keyPressed called for VK_A)
3. Typed "A" (keyTyped called for an "A")
4. Released the A key (keyReleased called for VK_A)
5. Released the SHIFT key (keyReleased called for VK_SHIFT)
On the other hand, if the user typed a lowercase "a" by simply pressing the A key, then only three events occur:
1. Pressed the A key (keyPressed called for VK_A)
2. Typed "a" (keyTyped called for an "a")
3. Released the A key (keyReleased called for VK_A)
Thus, the keyTyped procedure reports the character that was typed ("A" or "a"), whereas the keyPressed and keyReleased methods report on the actual keys that the user pressed.
To work with the keyPressed and keyReleased methods, you should first check the key code.
public void keyPressed(KeyEvent event)
{
int keyCode = event.getKeyCode();
. . .
}
The key code will equal one of the following (reasonably mnemonic) constants. They are defined in the KeyEvent class.
VK_A . . . VK_Z
VK_0 . . . VK_9
VK_COMMA, VK_PERIOD, VK_SLASH, VK_SEMICOLON, VK_EQUALS
VK_OPEN_BRACKET, VK_BACK_SLASH, VK_CLOSE_BRACKET
VK_BACK_QUOTE, VK_QUOTE
VK_GREATER, VK_LESS, VK_UNDERSCORE, VK_MINUS
VK_AMPERSAND, VK_ASTERISK, VK_AT, VK_BRACELEFT, VK_BRACERIGHT
VK_LEFT_PARENTHESIS, VK_RIGHT_PARENTHESIS
VK_CIRCUMFLEX, VK_COLON, VK_NUMBER_SIGN, VK_QUOTEDBL
VK_EXCLAMATION_MARK, VK_INVERTED_EXCLAMATION_MARK
VK_DEAD_ABOVEDOT, VK_DEAD_ABOVERING, VK_DEAD_ACUTE
VK_DEAD_BREVE
VK_DEAD_CARON, VK_DEAD_CEDILLA, VK_DEAD_CIRCUMFLEX
VK_DEAD_DIAERESIS
VK_DEAD_DOUBLEACUTE, VK_DEAD_GRAVE, VK_DEAD_IOTA, VK_DEAD_MACRON
VK_DEAD_OGONEK, VK_DEAD_SEMIVOICED_SOUND, VK_DEAD_TILDE VK_DEAD_VOICED_SOUND
VK_DOLLAR, VK_EURO_SIGN
VK_SPACE, VK_ENTER, VK_BACK_SPACE, VK_TAB, VK_ESCAPE
VK_SHIFT, VK_CONTROL, VK_ALT, VK_ALT_GRAPH, VK_META
VK_NUM_LOCK, VK_SCROLL_LOCK, VK_CAPS_LOCK
VK_PAUSE, VK_PRINTSCREEN
VK_PAGE_UP, VK_PAGE_DOWN, VK_END, VK_HOME, VK_LEFT, VK_UP, VK_RIGHT, VK_DOWN
VK_F1 . . .VK_F24
VK_NUMPAD0 . . . VK_NUMPAD9
VK_KP_DOWN, VK_KP_LEFT, VK_KP_RIGHT, VK_KP_UP
VK_MULTIPLY, VK_ADD, VK_SEPARATER [sic], VK_SUBTRACT, VK_DECIMAL, VK_DIVIDE
VK_DELETE, VK_INSERT
VK_HELP, VK_CANCEL, VK_CLEAR, VK_FINAL
VK_CONVERT, VK_NONCONVERT, VK_ACCEPT, VK_MODECHANGE
VK_AGAIN, VK_ALPHANUMERIC, VK_CODE_INPUT, VK_COMPOSE, VK_PROPS
VK_STOP
VK_ALL_CANDIDATES, VK_PREVIOUS_CANDIDATE
VK_COPY, VK_CUT, VK_PASTE, VK_UNDO
VK_FULL_WIDTH, VK_HALF_WIDTH
VK_HIRAGANA, VK_KATAKANA, VK_ROMAN_CHARACTERS
VK_KANA, VK_KANJI
VK_JAPANESE_HIRAGANA, VK_JAPANESE_KATAKANA, VK_JAPANESE_ROMAN
VK_WINDOWS, VK_CONTEXT_MENU
VK_UNDEFINED
To find the current state of the SHIFT, CONTROL, ALT, and META keys, you can, of course, track the VK_SHIFT, VK_CONTROL, VK_ALT, and VK_META key presses, but that is tedious. Instead, simply use the isShiftDown, isControlDown, isAltDown, and isMetaDown methods. (Sun and Macintosh keyboards have a special META key. On a Sun keyboard, the key is marked a diamond. On a Macintosh, the key is marked with an apple and a cloverleaf.)
For example, the following code tests whether the user presses SHIFT + RIGHT ARROW:
public void keyPressed(KeyEvent event)
{
int keyCode = event.getKeyCode();
if (keyCode == KeyEvent.VK_RIGHT && event.isShiftDown())
{
. . .
}
}
In the keyTyped method, you call the getKeyChar method to obtain the actual character that was typed.
NOTE
Not all keystrokes result in a call to keyTyped. Only those keystrokes that generate a Unicode character can be captured in the keyTyped method. You use the keyPressed method to check for cursor keys and other command keys.
Example 8-3 shows how to handle keystrokes. The program (shown in Figure 8-7) is a simple implementation of the Etch-A-Sketch™ toy.
Figure 8-7. A sketch program
You move a pen up, down, left, and right with the cursor keys. If you hold down the SHIFT key, the pen moves by a larger increment. Or, if you are experienced in using the vi editor, you can bypass the cursor keys and use the lowercase h, j, k, and l keys to move the pen. The uppercase H, J, K, and L move the pen by a larger increment. We trap the cursor keys in the keyPressed method and the characters in the keyTyped method.
There is one technicality: Normally, a panel does not receive any key events. To override this default, we call the setFocusable method. We discuss the concept of keyboard focus later in this chapter.
Example 8-3. Sketch.java
1. import java.awt.*;
2. import java.awt.geom.*;
3. import java.util.*;
4. import java.awt.event.*;
5. import javax.swing.*;
6.
7. public class Sketch
8. {
9. public static void main(String[] args)
10. {
11. SketchFrame frame = new SketchFrame();
12. frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
13. frame.setVisible(true);
14. }
15. }
16.
17. /**
18. A frame with a panel for sketching a figure
19. */
20. class SketchFrame extends JFrame
21. {
22. public SketchFrame()
23. {
24. setTitle("Sketch");
25. setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
26.
27. // add panel to frame
28.
29. SketchPanel panel = new SketchPanel();
30. add(panel);
31. }
32.
33. public static final int DEFAULT_WIDTH = 300;
34. public static final int DEFAULT_HEIGHT = 200;
35. }
36.
37. /**
38. A panel for sketching with the keyboard.
39. */
40. class SketchPanel extends JPanel
41. {
42. public SketchPanel()
43. {
44. last = new Point2D.Double(100, 100);
45. lines = new ArrayList<Line2D>();
46. KeyHandler listener = new KeyHandler();
47. addKeyListener(listener);
48. setFocusable(true);
49. }
50.
51. /**
52. Add a new line segment to the sketch.
53. @param dx the movement in x direction
54. @param dy the movement in y direction
55. */
56. public void add(int dx, int dy)
57. {
58. // compute new end point
59. Point2D end = new Point2D.Double(last.getX() + dx, last.getY() + dy);
60.
61. // add line segment
62. Line2D line = new Line2D.Double(last, end);
63. lines.add(line);
64. repaint();
65.
66. // remember new end point
67. last = end;
68. }
69.
70. public void paintComponent(Graphics g)
71. {
72. super.paintComponent(g);
73. Graphics2D g2 = (Graphics2D) g;
74.
75. // draw all lines
76. for (Line2D l : lines)
77. g2.draw(l);
78. }
79.
80. private Point2D last;
81. private ArrayList<Line2D> lines;
82.
83. private static final int SMALL_INCREMENT = 1;
84. private static final int LARGE_INCREMENT = 5;
85.
86. private class KeyHandler implements KeyListener
87. {
88. public void keyPressed(KeyEvent event)
89. {
90. int keyCode = event.getKeyCode();
91.
92. // set distance
93. int d;
94. if (event.isShiftDown())
95. d = LARGE_INCREMENT;
96. else
97. d = SMALL_INCREMENT;
98.
99. // add line segment
100. if (keyCode == KeyEvent.VK_LEFT) add(-d, 0);
101. else if (keyCode == KeyEvent.VK_RIGHT) add(d, 0);
102. else if (keyCode == KeyEvent.VK_UP) add(0, -d);
103. else if (keyCode == KeyEvent.VK_DOWN) add(0, d);
104. }
105.
106. public void keyReleased(KeyEvent event) {}
107.
108. public void keyTyped(KeyEvent event)
109. {
110. char keyChar = event.getKeyChar();
111.
112. // set distance
113. int d;
114. if (Character.isUpperCase(keyChar))
115. {
116. d = LARGE_INCREMENT;
117. keyChar = Character.toLowerCase(keyChar);
118. }
119. else
120. d = SMALL_INCREMENT;
121.
122. // add line segment
123. if (keyChar == 'h') add(-d, 0);
124. else if (keyChar == 'l') add(d, 0);
125. else if (keyChar == 'k') add(0, -d);
126. else if (keyChar == 'j') add(0, d);
127. }
128. }
129. }
java.awt.event.KeyEvent 1.1
char getKeyChar()
returns the character that the user typed.
int getKeyCode()
returns the virtual key code of this key event.
boolean isActionKey()
returns TRue if the key in this event is an "action" key. The following keys are action keys: HOME, END, PAGE UP, PAGE DOWN, UP, DOWN, LEFT, RIGHT, F1 ... F24, PRINT SCREEN, SCROLL LOCK, CAPS LOCK, NUM LOCK, PAUSE, INSERT, DELETE, ENTER, BACKSPACE, DELETE, and TAB.
static String getKeyText(int keyCode)
returns a string describing the key code. For example, getKeyText(KeyEvent.VK_END) is the string "End".
static String getKeyModifiersText(int modifiers)
returns a string describing the modifier keys, such as SHIFT or CTRL + SHIFT.
Parameters:
modifiers
The modifier state, as reported by getModifiers
java.awt.event.InputEvent 1.1
int getModifiers()
returns an integer whose bits describe the state of the modifiers SHIFT, CONTROL, ALT, and META. This method applies to both keyboard and mouse events. To see if a bit is set, test the return value against one of the bit masks SHIFT_MASK, CTRL_MASK, ALT_MASK, ALT_GRAPH_MASK, META_MASK, or use one of the following methods.
boolean isShiftDown()
boolean isControlDown()
boolean isAltDown()
boolean isAltGraphDown() 1.2
boolean isMetaDown()
return TRue if the modifier key was held down when this event was generated.
Mouse Events
You do not need to handle mouse events explicitly if you just want the user to be able to click on a button or menu. These mouse operations are handled internally by the various components in the user interface and then translated into the appropriate semantic event. However, if you want to enable the user to draw with the mouse, you will need to trap mouse move, click, and drag events.
In this section, we show you a simple graphics editor application that allows the user to place, move, and erase squares on a canvas (see Figure 8-8).
Figure 8-8. A mouse test program
When the user clicks a mouse button, three listener methods are called: mousePressed when the mouse is first pressed, mouseReleased when the mouse is released, and, finally, mouseClicked. If you are only interested in complete clicks, you can ignore the first two methods. By using the getX and getY methods on the MouseEvent argument, you can obtain the x- and y-coordinates of the mouse pointer when the mouse was clicked. To distinguish between single, double, and triple (!) clicks, use the getClickCount method.
Some user interface designers inflict mouse click and keyboard modifier combinations, such as CONTROL + SHIFT + CLICK, on their users. We find this practice reprehensible, but if you disagree, you will find that checking for mouse buttons and keyboard modifiers is a mess. In the original API, two of the button masks equal two keyboard modifier masks, namely:
BUTTON2_MASK == ALT_MASK
BUTTON3_MASK == META_MASK
This was done so that users with a one-button mouse could simulate the other mouse buttons by holding down modifier keys instead. However, as of JDK 1.4, a different approach is recommended. There are now masks
BUTTON1_DOWN_MASK
BUTTON2_DOWN_MASK
BUTTON3_DOWN_MASK
SHIFT_DOWN_MASK
CTRL_DOWN_MASK
ALT_DOWN_MASK
ALT_GRAPH_DOWN_MASK
META_DOWN_MASK
The getModifiersEx method accurately reports the mouse buttons and keyboard modifiers of a mouse event.
Note that BUTTON3_DOWN_MASK tests for the right (nonprimary) mouse button under Windows. For example, you can use code like this to detect whether the right mouse button is down:
if ((event.getModifiersEx() & InputEvent.BUTTON3_DOWN_MASK) != 0)
. . . // code for right click
In our sample program, we supply both a mousePressed and a mouseClicked method. When you click onto a pixel that is not inside any of the squares that have been drawn, a new square is added. We implemented this in the mousePressed method so that the user receives immediate feedback and does not have to wait until the mouse button is released. When a user double-clicks inside an existing square, it is erased. We implemented this in the mouseClicked method because we need the click count.
public void mousePressed(MouseEvent event)
{
current = find(event.getPoint());
if (current == null) // not inside a square
add(event.getPoint());
}
public void mouseClicked(MouseEvent event)
{
current = find(event.getPoint());
if (current != null && event.getClickCount() >= 2)
remove(current);
}
As the mouse moves over a window, the window receives a steady stream of mouse movement events. These are ignored by most applications. However, our test application traps the events to change the cursor to a different shape (a cross hair) when it is over a square. This is done with the getPredefinedCursor method of the Cursor class. Table 8-2 lists the constants to use with this method along with what the cursors look like under Windows. (Note that several of these cursors look the same, but you should check how they look on your platform.)
Table 8-2. Sample Cursor Shapes Icon
Constant
DEFAULT_CURSOR
CROSSHAIR_CURSOR
HAND_CURSOR
MOVE_CURSOR
TEXT_CURSOR
WAIT_CURSOR
N_RESIZE_CURSOR
NE_RESIZE_CURSOR
E_RESIZE_CURSOR
SE_RESIZE_CURSOR
S_RESIZE_CURSOR
SW_RESIZE_CURSOR
W_RESIZE_CURSOR
NW_RESIZE_CURSOR
TIP
You can find cursor images in the jre/lib/images/cursors directory. The file cursors.properties defines the cursor "hot spots." This is the point that the user associates with the pointing action of the cursor. For example, if the cursor has the shape of a magnifying glass, the hot spot would be the center of the lens.
Here is the mouseMoved method of the MouseMotionListener in our example program:
public void mouseMoved(MouseEvent event)
{
if (find(event.getPoint()) == null)
setCursor(Cursor.getDefaultCursor());
else
setCursor(Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR));
}
NOTE
You can also define your own cursor types through the use of the createCustomCursor method in the Toolkit class:
Toolkit tk = Toolkit.getDefaultToolkit();
Image img = tk.getImage("dynamite.gif");
Cursor dynamiteCursor = tk.createCustomCursor(img, new Point(10, 10), "dynamite stick");
The first parameter of the createCustomCursor points to the cursor image. The second parameter gives the offset of the "hot spot" of the cursor. The third parameter is a string that describes the cursor. This string can be used for accessibility support, for example, to read the cursor shape to a user who is visually impaired or who simply is not facing the screen.
If the user presses a mouse button while the mouse is in motion, mouseDragged calls are generated instead of mouseMoved calls. Our test application lets a user drag the square under the cursor. We simply update the currently dragged rectangle to be centered under the mouse position. Then, we repaint the canvas to show the new mouse position.
public void mouseDragged(MouseEvent event)
{
if (current != null)
{
int x = event.getX();
int y = event.getY();
current.setFrame(
x - SIDELENGTH / 2,
y - SIDELENGTH / 2,
SIDELENGTH,
SIDELENGTH);
repaint();
}
NOTE
The mouseMoved method is only called as long as the mouse stays inside the component. However, the mouseDragged method keeps getting called even when the mouse is being dragged outside the component.
There are two other mouse event methods: mouseEntered and mouseExited. These methods are called when the mouse enters or exits a component.
Finally, we explain how to listen to mouse events. Mouse clicks are reported through the mouseClicked procedure, which is part of the MouseListener interface. Because many applications are interested only in mouse clicks and not in mouse moves and because mouse move events occur so frequently, the mouse move and drag events are defined in a separate interface called MouseMotionListener.
In our program we are interested in both types of mouse events. We define two inner classes: MouseHandler and MouseMotionHandler. The MouseHandler class extends the MouseAdapter class because it defines only two of the five MouseListener methods. The MouseMotionHandler implements the MouseMotionListener and defines both methods of that interface.
Example 8-4 is the program listing.
Example 8-4. MouseTest.java
1. import java.awt.*;
2. import java.awt.event.*;
3. import java.util.*;
4. import java.awt.geom.*;
5. import javax.swing.*;
6.
7. public class MouseTest
8. {
9. public static void main(String[] args)
10. {
11. MouseFrame frame = new MouseFrame();
12. frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
13. frame.setVisible(true);
14. }
15. }
16.
17. /**
18. A frame containing a panel for testing mouse operations
19. */
20. class MouseFrame extends JFrame
21. {
22. public MouseFrame()
23. {
24. setTitle("MouseTest");
25. setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
26.
27. // add panel to frame
28.
29. MousePanel panel = new MousePanel();
30. add(panel);
31. }
32.
33. public static final int DEFAULT_WIDTH = 300;
34. public static final int DEFAULT_HEIGHT = 200;
35. }
36.
37. /**
38. A panel with mouse operations for adding and removing squares.
39. */
40. class MousePanel extends JPanel
41. {
42. public MousePanel()
43. {
44. squares = new ArrayList<Rectangle2D>();
45. current = null;
46.
47. addMouseListener(new MouseHandler());
48. addMouseMotionListener(new MouseMotionHandler());
49. }
50.
51. public void paintComponent(Graphics g)
52. {
53. super.paintComponent(g);
54. Graphics2D g2 = (Graphics2D) g;
55.
56. // draw all squares
57. for (Rectangle2D r : squares)
58. g2.draw(r);
59. }
60.
61. /**
62. Finds the first square containing a point.
63. @param p a point
64. @return the first square that contains p
65. */
66. public Rectangle2D find(Point2D p)
67. {
68. for (Rectangle2D r : squares)
69. {
70. if (r.contains(p)) return r;
71. }
72. return null;
73. }
74.
75. /**
76. Adds a square to the collection.
77. @param p the center of the square
78. */
79. public void add(Point2D p)
80. {
81. double x = p.getX();
82. double y = p.getY();
83.
84. current = new Rectangle2D.Double(
85. x - SIDELENGTH / 2,
86. y - SIDELENGTH / 2,
87. SIDELENGTH,
88. SIDELENGTH);
89. squares.add(current);
90. repaint();
91. }
92.
93. /**
94. Removes a square from the collection.
95. @param s the square to remove
96. */
97. public void remove(Rectangle2D s)
98. {
99. if (s == null) return;
100. if (s == current) current = null;
101. squares.remove(s);
102. repaint();
103. }
104.
105.
106. private static final int SIDELENGTH = 10;
107. private ArrayList<Rectangle2D> squares;
108. private Rectangle2D current;
109. // the square containing the mouse cursor
110.
111. private class MouseHandler extends MouseAdapter
112. {
113. public void mousePressed(MouseEvent event)
114. {
115. // add a new square if the cursor isn't inside a square
116. current = find(event.getPoint());
117. if (current == null)
118. add(event.getPoint());
119. }
120.
121. public void mouseClicked(MouseEvent event)
122. {
123. // remove the current square if double clicked
124. current = find(event.getPoint());
125. if (current != null && event.getClickCount() >= 2)
126. remove(current);
127. }
128. }
129.
130. private class MouseMotionHandler
131. implements MouseMotionListener
132. {
133. public void mouseMoved(MouseEvent event)
134. {
135. // set the mouse cursor to cross hairs if it is inside
136. // a rectangle
137.
138. if (find(event.getPoint()) == null)
139. setCursor(Cursor.getDefaultCursor());
140. else
141. setCursor(Cursor.getPredefinedCursor(Cursor.CROSSHAIR_CURSOR));
142. }
143.
144. public void mouseDragged(MouseEvent event)
145. {
146. if (current != null)
147. {
148. int x = event.getX();
149. int y = event.getY();
150.
151. // drag the current rectangle to center it at (x, y)
152. current.setFrame(
153. x - SIDELENGTH / 2,
154. y - SIDELENGTH / 2,
155. SIDELENGTH,
156. SIDELENGTH);
157. repaint();
158. }
159. }
160. }
161. }
java.awt.event.MouseEvent 1.1
int getX()
int getY()
Point getPoint()
return the x- (horizontal) and y- (vertical) coordinate, or point where the event happened, measured from the top-left corner of the component that is the event source.
void translatePoint(int x, int y)
translates the coordinates of the event by moving x units horizontally and y units vertically.
int getClickCount()
returns the number of consecutive mouse clicks associated with this event. (The time interval for what constitutes "consecutive" is system dependent.)
java.awt.event.InputEvent 1.1
int getModifiersEx() 1.4
returns the extended or "down" modifiers for this event. Use the following mask values to test the returned value:
BUTTON1_DOWN_MASK
BUTTON2_DOWN_MASK
BUTTON3_DOWN_MASK
SHIFT_DOWN_MASK
CTRL_DOWN_MASK
ALT_DOWN_MASK
ALT_GRAPH_DOWN_MASK
META_DOWN_MASK
static String getModifiersExText(int modifiers) 1.4
returns a string such as "Shift+Button1" describing the extended or "down" modifiers in the given flag set.
java.awt.Toolkit 1.0
public Cursor createCustomCursor(Image image, Point hotSpot, String name) 1.2
creates a new custom cursor object.
Parameters:
image
The image to display when the cursor is active
hotSpot
The cursor's hot spot (such as the tip of an arrow or the center of cross hairs)
name
A description of the cursor, to support special accessibility environments
java.awt.Component 1.0
public void setCursor(Cursor cursor) 1.1
sets the cursor image to the specified cursor.
Focus Events
When you use a mouse, you can point to any object on the screen. But when you type, your keystrokes must go to a specific screen object. The window manager (such as Windows or X Windows) directs all keystrokes to the active window. Often, the active window is distinguished by a highlighted title bar. Only one window can be active at any one time.
Now suppose the active window is controlled by a Java program. The Java window receives the keystrokes, and it in turn directs them toward a particular component. That component is said to have focus. Just like the active window is usually distinguished in some way, most Swing components give a visual cue if they currently have focus. A text field has a blinking caret, a button has a rectangle around the label, and so on. When a text field has focus, you can enter text into the text field. When a button has focus, you can "click" it by pressing the space bar.
Only one component in a window can have focus. A component loses focus if the user clicks on another component, which then gains focus. The user can also use the TAB key to give focus to each component in turn. This traverses all components that are able to receive input focus. By default, Swing components are traversed from left to right, then top to bottom, as they are laid out in the container. You can change the focus traversal order; see the next chapter for more on this subject.
NOTE
Unfortunately, focus handling in older versions of the JDK had quite a few problems, with over 100 separate bugs reported. The reasons for focus flakiness were twofold. Component focus interacts with window focus, which is the responsibility of the windowing system. Therefore, some focus behavior showed platform-dependent variations. Moreover, the implementation code had apparently mushroomed out of control, particularly with the addition of an unsatisfactory focus traversal architecture in JDK 1.3.
Winston Churchill once said "The Americans will always do the right thing... after they've exhausted all the alternatives." Apparently, the same is true for the Java team, and they decided to do the right thing in JDK 1.4. They completely reimplemented the focus handling code, and they provided a complete description of the expected behavior, including an explanation of unavoidable platform dependencies.
You can find the focus specification at http://java.sun.com/j2se/1.4/docs/api/java/awt/doc-files/FocusSpec.html.
Fortunately, most application programmers don't need to worry too much about focus handling. Before JDK 1.4, a common use for trapping component focus events was error checking or data validation. Suppose you have a text field that contains a credit card number. When the user is done editing the field and moves to another field, you trap the lost focus event. If the credit card format was not formatted properly, you can display an error message and give the focus back to the credit card field. However, JDK 1.4 has robust validation mechanisms that are easier to program. We discuss validation in Chapter 9.
Some components, such as labels or panels, do not get focus by default because it is assumed that they are just used for decoration or grouping. You need to override this default if you implement a drawing program with panels that paint something in reaction to keystrokes. As of JDK 1.4, you can simply call
panel.setFocusable(true);
NOTE
In older versions of the JDK, you had to override the isFocusTraversable method of the component to achieve the same effect. However, the old focus implementation had separate concepts for the ability to gain focus and participation in focus traversal. That distinction led to confusing behavior and has now been removed. The isFocusTraversable method has been deprecated.
In the remainder of this section, we discuss focus event details that you can safely skip until you have a special need that requires fine-grained control of focus handling.
In JDK 1.4, you can easily find out
The focus owner, that is, the component that has focus;
The focused window, that is, the window that contains the focus owner;
The active window, that is, the frame or dialog that contains the focus owner.
The focused window is usually the same as the active window. You get a different result only when the focus owner is contained in a top-level window with no frame decorations, such as a pop-up menu.
To obtain that information, first get the keyboard focus manager:
KeyboardFocusManager manager = KeyboardFocusManager.getCurrentKeyboardFocusManager();
Then call
Component owner = manager.getFocusOwner();
Window focused = manager.getFocusedWindow();
Window active = manager.getActiveWindow(); // a frame or dialog
For notification of focus changes, you need to install focus listeners into components or windows. A component focus listener must implement the FocusListener interface with two methods, focusGained and focusLost. These methods are triggered when the component gains or loses the focus. Each of these methods has a FocusEvent parameter. There are several useful methods for this class. The getComponent method reports the component that gained or lost the focus, and the isTemporary method returns true if the focus change was temporary. A temporary focus change occurs when a component loses control temporarily but will automatically get it back. This happens, for example, when the user selects a different active window. As soon as the user selects the current window again, the same component regains focus.
JDK 1.4 introduces the delivery of window focus events. You add a WindowFocusListener to a window and implement the windowGainedFocus and windowLostFocus methods.
As of JDK 1.4, you can find out the "opposite" component or window when focus is transferred. When a component or window has lost focus, the opposite is the component or window that gains focus. Conversely, when a component or window gains focus, then its opposite is the one that lost focus. The getOppositeComponent method of the FocusEvent class reports the opposite component, and the getOppositeWindow of the WindowEvent class reports the opposite window.
You can programmatically move the focus to another component by calling the requestFocus method of the Component class. However, the behavior is intrinsically platform dependent if the component is not contained in the currently focused window. To enable programmers to develop platform-independent code, JDK 1.4 adds a method requestFocusInWindow to the Component class. That method succeeds only if the component is contained in the focused window.
NOTE
You should not assume that your component has focus if requestFocus or requestFocusInWindow returns true. Wait for the FOCUS_GAINED event to be delivered to be sure.
NOTE
Some programmers are confused about the FOCUS_LOST event and try to stop another component from gaining focus by requesting the focus in the focusLost handler. However, at that time, the focus is already lost. If you must trap the focus in a particular component, install a "vetoable change listener" in the KeyboardFocusManager and veto the focusOwner property. See Chapter 8 of Volume 2 for details on property vetoes.
java.awt.Component 1.0
void requestFocus()
requests that this component gets the focus.
boolean requestFocusInWindow() 1.4
requests that this component gets the focus. Returns false if this component is not contained in the focused window or if the request cannot be fulfilled for another reason. Returns TRue if it is likely that the request will be fulfilled.
void setFocusable(boolean b) 1.4
boolean isFocusable() 1.4
set or get the "focusable" state of this component. If b is true, then this component can gain focus.
boolean isFocusOwner() 1.4
returns true if this component currently owns the focus.
java.awt.KeyboardFocusManager 1.4
static KeyboardFocusManager getCurrentKeyboardFocusManager()
gets the current focus manager.
Component getFocusOwner()
gets the component that owns the focus, or null if this focus manager does not manage the component that has focus.
Window getFocusedWindow()
gets the window that contains the component that owns the focus, or null if this focus manager does not manage the component that has focus.
Window getActiveWindow()
gets the dialog or frame that contains the focused window, or null if this focus manager does not manage the focused window.
java.awt.Window() 1.0
boolean isFocused() 1.4
returns true if this window is currently the focused window.
boolean isActive() 1.4
returns true if this frame or dialog is currently the active window. The title bars of active frames and dialogs are usually marked by the window manager.
java.awt.event.FocusEvent 1.1
Component getOppositeComponent() 1.4
returns the component that lost focus in the focusGained handler, or the component that gained focus in the focusLost handler.
java.awt.event.WindowEvent 1.4
Window getOppositeWindow() 1.4
returns the window that lost focus in the windowGainedFocus handler, the window that gained focus in the windowLostFocus handler, the window that was deactivated in the windowActivated handler, or the window that was activated in the windowDeactivated handler.
java.awt.event.WindowFocusListener 1.4
void windowGainedFocus(WindowEvent event)
is called when the event source window gained focus.
void windowLostFocus(WindowEvent event)
is called when the event source window lost focus.
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
Actions
It is common to have multiple ways to activate the same command. The user can choose a certain function through a menu, a keystroke, or a button on a toolbar. This is easy to achieve in the AWT event model: link all events to the same listener. For example, suppose blueAction is an action listener whose actionPerformed method changes the background color to blue. You can attach the same object as a listener to several event sources:
A toolbar button labeled "Blue"
A menu item labeled "Blue"
A keystroke CTRL+B
Then the color change command is handled in a uniform way, no matter whether it was caused by a button click, a menu selection, or a key press.
The Swing package provides a very useful mechanism to encapsulate commands and to attach them to multiple event sources: the Action interface. An action is an object that encapsulates
A description of the command (as a text string and an optional icon); and
Parameters that are necessary to carry out the command (such as the requested color in our example).
The Action interface has the following methods:
void actionPerformed(ActionEvent event)
void setEnabled(boolean b)
boolean isEnabled()
void putValue(String key, Object value)
Object getValue(String key)
void addPropertyChangeListener(PropertyChangeListener listener)
void removePropertyChangeListener(PropertyChangeListener listener)
The first method is the familiar method in the ActionListener interface: in fact, the Action interface extends the ActionListener interface. Therefore, you can use an Action object whenever an ActionListener object is expected.
The next two methods let you enable or disable the action and check whether the action is currently enabled. When an action is attached to a menu or toolbar and the action is disabled, then the option is grayed out.
The putValue and getValue methods let you store and retrieve arbitrary name/value pairs in the action object. A couple of important predefined strings, namely, Action.NAME and Action.SMALL_ICON, store action names and icons into an action object:
action.putValue(Action.NAME, "Blue");
action.putValue(Action.SMALL_ICON, new ImageIcon("blue-ball.gif"));
Table 8-3 shows all predefined action table names.
Table 8-3. Predefined Action Table Names Name
Value
NAME
The name of the action; displayed on buttons and menu items.
SMALL_ICON
A place to store a small icon; for display in a button, menu item, or toolbar.
SHORT_DESCRIPTION
A short description of the icon; for display in a tooltip.
LONG_DESCRIPTION
A long description of the icon; for potential use in online help. No Swing component uses this value.
MNEMONIC_KEY
A mnemonic abbreviation; for display in menu items (see Chapter 9)
ACCELERATOR_KEY
A place to store an accelerator keystroke. No Swing component uses this value.
ACTION_COMMAND_KEY
Historically, used in the now obsolete registerKeyboardAction method.
DEFAULT
Potentially useful catch-all property. No Swing component uses this value.
If the action object is added to a menu or toolbar, then the name and icon are automatically retrieved and displayed in the menu item or toolbar button. The SHORT_DESCRIPTION value turns into a tooltip.
The final two methods of the Action interface allow other objects, in particular menus or toolbars that trigger the action, to be notified when the properties of the action object change. For example, if a menu is added as a property change listener of an action object and the action object is subsequently disabled, then the menu is called and can gray out the action name. Property change listeners are a general construct that is a part of the "JavaBeans" component model. You can find out more about beans and their properties in Volume 2.
Note that Action is an interface, not a class. Any class implementing this interface must implement the seven methods we just discussed. Fortunately, a friendly soul has provided a class AbstractAction that implements all methods except for actionPerformed. That class takes care of storing all name/value pairs and managing the property change listeners. You simply extend AbstractAction and supply an actionPerformed method.
Let's build an action object that can execute color change commands. We store the name of the command, an icon, and the desired color. We store the color in the table of name/value pairs that the AbstractAction class provides. Here is the code for the ColorAction class. The constructor sets the name/value pairs, and the actionPerformed method carries out the color change action.
public class ColorAction extends AbstractAction
{
public ColorAction(String name, Icon icon, Color c)
{
putValue(Action.NAME, name);
putValue(Action.SMALL_ICON, icon);
putValue("color", c);
putValue(Action.SHORT_DESCRIPTION, "Set panel color to " + name.toLowerCase());
}
public void actionPerformed(ActionEvent event)
{
Color c = (Color) getValue("color");
setBackground(c);
}
}
Our test program creates three objects of this class, such as
Action blueAction = new ColorAction("Blue", new ImageIcon("blue-ball.gif"), Color.BLUE);
Next, let's associate this action with a button. That is easy because we can use a JButton constructor that takes an Action object.
JButton blueButton = new JButton(blueAction);
That constructor reads the name and icon from the action, sets the short description as the tooltip, and sets the action as the listener. You can see the icons and a tooltip in Figure 8-9.
Figure 8-9. Buttons display the icons from the action objects
As we demonstrate in the next chapter, it is just as easy to add the same action to a menu.
Finally, we want to add the action objects to keystrokes so that the actions are carried out when the user types keyboard commands. Now we run into a technical complexity. Keystrokes are delivered to the component that has focus. Our sample application is made up of several components, namely, three buttons inside a panel. Therefore, at any time, any one of the three buttons may have focus. Each of the buttons would need to handle key events and listen to the CTRL+Y, CTRL+B, and CTRL+R keys.
This is a common problem, and the Swing designers came up with a convenient solution for solving it.
NOTE
In fact, in JDK version 1.2, there were two different solutions for binding keys to actions: the registerKeyboardAction method of the JComponent class and the KeyMap concept for JTextComponent commands. As of JDK version 1.3, these two mechanisms are unified. This section describes the unified approach.
To associate actions with keystrokes, you first need to generate objects of the KeyStroke class. This is a convenience class that encapsulates the description of a key. To generate a KeyStroke object, you don't call a constructor but instead use the static getKeyStroke method of the KeyStroke class. You specify the virtual key code and the flags (such as SHIFT and CONTROL key combinations):
KeyStroke ctrlBKey = KeyStroke.getKeyStroke(KeyEvent.VK_B, InputEvent.CTRL_MASK);
Alternatively, you can describe the keystroke as a string:
KeyStroke ctrlBKey = KeyStroke.getKeyStroke("ctrl B");
Every JComponent has three input maps, each mapping KeyStroke objects to associated actions. The three input maps correspond to three different conditions (see Table 8-4).
Table 8-4. Input Map Conditions Flag
Invoke Action
WHEN_FOCUSED
When this component has keyboard focus
WHEN_ANCESTOR_OF_FOCUSED_COMPONENT
When this component contains the component that has keyboard focus
WHEN_IN_FOCUSED_WINDOW
When this component is contained in the same window as the component that has keyboard focus
Keystroke processing checks these maps in the following order:
Check the WHEN_FOCUSED map of the component with input focus. If the keystroke exists, execute the corresponding action. If the action is enabled, stop processing.
Starting from the component with input focus, check the WHEN_ANCESTOR_OF_FOCUSED_COMPONENT maps of its parent components. As soon as a map with the keystroke is found, execute the corresponding action. If the action is enabled, stop processing.
Look at all visible and enabled components in the window with input focus that have this keystroke registered in a WHEN_IN_FOCUSED_WINDOW map. Give these components (in the order of their keystroke registration) a chance to execute the corresponding action. As soon as the first enabled action is executed, stop processing. This part of the process is somewhat fragile if a keystroke appears in more than one WHEN_IN_FOCUSED_WINDOW map.
You obtain an input map from the component with the getInputMap method, for example:
InputMap imap = panel.getInputMap(JComponent.WHEN_FOCUSED);
The WHEN_FOCUSED condition means that this map is consulted when the current component has the keyboard focus. In our situation, that isn't the map we want. One of the buttons, not the panel, has the input focus. Either of the other two map choices works fine for inserting the color change keystrokes. We use WHEN_ANCESTOR_OF_FOCUSED_COMPONENT in our example program.
The InputMap doesn't directly map KeyStroke objects to Action objects. Instead, it maps to arbitrary objects, and a second map, implemented by the ActionMap class, maps objects to actions. That makes it easier to share the same actions among keystrokes that come from different input maps.
Thus, each component has three input maps and one action map. To tie them together, you need to come up with names for the actions. Here is how you can tie a key to an action:
imap.put(KeyStroke.getKeyStroke("ctrl Y"), "panel.yellow");
ActionMap amap = panel.getActionMap();
amap.put("panel.yellow", yellowAction);
It is customary to use the string "none" for a do-nothing action. That makes it easy to deactivate a key:
imap.put(KeyStroke.getKeyStroke("ctrl C"), "none");
CAUTION
The JDK documentation suggests using the action name as the action's key. We don't think that is a good idea. The action name is displayed on buttons and menu items; thus, it can change at the whim of the UI designer and it may be translated into multiple languages. Such unstable strings are poor choices for lookup keys. Instead, we recommend that you come up with action names that are independent of the displayed names.
To summarize, here is what you do to carry out the same action in response to a button, a menu item, or a keystroke:
Implement a class that extends the AbstractAction class. You may be able to use the same class for multiple related actions.
Construct an object of the action class.
Construct a button or menu item from the action object. The constructor will read the label text and icon from the action object.
For actions that can be triggered by keystrokes, you have to carry out additional steps. First locate the top-level component of the window, such as a panel that contains all other components.
Then get the WHEN_ANCESTOR_OF_FOCUSED_COMPONENT input map of the top-level component. Make a KeyStroke object for the desired keystroke. Make an action key object, such as a string that describes your action. Add the pair (keystroke, action key) into the input map.
Finally, get the action map of the top-level component. Add the pair (action key, action object) into the map.
Example 8-5 shows the complete code of the program that maps both buttons and keystrokes to action objects. Try it out-clicking either the buttons or pressing CTRL+Y, CTRL+B, or CTRL+R changes the panel color.
Example 8-5. ActionTest.java
1. import java.awt.*;
2. import java.awt.event.*;
3. import javax.swing.*;
4.
5. public class ActionTest
6. {
7. public static void main(String[] args)
8. {
9. ActionFrame frame = new ActionFrame();
10. frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
11. frame.setVisible(true);
12. }
13. }
14.
15. /**
16. A frame with a panel that demonstrates color change actions.
17. */
18. class ActionFrame extends JFrame
19. {
20. public ActionFrame()
21. {
22. setTitle("ActionTest");
23. setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
24.
25. // add panel to frame
26.
27. ActionPanel panel = new ActionPanel();
28. add(panel);
29. }
30.
31. public static final int DEFAULT_WIDTH = 300;
32. public static final int DEFAULT_HEIGHT = 200;
33. }
34.
35. /**
36. A panel with buttons and keyboard shortcuts to change
37. the background color.
38. */
39. class ActionPanel extends JPanel
40. {
41. public ActionPanel()
42. {
43. // define actions
44.
45. Action yellowAction = new ColorAction("Yellow",
46. new ImageIcon("yellow-ball.gif"),
47. Color.YELLOW);
48. Action blueAction = new ColorAction("Blue",
49. new ImageIcon("blue-ball.gif"),
50. Color.BLUE);
51. Action redAction = new ColorAction("Red",
52. new ImageIcon("red-ball.gif"),
53. Color.RED);
54.
55. // add buttons for these actions
56.
57. add(new JButton(yellowAction));
58. add(new JButton(blueAction));
59. add(new JButton(redAction));
60.
61. // associate the Y, B, and R keys with names
62.
63. InputMap imap = getInputMap(JComponent.WHEN_ANCESTOR_OF_FOCUSED_COMPONENT);
64.
65. imap.put(KeyStroke.getKeyStroke("ctrl Y"), "panel.yellow");
66. imap.put(KeyStroke.getKeyStroke("ctrl B"), "panel.blue");
67. imap.put(KeyStroke.getKeyStroke("ctrl R"), "panel.red");
68.
69. // associate the names with actions
70.
71. ActionMap amap = getActionMap();
72. amap.put("panel.yellow", yellowAction);
73. amap.put("panel.blue", blueAction);
74. amap.put("panel.red", redAction);
75. }
76.
77. public class ColorAction extends AbstractAction
78. {
79. /**
80. Constructs a color action.
81. @param name the name to show on the button
82. @param icon the icon to display on the button
83. @param c the background color
84. */
85. public ColorAction(String name, Icon icon, Color c)
86. {
87. putValue(Action.NAME, name);
88. putValue(Action.SMALL_ICON, icon);
89. putValue(Action.SHORT_DESCRIPTION, "Set panel color to " + name.toLowerCase());
90. putValue("color", c);
91. }
92.
93. public void actionPerformed(ActionEvent event)
94. {
95. Color c = (Color) getValue("color");
96. setBackground(c);
97. }
98. }
99. }
javax.swing.Action 1.2
void setEnabled(boolean b)
enables or disables this action.
boolean isEnabled()
returns TRue if this action is enabled.
void putValue(String key, Object value)
places a name/value pair inside the action object.
Parameters:
key
The name of the feature to store with the action object. This can be any string, but several names have predefined meanings-see Table 8-3 on page 325.
value
The object associated with the name.
Object getValue(String key)
returns the value of a stored name/value pair.
javax.swing.JMenu 1.2
JMenuItem add(Action a)
adds a menu item to the menu that invokes the action a when selected; returns the added menu item.
javax.swing.KeyStroke 1.2
static KeyStroke getKeyStroke(char keyChar)
creates a KeyStroke object that encapsulates a keystroke corresponding to a KEY_TYPED event.
static KeyStroke getKeyStroke(int keyCode, int modifiers)
static KeyStroke getKeyStroke(int keyCode, int modifiers, boolean onRelease)
create a KeyStroke object that encapsulates a keystroke corresponding to a KEY_PRESSED or KEY_RELEASED event.
Parameters:
keyCode
The virtual key code
modifiers
Any combination of InputEvent.SHIFT_MASK, InputEvent.CTRL_MASK, InputEvent.ALT_MASK, InputEvent.META_MASK
onRelease
TRue if the keystroke is to be recognized when the key is released
static KeyStroke getKeyStroke(String description)
constructs a keystroke from a humanly readable description. The description is a sequence of whitespace-delimited strings in the following format:
The strings shift control ctrl meta alt button1 button2 button3 are translated to the appropriate mask bits.
The string typed must be followed by a one-character string, for example, "typed a".
The string pressed or released indicates a key press or release. (Key press is the default.)
Otherwise, the string, when prefixed with VK_, should correspond to a KeyEvent constant, for example, "INSERT" corresponds to KeyEvent.VK_INSERT.
For example, "released ctrl Y" corresponds to: getKeyStroke(KeyEvent.VK_Y, Event.CTRL_MASK, true)
javax.swing.JComponent 1.2
ActionMap getActionMap() 1.3
returns the action map that maps keystrokes to action keys.
InputMap getInputMap(int flag) 1.3
gets the input map that maps action keys to action objects.
Parameters:
flag
A condition on the keyboard focus to trigger the action, one of the values in Table 8-4 on page 327
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
Multicasting
In the preceding section, we had several event sources report to the same event listener. In this section, we do the opposite. All AWT event sources support a multicast model for listeners. This means that the same event can be sent to more than one listener object. Multicasting is useful if an event is potentially of interest to many parties. Simply add multiple listeners to an event source to give all registered listeners a chance to react to the events.
CAUTION
According to the JDK documentation, "The API makes no guarantees about the order in which the events are delivered to a set of registered listeners for a given event on a given source." Therefore, don't implement program logic that depends on the delivery order.
Here we show a simple application of multicasting. We will have a frame that can spawn multiple windows with the New button, and it can close all windows with the Close all button-see Figure 8-10.
Figure 8-10. All frames listen to the Close all command
[View full size image]
The listener to the New button of the MulticastPanel is the newListener object constructed in the MulticastPanel constructor-it makes a new frame whenever the button is clicked.
But the Close all button of the MulticastPanel has multiple listeners. Each time the BlankFrame constructor executes, it adds another action listener to the Close all button. Each of those listeners is responsible for closing a single frame in its actionPerformed method. When the user clicks the Close all button, each of the listeners is activated and each of them closes its frame.
Furthermore, the actionPerformed method removes the listener from the Close all button because it is no longer needed once the frame is closed.
Example 8-6 shows the source code.
Example 8-6. MulticastTest.java
1. import java.awt.*;
2. import java.awt.event.*;
3. import javax.swing.*;
4.
5. public class MulticastTest
6. {
7. public static void main(String[] args)
8. {
9. MulticastFrame frame = new MulticastFrame();
10. frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
11. frame.setVisible(true);
12. }
13. }
14.
15. /**
16. A frame with buttons to make and close secondary frames
17. */
18. class MulticastFrame extends JFrame
19. {
20. public MulticastFrame()
21. {
22. setTitle("MulticastTest");
23. setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
24.
25. // add panel to frame
26.
27. MulticastPanel panel = new MulticastPanel();
28. add(panel);
29. }
30.
31. public static final int DEFAULT_WIDTH = 300;
32. public static final int DEFAULT_HEIGHT = 200;
33. }
34.
35. /**
36. A panel with buttons to create and close sample frames.
37. */
38. class MulticastPanel extends JPanel
39. {
40. public MulticastPanel()
41. {
42. // add "New" button
43.
44. JButton newButton = new JButton("New");
45. add(newButton);
46. final JButton closeAllButton = new JButton("Close all");
47. add(closeAllButton);
48.
49. ActionListener newListener = new
50. ActionListener()
51. {
52. public void actionPerformed(ActionEvent event)
53. {
54. BlankFrame frame = new BlankFrame(closeAllButton);
55. frame.setVisible(true);
56. }
57. };
58.
59. newButton.addActionListener(newListener);
60. }
61. }
62.
63. /**
64. A blank frame that can be closed by clicking a button.
65. */
66. class BlankFrame extends JFrame
67. {
68. /**
69. Constructs a blank frame
70. @param closeButton the button to close this frame
71. */
72. public BlankFrame(final JButton closeButton)
73. {
74. counter++;
75. setTitle("Frame " + counter);
76. setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
77. setLocation(SPACING * counter, SPACING * counter);
78.
79. closeListener = new
80. ActionListener()
81. {
82. public void actionPerformed(ActionEvent event)
83. {
84. closeButton.removeActionListener(closeListener);
85. dispose();
86. }
87. };
88. closeButton.addActionListener(closeListener);
89. }
90.
91. private ActionListener closeListener;
92. public static final int DEFAULT_WIDTH = 200;
93. public static final int DEFAULT_HEIGHT = 150;
94. public static final int SPACING = 40;
95. private static int counter = 0;
96. }
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
Implementing Event Sources
In the last section of this chapter, we show you how to implement a class that generates its own events and notifies interested listeners. This is occasionally necessary when you use advanced Swing components. It is also interesting to see what goes on behind the scenes when you add a listener to a component.
Our event source will be a PaintCountPanel that counts how often the paintComponent method was called. Every time the count is incremented, the PaintCountPanel notifies all listeners. In our sample program, we will attach just one listener that updates the frame title-see Figure 8-11.
Figure 8-11. Counting how often the panel is painted
Whenever you define an event source, you need three ingredients:
an event type. We could define our own event class, but we will simply use the existing PropertyChangeEvent class.
an event listener interface. Again, we could define our own interface, but we will use the existing PropertyChangeListener interface. That interface has a single method.
public void propertyChange(PropertyChangeEvent event)
methods for adding and removing listeners. We will supply two methods in the PaintCountPanel class:
public void addPropertyChangeListener(PropertyChangeListener listener)
public void removePropertyChangeListener(PropertyChangeListener listener)
How do we make sure that events are sent to interested parties? This is the responsibility of the event source. It must construct an event object and pass it to the registered listeners whenever an event occurs.
Event management is a common task, and the Swing designers provide a convenience class, EventListenerList, to make it easy to implement the methods for adding and removing listeners and for firing events. The class takes care of the tricky details that can arise when multiple threads attempt to add, remove, or dispatch events at the same time.
Because some event sources accept listeners of multiple types, each listener in the event listener list is associated with a particular class. The add and remove methods are intended for the implementation of addXxxListener methods. For example,
public void addPropertyChangeListener(PropertyChangeListener listener)
{
listenerList.add(PropertyChangeListener.class, listener);
}
public void removePropertyChangeListener(PropertyChangeListener listener)
{
listenerList.remove(PropertyChangeListener.class, listener);
}
NOTE
You may wonder why the EventListenerList doesn't simply check which interface the listener object implements. But an object can implement multiple interfaces. For example, it is possible that listener happens to implement both the PropertyChangeListener and the ActionListener interface, but a programmer may choose only to add it as a PropertyChangeListener by calling the addPropertyChangeListener. The EventListenerList must respect that choice.
Whenever the paintComponent method is called, the PaintCountPanel class constructs a PropertyChangeEvent object, specifying the event source, the property name, and the old and new property values. It then calls the firePropertyChangeEvent helper method.
public void paintComponent(Graphics g)
{
int oldPaintCount = paintCount;
paintCount++;
firePropertyChangeEvent(new PropertyChangeEvent(this,
"paintCount", oldPaintCount, paintCount));
super.paintComponent(g);
}
The firePropertyChangeEvent method locates all registered listeners and calls their propertyChange methods.
public void firePropertyChangeEvent(PropertyChangeEvent event)
{
EventListener[] listeners = listenerList.getListeners(PropertyChangeListener.class);
for (EventListener l : listeners)
((PropertyChangeListener) l).propertyChange(event);
}
Example 8-7 shows the source code of a sample program that listens to a PaintCountPanel. The frame constructor adds a property change listener to the panel that updates the frame title:
panel.addPropertyChangeListener(new
PropertyChangeListener()
{
public void propertyChange(PropertyChangeEvent event)
{
setTitle("EventSourceTest - " + event.getNewValue());
}
});
This ends our discussion of event handling. In the next chapter, you will learn more about user interface components. Of course, to program user interfaces, you will put your knowledge of event handling to work by capturing the events that the user interface components generate.
Example 8-7. EventSourceTest.java
1. import java.awt.*;
2. import java.awt.event.*;
3. import java.util.*;
4. import javax.swing.*;
5. import java.beans.*;
6.
7. public class EventSourceTest
8. {
9. public static void main(String[] args)
10. {
11. EventSourceFrame frame = new EventSourceFrame();
12. frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
13. frame.setVisible(true);
14. }
15. }
16.
17. /**
18. A frame that contains a panel with drawings
19. */
20. class EventSourceFrame extends JFrame
21. {
22. public EventSourceFrame()
23. {
24. setTitle("EventSourceTest");
25. setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
26.
27. // add panel to frame
28.
29. final PaintCountPanel panel = new PaintCountPanel();
30. add(panel);
31.
32. panel.addPropertyChangeListener(new
33. PropertyChangeListener()
34. {
35. public void propertyChange(PropertyChangeEvent event)
36. {
37. setTitle("EventSourceTest - " + event.getNewValue());
38. }
39. });
40. }
41.
42. public static final int DEFAULT_WIDTH = 400;
43. public static final int DEFAULT_HEIGHT = 200;
44. }
45.
46. /**
47. A panel that counts how often it is painted.
48. */
49. class PaintCountPanel extends JPanel
50. {
51. public void paintComponent(Graphics g)
52. {
53. int oldPaintCount = paintCount;
54. paintCount++;
55. firePropertyChangeEvent(new PropertyChangeEvent(this,
56. "paintCount", oldPaintCount, paintCount));
57. super.paintComponent(g);
58. }
59.
60. /**
61. Adds a change listener
62. @param listener the listener to add
63. */
64. public void addPropertyChangeListener(PropertyChangeListener listener)
65. {
66. listenerList.add(PropertyChangeListener.class, listener);
67. }
68.
69. /**
70. Removes a change listener
71. @param listener the listener to remove
72. */
73. public void removePropertyChangeListener(PropertyChangeListener listener)
74. {
75. listenerList.remove(PropertyChangeListener.class, listener);
76. }
77.
78. public void firePropertyChangeEvent(PropertyChangeEvent event)
79. {
80. EventListener[] listeners = listenerList.getListeners(PropertyChangeListener.class);
81. for (EventListener l : listeners)
82. ((PropertyChangeListener) l).propertyChange(event);
83. }
84.
85. public int getPaintCount()
86. {
87. return paintCount;
88. }
89.
90. private int paintCount;
91. }
javax.swing.event.EventListenerList 1.2
void add(Class t, EventListener l)
adds an event listener and its class to the list. The class is stored so that event firing methods can selectively call events. Typical usage is in an addXxxListener method:
public void addXxxListener(XxxListener l)
{
listenerList.add(XxxListener.class, l);
}
Parameters:
t
The listener type
l
The listener
void remove(Class t, EventListener l)
removes an event listener and its class from the list. Typical usage is in a removeXxxListener method:
public void removeXxxListener(XxxListener l)
{
listenerList.remove(XxxListener.class, l);
}
Parameters:
t
The listener type
l
The listener
EventListener[] getListeners(Class t) 1.3
returns an array of all the listeners of the given type. The array is guaranteed to be non-null.
Object[] getListenerList()
returns an array whose elements with an even-numbered index are listener classes and whose elements with an odd-numbered index are listener objects. The array is guaranteed to be non-null.
java.beans.PropertyChangeEvent 1.1
PropertyChangeEvent(Object source, String name, Object oldValue, Object newValue)
constructs a property change event.
Parameters:
source
The event source, that is, the object that reports a property change
name
The name of the property
oldValue
The value of the property before the change
newValue
The value of the property after the change
java.beans.PropertyChangeListener 1.1
void propertyChange(PropertyChangeEvent event)
called when a property value has changed.
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
Introduction to Layout Management
Before we go on to discussing individual Swing components, such as text fields and radio buttons, we briefly cover how to arrange these components inside a frame. Unlike Visual Basic, the JDK has no form designer. You need to write code to position (lay out) the user interface components where you want them to be.
Of course, if you have a Java-enabled development environment, it will probably have a layout tool that automates some or all of these tasks. Nevertheless, it is important to know exactly what goes on "under the hood" because even the best of these tools will usually require hand-tweaking.
Let's start by reviewing the program from the last chapter that used buttons to change the background color of a frame (see Figure 9-5).
Figure 9-5. A panel with three buttons
Let us quickly recall how we built this program:
We defined the appearance of each button by setting the label string in the constructor, for example:
JButton yellowButton = new JButton("Yellow")
We then added the individual buttons to a panel, for example, with
panel.add(yellowButton);
We then added the needed event handlers, for example:
yellowButton.addActionListener(listener);
What happens if we add more buttons? Figure 9-6 shows what happens when you add six buttons to the panel. As you can see, they are centered in a row, and when there is no more room, a new row is started.
Figure 9-6. A panel with six buttons managed by a flow layout
Moreover, the buttons stay centered in the panel, even when the user resizes the frame (see Figure 9-7).
Figure 9-7. Changing the panel size rearranges the buttons automatically
An elegant concept enables this dynamic layout: all components in a container are positioned by a layout manager. In our example, the buttons are managed by the flow layout manager, the default layout manager for a panel.
The flow layout manager lines the components horizontally until there is no more room and then starts a new row of components.
When the user resizes the container, the layout manager automatically reflows the components to fill the available space.
You can choose how you want to arrange the components in each row. The default is to center them in the container. The other choices are to align them to the left or to the right of the container. To use one of these alignments, specify LEFT or RIGHT in the constructor of the FlowLayout object.
panel.setLayout(new FlowLayout(FlowLayout.LEFT));
NOTE
Normally, you just let the flow layout manager control the vertical and horizontal gaps between the components. You can, however, force a specific horizontal or vertical gap by using another version of the flow layout constructor. (See the API notes.)
java.awt.Container 1.0
setLayout(LayoutManager m)
sets the layout manager for this container.
java.awt.FlowLayout 1.0
FlowLayout(int align)
constructs a new FlowLayout with the specified alignment.
Parameters:
align
One of LEFT, CENTER, or RIGHT
FlowLayout(int align, int hgap, int vgap)
constructs a new FlowLayout with the specified alignment and the specified horizontal and vertical gaps between components.
Parameters:
align
One of LEFT, CENTER, or RIGHT
hgap
The horizontal gap to use in pixels (negative values force an overlap)
vgap
The vertical gap to use in pixels (negative values force an overlap)
Border Layout
Java comes with several layout managers, and you can also make your own layout managers. We cover all of them later in this chapter. However, to enable us to give you more interesting examples right away, we briefly describe another layout manager called the border layout manager. This is the default layout manager of the content pane of every JFrame. Unlike the flow layout manager, which completely controls the position of each component, the border layout manager lets you choose where you want to place each component. You can choose to place the component in the center, north, south, east, or west of the content pane (see Figure 9-8).
Figure 9-8. Border layout
For example:
panel.setLayout(new BorderLayout());
panel.add(yellowButton, BorderLayout.SOUTH);
The edge components are laid out first, and the remaining available space is occupied by the center. When the container is resized, the dimensions of the edge components are unchanged, but the center component changes its size. You add components by specifying a constant CENTER, NORTH, SOUTH, EAST, or WEST of the BorderLayout class. Not all of the positions need to be occupied. If you don't supply any value, CENTER is assumed.
NOTE
The BorderLayout constants are defined as strings. For example, BorderLayout.SOUTH is defined as the string "South". Many programmers prefer to use the strings directly because they are shorter, for example, frame.add(component, "South"). However, if you accidentally misspell a string, the compiler won't catch that error.
Unlike the flow layout, the border layout grows all components to fill the available space. (The flow layout leaves each component at its preferred size.)
As with flow layouts, if you want to specify a gap between the regions, you can do so in the constructor of the BorderLayout.
As previously noted, the content pane of a JFrame uses a border layout. Up to now, we never took advantage of this-we simply added panels into the default (center) area. But you can add components into the other areas as well:
frame.add(yellowButton, BorderLayout.SOUTH);
However, this code fragment has a problem, which we take up in the next section.
java.awt.Container 1.0
void add(Component c, Object constraints) 1.1
adds a component to this container.
Parameters:
c
The component to add
constraints
An identifier understood by the layout manager
java.awt.Borderlayout 1.0
BorderLayout(int hgap, int vgap)
constructs a new BorderLayout with the specified horizontal and vertical gaps between components.
Parameters:
hgap
The horizontal gap to use in pixels (negative values force an overlap)
vgap
The vertical gap to use in pixels (negative values force an overlap)
Panels
A BorderLayout is not very useful by itself. Figure 9-9 shows what happens when you use the code fragment above. The button has grown to fill the entire southern region of the frame. And, if you were to add another button to the southern region, it would just displace the first button.
Figure 9-9. A single button managed by a border layout
One common method to overcome this problem is to use additional panels. Panels act as (smaller) containers for interface elements and can themselves be arranged inside a larger panel under the control of a layout manager. For example, you can have one panel in the southern area for the buttons and another in the center for text. By nesting panels and using a mixture of border layouts and flow layouts, you can achieve fairly precise positioning of components. This approach to layout is certainly enough for prototyping, and it is the approach that we use for the example programs in the first part of this chapter. See the section on the GridBagLayout later in this chapter for the most precise way to position components.
For example, look at Figure 9-10. The three buttons at the bottom of the screen are all contained in a panel. The panel is put into the southern region of the content pane.
Figure 9-10. Panel placed at the southern region of the frame
So, suppose you want to add a panel with three buttons as in Figure 9-10. First create a new JPanel object, then add the individual buttons to the panel. The default layout manager for a panel is a FlowLayout, which is a good choice for this situation. Finally, you add the individual buttons, using the add method you have seen before. Because you are adding buttons to a panel and haven't changed the default layout manager, the position and size of the buttons is under the control of the FlowLayout manager. This means the buttons stay centered within the panel, and they do not expand to fill the entire panel area.
Here's a code fragment that adds a panel containing three buttons in the southern region of a frame.
JPanel panel = new JPanel();
panel.add(yellowButton);
panel.add(blueButton);
panel.add(redButton);
frame.add(panel, BorderLayout.SOUTH);
NOTE
The panel boundaries are not visible to the user. Panels are just an organizing mechanism for the user interface designer.
As you just saw, the JPanel class uses a FlowLayout as the default layout manager. For a JPanel, you can supply a different layout manager object in the constructor. However, most other containers do not have such a constructor. But all containers have a setLayout method to set the layout manager to something other than the default for the container.
javax.swing.JPanel 1.2
JPanel(LayoutManager m)
sets the layout manager for the panel.
Grid Layout
The grid layout arranges all components in rows and columns like a spreadsheet. However, for a grid layout, cells are always the same size. The calculator program in Figure 9-11 uses a grid layout to arrange the calculator buttons. When you resize the window, the buttons grow and shrink, but all buttons have identical sizes.
Figure 9-11. A calculator
In the constructor of the grid layout object, you specify how many rows and columns you need.
panel.setLayout(new GridLayout(5, 4));
As with the border layout and flow layout managers, you can also specify the vertical and horizontal gaps you want.
panel.setLayout(new GridLayout(5, 4, 3, 3));
The last two parameters of this constructor specify the size of the horizontal and vertical gaps (in pixels) between the components.
You add the components, starting with the first entry in the first row, then the second entry in the first row, and so on.
panel.add(new JButton("1"));
panel.add(new JButton("2"));
Example 9-1 is the source listing for the calculator program. This is a regular calculator, not the "reverse Polish" variety that is so oddly popular in Java tutorials. In this program, we call the pack method after adding the component to the frame. This method uses the preferred sizes of all components to compute the width and height of the frame.
Of course, few applications have as rigid a layout as the face of a calculator. In practice, small grids (usually with just one row or one column) can be useful to organize partial areas of a window. For example, if you want to have a row of buttons with identical size, then you can put the buttons inside a panel that is governed by a grid layout with a single row.
Example 9-1. Calculator.java
1. import java.awt.*;
2. import java.awt.event.*;
3. import javax.swing.*;
4.
5. public class Calculator
6. {
7. public static void main(String[] args)
8. {
9. CalculatorFrame frame = new CalculatorFrame();
10. frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
11. frame.setVisible(true);
12. }
13. }
14.
15. /**
16. A frame with a calculator panel.
17. */
18. class CalculatorFrame extends JFrame
19. {
20. public CalculatorFrame()
21. {
22. setTitle("Calculator");
23. CalculatorPanel panel = new CalculatorPanel();
24. add(panel);
25. pack();
26. }
27. }
28.
29. /**
30. A panel with calculator buttons and a result display.
31. */
32. class CalculatorPanel extends JPanel
33. {
34. public CalculatorPanel()
35. {
36. setLayout(new BorderLayout());
37.
38. result = 0;
39. lastCommand = "=";
40. start = true;
41.
42. // add the display
43.
44. display = new JButton("0");
45. display.setEnabled(false);
46. add(display, BorderLayout.NORTH);
47.
48. ActionListener insert = new InsertAction();
49. ActionListener command = new CommandAction();
50.
51. // add the buttons in a 4 x 4 grid
52.
53. panel = new JPanel();
54. panel.setLayout(new GridLayout(4, 4));
55.
56. addButton("7", insert);
57. addButton("8", insert);
58. addButton("9", insert);
59. addButton("/", command);
60.
61. addButton("4", insert);
62. addButton("5", insert);
63. addButton("6", insert);
64. addButton("*", command);
65.
66. addButton("1", insert);
67. addButton("2", insert);
68. addButton("3", insert);
69. addButton("-", command);
70.
71. addButton("0", insert);
72. addButton(".", insert);
73. addButton("=", command);
74. addButton("+", command);
75.
76. add(panel, BorderLayout.CENTER);
77. }
78.
79. /**
80. Adds a button to the center panel.
81. @param label the button label
82. @param listener the button listener
83. */
84. private void addButton(String label, ActionListener listener)
85. {
86. JButton button = new JButton(label);
87. button.addActionListener(listener);
88. panel.add(button);
89. }
90.
91. /**
92. This action inserts the button action string to the
93. end of the display text.
94. */
95. private class InsertAction implements ActionListener
96. {
97. public void actionPerformed(ActionEvent event)
98. {
99. String input = event.getActionCommand();
100. if (start)
101. {
102. display.setText("");
103. start = false;
104. }
105. display.setText(display.getText() + input);
106. }
107. }
108.
109. /**
110. This action executes the command that the button
111. action string denotes.
112. */
113. private class CommandAction implements ActionListener
114. {
115. public void actionPerformed(ActionEvent event)
116. {
117. String command = event.getActionCommand();
118.
119. if (start)
120. {
121. if (command.equals("-"))
122. {
123. display.setText(command);
124. start = false;
125. }
126. else
127. lastCommand = command;
128. }
129. else
130. {
131. calculate(Double.parseDouble(display.getText()));
132. lastCommand = command;
133. start = true;
134. }
135. }
136. }
137.
138. /**
139. Carries out the pending calculation.
140. @param x the value to be accumulated with the prior result.
141. */
142. public void calculate(double x)
143. {
144. if (lastCommand.equals("+")) result += x;
145. else if (lastCommand.equals("-")) result -= x;
146. else if (lastCommand.equals("*")) result *= x;
147. else if (lastCommand.equals("/")) result /= x;
148. else if (lastCommand.equals("=")) result = x;
149. display.setText("" + result);
150. }
151.
152. private JButton display;
153. private JPanel panel;
154. private double result;
155. private String lastCommand;
156. private boolean start;
157. }
java.awt.GridLayout 1.0
GridLayout(int rows, int cols)
constructs a new GridLayout.
Parameters:
rows
The number of rows in the grid
columns
The number of columns in the grid
GridLayout(int rows, int columns, int hgap, int vgap)
constructs a new GridLayout with horizontal and vertical gaps between components.
Parameters:
rows
The number of rows in the grid
columns
The number of columns in the grid
hgap
The horizontal gap to use in pixels (negative values force an overlap)
vgap
The vertical gap to use in pixels (negative values force an overlap)
java.awt.Window 1.0
void pack()
resizes this window, taking into account the preferred sizes of its components.
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
Text Input
We are finally ready to start introducing the Swing user interface components. We start with components that let a user input and edit text. You can use the JTextField and JTextArea components for gathering text input. A text field can accept only one line of text; a text area can accept multiple lines of text.
Both of these classes inherit from a class called JTextComponent. You will not be able to construct a JTextComponent yourself because it is an abstract class. On the other hand, as is so often the case in Java, when you go searching through the API documentation, you may find that the methods you are looking for are actually in the parent class JTextComponent rather than in the derived class. For example, the methods that get or set the text in a text field or text area are actually methods in JTextComponent.
javax.swing.text.JTextComponent 1.2
void setText(String t)
changes the text of a text component.
Parameters:
t
The new text
String getText()
returns the text contained in this text component.
void setEditable(boolean b)
determines whether the user can edit the content of the JTextComponent.
Text Fields
The usual way to add a text field to a window is to add it to a panel or other container-just as you would a button:
JPanel panel = new JPanel();
JTextField textField = new JTextField("Default input", 20);
panel.add(textField);
This code adds a text field and initializes the text field by placing the string "Default input" inside it. The second parameter of this constructor sets the width. In this case, the width is 20 "columns." Unfortunately, a column is a rather imprecise measurement. One column is the expected width of one character in the font you are using for the text. The idea is that if you expect the inputs to be n characters or less, you are supposed to specify n as the column width. In practice, this measurement doesn't work out too well, and you should add 1 or 2 to the maximum input length to be on the safe side. Also, keep in mind that the number of columns is only a hint to the AWT that gives the preferred size. If the layout manager needs to grow or shrink the text field, it can adjust its size. The column width that you set in the JTextField constructor is not an upper limit on the number of characters the user can enter. The user can still type in longer strings, but the input scrolls when the text exceeds the length of the field. Users tend to find scrolling text fields irritating, so you should size the fields generously. If you need to reset the number of columns at run time, you can do that with the setColumns method.
TIP
After changing the size of a text box with the setColumns method, call the revalidate method of the surrounding container.
textField.setColumns(10);
panel.revalidate();
The revalidate method recomputes the size and layout of all components in a container. After you use the revalidate method, the layout manager resizes the container, and the changed size of the text field will be visible.
The revalidate method belongs to the JComponent class. It doesn't immediately resize the component but merely marks it for resizing. This approach avoids repetitive calculations if multiple components request to be resized. However, if you want to recompute all components inside a JFrame, you have to call the validate method-JFrame doesn't extend JComponent.
In general, you want to let the user add text (or edit the existing text) in a text field. Quite often these text fields start out blank. To make a blank text field, just leave out the string as a parameter for the JTextField constructor:
JTextField textField = new JTextField(20);
You can change the content of the text field at any time by using the setText method from the JTextComponent parent class mentioned in the previous section. For example:
textField.setText("Hello!");
And, as was also mentioned in the previous section, you can find out what the user typed by calling the getText method. This method returns the exact text that the user typed. To trim any extraneous leading and trailing spaces from the data in a text field, apply the trim method to the return value of getText:
String text = textField.getText().trim();
To change the font in which the user text appears, use the setFont method.
javax.swing.JTextField 1.2
JTextField(int cols)
constructs an empty JTextField with a specified number of columns.
Parameters:
cols
The number of columns in the field
JTextField(String text, int cols)
constructs a new JTextField with an initial string and the specified number of columns.
Parameters:
text
The text to display
cols
The number of columns
void setColumns(int cols)
tells the text field the number of columns it should use.
Parameters:
cols
The number of columns
javax.swing.JComponent 1.2
void revalidate()
causes the position and size of a component to be recomputed.
java.awt.Component 1.0
void validate()
recomputes the position and size of a component. If the component is a container, the positions and sizes of its components are recomputed.
Labels and Labeling Components
Labels are components that hold text. They have no decorations (for example, no boundaries). They also do not react to user input. You can use a label to identify components. For example, unlike buttons, text fields have no label to identify them. To label a component that does not itself come with an identifier:
Construct a JLabel component with the correct text.
Place it close enough to the component you want to identify so that the user can see that the label identifies the correct component.
The constructor for a JLabel lets you specify the initial text or icon, and optionally, the alignment of the content. You use constants from the SwingConstants interface to specify alignment. That interface defines a number of useful constants such as LEFT, RIGHT, CENTER, NORTH, EAST, and so on. The JLabel class is one of several Swing classes that implement this interface. Therefore, you can specify a right-aligned label either as
JLabel label = new JLabel("Minutes", SwingConstants.RIGHT);
or
JLabel label = new JLabel("Minutes", JLabel.RIGHT);
The setText and setIcon methods let you set the text and icon of the label at run time.
TIP
Beginning with JDK 1.3, you can use both plain and HTML text in buttons, labels, and menu items. We don't recommend HTML in buttons-it interferes with the look and feel. But HTML in labels can be very effective. Simply surround the label string with <html>. . .</html>, like this:
label = new JLabel("<html>Required entry:</html>");
Fair warning-the first component with an HTML label takes some time to be displayed because the rather complex HTML rendering code must be loaded.
Labels can be positioned inside a container like any other component. This means you can use the techniques you have seen before to place labels where you need them.
javax.swing.JLabel 1.2
JLabel(String text)
constructs a label with left-aligned text.
Parameters:
text
The text in the label
JLabel(Icon icon)
constructs a label with a left-aligned icon.
Parameters:
icon
The icon in the label
JLabel(String text, int align)
constructs a label with the given text and alignment
Parameters:
text
The text in the label
align
One of the SwingConstants constants LEFT, CENTER, or RIGHT
JLabel(String text, Icon icon, int align)
constructs a label with both text and an icon. The icon is to the left of the text.
Parameters:
text
The text in the label
icon
The icon in the label
align
One of the SwingConstants constants LEFT, CENTER, or RIGHT
void setText(String text)
sets the text of this label.
Parameters:
text
The text in the label
void setIcon(Icon icon)
sets the icon of this label.
Parameters:
icon
The icon in the label
Change Tracking in Text Fields
Let us put a few text fields to work. Figure 9-12 shows the running application listed in Example 9-2. The program shows a clock and two text fields that enter the hours and minutes. Whenever the content of the text fields changes, the clock is updated.
Figure 9-12. Text field example
To track every change in the text field requires a bit of an effort. First of all, note that it is not a good idea to monitor keystrokes. Some keystrokes (such as the arrow keys) don't change the text. And, depending on the look and feel, there may be mouse actions that result in text changes. As you saw in the beginning of this chapter, the Swing text field is implemented in a rather general way: the string that you see in the text field is just a visible manifestation (the view) of an underlying data structure (the model). Of course, for a humble text field, there is no great difference between the two. The view is a displayed string, and the model is a string object. But the same architecture is used in more advanced editing components to present formatted text, with fonts, paragraphs, and other attributes that are internally represented by a more complex data structure. The model for all text components is described by the Document interface, which covers both plain text and formatted text (such as HTML). The point is that you can ask the document (and not the text component) to notify you whenever the data has changed, by installing a document listener:
textField.getDocument().addDocumentListener(listener);
When the text has changed, one of the following DocumentListener methods is called:
void insertUpdate(DocumentEvent event)
void removeUpdate(DocumentEvent event)
void changedUpdate(DocumentEvent event)
The first two methods are called when characters have been inserted or removed. The third method is not called at all for text fields. For more complex document types, it would be called when some other change, such as a change in formatting, has occurred. Unfortunately, there is no single callback to tell you that the text has changed-usually you don't much care how it has changed. And there is no adapter class either. Thus, your document listener must implement all three methods. Here is what we do in our sample program:
private class ClockFieldListener implements DocumentListener
{
public void insertUpdate(DocumentEvent event) { setClock(); }
public void removeUpdate(DocumentEvent event) { setClock(); }
public void changedUpdate(DocumentEvent event) {}
}
The setClock method uses the getText method to obtain the current user-input strings from the text fields. Unfortunately, that is what we get: strings. We need to convert the strings to integers by using the familiar, if cumbersome, incantation:
int hours = Integer.parseInt(hourField.getText().trim());
int minutes = Integer.parseInt(minuteField.getText().trim());
But this code won't work right when the user types a noninteger string, such as "two", into the text field or even leaves the field blank. For now, we catch the NumberFormatException that the parseInt method throws, and we simply don't update the clock when the text field entry is not a number. In the next section, you see how you can prevent the user from entering invalid input in the first place.
NOTE
Instead of listening to document events, you can also add an action event listener to a text field. The action listener is notified whenever the user presses the ENTER key. We don't recommend this approach, because users don't always remember to press ENTER when they are done entering data. If you use an action listener, you should also install a focus listener so that you can track when the user leaves the text field.
Finally, note how the ClockPanel constructor sets the preferred size:
public ClockPanel()
{
setPreferredSize(new Dimension(2 * RADIUS + 1, 2 * RADIUS + 1));
}
When the frame's pack method computes the frame size, it uses the panel's preferred size.
Example 9-2. TextTest.java
1. import java.awt.*;
2. import java.awt.event.*;
3. import java.awt.geom.*;
4. import javax.swing.*;
5. import javax.swing.event.*;
6.
7. public class TextTest
8. {
9. public static void main(String[] args)
10. {
11. TextTestFrame frame = new TextTestFrame();
12. frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
13. frame.setVisible(true);
14. }
15. }
16.
17. /**
18. A frame with two text fields to set a clock.
19. */
20. class TextTestFrame extends JFrame
21. {
22. public TextTestFrame()
23. {
24. setTitle("TextTest");
25.
26. DocumentListener listener = new ClockFieldListener();
27.
28. // add a panel with text fields
29.
30. JPanel panel = new JPanel();
31.
32. panel.add(new JLabel("Hours:"));
33. hourField = new JTextField("12", 3);
34. panel.add(hourField);
35. hourField.getDocument().addDocumentListener(listener);
36.
37. panel.add(new JLabel("Minutes:"));
38. minuteField = new JTextField("00", 3);
39. panel.add(minuteField);
40. minuteField.getDocument().addDocumentListener(listener);
41.
42. add(panel, BorderLayout.SOUTH);
43.
44. // add the clock
45.
46. clock = new ClockPanel();
47. add(clock, BorderLayout.CENTER);
48. pack();
49. }
50.
51. /**
52. Set the clock to the values stored in the text fields.
53. */
54. public void setClock()
55. {
56. try
57. {
58. int hours = Integer.parseInt(hourField.getText().trim());
59. int minutes = Integer.parseInt(minuteField.getText().trim());
60. clock.setTime(hours, minutes);
61. }
62. catch (NumberFormatException e) {}
63. // don't set the clock if the input can't be parsed
64. }
65.
66. public static final int DEFAULT_WIDTH = 300;
67. public static final int DEFAULT_HEIGHT = 300;
68.
69. private JTextField hourField;
70. private JTextField minuteField;
71. private ClockPanel clock;
72.
73. private class ClockFieldListener implements DocumentListener
74. {
75. public void insertUpdate(DocumentEvent event) { setClock(); }
76. public void removeUpdate(DocumentEvent event) { setClock(); }
77. public void changedUpdate(DocumentEvent event) {}
78. }
79. }
80.
81. /**
82. A panel that draws a clock.
83. */
84. class ClockPanel extends JPanel
85. {
86. public ClockPanel()
87. {
88. setPreferredSize(new Dimension(2 * RADIUS + 1, 2 * RADIUS + 1));
89. }
90.
91. public void paintComponent(Graphics g)
92. {
93. // draw the circular boundary
94.
95. super.paintComponent(g);
96. Graphics2D g2 = (Graphics2D) g;
97. Ellipse2D circle = new Ellipse2D.Double(0, 0, 2 * RADIUS, 2 * RADIUS);
98. g2.draw(circle);
99.
100. // draw the hour hand
101.
102. double hourAngle = Math.toRadians(90 - 360 * minutes / (12 * 60));
103. drawHand(g2, hourAngle, HOUR_HAND_LENGTH);
104.
105. // draw the minute hand
106.
107. double minuteAngle = Math.toRadians(90 - 360 * minutes / 60);
108. drawHand(g2, minuteAngle, MINUTE_HAND_LENGTH);
109. }
110.
111. public void drawHand(Graphics2D g2, double angle, double handLength)
112. {
113. Point2D end = new Point2D.Double(
114. RADIUS + handLength * Math.cos(angle),
115. RADIUS - handLength * Math.sin(angle));
116. Point2D center = new Point2D.Double(RADIUS, RADIUS);
117. g2.draw(new Line2D.Double(center, end));
118. }
119.
120. /**
121. Set the time to be displayed on the clock
122. @param h hours
123. @param m minutes
124. */
125. public void setTime(int h, int m)
126. {
127. minutes = h * 60 + m;
128. repaint();
129. }
130.
131. private double minutes = 0;
132. private int RADIUS = 100;
133. private double MINUTE_HAND_LENGTH = 0.8 * RADIUS;
134. private double HOUR_HAND_LENGTH = 0.6 * RADIUS;
135. }
javax.swing.JComponent 1.2
void setPreferredSize(Dimension d)
sets the preferred size of this component.
javax.swing.text.Document 1.2
int getLength()
returns the number of characters currently in the document.
String getText(int offset, int length)
returns the text contained within the given portion of the document.
Parameters:
offset
The start of the text
length
The length of the desired string
void addDocumentListener(DocumentListener listener)
registers the listener to be notified when the document changes.
javax.swing.event.DocumentEvent 1.2
Document getDocument()
gets the document that is the source of the event.
javax.swing.event.DocumentListener 1.2
void changedUpdate(DocumentEvent event)
is called whenever an attribute or set of attributes changes.
void insertUpdate(DocumentEvent event)
is called whenever an insertion into the document occurs.
void removeUpdate(DocumentEvent event)
is called whenever a portion of the document has been removed.
Password Fields
Password fields are a special kind of text field. To avoid nosy bystanders being able to glance at a password, the characters that the user entered are not actually displayed. Instead, each typed character is represented by an echo character, typically an asterisk (*). Swing supplies a JPasswordField class that implements such a text field.
The password field is another example of the power of the model-view-controller architecture pattern. The password field uses the same model to store the data as a regular text field, but its view has been changed to display all characters as echo characters.
javax.swing.JPasswordField 1.2
JPasswordField(String text, int columns)
constructs a new password field.
Parameters:
text
The text to be displayed, null if none
columns
The number of columns
void setEchoChar(char echo)
sets the echo character for this password field. This is advisory; a particular look and feel may insist on its own choice of echo character. A value of 0 resets the echo character to the default.
Parameters:
echo
The echo character to display instead of the text characters
char[] getPassword()
returns the text contained in this password field. For stronger security, you should overwrite the content of the returned array after use. (The password is not returned as a String because a string would stay in the virtual machine until it is garbage-collected.)
Formatted Input Fields
In the last example program, we wanted the program user to type numbers, not arbitrary strings. That is, the user is allowed to enter only digits 0 through 9 and a hyphen (-). The hyphen, if present at all, must be the first symbol of the input string.
On the surface, this input validation task sounds simple. We can install a key listener to the text field and then consume all key events that aren't digits or a hyphen. Unfortunately, this simple approach, although commonly recommended as a method for input validation, does not work well in practice. First, not every combination of the valid input characters is a valid number. For example, --3 and 3-3 aren't valid, even though they are made up from valid input characters. But, more important, there are other ways of changing the text that don't involve typing character keys. Depending on the look and feel, certain key combinations can be used to cut, copy, and paste text. For example, in the Metal look and feel, the CTRL+V key combination pastes the content of the paste buffer into the text field. That is, we also need to monitor that the user doesn't paste in an invalid character. Clearly, trying to filter keystrokes to ensure that the content of the text field is always valid begins to look like a real chore. This is certainly not something that an application programmer should have to worry about.
Perhaps surprisingly, before JDK 1.4, there were no components for entering numeric values. Starting with the first edition of Core Java, we supplied an implementation for an IntTextField, a text field for entering a properly formatted integer. In every new edition, we changed the implementation to take whatever limited advantage we could from the various half-baked validation schemes that were added to each version of the JDK. Finally, in JDK 1.4, the Swing designers faced the issues head-on and supplied a versatile JFormattedTextField class that can be used not just for numeric input but also for dates and for even more esoteric formatted values such as IP addresses.
Integer Input
Let's get started with an easy case first: a text field for integer input.
JFormattedTextField intField = new JFormattedTextField(NumberFormat.getIntegerInstance());
The NumberFormat.getIntegerInstance returns a formatter object that formats integers, using the current locale. In the US locale, commas are used as decimal separators, allowing users to enter values such as 1,729. The internationalization chapter in Volume 2 explains in detail how you can select other locales.
As with any text field, you can set the number of columns:
intField.setColumns(6);
You can set a default value with the setValue method. That method takes an Object parameter, so you'll need to wrap the default int value in an Integer object:
intField.setValue(new Integer(100));
Typically, users will supply inputs in multiple text fields and then click a button to read all values. When the button is clicked, you can get the user-supplied value with the getValue method. That method returns an Object result, and you need to cast it into the appropriate type. The JFormattedTextField returns an object of type Long if the user edited the value. However, if the user made no changes, the original Integer object is returned. Therefore, you should cast the return value to the common superclass Number:
Number value = (Number) intField.getValue();
int v = value.intValue();
The formatted text field is not very interesting until you consider what happens when a user provides illegal input. That is the topic of the next section.
Behavior on Loss of Focus
Consider what happens when a user supplies input to a text field. The user types input and eventually decides to leave the field, perhaps by clicking on another component with the mouse. Then the text field loses focus. The I-beam cursor is no longer visible in the text field, and keystrokes are directed toward a different component.
When the formatted text field loses focus, the formatter looks at the text string that the user produced. If the formatter knows how to convert the text string to an object, the text is valid. Otherwise it is invalid. You can use the isEditValid method to check whether the current content of the text field is valid.
The default behavior on loss of focus is called "commit or revert." If the text string is valid, it is committed. The formatter converts it to an object. That object becomes the current value of the field (that is, the return value of the getValue method that you saw in the preceding section). The value is then converted back to a string, which becomes the text string that is visible in the field. For example, the integer formatter recognizes the input 1729 as valid, sets the current value to new Long(1729) and then converts it back into a string with a decimal comma: 1,729.
Conversely, if the text string is invalid, then the current value is not changed and the text field reverts to the string that represents the old value. For example, if the user enters a bad value, such as x1, then the old value is restored when the text field loses focus.
NOTE
The integer formatter regards a text string as valid if it starts with an integer. For example, 1729x is a valid string. It is converted to the number 1729, which is then formatted as the string 1,729.
You can set other behaviors with the setFocusLostBehavior method. The "commit" behavior is subtly different from the default. If the text string is invalid, then both the text string and the field value stay unchanged-they are now out of sync. The "persist" behavior is even more conservative. Even if the text string is valid, neither the text field nor the current value are changed. You would need to call commitEdit, setValue, or setText to bring them back in sync. Finally, there is a "revert" behavior that doesn't ever seem to be useful. Whenever focus is lost, the user input is disregarded, and the text string reverts to the old value.
NOTE
Generally, the "commit or revert" default behavior is reasonable. There is just one potential problem. Suppose a dialog box contains a text field for an integer value. A user enters a string " 1729", with a leading space and then clicks the OK button. The leading space makes the number invalid, and the field value reverts to the old value. The action listener of the OK button retrieves the field value and closes the dialog. The user never knows that the new value has been rejected. In this situation, it is appropriate to select the "commit" behavior and have the OK button listener check that all field edits are valid before closing the dialog.
Filters
This basic functionality of formatted text fields is straightforward and sufficient for most uses. However, you can add a couple of refinements. Perhaps you want to prevent the user from entering nondigits altogether. You achieve that behavior with a document filter. Recall that in the model-view-controller architecture, the controller translates input events into commands that modify the underlying document of the text field, that is, the text string that is stored in a PlainDocument object. For example, whenever the controller processes a command that causes text to be inserted into the document, it calls the "insert string" command. The string to be inserted can be either a single character or the content of the paste buffer. A document filter can intercept this command and modify the string or cancel the insertion altogether. Here is the code for the insertString method of a filter that analyzes the string to be inserted and inserts only the characters that are digits or a - sign. (The code handles supplementary Unicode characters, as explained in Chapter 3. See Chapter 12 for the StringBuilder class.)
public void insertString(FilterBypass fb, int offset, String string, AttributeSet attr)
throws BadLocationException
{
StringBuilder builder = new StringBuilder(string);
for (int i = builder.length() - 1; i >= 0; i--)
{
int cp = builder.codePointAt(i);
if (!Character.isDigit(cp) && cp != '-')
{
builder.deleteCharAt(i);
if (Character.isSupplementaryCodePoint(cp))
{
i--;
builder.deleteCharAt(i);
}
}
}
super.insertString(fb, offset, builder.toString(), attr);
}
You should also override the replace method of the DocumentFilter class-it is called when text is selected and then replaced. The implementation of the replace method is straightforward-see the program at the end of this section.
Now you need to install the document filter. Unfortunately, there is no straightforward method to do that. You need to override the getdocumentFilter method of a formatter class, and pass an object of that formatter class to the JFormattedTextField. The integer text field uses an InternationalFormatter that is initialized with NumberFormat.getIntegerInstance(). Here is how you install a formatter to yield the desired filter:
JFormattedTextField intField = new JFormattedTextField(new
InternationalFormatter(NumberFormat.getIntegerInstance())
{
protected DocumentFilter getDocumentFilter()
{
return filter;
}
private DocumentFilter filter = new IntFilter();
});
NOTE
The JDK documentation states that the DocumentFilter class was invented to avoid subclassing. Until JDK 1.3, filtering in a text field was achieved by extending the PlainDocument class and overriding the insertString and replace methods. Now the PlainDocument class has a pluggable filter instead. That is a splendid improvement. It would have been even more splendid if the filter had also been made pluggable in the formatter class. Alas, it was not, and we must subclass the formatter.
Try out the FormatTest example program at the end of this section. The third text field has a filter installed. You can insert only digits or the minus ('-') character. Note that you can still enter invalid strings such as "1-2-3". In general, it is impossible to avoid all invalid strings through filtering. For example, the string "-" is invalid, but a filter can't reject it because it is a prefix of a legal string "-1". Even though filters can't give perfect protection, it makes sense to use them to reject inputs that are obviously invalid.
TIP
Another use for filtering is to turn all characters of a string to upper case. Such a filter is easy to write. In the insertString and replace methods of the filter, convert the string to be inserted to upper case and then invoke the superclass method.
Verifiers
There is another potentially useful mechanism to alert users to invalid inputs. You can attach a verifier to any JComponent. If the component loses focus, then the verifier is queried. If the verifier reports the content of the component to be invalid, the component immediately regains focus. The user is thus forced to fix the content before supplying other inputs.
A verifier must extend the abstract InputVerifier class and define a verify method. It is particularly easy to define a verifier that checks formatted text fields. The isEditValid method of the JFormattedTextField class calls the formatter and returns TRue if the formatter can turn the text string into an object. Here is the verifier.
class FormattedTextFieldVerifier extends InputVerifier
{
public boolean verify(JComponent component)
{
JFormattedTextField field = (JFormattedTextField) component;
return field.isEditValid();
}
}
You can attach it to any JFormattedTextField:
intField.setInputVerifier(new FormattedTextFieldVerifier());
However, a verifier is not entirely foolproof. If you click on a button, then the button notifies its action listeners before an invalid component regains focus. The action listeners can then get an invalid result from the component that failed verification. There is a reason for this behavior: users may want to press a Cancel button without first having to fix an invalid input.
The fourth text field in the example program has a verifier attached. Try entering an invalid number (such as x1729) and press the TAB key or click with the mouse on another text field. Note that the field immediately regains focus. However, if you press the OK button, the action listener calls getValue, which reports the last good value.
Other Standard Formatters
Besides the integer formatter, the JFormattedTextField supports several other formatters. The NumberFormat class has static methods
getNumberInstance
getCurrencyInstance
getPercentInstance
that yield formatters of floating-point numbers, currency values, and percentages. For example, you can obtain a text field for the input of currency values by calling
JFormattedTextField currencyField = new JFormattedTextField(NumberFormat
.getCurrencyInstance());
To edit dates and times, call one of the static methods of the DateFormat class:
getDateInstance
getTimeInstance
getDateTimeInstance
For example,
JFormattedTextField dateField = new JFormattedTextField(DateFormat.getDateInstance());
This field edits a date in the default or "medium" format such as
Feb 24, 2002
You can instead choose a "short" format such as
2/24/02
by calling
DateFormat.getDateInstance(DateFormat.SHORT)
NOTE
By default, the date format is "lenient." That is, an invalid date such as February 31, 2002, is rolled over to the next valid date, March 3, 2002. That behavior may be surprising to your users. In that case, call setLenient(false) on the DateFormat object.
The DefaultFormatter can format objects of any class that has a constructor with a string parameter and a matching toString method. For example, the URL class has a URL(String) constructor that can be used to construct a URL from a string, such as
URL url = new URL("http://java.sun.com");
Therefore, you can use the DefaultFormatter to format URL objects. The formatter calls toString on the field value to initialize the field text. When the field loses focus, the formatter constructs a new object of the same class as the current value, using the constructor with a String parameter. If that constructor throws an exception, then the edit is not valid. You can try that out in the example program by entering a URL that does not start with a prefix such as "http:".
NOTE
By default, the DefaultFormatter is in overwrite mode. That is different from the other formatters and not very useful. Call setOverwriteMode(false) to turn off overwrite mode.
Finally, the MaskFormatter is useful for fixed-size patterns that contain some constant and some variable characters. For example, social security numbers (such as 078-05-1120) can be formatted with a
new MaskFormatter("###-##-####")
The # symbol denotes a single digit. Table 9-2 shows the symbols that you can use in a mask formatter.
Table 9-2. MaskFormatter Symbols #
A digit
?
A letter
U
A letter, converted to upper case
L
A letter, converted to lower case
A
A letter or digit
H
A hexadecimal digit [0-9A-Fa-f]
*
Any character
'
Escape character to include a symbol in the pattern
You can restrict the characters that can be typed into the field by calling one of the methods of the MaskFormatter class:
setValidCharacters
setInvalidCharacters
For example, to read in a letter grade (such as A+ or F), you could use
MaskFormatter formatter = new MaskFormatter("U*");
formatter.setValidCharacters("ABCDF+- ");
However, there is no way of specifying that the second character cannot be a letter.
Note that the string that is formatted by the mask formatter has exactly the same length as the mask. If the user erases characters during editing, then they are replaced with the placeholder character. The default placeholder character is a space, but you can change it with the setPlaceholderCharacter method, for example,
formatter.setPlaceholderCharacter('0');
By default, a mask formatter is in overtype mode, which is quite intuitive-try it out in the example program. Also note that the caret position jumps over the fixed characters in the mask.
The mask formatter is very effective for rigid patterns such as social security numbers or American telephone numbers. However, note that no variation at all is permitted in the mask pattern. For example, you cannot use a mask formatter for international telephone numbers that have a variable number of digits.
Custom Formatters
If none of the standard formatters is appropriate, it is fairly easy to define your own formatter. Consider 4-byte IP addresses such as
130.65.86.66
You can't use a MaskFormatter because each byte might be represented by one, two, or three digits. Also, we want to check in the formatter that each byte's value is at most 255.
To define your own formatter, extend the DefaultFormatter class and override the methods
String valueToString(Object value)
Object stringToValue(String text)
The first method turns the field value into the string that is displayed in the text field. The second method parses the text that the user typed and turns it back into an object. If either method detects an error, it should throw a ParseException.
In our example program, we store an IP address in a byte[] array of length 4. The valueToString method forms a string that separates the bytes with periods. Note that byte values are signed quantities between -128 and 127. To turn negative byte values into unsigned integer values, you add 256.
public String valueToString(Object value) throws ParseException
{
if (!(value instanceof byte[]))
throw new ParseException("Not a byte[]", 0);
byte[] a = (byte[]) value;
if (a.length != 4)
throw new ParseException("Length != 4", 0);
StringBuilder builder = new StringBuilder();
for (int i = 0; i < 4; i++)
{
int b = a[i];
if (b < 0) b += 256;
builder.append(String.valueOf(b));
if (i < 3) builder.append('.');
}
return builder.toString();
}
Conversely, the stringToValue method parses the string and produces a byte[] object if the string is valid. If not, it throws a ParseException.
public Object stringToValue(String text) throws ParseException
{
StringTokenizer tokenizer = new StringTokenizer(text, ".");
byte[] a = new byte[4];
for (int i = 0; i < 4; i++)
{
int b = 0;
try
{
b = Integer.parseInt(tokenizer.nextToken());
}
catch (NumberFormatException e)
{
throw new ParseException("Not an integer", 0);
}
if (b < 0 || b >= 256)
throw new ParseException("Byte out of range", 0);
a[i] = (byte) b;
}
return a;
}
Try out the IP address field in the sample program. If you enter an invalid address, the field reverts to the last valid address.
The program in Example 9-3 shows various formatted text fields in action (see Figure 9-13). Click the Ok button to retrieve the current values from the fields.
Figure 9-13. The FormatTest program
[View full size image]
NOTE
The "Swing Connection" online newsletter has a short article describing a formatter that matches any regular expression. See http://java.sun.com/products/jfc/tsc/articles/reftf/.
Example 9-3. FormatTest.java
1. import java.awt.*;
2. import java.awt.event.*;
3. import java.lang.reflect.*;
4. import java.net.*;
5. import java.text.*;
6. import java.util.*;
7. import javax.swing.*;
8. import javax.swing.text.*;
9.
10. /**
11. A program to test formatted text fields
12. */
13. public class FormatTest
14. {
15. public static void main(String[] args)
16. {
17. FormatTestFrame frame = new FormatTestFrame();
18. frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
19. frame.setVisible(true);
20. }
21. }
22.
23. /**
24. A frame with a collection of formatted text fields and
25. a button that displays the field values.
26. */
27. class FormatTestFrame extends JFrame
28. {
29. public FormatTestFrame()
30. {
31. setTitle("FormatTest");
32. setSize(WIDTH, HEIGHT);
33.
34. JPanel buttonPanel = new JPanel();
35. okButton = new JButton("Ok");
36. buttonPanel.add(okButton);
37. add(buttonPanel, BorderLayout.SOUTH);
38.
39. mainPanel = new JPanel();
40. mainPanel.setLayout(new GridLayout(0, 3));
41. add(mainPanel, BorderLayout.CENTER);
42.
43. JFormattedTextField intField = new JFormattedTextField(NumberFormat
.getIntegerInstance());
44. intField.setValue(new Integer(100));
45. addRow("Number:", intField);
46.
47. JFormattedTextField intField2 = new JFormattedTextField(NumberFormat
.getIntegerInstance());
48. intField2.setValue(new Integer(100));
49. intField2.setFocusLostBehavior(JFormattedTextField.COMMIT);
50. addRow("Number (Commit behavior):", intField2);
51.
52. JFormattedTextField intField3
53. = new JFormattedTextField(new
54. InternationalFormatter(NumberFormat.getIntegerInstance())
55. {
56. protected DocumentFilter getDocumentFilter()
57. {
58. return filter;
59. }
60. private DocumentFilter filter = new IntFilter();
61. });
62. intField3.setValue(new Integer(100));
63. addRow("Filtered Number", intField3);
64.
65. JFormattedTextField intField4 = new JFormattedTextField(NumberFormat
.getIntegerInstance());
66. intField4.setValue(new Integer(100));
67. intField4.setInputVerifier(new FormattedTextFieldVerifier());
68. addRow("Verified Number:", intField4);
69.
70. JFormattedTextField currencyField
71. = new JFormattedTextField(NumberFormat.getCurrencyInstance());
72. currencyField.setValue(new Double(10));
73. addRow("Currency:", currencyField);
74.
75. JFormattedTextField dateField = new JFormattedTextField(DateFormat
.getDateInstance());
76. dateField.setValue(new Date());
77. addRow("Date (default):", dateField);
78.
79. DateFormat format = DateFormat.getDateInstance(DateFormat.SHORT);
80. format.setLenient(false);
81. JFormattedTextField dateField2 = new JFormattedTextField(format);
82. dateField2.setValue(new Date());
83. addRow("Date (short, not lenient):", dateField2);
84.
85. try
86. {
87. DefaultFormatter formatter = new DefaultFormatter();
88. formatter.setOverwriteMode(false);
89. JFormattedTextField urlField = new JFormattedTextField(formatter);
90. urlField.setValue(new URL("http://java.sun.com"));
91. addRow("URL:", urlField);
92. }
93. catch (MalformedURLException e)
94. {
95. e.printStackTrace();
96. }
97.
98. try
99. {
100. MaskFormatter formatter = new MaskFormatter("###-##-####");
101. formatter.setPlaceholderCharacter('0');
102. JFormattedTextField ssnField = new JFormattedTextField(formatter);
103. ssnField.setValue("078-05-1120");
104. addRow("SSN Mask:", ssnField);
105. }
106. catch (ParseException exception)
107. {
108. exception.printStackTrace();
109. }
110.
111. JFormattedTextField ipField = new JFormattedTextField(new IPAddressFormatter());
112. ipField.setValue(new byte[] { (byte) 130, 65, 86, 66 });
113. addRow("IP Address:", ipField);
114. }
115.
116. /**
117. Adds a row to the main panel.
118. @param labelText the label of the field
119. @param field the sample field
120. */
121. public void addRow(String labelText, final JFormattedTextField field)
122. {
123. mainPanel.add(new JLabel(labelText));
124. mainPanel.add(field);
125. final JLabel valueLabel = new JLabel();
126. mainPanel.add(valueLabel);
127. okButton.addActionListener(new
128. ActionListener()
129. {
130. public void actionPerformed(ActionEvent event)
131. {
132. Object value = field.getValue();
133. if (value.getClass().isArray())
134. {
135. StringBuilder builder = new StringBuilder();
136. builder.append('{');
137. for (int i = 0; i < Array.getLength(value); i++)
138. {
139. if (i > 0) builder.append(',');
140. builder.append(Array.get(value, i).toString());
141. }
142. builder.append('}');
143. valueLabel.setText(builder.toString());
144. }
145. else
146. valueLabel.setText(value.toString());
147. }
148. });
149. }
150.
151. public static final int WIDTH = 500;
152. public static final int HEIGHT = 250;
153.
154. private JButton okButton;
155. private JPanel mainPanel;
156. }
157.
158. /**
159. A filter that restricts input to digits and a '-' sign.
160. */
161. class IntFilter extends DocumentFilter
162. {
163. public void insertString(FilterBypass fb, int offset, String string, AttributeSet
attr)
164. throws BadLocationException
165. {
166. StringBuilder builder = new StringBuilder(string);
167. for (int i = builder.length() - 1; i >= 0; i--)
168. {
169. int cp = builder.codePointAt(i);
170. if (!Character.isDigit(cp) && cp != '-')
171. {
172. builder.deleteCharAt(i);
173. if (Character.isSupplementaryCodePoint(cp))
174. {
175. i--;
176. builder.deleteCharAt(i);
177. }
178. }
179. }
180. super.insertString(fb, offset, builder.toString(), attr);
181. }
182.
183. public void replace(FilterBypass fb, int offset, int length, String string,
AttributeSet attr)
184. throws BadLocationException
185. {
186. if (string != null)
187. {
188. StringBuilder builder = new StringBuilder(string);
189. for (int i = builder.length() - 1; i >= 0; i--)
190. {
191. int cp = builder.codePointAt(i);
192. if (!Character.isDigit(cp) && cp != '-')
193. {
194. builder.deleteCharAt(i);
195. if (Character.isSupplementaryCodePoint(cp))
196. {
197. i--;
198. builder.deleteCharAt(i);
199. }
200. }
201. }
202. string = builder.toString();
203. }
204. super.replace(fb, offset, length, string, attr);
205. }
206. }
207.
208. /**
209. A verifier that checks whether the content of
210. a formatted text field is valid.
211. */
212. class FormattedTextFieldVerifier extends InputVerifier
213. {
214. public boolean verify(JComponent component)
215. {
216. JFormattedTextField field = (JFormattedTextField) component;
217. return field.isEditValid();
218. }
219. }
220.
221. /**
222. A formatter for 4-byte IP addresses of the form a.b.c.d
223. */
224. class IPAddressFormatter extends DefaultFormatter
225. {
226. public String valueToString(Object value)
227. throws ParseException
228. {
229. if (!(value instanceof byte[]))
230. throw new ParseException("Not a byte[]", 0);
231. byte[] a = (byte[]) value;
232. if (a.length != 4)
233. throw new ParseException("Length != 4", 0);
234. StringBuilder builder = new StringBuilder();
235. for (int i = 0; i < 4; i++)
236. {
237. int b = a[i];
238. if (b < 0) b += 256;
239. builder.append(String.valueOf(b));
240. if (i < 3) builder.append('.');
241. }
242. return builder.toString();
243. }
244.
245. public Object stringToValue(String text) throws ParseException
246. {
247. StringTokenizer tokenizer = new StringTokenizer(text, ".");
248. byte[] a = new byte[4];
249. for (int i = 0; i < 4; i++)
250. {
251. int b = 0;
252. if (!tokenizer.hasMoreTokens())
253. throw new ParseException("Too few bytes", 0);
254. try
255. {
256. b = Integer.parseInt(tokenizer.nextToken());
257. }
258. catch (NumberFormatException e)
259. {
260. throw new ParseException("Not an integer", 0);
261. }
262. if (b < 0 || b >= 256)
263. throw new ParseException("Byte out of range", 0);
264. a[i] = (byte) b;
265. }
266. if (tokenizer.hasMoreTokens())
267. throw new ParseException("Too many bytes", 0);
268. return a;
269. }
270. }
javax.swing.JFormattedTextField 1.4
JFormattedTextField(Format fmt)
constructs a text field that uses the specified format.
JFormattedTextField(JFormattedTextField.AbstractFormatter formatter)
constructs a text field that uses the specified formatter. Note that DefaultFormatter and InternationalFormatter are subclasses of JFormattedTextField.AbstractFormatter.
Object getValue()
returns the current valid value of the field. Note that this may not correspond to the string that is being edited.
void setValue(Object value)
attempts to set the value of the given object. The attempt fails if the formatter cannot convert the object to a string.
void commitEdit()
attempts to set the valid value of the field from the edited string. The attempt may fail if the formatter cannot convert the string.
boolean isEditValid()
checks whether the edited string represents a valid value.
void setFocusLostBehavior(int behavior)
int getFocusLostBehavior()
set or get the "focus lost" behavior. Legal values for behavior are the constants COMMIT_OR_REVERT, REVERT, COMMIT, and PERSIST of the JFormattedTextField class.
java.text.DateFormat 1.1\
static DateFormat getDateInstance()
static DateFormat getDateInstance(int dateStyle)
static DateFormat getTimeInstance()
static DateFormat getTimeInstance(int timeStyle)
static DateFormat getDateTimeInstance()
static DateFormat getDateTimeInstance(int dateStyle, int timeStyle)
return formatters that yield the date, time, or both date and time of Date objects. Legal values for dateStyle and timeStyle are the constants SHORT, MEDIUM, LONG, FULL, and DEFAULT of the DateFormat class.
javax.swing.JFormattedTextField.AbstractFormatter 1.4
abstract String valueToString(Object value)
converts a value to an editable string. Throws a ParseException if value is not appropriate for this formatter.
abstract Object stringToValue(String s)
converts a string to a value. Throws a ParseException if s is not in the appropriate format.
DocumentFilter getDocumentFilter()
override this method to provide a document filter that restricts inputs into the text field. A return value of null indicates that no filtering is needed.
javax.swing.text.DefaultFormatter 1.3
void setOverwriteMode(boolean mode)
boolean getOverwriteMode()
set or get the overwrite mode. If mode is true, then new characters overwrite existing characters when editing text.
javax.swing.text.DocumentFilter 1.4
void insertString(DocumentFilter.FilterBypass bypass, int offset, String text, AttributeSet attrib)
is invoked before a string is inserted into a document. You can override the method and modify the string. You can disable insertion by not calling super.insertString or by calling bypass methods to modify the document without filtering.
Parameters:
bypass
An object that allows you to execute edit commands that bypass the filter
offset
The offset at which to insert the text
text
The characters to insert
attrib
The formatting attributes of the inserted text
void replace(DocumentFilter.FilterBypass bypass, int offset, int length, String text, AttributeSet attrib)
is invoked before a part of a document is replaced with a new string. You can override the method and modify the string. You can disable replacement by not calling super.replace or by calling bypass methods to modify the document without filtering.
Parameters:
bypass
An object that allows you to execute edit commands that bypass the filter
offset
The offset at which to insert the text
length
The length of the part to be replaced
text
The characters to insert
attrib
The formatting attributes of the inserted text
void remove(DocumentFilter.FilterBypass bypass, int offset, int length)
is invoked before a part of a document is removed. Get the document by calling bypass.getDocument() if you need to analyze the effect of the removal.
Parameters:
bypass
An object that allows you to execute edit commands that bypass the filter
offset
The offset of the part to be removed
length
The length of the part to be removed
javax.swing.text.MaskFormatter 1.4
MaskFormatter(String mask)
constructs a mask formatter with the given mask. See Table 9-2 on page 367 for the symbols in a mask.
void setValidCharacters(String characters)
String getValidCharacters()
set or get the valid editing characters. Only the characters in the given string are accepted for the variable parts of the mask.
void setInvalidCharacters(String characters)
String getInvalidCharacters()
set or get the invalid editing characters. None of the characters in the given string are accepted as input.
void setPlaceholderCharacter(char ch)
char getPlaceholderCharacter()
set or get the placeholder character that is used for variable characters in the mask that the user has not yet supplied. The default placeholder character is a space.
void setPlaceholder(String s)
String getPlaceholder()
set or get the placeholder string. Its tail end is used if the user has not supplied all variable characters in the mask. If it is null or shorter than the mask, then the placeholder character fills remaining inputs.
void setValueContainsLiteralCharacters(boolean b)
boolean getValueContainsLiteralCharacters()
set or get the "value contains literal characters" flag. If this flag is true, then the field value contains the literal (nonvariable) parts of the mask. If it is false, then the literal characters are removed. The default is TRue.
Text Areas
Sometimes, you need to collect user input that is more than one line long. As mentioned earlier, you use the JTextArea component for this collection. When you place a text area component in your program, a user can enter any number of lines of text, using the ENTER key to separate them. Each line ends with a '
'. If you need to break up the user's entry into separate lines, you can use the StringTokenizer class (see Chapter 12). Figure 9-14 shows a text area at work.
Figure 9-14. A text area
In the constructor for the JTextArea component, you specify the number of rows and columns for the text area. For example:
textArea = new JTextArea(8, 40); // 8 lines of 40 columns each
where the columns parameter works as before-and you still need to add a few more columns for safety's sake. Also, as before, the user is not restricted to the number of rows and columns; the text simply scrolls when the user inputs too much. You can also use the setColumns method to change the number of columns, and the setRows method to change the number of rows. These numbers only indicate the preferred size-the layout manager can still grow or shrink the text area.
If there is more text than the text area can display, then the remaining text is simply clipped. You can avoid clipping long lines by turning on line wrapping:
textArea.setLineWrap(true); // long lines are wrapped
This wrapping is a visual effect only; the text in the document is not changed-no '
' characters are inserted into the text.
In Swing, a text area does not have scrollbars. If you want scrollbars, you have to insert the text area inside a scroll pane.
textArea = new JTextArea(8, 40);
JScrollPane scrollPane = new JScrollPane(textArea);
The scroll pane now manages the view of the text area. Scrollbars automatically appear if there is more text than the text area can display, and they vanish again if text is deleted and the remaining text fits inside the area. The scrolling is handled internally in the scroll pane-your program does not need to process scroll events.
TIP
This is a general mechanism that you will encounter many times when working with Swing-to add scrollbars to a component, put them inside a scroll pane.
Example 9-4 is the complete code for the text area demo. This program simply lets you edit text in a text area. Click on "Insert" to insert a sentence at the end of the text. Click the second button to turn line wrapping on and off. (Its name toggles between "Wrap" and "No wrap".) Of course, you can simply use the keyboard to edit the text in the text area. Note how you can highlight a section of text, and how you can cut, copy, and paste with the CTRL+X, CTRL+C, and CTRL+V keys. (Keyboard shortcuts are specific to the look and feel. These particular key combinations work for the Metal and Windows look and feel.)
NOTE
The JTextArea component displays plain text only, without special fonts or formatting. To display formatted text (such as HTML or RTF), you can use the JEditorPane and JTextPane classes. These classes are discussed in Volume 2.
Example 9-4. TextAreaTest.java
1. import java.awt.*;
2. import java.awt.event.*;
3. import javax.swing.*;
4.
5. public class TextAreaTest
6. {
7. public static void main(String[] args)
8. {
9. TextAreaFrame frame = new TextAreaFrame();
10. frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
11. frame.setVisible(true);
12. }
13. }
14.
15. /**
16. A frame with a text area and buttons for text editing
17. */
18. class TextAreaFrame extends JFrame
19. {
20. public TextAreaFrame()
21. {
22. setTitle("TextAreaTest");
23. setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
24.
25. buttonPanel = new JPanel();
26.
27. // add button to append text into the text area
28.
29. JButton insertButton = new JButton("Insert");
30. buttonPanel.add(insertButton);
31. insertButton.addActionListener(new
32. ActionListener()
33. {
34. public void actionPerformed(ActionEvent event)
35. {
36. textArea.append("The quick brown fox jumps over the lazy dog. ");
37. }
38. });
39.
40. // add button to turn line wrapping on and off
41.
42. wrapButton = new JButton("Wrap");
43. buttonPanel.add(wrapButton);
44. wrapButton.addActionListener(new
45. ActionListener()
46. {
47. public void actionPerformed(ActionEvent event)
48. {
49. boolean wrap = !textArea.getLineWrap();
50. textArea.setLineWrap(wrap);
51. scrollPane.revalidate();
52. wrapButton.setText(wrap ? "No Wrap" : "Wrap");
53. }
54. });
55.
56. add(buttonPanel, BorderLayout.SOUTH);
57.
58. // add a text area with scrollbars
59.
60. textArea = new JTextArea(8, 40);
61. scrollPane = new JScrollPane(textArea);
62.
63. add(scrollPane, BorderLayout.CENTER);
64. }
65.
66. public static final int DEFAULT_WIDTH = 300;
67. public static final int DEFAULT_HEIGHT = 300;
68.
69. private JTextArea textArea;
70. private JScrollPane scrollPane;
71. private JPanel buttonPanel;
72. private JButton wrapButton;
73. }
javax.swing.JTextArea 1.2
JTextArea(int rows, int cols)
constructs a new text area.
Parameters:
rows
The number of rows
cols
The number of columns
JTextArea(String text, int rows, int cols)
constructs a new text area with an initial text.
Parameters:
text
The initial text
rows
The number of rows
cols
The number of columns
void setColumns(int cols)
tells the text area the preferred number of columns it should use.
Parameters:
cols
The number of columns
void setRows(int rows)
tells the text area the preferred number of rows it should use.
Parameters:
rows
The number of rows
void append(String newText)
appends the given text to the end of the text already in the text area.
Parameters:
newText
The text to append
void setLineWrap(boolean wrap)
turns line wrapping on or off.
Parameters:
wrap
true if lines should be wrapped
void setWrapStyleWord(boolean word)
If word is true, then long lines are wrapped at word boundaries. If it is false, then long lines are broken without taking word boundaries into account.
void setTabSize(int c)
sets tab stops every c columns. Note that the tabs aren't converted to spaces but cause alignment with the next tab stop.
Parameters:
c
The number of columns for a tab stop
javax.swing.JScrollPane 1.2
JScrollPane(Component c)
creates a scroll pane that displays the content of the specified component. Scrollbars are supplied when the component is larger than the view.
Parameters:
c
The component to scroll
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
Text Input
We are finally ready to start introducing the Swing user interface components. We start with components that let a user input and edit text. You can use the JTextField and JTextArea components for gathering text input. A text field can accept only one line of text; a text area can accept multiple lines of text.
Both of these classes inherit from a class called JTextComponent. You will not be able to construct a JTextComponent yourself because it is an abstract class. On the other hand, as is so often the case in Java, when you go searching through the API documentation, you may find that the methods you are looking for are actually in the parent class JTextComponent rather than in the derived class. For example, the methods that get or set the text in a text field or text area are actually methods in JTextComponent.
javax.swing.text.JTextComponent 1.2
void setText(String t)
changes the text of a text component.
Parameters:
t
The new text
String getText()
returns the text contained in this text component.
void setEditable(boolean b)
determines whether the user can edit the content of the JTextComponent.
Text Fields
The usual way to add a text field to a window is to add it to a panel or other container-just as you would a button:
JPanel panel = new JPanel();
JTextField textField = new JTextField("Default input", 20);
panel.add(textField);
This code adds a text field and initializes the text field by placing the string "Default input" inside it. The second parameter of this constructor sets the width. In this case, the width is 20 "columns." Unfortunately, a column is a rather imprecise measurement. One column is the expected width of one character in the font you are using for the text. The idea is that if you expect the inputs to be n characters or less, you are supposed to specify n as the column width. In practice, this measurement doesn't work out too well, and you should add 1 or 2 to the maximum input length to be on the safe side. Also, keep in mind that the number of columns is only a hint to the AWT that gives the preferred size. If the layout manager needs to grow or shrink the text field, it can adjust its size. The column width that you set in the JTextField constructor is not an upper limit on the number of characters the user can enter. The user can still type in longer strings, but the input scrolls when the text exceeds the length of the field. Users tend to find scrolling text fields irritating, so you should size the fields generously. If you need to reset the number of columns at run time, you can do that with the setColumns method.
TIP
After changing the size of a text box with the setColumns method, call the revalidate method of the surrounding container.
textField.setColumns(10);
panel.revalidate();
The revalidate method recomputes the size and layout of all components in a container. After you use the revalidate method, the layout manager resizes the container, and the changed size of the text field will be visible.
The revalidate method belongs to the JComponent class. It doesn't immediately resize the component but merely marks it for resizing. This approach avoids repetitive calculations if multiple components request to be resized. However, if you want to recompute all components inside a JFrame, you have to call the validate method-JFrame doesn't extend JComponent.
In general, you want to let the user add text (or edit the existing text) in a text field. Quite often these text fields start out blank. To make a blank text field, just leave out the string as a parameter for the JTextField constructor:
JTextField textField = new JTextField(20);
You can change the content of the text field at any time by using the setText method from the JTextComponent parent class mentioned in the previous section. For example:
textField.setText("Hello!");
And, as was also mentioned in the previous section, you can find out what the user typed by calling the getText method. This method returns the exact text that the user typed. To trim any extraneous leading and trailing spaces from the data in a text field, apply the trim method to the return value of getText:
String text = textField.getText().trim();
To change the font in which the user text appears, use the setFont method.
javax.swing.JTextField 1.2
JTextField(int cols)
constructs an empty JTextField with a specified number of columns.
Parameters:
cols
The number of columns in the field
JTextField(String text, int cols)
constructs a new JTextField with an initial string and the specified number of columns.
Parameters:
text
The text to display
cols
The number of columns
void setColumns(int cols)
tells the text field the number of columns it should use.
Parameters:
cols
The number of columns
javax.swing.JComponent 1.2
void revalidate()
causes the position and size of a component to be recomputed.
java.awt.Component 1.0
void validate()
recomputes the position and size of a component. If the component is a container, the positions and sizes of its components are recomputed.
Labels and Labeling Components
Labels are components that hold text. They have no decorations (for example, no boundaries). They also do not react to user input. You can use a label to identify components. For example, unlike buttons, text fields have no label to identify them. To label a component that does not itself come with an identifier:
Construct a JLabel component with the correct text.
Place it close enough to the component you want to identify so that the user can see that the label identifies the correct component.
The constructor for a JLabel lets you specify the initial text or icon, and optionally, the alignment of the content. You use constants from the SwingConstants interface to specify alignment. That interface defines a number of useful constants such as LEFT, RIGHT, CENTER, NORTH, EAST, and so on. The JLabel class is one of several Swing classes that implement this interface. Therefore, you can specify a right-aligned label either as
JLabel label = new JLabel("Minutes", SwingConstants.RIGHT);
or
JLabel label = new JLabel("Minutes", JLabel.RIGHT);
The setText and setIcon methods let you set the text and icon of the label at run time.
TIP
Beginning with JDK 1.3, you can use both plain and HTML text in buttons, labels, and menu items. We don't recommend HTML in buttons-it interferes with the look and feel. But HTML in labels can be very effective. Simply surround the label string with <html>. . .</html>, like this:
label = new JLabel("<html>Required entry:</html>");
Fair warning-the first component with an HTML label takes some time to be displayed because the rather complex HTML rendering code must be loaded.
Labels can be positioned inside a container like any other component. This means you can use the techniques you have seen before to place labels where you need them.
javax.swing.JLabel 1.2
JLabel(String text)
constructs a label with left-aligned text.
Parameters:
text
The text in the label
JLabel(Icon icon)
constructs a label with a left-aligned icon.
Parameters:
icon
The icon in the label
JLabel(String text, int align)
constructs a label with the given text and alignment
Parameters:
text
The text in the label
align
One of the SwingConstants constants LEFT, CENTER, or RIGHT
JLabel(String text, Icon icon, int align)
constructs a label with both text and an icon. The icon is to the left of the text.
Parameters:
text
The text in the label
icon
The icon in the label
align
One of the SwingConstants constants LEFT, CENTER, or RIGHT
void setText(String text)
sets the text of this label.
Parameters:
text
The text in the label
void setIcon(Icon icon)
sets the icon of this label.
Parameters:
icon
The icon in the label
Change Tracking in Text Fields
Let us put a few text fields to work. Figure 9-12 shows the running application listed in Example 9-2. The program shows a clock and two text fields that enter the hours and minutes. Whenever the content of the text fields changes, the clock is updated.
Figure 9-12. Text field example
To track every change in the text field requires a bit of an effort. First of all, note that it is not a good idea to monitor keystrokes. Some keystrokes (such as the arrow keys) don't change the text. And, depending on the look and feel, there may be mouse actions that result in text changes. As you saw in the beginning of this chapter, the Swing text field is implemented in a rather general way: the string that you see in the text field is just a visible manifestation (the view) of an underlying data structure (the model). Of course, for a humble text field, there is no great difference between the two. The view is a displayed string, and the model is a string object. But the same architecture is used in more advanced editing components to present formatted text, with fonts, paragraphs, and other attributes that are internally represented by a more complex data structure. The model for all text components is described by the Document interface, which covers both plain text and formatted text (such as HTML). The point is that you can ask the document (and not the text component) to notify you whenever the data has changed, by installing a document listener:
textField.getDocument().addDocumentListener(listener);
When the text has changed, one of the following DocumentListener methods is called:
void insertUpdate(DocumentEvent event)
void removeUpdate(DocumentEvent event)
void changedUpdate(DocumentEvent event)
The first two methods are called when characters have been inserted or removed. The third method is not called at all for text fields. For more complex document types, it would be called when some other change, such as a change in formatting, has occurred. Unfortunately, there is no single callback to tell you that the text has changed-usually you don't much care how it has changed. And there is no adapter class either. Thus, your document listener must implement all three methods. Here is what we do in our sample program:
private class ClockFieldListener implements DocumentListener
{
public void insertUpdate(DocumentEvent event) { setClock(); }
public void removeUpdate(DocumentEvent event) { setClock(); }
public void changedUpdate(DocumentEvent event) {}
}
The setClock method uses the getText method to obtain the current user-input strings from the text fields. Unfortunately, that is what we get: strings. We need to convert the strings to integers by using the familiar, if cumbersome, incantation:
int hours = Integer.parseInt(hourField.getText().trim());
int minutes = Integer.parseInt(minuteField.getText().trim());
But this code won't work right when the user types a noninteger string, such as "two", into the text field or even leaves the field blank. For now, we catch the NumberFormatException that the parseInt method throws, and we simply don't update the clock when the text field entry is not a number. In the next section, you see how you can prevent the user from entering invalid input in the first place.
NOTE
Instead of listening to document events, you can also add an action event listener to a text field. The action listener is notified whenever the user presses the ENTER key. We don't recommend this approach, because users don't always remember to press ENTER when they are done entering data. If you use an action listener, you should also install a focus listener so that you can track when the user leaves the text field.
Finally, note how the ClockPanel constructor sets the preferred size:
public ClockPanel()
{
setPreferredSize(new Dimension(2 * RADIUS + 1, 2 * RADIUS + 1));
}
When the frame's pack method computes the frame size, it uses the panel's preferred size.
Example 9-2. TextTest.java
1. import java.awt.*;
2. import java.awt.event.*;
3. import java.awt.geom.*;
4. import javax.swing.*;
5. import javax.swing.event.*;
6.
7. public class TextTest
8. {
9. public static void main(String[] args)
10. {
11. TextTestFrame frame = new TextTestFrame();
12. frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
13. frame.setVisible(true);
14. }
15. }
16.
17. /**
18. A frame with two text fields to set a clock.
19. */
20. class TextTestFrame extends JFrame
21. {
22. public TextTestFrame()
23. {
24. setTitle("TextTest");
25.
26. DocumentListener listener = new ClockFieldListener();
27.
28. // add a panel with text fields
29.
30. JPanel panel = new JPanel();
31.
32. panel.add(new JLabel("Hours:"));
33. hourField = new JTextField("12", 3);
34. panel.add(hourField);
35. hourField.getDocument().addDocumentListener(listener);
36.
37. panel.add(new JLabel("Minutes:"));
38. minuteField = new JTextField("00", 3);
39. panel.add(minuteField);
40. minuteField.getDocument().addDocumentListener(listener);
41.
42. add(panel, BorderLayout.SOUTH);
43.
44. // add the clock
45.
46. clock = new ClockPanel();
47. add(clock, BorderLayout.CENTER);
48. pack();
49. }
50.
51. /**
52. Set the clock to the values stored in the text fields.
53. */
54. public void setClock()
55. {
56. try
57. {
58. int hours = Integer.parseInt(hourField.getText().trim());
59. int minutes = Integer.parseInt(minuteField.getText().trim());
60. clock.setTime(hours, minutes);
61. }
62. catch (NumberFormatException e) {}
63. // don't set the clock if the input can't be parsed
64. }
65.
66. public static final int DEFAULT_WIDTH = 300;
67. public static final int DEFAULT_HEIGHT = 300;
68.
69. private JTextField hourField;
70. private JTextField minuteField;
71. private ClockPanel clock;
72.
73. private class ClockFieldListener implements DocumentListener
74. {
75. public void insertUpdate(DocumentEvent event) { setClock(); }
76. public void removeUpdate(DocumentEvent event) { setClock(); }
77. public void changedUpdate(DocumentEvent event) {}
78. }
79. }
80.
81. /**
82. A panel that draws a clock.
83. */
84. class ClockPanel extends JPanel
85. {
86. public ClockPanel()
87. {
88. setPreferredSize(new Dimension(2 * RADIUS + 1, 2 * RADIUS + 1));
89. }
90.
91. public void paintComponent(Graphics g)
92. {
93. // draw the circular boundary
94.
95. super.paintComponent(g);
96. Graphics2D g2 = (Graphics2D) g;
97. Ellipse2D circle = new Ellipse2D.Double(0, 0, 2 * RADIUS, 2 * RADIUS);
98. g2.draw(circle);
99.
100. // draw the hour hand
101.
102. double hourAngle = Math.toRadians(90 - 360 * minutes / (12 * 60));
103. drawHand(g2, hourAngle, HOUR_HAND_LENGTH);
104.
105. // draw the minute hand
106.
107. double minuteAngle = Math.toRadians(90 - 360 * minutes / 60);
108. drawHand(g2, minuteAngle, MINUTE_HAND_LENGTH);
109. }
110.
111. public void drawHand(Graphics2D g2, double angle, double handLength)
112. {
113. Point2D end = new Point2D.Double(
114. RADIUS + handLength * Math.cos(angle),
115. RADIUS - handLength * Math.sin(angle));
116. Point2D center = new Point2D.Double(RADIUS, RADIUS);
117. g2.draw(new Line2D.Double(center, end));
118. }
119.
120. /**
121. Set the time to be displayed on the clock
122. @param h hours
123. @param m minutes
124. */
125. public void setTime(int h, int m)
126. {
127. minutes = h * 60 + m;
128. repaint();
129. }
130.
131. private double minutes = 0;
132. private int RADIUS = 100;
133. private double MINUTE_HAND_LENGTH = 0.8 * RADIUS;
134. private double HOUR_HAND_LENGTH = 0.6 * RADIUS;
135. }
javax.swing.JComponent 1.2
void setPreferredSize(Dimension d)
sets the preferred size of this component.
javax.swing.text.Document 1.2
int getLength()
returns the number of characters currently in the document.
String getText(int offset, int length)
returns the text contained within the given portion of the document.
Parameters:
offset
The start of the text
length
The length of the desired string
void addDocumentListener(DocumentListener listener)
registers the listener to be notified when the document changes.
javax.swing.event.DocumentEvent 1.2
Document getDocument()
gets the document that is the source of the event.
javax.swing.event.DocumentListener 1.2
void changedUpdate(DocumentEvent event)
is called whenever an attribute or set of attributes changes.
void insertUpdate(DocumentEvent event)
is called whenever an insertion into the document occurs.
void removeUpdate(DocumentEvent event)
is called whenever a portion of the document has been removed.
Password Fields
Password fields are a special kind of text field. To avoid nosy bystanders being able to glance at a password, the characters that the user entered are not actually displayed. Instead, each typed character is represented by an echo character, typically an asterisk (*). Swing supplies a JPasswordField class that implements such a text field.
The password field is another example of the power of the model-view-controller architecture pattern. The password field uses the same model to store the data as a regular text field, but its view has been changed to display all characters as echo characters.
javax.swing.JPasswordField 1.2
JPasswordField(String text, int columns)
constructs a new password field.
Parameters:
text
The text to be displayed, null if none
columns
The number of columns
void setEchoChar(char echo)
sets the echo character for this password field. This is advisory; a particular look and feel may insist on its own choice of echo character. A value of 0 resets the echo character to the default.
Parameters:
echo
The echo character to display instead of the text characters
char[] getPassword()
returns the text contained in this password field. For stronger security, you should overwrite the content of the returned array after use. (The password is not returned as a String because a string would stay in the virtual machine until it is garbage-collected.)
Formatted Input Fields
In the last example program, we wanted the program user to type numbers, not arbitrary strings. That is, the user is allowed to enter only digits 0 through 9 and a hyphen (-). The hyphen, if present at all, must be the first symbol of the input string.
On the surface, this input validation task sounds simple. We can install a key listener to the text field and then consume all key events that aren't digits or a hyphen. Unfortunately, this simple approach, although commonly recommended as a method for input validation, does not work well in practice. First, not every combination of the valid input characters is a valid number. For example, --3 and 3-3 aren't valid, even though they are made up from valid input characters. But, more important, there are other ways of changing the text that don't involve typing character keys. Depending on the look and feel, certain key combinations can be used to cut, copy, and paste text. For example, in the Metal look and feel, the CTRL+V key combination pastes the content of the paste buffer into the text field. That is, we also need to monitor that the user doesn't paste in an invalid character. Clearly, trying to filter keystrokes to ensure that the content of the text field is always valid begins to look like a real chore. This is certainly not something that an application programmer should have to worry about.
Perhaps surprisingly, before JDK 1.4, there were no components for entering numeric values. Starting with the first edition of Core Java, we supplied an implementation for an IntTextField, a text field for entering a properly formatted integer. In every new edition, we changed the implementation to take whatever limited advantage we could from the various half-baked validation schemes that were added to each version of the JDK. Finally, in JDK 1.4, the Swing designers faced the issues head-on and supplied a versatile JFormattedTextField class that can be used not just for numeric input but also for dates and for even more esoteric formatted values such as IP addresses.
Integer Input
Let's get started with an easy case first: a text field for integer input.
JFormattedTextField intField = new JFormattedTextField(NumberFormat.getIntegerInstance());
The NumberFormat.getIntegerInstance returns a formatter object that formats integers, using the current locale. In the US locale, commas are used as decimal separators, allowing users to enter values such as 1,729. The internationalization chapter in Volume 2 explains in detail how you can select other locales.
As with any text field, you can set the number of columns:
intField.setColumns(6);
You can set a default value with the setValue method. That method takes an Object parameter, so you'll need to wrap the default int value in an Integer object:
intField.setValue(new Integer(100));
Typically, users will supply inputs in multiple text fields and then click a button to read all values. When the button is clicked, you can get the user-supplied value with the getValue method. That method returns an Object result, and you need to cast it into the appropriate type. The JFormattedTextField returns an object of type Long if the user edited the value. However, if the user made no changes, the original Integer object is returned. Therefore, you should cast the return value to the common superclass Number:
Number value = (Number) intField.getValue();
int v = value.intValue();
The formatted text field is not very interesting until you consider what happens when a user provides illegal input. That is the topic of the next section.
Behavior on Loss of Focus
Consider what happens when a user supplies input to a text field. The user types input and eventually decides to leave the field, perhaps by clicking on another component with the mouse. Then the text field loses focus. The I-beam cursor is no longer visible in the text field, and keystrokes are directed toward a different component.
When the formatted text field loses focus, the formatter looks at the text string that the user produced. If the formatter knows how to convert the text string to an object, the text is valid. Otherwise it is invalid. You can use the isEditValid method to check whether the current content of the text field is valid.
The default behavior on loss of focus is called "commit or revert." If the text string is valid, it is committed. The formatter converts it to an object. That object becomes the current value of the field (that is, the return value of the getValue method that you saw in the preceding section). The value is then converted back to a string, which becomes the text string that is visible in the field. For example, the integer formatter recognizes the input 1729 as valid, sets the current value to new Long(1729) and then converts it back into a string with a decimal comma: 1,729.
Conversely, if the text string is invalid, then the current value is not changed and the text field reverts to the string that represents the old value. For example, if the user enters a bad value, such as x1, then the old value is restored when the text field loses focus.
NOTE
The integer formatter regards a text string as valid if it starts with an integer. For example, 1729x is a valid string. It is converted to the number 1729, which is then formatted as the string 1,729.
You can set other behaviors with the setFocusLostBehavior method. The "commit" behavior is subtly different from the default. If the text string is invalid, then both the text string and the field value stay unchanged-they are now out of sync. The "persist" behavior is even more conservative. Even if the text string is valid, neither the text field nor the current value are changed. You would need to call commitEdit, setValue, or setText to bring them back in sync. Finally, there is a "revert" behavior that doesn't ever seem to be useful. Whenever focus is lost, the user input is disregarded, and the text string reverts to the old value.
NOTE
Generally, the "commit or revert" default behavior is reasonable. There is just one potential problem. Suppose a dialog box contains a text field for an integer value. A user enters a string " 1729", with a leading space and then clicks the OK button. The leading space makes the number invalid, and the field value reverts to the old value. The action listener of the OK button retrieves the field value and closes the dialog. The user never knows that the new value has been rejected. In this situation, it is appropriate to select the "commit" behavior and have the OK button listener check that all field edits are valid before closing the dialog.
Filters
This basic functionality of formatted text fields is straightforward and sufficient for most uses. However, you can add a couple of refinements. Perhaps you want to prevent the user from entering nondigits altogether. You achieve that behavior with a document filter. Recall that in the model-view-controller architecture, the controller translates input events into commands that modify the underlying document of the text field, that is, the text string that is stored in a PlainDocument object. For example, whenever the controller processes a command that causes text to be inserted into the document, it calls the "insert string" command. The string to be inserted can be either a single character or the content of the paste buffer. A document filter can intercept this command and modify the string or cancel the insertion altogether. Here is the code for the insertString method of a filter that analyzes the string to be inserted and inserts only the characters that are digits or a - sign. (The code handles supplementary Unicode characters, as explained in Chapter 3. See Chapter 12 for the StringBuilder class.)
public void insertString(FilterBypass fb, int offset, String string, AttributeSet attr)
throws BadLocationException
{
StringBuilder builder = new StringBuilder(string);
for (int i = builder.length() - 1; i >= 0; i--)
{
int cp = builder.codePointAt(i);
if (!Character.isDigit(cp) && cp != '-')
{
builder.deleteCharAt(i);
if (Character.isSupplementaryCodePoint(cp))
{
i--;
builder.deleteCharAt(i);
}
}
}
super.insertString(fb, offset, builder.toString(), attr);
}
You should also override the replace method of the DocumentFilter class-it is called when text is selected and then replaced. The implementation of the replace method is straightforward-see the program at the end of this section.
Now you need to install the document filter. Unfortunately, there is no straightforward method to do that. You need to override the getdocumentFilter method of a formatter class, and pass an object of that formatter class to the JFormattedTextField. The integer text field uses an InternationalFormatter that is initialized with NumberFormat.getIntegerInstance(). Here is how you install a formatter to yield the desired filter:
JFormattedTextField intField = new JFormattedTextField(new
InternationalFormatter(NumberFormat.getIntegerInstance())
{
protected DocumentFilter getDocumentFilter()
{
return filter;
}
private DocumentFilter filter = new IntFilter();
});
NOTE
The JDK documentation states that the DocumentFilter class was invented to avoid subclassing. Until JDK 1.3, filtering in a text field was achieved by extending the PlainDocument class and overriding the insertString and replace methods. Now the PlainDocument class has a pluggable filter instead. That is a splendid improvement. It would have been even more splendid if the filter had also been made pluggable in the formatter class. Alas, it was not, and we must subclass the formatter.
Try out the FormatTest example program at the end of this section. The third text field has a filter installed. You can insert only digits or the minus ('-') character. Note that you can still enter invalid strings such as "1-2-3". In general, it is impossible to avoid all invalid strings through filtering. For example, the string "-" is invalid, but a filter can't reject it because it is a prefix of a legal string "-1". Even though filters can't give perfect protection, it makes sense to use them to reject inputs that are obviously invalid.
TIP
Another use for filtering is to turn all characters of a string to upper case. Such a filter is easy to write. In the insertString and replace methods of the filter, convert the string to be inserted to upper case and then invoke the superclass method.
Verifiers
There is another potentially useful mechanism to alert users to invalid inputs. You can attach a verifier to any JComponent. If the component loses focus, then the verifier is queried. If the verifier reports the content of the component to be invalid, the component immediately regains focus. The user is thus forced to fix the content before supplying other inputs.
A verifier must extend the abstract InputVerifier class and define a verify method. It is particularly easy to define a verifier that checks formatted text fields. The isEditValid method of the JFormattedTextField class calls the formatter and returns TRue if the formatter can turn the text string into an object. Here is the verifier.
class FormattedTextFieldVerifier extends InputVerifier
{
public boolean verify(JComponent component)
{
JFormattedTextField field = (JFormattedTextField) component;
return field.isEditValid();
}
}
You can attach it to any JFormattedTextField:
intField.setInputVerifier(new FormattedTextFieldVerifier());
However, a verifier is not entirely foolproof. If you click on a button, then the button notifies its action listeners before an invalid component regains focus. The action listeners can then get an invalid result from the component that failed verification. There is a reason for this behavior: users may want to press a Cancel button without first having to fix an invalid input.
The fourth text field in the example program has a verifier attached. Try entering an invalid number (such as x1729) and press the TAB key or click with the mouse on another text field. Note that the field immediately regains focus. However, if you press the OK button, the action listener calls getValue, which reports the last good value.
Other Standard Formatters
Besides the integer formatter, the JFormattedTextField supports several other formatters. The NumberFormat class has static methods
getNumberInstance
getCurrencyInstance
getPercentInstance
that yield formatters of floating-point numbers, currency values, and percentages. For example, you can obtain a text field for the input of currency values by calling
JFormattedTextField currencyField = new JFormattedTextField(NumberFormat
.getCurrencyInstance());
To edit dates and times, call one of the static methods of the DateFormat class:
getDateInstance
getTimeInstance
getDateTimeInstance
For example,
JFormattedTextField dateField = new JFormattedTextField(DateFormat.getDateInstance());
This field edits a date in the default or "medium" format such as
Feb 24, 2002
You can instead choose a "short" format such as
2/24/02
by calling
DateFormat.getDateInstance(DateFormat.SHORT)
NOTE
By default, the date format is "lenient." That is, an invalid date such as February 31, 2002, is rolled over to the next valid date, March 3, 2002. That behavior may be surprising to your users. In that case, call setLenient(false) on the DateFormat object.
The DefaultFormatter can format objects of any class that has a constructor with a string parameter and a matching toString method. For example, the URL class has a URL(String) constructor that can be used to construct a URL from a string, such as
URL url = new URL("http://java.sun.com");
Therefore, you can use the DefaultFormatter to format URL objects. The formatter calls toString on the field value to initialize the field text. When the field loses focus, the formatter constructs a new object of the same class as the current value, using the constructor with a String parameter. If that constructor throws an exception, then the edit is not valid. You can try that out in the example program by entering a URL that does not start with a prefix such as "http:".
NOTE
By default, the DefaultFormatter is in overwrite mode. That is different from the other formatters and not very useful. Call setOverwriteMode(false) to turn off overwrite mode.
Finally, the MaskFormatter is useful for fixed-size patterns that contain some constant and some variable characters. For example, social security numbers (such as 078-05-1120) can be formatted with a
new MaskFormatter("###-##-####")
The # symbol denotes a single digit. Table 9-2 shows the symbols that you can use in a mask formatter.
Table 9-2. MaskFormatter Symbols #
A digit
?
A letter
U
A letter, converted to upper case
L
A letter, converted to lower case
A
A letter or digit
H
A hexadecimal digit [0-9A-Fa-f]
*
Any character
'
Escape character to include a symbol in the pattern
You can restrict the characters that can be typed into the field by calling one of the methods of the MaskFormatter class:
setValidCharacters
setInvalidCharacters
For example, to read in a letter grade (such as A+ or F), you could use
MaskFormatter formatter = new MaskFormatter("U*");
formatter.setValidCharacters("ABCDF+- ");
However, there is no way of specifying that the second character cannot be a letter.
Note that the string that is formatted by the mask formatter has exactly the same length as the mask. If the user erases characters during editing, then they are replaced with the placeholder character. The default placeholder character is a space, but you can change it with the setPlaceholderCharacter method, for example,
formatter.setPlaceholderCharacter('0');
By default, a mask formatter is in overtype mode, which is quite intuitive-try it out in the example program. Also note that the caret position jumps over the fixed characters in the mask.
The mask formatter is very effective for rigid patterns such as social security numbers or American telephone numbers. However, note that no variation at all is permitted in the mask pattern. For example, you cannot use a mask formatter for international telephone numbers that have a variable number of digits.
Custom Formatters
If none of the standard formatters is appropriate, it is fairly easy to define your own formatter. Consider 4-byte IP addresses such as
130.65.86.66
You can't use a MaskFormatter because each byte might be represented by one, two, or three digits. Also, we want to check in the formatter that each byte's value is at most 255.
To define your own formatter, extend the DefaultFormatter class and override the methods
String valueToString(Object value)
Object stringToValue(String text)
The first method turns the field value into the string that is displayed in the text field. The second method parses the text that the user typed and turns it back into an object. If either method detects an error, it should throw a ParseException.
In our example program, we store an IP address in a byte[] array of length 4. The valueToString method forms a string that separates the bytes with periods. Note that byte values are signed quantities between -128 and 127. To turn negative byte values into unsigned integer values, you add 256.
public String valueToString(Object value) throws ParseException
{
if (!(value instanceof byte[]))
throw new ParseException("Not a byte[]", 0);
byte[] a = (byte[]) value;
if (a.length != 4)
throw new ParseException("Length != 4", 0);
StringBuilder builder = new StringBuilder();
for (int i = 0; i < 4; i++)
{
int b = a[i];
if (b < 0) b += 256;
builder.append(String.valueOf(b));
if (i < 3) builder.append('.');
}
return builder.toString();
}
Conversely, the stringToValue method parses the string and produces a byte[] object if the string is valid. If not, it throws a ParseException.
public Object stringToValue(String text) throws ParseException
{
StringTokenizer tokenizer = new StringTokenizer(text, ".");
byte[] a = new byte[4];
for (int i = 0; i < 4; i++)
{
int b = 0;
try
{
b = Integer.parseInt(tokenizer.nextToken());
}
catch (NumberFormatException e)
{
throw new ParseException("Not an integer", 0);
}
if (b < 0 || b >= 256)
throw new ParseException("Byte out of range", 0);
a[i] = (byte) b;
}
return a;
}
Try out the IP address field in the sample program. If you enter an invalid address, the field reverts to the last valid address.
The program in Example 9-3 shows various formatted text fields in action (see Figure 9-13). Click the Ok button to retrieve the current values from the fields.
Figure 9-13. The FormatTest program
[View full size image]
NOTE
The "Swing Connection" online newsletter has a short article describing a formatter that matches any regular expression. See http://java.sun.com/products/jfc/tsc/articles/reftf/.
Example 9-3. FormatTest.java
1. import java.awt.*;
2. import java.awt.event.*;
3. import java.lang.reflect.*;
4. import java.net.*;
5. import java.text.*;
6. import java.util.*;
7. import javax.swing.*;
8. import javax.swing.text.*;
9.
10. /**
11. A program to test formatted text fields
12. */
13. public class FormatTest
14. {
15. public static void main(String[] args)
16. {
17. FormatTestFrame frame = new FormatTestFrame();
18. frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
19. frame.setVisible(true);
20. }
21. }
22.
23. /**
24. A frame with a collection of formatted text fields and
25. a button that displays the field values.
26. */
27. class FormatTestFrame extends JFrame
28. {
29. public FormatTestFrame()
30. {
31. setTitle("FormatTest");
32. setSize(WIDTH, HEIGHT);
33.
34. JPanel buttonPanel = new JPanel();
35. okButton = new JButton("Ok");
36. buttonPanel.add(okButton);
37. add(buttonPanel, BorderLayout.SOUTH);
38.
39. mainPanel = new JPanel();
40. mainPanel.setLayout(new GridLayout(0, 3));
41. add(mainPanel, BorderLayout.CENTER);
42.
43. JFormattedTextField intField = new JFormattedTextField(NumberFormat
.getIntegerInstance());
44. intField.setValue(new Integer(100));
45. addRow("Number:", intField);
46.
47. JFormattedTextField intField2 = new JFormattedTextField(NumberFormat
.getIntegerInstance());
48. intField2.setValue(new Integer(100));
49. intField2.setFocusLostBehavior(JFormattedTextField.COMMIT);
50. addRow("Number (Commit behavior):", intField2);
51.
52. JFormattedTextField intField3
53. = new JFormattedTextField(new
54. InternationalFormatter(NumberFormat.getIntegerInstance())
55. {
56. protected DocumentFilter getDocumentFilter()
57. {
58. return filter;
59. }
60. private DocumentFilter filter = new IntFilter();
61. });
62. intField3.setValue(new Integer(100));
63. addRow("Filtered Number", intField3);
64.
65. JFormattedTextField intField4 = new JFormattedTextField(NumberFormat
.getIntegerInstance());
66. intField4.setValue(new Integer(100));
67. intField4.setInputVerifier(new FormattedTextFieldVerifier());
68. addRow("Verified Number:", intField4);
69.
70. JFormattedTextField currencyField
71. = new JFormattedTextField(NumberFormat.getCurrencyInstance());
72. currencyField.setValue(new Double(10));
73. addRow("Currency:", currencyField);
74.
75. JFormattedTextField dateField = new JFormattedTextField(DateFormat
.getDateInstance());
76. dateField.setValue(new Date());
77. addRow("Date (default):", dateField);
78.
79. DateFormat format = DateFormat.getDateInstance(DateFormat.SHORT);
80. format.setLenient(false);
81. JFormattedTextField dateField2 = new JFormattedTextField(format);
82. dateField2.setValue(new Date());
83. addRow("Date (short, not lenient):", dateField2);
84.
85. try
86. {
87. DefaultFormatter formatter = new DefaultFormatter();
88. formatter.setOverwriteMode(false);
89. JFormattedTextField urlField = new JFormattedTextField(formatter);
90. urlField.setValue(new URL("http://java.sun.com"));
91. addRow("URL:", urlField);
92. }
93. catch (MalformedURLException e)
94. {
95. e.printStackTrace();
96. }
97.
98. try
99. {
100. MaskFormatter formatter = new MaskFormatter("###-##-####");
101. formatter.setPlaceholderCharacter('0');
102. JFormattedTextField ssnField = new JFormattedTextField(formatter);
103. ssnField.setValue("078-05-1120");
104. addRow("SSN Mask:", ssnField);
105. }
106. catch (ParseException exception)
107. {
108. exception.printStackTrace();
109. }
110.
111. JFormattedTextField ipField = new JFormattedTextField(new IPAddressFormatter());
112. ipField.setValue(new byte[] { (byte) 130, 65, 86, 66 });
113. addRow("IP Address:", ipField);
114. }
115.
116. /**
117. Adds a row to the main panel.
118. @param labelText the label of the field
119. @param field the sample field
120. */
121. public void addRow(String labelText, final JFormattedTextField field)
122. {
123. mainPanel.add(new JLabel(labelText));
124. mainPanel.add(field);
125. final JLabel valueLabel = new JLabel();
126. mainPanel.add(valueLabel);
127. okButton.addActionListener(new
128. ActionListener()
129. {
130. public void actionPerformed(ActionEvent event)
131. {
132. Object value = field.getValue();
133. if (value.getClass().isArray())
134. {
135. StringBuilder builder = new StringBuilder();
136. builder.append('{');
137. for (int i = 0; i < Array.getLength(value); i++)
138. {
139. if (i > 0) builder.append(',');
140. builder.append(Array.get(value, i).toString());
141. }
142. builder.append('}');
143. valueLabel.setText(builder.toString());
144. }
145. else
146. valueLabel.setText(value.toString());
147. }
148. });
149. }
150.
151. public static final int WIDTH = 500;
152. public static final int HEIGHT = 250;
153.
154. private JButton okButton;
155. private JPanel mainPanel;
156. }
157.
158. /**
159. A filter that restricts input to digits and a '-' sign.
160. */
161. class IntFilter extends DocumentFilter
162. {
163. public void insertString(FilterBypass fb, int offset, String string, AttributeSet
attr)
164. throws BadLocationException
165. {
166. StringBuilder builder = new StringBuilder(string);
167. for (int i = builder.length() - 1; i >= 0; i--)
168. {
169. int cp = builder.codePointAt(i);
170. if (!Character.isDigit(cp) && cp != '-')
171. {
172. builder.deleteCharAt(i);
173. if (Character.isSupplementaryCodePoint(cp))
174. {
175. i--;
176. builder.deleteCharAt(i);
177. }
178. }
179. }
180. super.insertString(fb, offset, builder.toString(), attr);
181. }
182.
183. public void replace(FilterBypass fb, int offset, int length, String string,
AttributeSet attr)
184. throws BadLocationException
185. {
186. if (string != null)
187. {
188. StringBuilder builder = new StringBuilder(string);
189. for (int i = builder.length() - 1; i >= 0; i--)
190. {
191. int cp = builder.codePointAt(i);
192. if (!Character.isDigit(cp) && cp != '-')
193. {
194. builder.deleteCharAt(i);
195. if (Character.isSupplementaryCodePoint(cp))
196. {
197. i--;
198. builder.deleteCharAt(i);
199. }
200. }
201. }
202. string = builder.toString();
203. }
204. super.replace(fb, offset, length, string, attr);
205. }
206. }
207.
208. /**
209. A verifier that checks whether the content of
210. a formatted text field is valid.
211. */
212. class FormattedTextFieldVerifier extends InputVerifier
213. {
214. public boolean verify(JComponent component)
215. {
216. JFormattedTextField field = (JFormattedTextField) component;
217. return field.isEditValid();
218. }
219. }
220.
221. /**
222. A formatter for 4-byte IP addresses of the form a.b.c.d
223. */
224. class IPAddressFormatter extends DefaultFormatter
225. {
226. public String valueToString(Object value)
227. throws ParseException
228. {
229. if (!(value instanceof byte[]))
230. throw new ParseException("Not a byte[]", 0);
231. byte[] a = (byte[]) value;
232. if (a.length != 4)
233. throw new ParseException("Length != 4", 0);
234. StringBuilder builder = new StringBuilder();
235. for (int i = 0; i < 4; i++)
236. {
237. int b = a[i];
238. if (b < 0) b += 256;
239. builder.append(String.valueOf(b));
240. if (i < 3) builder.append('.');
241. }
242. return builder.toString();
243. }
244.
245. public Object stringToValue(String text) throws ParseException
246. {
247. StringTokenizer tokenizer = new StringTokenizer(text, ".");
248. byte[] a = new byte[4];
249. for (int i = 0; i < 4; i++)
250. {
251. int b = 0;
252. if (!tokenizer.hasMoreTokens())
253. throw new ParseException("Too few bytes", 0);
254. try
255. {
256. b = Integer.parseInt(tokenizer.nextToken());
257. }
258. catch (NumberFormatException e)
259. {
260. throw new ParseException("Not an integer", 0);
261. }
262. if (b < 0 || b >= 256)
263. throw new ParseException("Byte out of range", 0);
264. a[i] = (byte) b;
265. }
266. if (tokenizer.hasMoreTokens())
267. throw new ParseException("Too many bytes", 0);
268. return a;
269. }
270. }
javax.swing.JFormattedTextField 1.4
JFormattedTextField(Format fmt)
constructs a text field that uses the specified format.
JFormattedTextField(JFormattedTextField.AbstractFormatter formatter)
constructs a text field that uses the specified formatter. Note that DefaultFormatter and InternationalFormatter are subclasses of JFormattedTextField.AbstractFormatter.
Object getValue()
returns the current valid value of the field. Note that this may not correspond to the string that is being edited.
void setValue(Object value)
attempts to set the value of the given object. The attempt fails if the formatter cannot convert the object to a string.
void commitEdit()
attempts to set the valid value of the field from the edited string. The attempt may fail if the formatter cannot convert the string.
boolean isEditValid()
checks whether the edited string represents a valid value.
void setFocusLostBehavior(int behavior)
int getFocusLostBehavior()
set or get the "focus lost" behavior. Legal values for behavior are the constants COMMIT_OR_REVERT, REVERT, COMMIT, and PERSIST of the JFormattedTextField class.
java.text.DateFormat 1.1\
static DateFormat getDateInstance()
static DateFormat getDateInstance(int dateStyle)
static DateFormat getTimeInstance()
static DateFormat getTimeInstance(int timeStyle)
static DateFormat getDateTimeInstance()
static DateFormat getDateTimeInstance(int dateStyle, int timeStyle)
return formatters that yield the date, time, or both date and time of Date objects. Legal values for dateStyle and timeStyle are the constants SHORT, MEDIUM, LONG, FULL, and DEFAULT of the DateFormat class.
javax.swing.JFormattedTextField.AbstractFormatter 1.4
abstract String valueToString(Object value)
converts a value to an editable string. Throws a ParseException if value is not appropriate for this formatter.
abstract Object stringToValue(String s)
converts a string to a value. Throws a ParseException if s is not in the appropriate format.
DocumentFilter getDocumentFilter()
override this method to provide a document filter that restricts inputs into the text field. A return value of null indicates that no filtering is needed.
javax.swing.text.DefaultFormatter 1.3
void setOverwriteMode(boolean mode)
boolean getOverwriteMode()
set or get the overwrite mode. If mode is true, then new characters overwrite existing characters when editing text.
javax.swing.text.DocumentFilter 1.4
void insertString(DocumentFilter.FilterBypass bypass, int offset, String text, AttributeSet attrib)
is invoked before a string is inserted into a document. You can override the method and modify the string. You can disable insertion by not calling super.insertString or by calling bypass methods to modify the document without filtering.
Parameters:
bypass
An object that allows you to execute edit commands that bypass the filter
offset
The offset at which to insert the text
text
The characters to insert
attrib
The formatting attributes of the inserted text
void replace(DocumentFilter.FilterBypass bypass, int offset, int length, String text, AttributeSet attrib)
is invoked before a part of a document is replaced with a new string. You can override the method and modify the string. You can disable replacement by not calling super.replace or by calling bypass methods to modify the document without filtering.
Parameters:
bypass
An object that allows you to execute edit commands that bypass the filter
offset
The offset at which to insert the text
length
The length of the part to be replaced
text
The characters to insert
attrib
The formatting attributes of the inserted text
void remove(DocumentFilter.FilterBypass bypass, int offset, int length)
is invoked before a part of a document is removed. Get the document by calling bypass.getDocument() if you need to analyze the effect of the removal.
Parameters:
bypass
An object that allows you to execute edit commands that bypass the filter
offset
The offset of the part to be removed
length
The length of the part to be removed
javax.swing.text.MaskFormatter 1.4
MaskFormatter(String mask)
constructs a mask formatter with the given mask. See Table 9-2 on page 367 for the symbols in a mask.
void setValidCharacters(String characters)
String getValidCharacters()
set or get the valid editing characters. Only the characters in the given string are accepted for the variable parts of the mask.
void setInvalidCharacters(String characters)
String getInvalidCharacters()
set or get the invalid editing characters. None of the characters in the given string are accepted as input.
void setPlaceholderCharacter(char ch)
char getPlaceholderCharacter()
set or get the placeholder character that is used for variable characters in the mask that the user has not yet supplied. The default placeholder character is a space.
void setPlaceholder(String s)
String getPlaceholder()
set or get the placeholder string. Its tail end is used if the user has not supplied all variable characters in the mask. If it is null or shorter than the mask, then the placeholder character fills remaining inputs.
void setValueContainsLiteralCharacters(boolean b)
boolean getValueContainsLiteralCharacters()
set or get the "value contains literal characters" flag. If this flag is true, then the field value contains the literal (nonvariable) parts of the mask. If it is false, then the literal characters are removed. The default is TRue.
Text Areas
Sometimes, you need to collect user input that is more than one line long. As mentioned earlier, you use the JTextArea component for this collection. When you place a text area component in your program, a user can enter any number of lines of text, using the ENTER key to separate them. Each line ends with a '
'. If you need to break up the user's entry into separate lines, you can use the StringTokenizer class (see Chapter 12). Figure 9-14 shows a text area at work.
Figure 9-14. A text area
In the constructor for the JTextArea component, you specify the number of rows and columns for the text area. For example:
textArea = new JTextArea(8, 40); // 8 lines of 40 columns each
where the columns parameter works as before-and you still need to add a few more columns for safety's sake. Also, as before, the user is not restricted to the number of rows and columns; the text simply scrolls when the user inputs too much. You can also use the setColumns method to change the number of columns, and the setRows method to change the number of rows. These numbers only indicate the preferred size-the layout manager can still grow or shrink the text area.
If there is more text than the text area can display, then the remaining text is simply clipped. You can avoid clipping long lines by turning on line wrapping:
textArea.setLineWrap(true); // long lines are wrapped
This wrapping is a visual effect only; the text in the document is not changed-no '
' characters are inserted into the text.
In Swing, a text area does not have scrollbars. If you want scrollbars, you have to insert the text area inside a scroll pane.
textArea = new JTextArea(8, 40);
JScrollPane scrollPane = new JScrollPane(textArea);
The scroll pane now manages the view of the text area. Scrollbars automatically appear if there is more text than the text area can display, and they vanish again if text is deleted and the remaining text fits inside the area. The scrolling is handled internally in the scroll pane-your program does not need to process scroll events.
TIP
This is a general mechanism that you will encounter many times when working with Swing-to add scrollbars to a component, put them inside a scroll pane.
Example 9-4 is the complete code for the text area demo. This program simply lets you edit text in a text area. Click on "Insert" to insert a sentence at the end of the text. Click the second button to turn line wrapping on and off. (Its name toggles between "Wrap" and "No wrap".) Of course, you can simply use the keyboard to edit the text in the text area. Note how you can highlight a section of text, and how you can cut, copy, and paste with the CTRL+X, CTRL+C, and CTRL+V keys. (Keyboard shortcuts are specific to the look and feel. These particular key combinations work for the Metal and Windows look and feel.)
NOTE
The JTextArea component displays plain text only, without special fonts or formatting. To display formatted text (such as HTML or RTF), you can use the JEditorPane and JTextPane classes. These classes are discussed in Volume 2.
Example 9-4. TextAreaTest.java
1. import java.awt.*;
2. import java.awt.event.*;
3. import javax.swing.*;
4.
5. public class TextAreaTest
6. {
7. public static void main(String[] args)
8. {
9. TextAreaFrame frame = new TextAreaFrame();
10. frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
11. frame.setVisible(true);
12. }
13. }
14.
15. /**
16. A frame with a text area and buttons for text editing
17. */
18. class TextAreaFrame extends JFrame
19. {
20. public TextAreaFrame()
21. {
22. setTitle("TextAreaTest");
23. setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
24.
25. buttonPanel = new JPanel();
26.
27. // add button to append text into the text area
28.
29. JButton insertButton = new JButton("Insert");
30. buttonPanel.add(insertButton);
31. insertButton.addActionListener(new
32. ActionListener()
33. {
34. public void actionPerformed(ActionEvent event)
35. {
36. textArea.append("The quick brown fox jumps over the lazy dog. ");
37. }
38. });
39.
40. // add button to turn line wrapping on and off
41.
42. wrapButton = new JButton("Wrap");
43. buttonPanel.add(wrapButton);
44. wrapButton.addActionListener(new
45. ActionListener()
46. {
47. public void actionPerformed(ActionEvent event)
48. {
49. boolean wrap = !textArea.getLineWrap();
50. textArea.setLineWrap(wrap);
51. scrollPane.revalidate();
52. wrapButton.setText(wrap ? "No Wrap" : "Wrap");
53. }
54. });
55.
56. add(buttonPanel, BorderLayout.SOUTH);
57.
58. // add a text area with scrollbars
59.
60. textArea = new JTextArea(8, 40);
61. scrollPane = new JScrollPane(textArea);
62.
63. add(scrollPane, BorderLayout.CENTER);
64. }
65.
66. public static final int DEFAULT_WIDTH = 300;
67. public static final int DEFAULT_HEIGHT = 300;
68.
69. private JTextArea textArea;
70. private JScrollPane scrollPane;
71. private JPanel buttonPanel;
72. private JButton wrapButton;
73. }
javax.swing.JTextArea 1.2
JTextArea(int rows, int cols)
constructs a new text area.
Parameters:
rows
The number of rows
cols
The number of columns
JTextArea(String text, int rows, int cols)
constructs a new text area with an initial text.
Parameters:
text
The initial text
rows
The number of rows
cols
The number of columns
void setColumns(int cols)
tells the text area the preferred number of columns it should use.
Parameters:
cols
The number of columns
void setRows(int rows)
tells the text area the preferred number of rows it should use.
Parameters:
rows
The number of rows
void append(String newText)
appends the given text to the end of the text already in the text area.
Parameters:
newText
The text to append
void setLineWrap(boolean wrap)
turns line wrapping on or off.
Parameters:
wrap
true if lines should be wrapped
void setWrapStyleWord(boolean word)
If word is true, then long lines are wrapped at word boundaries. If it is false, then long lines are broken without taking word boundaries into account.
void setTabSize(int c)
sets tab stops every c columns. Note that the tabs aren't converted to spaces but cause alignment with the next tab stop.
Parameters:
c
The number of columns for a tab stop
javax.swing.JScrollPane 1.2
JScrollPane(Component c)
creates a scroll pane that displays the content of the specified component. Scrollbars are supplied when the component is larger than the view.
Parameters:
c
The component to scroll
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
Choice Components
You now know how to collect text input from users, but there are many occasions for which you would rather give users a finite set of choices than have them enter the data in a text component. Using a set of buttons or a list of items tells your users what choices they have. (It also saves you the trouble of error checking.) In this section, you learn how to program checkboxes, radio buttons, lists of choices, and sliders.
Checkboxes
If you want to collect just a "yes" or "no" input, use a checkbox component. Checkboxes automatically come with labels that identify them. The user usually checks the box by clicking inside it and turns off the check mark by clicking inside the box again. To toggle the check mark, the user can also press the space bar when the focus is in the checkbox.
Figure 9-15 shows a simple program with two checkboxes, one to turn on or off the italic attribute of a font, and the other for boldface. Note that the second checkbox has focus, as indicated by the rectangle around the label. Each time the user clicks one of the checkboxes, we refresh the screen, using the new font attributes.
Figure 9-15. Checkboxes
Checkboxes need a label next to them to identify their purpose. You give the label text in the constructor.
bold = new JCheckBox("Bold");
You use the setSelected method to turn a checkbox on or off. For example,
bold.setSelected(true);
The isSelected method then retrieves the current state of each checkbox. It is false if unchecked; true if checked.
When the user clicks on a checkbox, this triggers an action event. As always, you attach an action listener to the checkbox. In our program, the two checkboxes share the same action listener.
ActionListener listener = . . .
bold.addActionListener(listener);
italic.addActionListener(listener);
The actionPerformed method queries the state of the bold and italic checkboxes and sets the font of the panel to plain, bold, italic, or both bold and italic.
public void actionPerformed(ActionEvent event)
{
int mode = 0;
if (bold.isSelected()) mode += Font.BOLD;
if (italic.isSelected()) mode += Font.ITALIC;
label.setFont(new Font("Serif", mode, FONTSIZE));
}
Example 9-5 is the complete program listing for the checkbox example.
Example 9-5. CheckBoxTest.java
1. import java.awt.*;
2. import java.awt.event.*;
3. import javax.swing.*;
4.
5. public class CheckBoxTest
6. {
7. public static void main(String[] args)
8. {
9. CheckBoxFrame frame = new CheckBoxFrame();
10. frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
11. frame.setVisible(true);
12. }
13. }
14.
15. /**
16. A frame with a sample text label and checkboxes for
17. selecting font attributes.
18. */
19. class CheckBoxFrame extends JFrame
20. {
21. public CheckBoxFrame()
22. {
23. setTitle("CheckBoxTest");
24. setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
25.
26. // add the sample text label
27.
28. label = new JLabel("The quick brown fox jumps over the lazy dog.");
29. label.setFont(new Font("Serif", Font.PLAIN, FONTSIZE));
30. add(label, BorderLayout.CENTER);
31.
32. // this listener sets the font attribute of
33. // the label to the checkbox state
34.
35. ActionListener listener = new
36. ActionListener()
37. {
38. public void actionPerformed(ActionEvent event)
39. {
40. int mode = 0;
41. if (bold.isSelected()) mode += Font.BOLD;
42. if (italic.isSelected()) mode += Font.ITALIC;
43. label.setFont(new Font("Serif", mode, FONTSIZE));
44. }
45. };
46.
47. // add the checkboxes
48.
49. JPanel buttonPanel = new JPanel();
50.
51. bold = new JCheckBox("Bold");
52. bold.addActionListener(listener);
53. buttonPanel.add(bold);
54.
55. italic = new JCheckBox("Italic");
56. italic.addActionListener(listener);
57. buttonPanel.add(italic);
58.
59. add(buttonPanel, BorderLayout.SOUTH);
60. }
61.
62. public static final int DEFAULT_WIDTH = 300;
63. public static final int DEFAULT_HEIGHT = 200;
64.
65. private JLabel label;
66. private JCheckBox bold;
67. private JCheckBox italic;
68.
69. private static final int FONTSIZE = 12;
70. }
javax.swing.JCheckBox 1.2
JCheckBox(String label)
constructs a checkbox with the given label that is initially unselected.
JCheckBox(String label, boolean state)
constructs a checkbox with the given label and initial state.
JCheckBox(String label, Icon icon)
constructs a checkbox with the given label and icon that is initially unselected.
boolean isSelected ()
returns the state of the checkbox.
void setSelected(boolean state)
sets the checkbox to a new state.
Radio Buttons
In the previous example, the user could check either, both, or neither of the two checkboxes. In many cases, we want to require the user to check only one of several boxes. When another box is checked, the previous box is automatically unchecked. Such a group of boxes is often called a radio button group because the buttons work like the station selector buttons on a radio. When you push in one button, the previously depressed button pops out. Figure 9-16 shows a typical example. We allow the user to select a font size from among the choices-Small, Medium, Large, and Extra large-but, of course, we will allow the user to select only one size at a time.
Figure 9-16. A radio button group
Implementing radio button groups is easy in Swing. You construct one object of type ButtonGroup for every group of buttons. Then, you add objects of type JRadioButton to the button group. The button group object is responsible for turning off the previously set button when a new button is clicked.
ButtonGroup group = new ButtonGroup();
JRadioButton smallButton = new JRadioButton("Small", false);
group.add(smallButton);
JRadioButton mediumButton = new JRadioButton("Medium", true);
group.add(mediumButton);]
. . .
The second argument of the constructor is true for the button that should be checked initially and false for all others. Note that the button group controls only the behavior of the buttons; if you want to group the buttons for layout purposes, you also need to add them to a container such as a JPanel.
If you look again at Figures 9-15 and 9-16, you will note that the appearance of the radio buttons is different from that of checkboxes. Checkboxes are square and contain a check mark when selected. Radio buttons are round and contain a dot when selected.
The event notification mechanism for radio buttons is the same as for any other buttons. When the user checks a radio button, the radio button generates an action event. In our example program, we define an action listener that sets the font size to a particular value:
ActionListener listener = new
ActionListener()
{
public void actionPerformed(ActionEvent event)
{
// size refers to the final parameter of the addRadioButton method
label.setFont(new Font("Serif", Font.PLAIN, size));
}
};
Compare this listener setup with that of the checkbox example. Each radio button gets a different listener object. Each listener object knows exactly what it needs to do-set the font size to a particular value. In the case of the checkboxes, we used a different approach. Both checkboxes have the same action listener. It called a method that looked at the current state of both checkboxes.
Could we follow the same approach here? We could have a single listener that computes the size as follows:
if (smallButton.isSelected()) size = 8;
else if (mediumButton.isSelected()) size = 12;
. . .
However, we prefer to use separate action listener objects because they tie the size values more closely to the buttons.
NOTE
If you have a group of radio buttons, you know that only one of them is selected. It would be nice to be able to quickly find out which one without having to query all the buttons in the group. Because the ButtonGroup object controls all buttons, it would be convenient if this object could give us a reference to the selected button. Indeed, the ButtonGroup class has a getSelection method, but that method doesn't return the radio button that is selected. Instead, it returns a ButtonModel reference to the model attached to the button. Unfortunately, none of the ButtonModel methods are very helpful. The ButtonModel interface inherits a method getSelectedObjects from the ItemSelectable interface that, rather uselessly, returns null. The getActionCommand method looks promising because the "action command" of a radio button is its text label. But the action command of its model is null. Only if you explicitly set the action commands of all radio buttons with the setActionCommand method do the models' action command values also get set. Then you can retrieve the action command of the currently selected button with buttonGroup.getSelection().getActionCommand().
Example 9-6 is the complete program for font size selection that puts a set of radio buttons to work.
Example 9-6. RadioButtonTest.java
1. import java.awt.*;
2. import java.awt.event.*;
3. import javax.swing.*;
4.
5. public class RadioButtonTest
6. {
7. public static void main(String[] args)
8. {
9. RadioButtonFrame frame = new RadioButtonFrame();
10. frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
11. frame.setVisible(true);
12. }
13. }
14.
15. /**
16. A frame with a sample text label and radio buttons for
17. selecting font sizes.
18. */
19. class RadioButtonFrame extends JFrame
20. {
21. public RadioButtonFrame()
22. {
23. setTitle("RadioButtonTest");
24. setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
25.
26. // add the sample text label
27.
28. label = new JLabel("The quick brown fox jumps over the lazy dog.");
29. label.setFont(new Font("Serif", Font.PLAIN, DEFAULT_SIZE));
30. add(label, BorderLayout.CENTER);
31.
32. // add the radio buttons
33.
34. buttonPanel = new JPanel();
35. group = new ButtonGroup();
36.
37. addRadioButton("Small", 8);
38. addRadioButton("Medium", 12);
39. addRadioButton("Large", 18);
40. addRadioButton("Extra large", 36);
41.
42. add(buttonPanel, BorderLayout.SOUTH);
43. }
44.
45. /**
46. Adds a radio button that sets the font size of the
47. sample text.
48. @param name the string to appear on the button
49. @param size the font size that this button sets
50. */
51. public void addRadioButton(String name, final int size)
52. {
53. boolean selected = size == DEFAULT_SIZE;
54. JRadioButton button = new JRadioButton(name, selected);
55. group.add(button);
56. buttonPanel.add(button);
57.
58. // this listener sets the label font size
59.
60. ActionListener listener = new
61. ActionListener()
62. {
63. public void actionPerformed(ActionEvent event)
64. {
65. // size refers to the final parameter of the addRadioButton method
66. label.setFont(new Font("Serif", Font.PLAIN, size));
67. }
68. };
69.
70. button.addActionListener(listener);
71. }
72.
73. public static final int DEFAULT_WIDTH = 400;
74. public static final int DEFAULT_HEIGHT = 200;
75.
76. private JPanel buttonPanel;
77. private ButtonGroup group;
78. private JLabel label;
79.
80. private static final int DEFAULT_SIZE = 12;
81. }
javax.swing.JRadioButton 1.2
JRadioButton(String label, boolean state)
constructs a radio button with the given label and initial state.
JRadioButton(String label, Icon icon)
constructs a radio button with the given label and icon that is initially unselected.
javax.swing.ButtonGroup 1.2
void add(AbstractButton b)
adds the button to the group.
ButtonModel getSelection()
returns the button model of the selected button.
javax.swing.ButtonModel 1.2
String getActionCommand()
returns the action command for this button model.
javax.swing.AbstractButton 1.2
void setActionCommand(String s)
sets the action command for this button and its model.
Borders
If you have multiple groups of radio buttons in a window, you will want to visually indicate which buttons are grouped. Swing provides a set of useful borders for this purpose. You can apply a border to any component that extends JComponent. The most common usage is to place a border around a panel and fill that panel with other user interface elements such as radio buttons.
You can choose from quite a few borders, but you follow the same steps for all of them.
1. Call a static method of the BorderFactory to create a border. You can choose among the following styles (see Figure 9-17):
Lowered bevel
Raised bevel
Etched
Line
Matte
Empty (just to create some blank space around the component)
Figure 9-17. Testing border types
[View full size image]
2. If you like, add a title to your border by passing your border to BorderFactory.createTitledBorder.
3. If you really want to go all out, combine several borders with a call to BorderFactory.createCompoundBorder.
4. Add the resulting border to your component by calling the setBorder method of the JComponent class.
For example, here is how you add an etched border with a title to a panel:
Border etched = BorderFactory.createEtchedBorder()
Border titled = BorderFactory.createTitledBorder(etched, "A Title");
panel.setBorder(titled);
Run the program in Example 9-7 to get an idea what the various borders look like.
The various borders have different options for setting border widths and colors. See the API notes for details. True border enthusiasts will appreciate that there is also a SoftBevelBorder class for beveled borders with softened corners and that a LineBorder can have rounded corners as well. You can construct these borders only by using one of the class constructors-there is no BorderFactory method for them.
Example 9-7. BorderTest.java
1. import java.awt.*;
2. import java.awt.event.*;
3. import javax.swing.*;
4. import javax.swing.border.*;
5.
6. public class BorderTest
7. {
8. public static void main(String[] args)
9. {
10. BorderFrame frame = new BorderFrame();
11. frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
12. frame.setVisible(true);
13. }
14. }
15.
16. /**
17. A frame with radio buttons to pick a border style.
18. */
19. class BorderFrame extends JFrame
20. {
21. public BorderFrame()
22. {
23. setTitle("BorderTest");
24. setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
25.
26. demoPanel = new JPanel();
27. buttonPanel = new JPanel();
28. group = new ButtonGroup();
29.
30. addRadioButton("Lowered bevel", BorderFactory.createLoweredBevelBorder());
31. addRadioButton("Raised bevel", BorderFactory.createRaisedBevelBorder());
32. addRadioButton("Etched", BorderFactory.createEtchedBorder());
33. addRadioButton("Line", BorderFactory.createLineBorder(Color.BLUE));
34. addRadioButton("Matte", BorderFactory.createMatteBorder(10, 10, 10, 10, Color
.BLUE));
35. addRadioButton("Empty", BorderFactory.createEmptyBorder());
36.
37. Border etched = BorderFactory.createEtchedBorder();
38. Border titled = BorderFactory.createTitledBorder(etched, "Border types");
39. buttonPanel.setBorder(titled);
40.
41. setLayout(new GridLayout(2, 1));
42. add(buttonPanel);
43. add(demoPanel);
44. }
45.
46. public void addRadioButton(String buttonName, final Border b)
47. {
48. JRadioButton button = new JRadioButton(buttonName);
49. button.addActionListener(new
50. ActionListener()
51. {
52. public void actionPerformed(ActionEvent event)
53. {
54. demoPanel.setBorder(b);
55. validate();
56. }
57. });
58. group.add(button);
59. buttonPanel.add(button);
60. }
61.
62. public static final int DEFAULT_WIDTH = 600;
63. public static final int DEFAULT_HEIGHT = 200;
64.
65. private JPanel demoPanel;
66. private JPanel buttonPanel;
67. private ButtonGroup group;
68. }
javax.swing.BorderFactory 1.2
static Border createLineBorder(Color color)
static Border createLineBorder(Color color, int thickness)
create a simple line border.
static MatteBorder createMatteBorder(int top, int left, int bottom, int right, Color color)
static MatteBorder createMatteBorder(int top, int left, int bottom, int right, Icon tileIcon)
create a thick border that is filled with a color or a repeating icon.
static Border createEmptyBorder()
static Border createEmptyBorder(int top, int left, int bottom, int right)
create an empty border.
static Border createEtchedBorder()
static Border createEtchedBorder(Color highlight, Color shadow)
static Border createEtchedBorder(int type)
static Border createEtchedBorder(int type, Color highlight, Color shadow)
create a line border with a 3D effect.
Parameters:
highlight, shadow
Colors for 3D effect
type
One of EtchedBorder.RAISED, EtchedBorder.LOWERED
static Border createBevelBorder(int type)
static Border createBevelBorder(int type, Color highlight, Color shadow)
static Border createLoweredBevelBorder()
static Border createRaisedBevelBorder()
create a border that gives the effect of a lowered or raised surface.
Parameters:
type
One of BevelBorder.LOWERED, BevelBorder.RAISED
highlight, shadow
Colors for 3D effect
static TitledBorder createTitledBorder(String title)
static TitledBorder createTitledBorder(Border border)
static TitledBorder createTitledBorder(Border border, String title)
static TitledBorder createTitledBorder(Border border, String title, int justification, int position)
static TitledBorder createTitledBorder(Border border, String title, int justification, int position, Font font)
static TitledBorder createTitledBorder(Border border, String title, int justification, int position, Font font, Color color)
Creates a titled border with the specified properties.
Parameters:
title
The title string
border
The border to decorate with the title
justification
One of the TitledBorder constants LEFT, CENTER, RIGHT, LEADING, trAILING, or DEFAULT_JUSTIFICATION (left)
position
One of the TitledBorder constants ABOVE_TOP, TOP, BELOW_TOP, ABOVE_BOTTOM, BOTTOM, BELOW_BOTTOM, or DEFAULT_POSITION (top)
font
The font for the title
color
The color of the title
static CompoundBorder createCompoundBorder(Border outsideBorder, Border insideBorder)
combines two borders to a new border.
javax.swing.border.SoftBevelBorder 1.2
SoftBevelBorder(int type)
SoftBevelBorder(int type, Color highlight, Color shadow)
create a bevel border with softened corners.
Parameters:
type
One of BevelBorder.LOWERED, BevelBorder.RAISED
highlight, shadow
Colors for 3D effect
javax.swing.border.LineBorder 1.2
public LineBorder(Color color, int thickness, boolean roundedCorners)
creates a line border with the given color and thickness. If roundedCorners is true, the border has rounded corners.
javax.swing.JComponent 1.2
void setBorder(Border border)
sets the border of this component.
Combo Boxes
If you have more than a handful of alternatives, radio buttons are not a good choice because they take up too much screen space. Instead, you can use a combo box. When the user clicks on the component, a list of choices drops down, and the user can then select one of them (see Figure 9-18).
Figure 9-18. A combo box
If the drop-down list box is set to be editable, then you can edit the current selection as if it were a text field. For that reason, this component is called a combo box-it combines the flexibility of a text field with a set of predefined choices. The JComboBox class provides a combo box component.
You call the setEditable method to make the combo box editable. Note that editing affects only the current item. It does not change the content of the list.
You can obtain the current selection or edited text by calling the getSelectedItem method.
In the example program, the user can choose a font style from a list of styles (Serif, SansSerif, Monospaced, etc.). The user can also type in another font.
You add the choice items with the addItem method. In our program, addItem is called only in the constructor, but you can call it any time.
faceCombo = new JComboBox();
faceCombo.setEditable(true);
faceCombo.addItem("Serif");
faceCombo.addItem("SansSerif");
. . .
This method adds the string at the end of the list. You can add new items anywhere in the list with the insertItemAt method:
faceCombo.insertItemAt("Monospaced", 0); // add at the beginning
You can add items of any type-the combo box invokes each item's toString method to display it.
If you need to remove items at run time, you use the removeItem or removeItemAt method, depending on whether you supply the item to be removed or its position.
faceCombo.removeItem("Monospaced");
faceCombo.removeItemAt(0); // remove first item
The removeAllItems method removes all items at once.
TIP
If you need to add a large number of items to a combo box, the addItem method will perform poorly. Instead, construct a DefaultComboBoxModel, populate it by calling addElement, and then call the setModel method of the JComboBox class.
When the user selects an item from a combo box, the combo box generates an action event. To find out which item was selected, call getSource on the event parameter to get a reference to the combo box that sent the event. Then call the getSelectedItem method to retrieve the currently selected item. You need to cast the returned value to the appropriate type, usually String.
public void actionPerformed(ActionEvent event)
{
label.setFont(new Font(
(String) faceCombo.getSelectedItem(),
Font.PLAIN,
DEFAULT_SIZE));
}
Example 9-8 shows the complete program.
NOTE
If you want to show a permanently displayed list instead of a dropdown list, use the JList component. We cover JList in Chapter 6 of Volume 2.
Example 9-8. ComboBoxTest.java
1. import java.awt.*;
2. import java.awt.event.*;
3. import javax.swing.*;
4.
5. public class ComboBoxTest
6. {
7. public static void main(String[] args)
8. {
9. ComboBoxFrame frame = new ComboBoxFrame();
10. frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
11. frame.setVisible(true);
12. }
13. }
14.
15. /**
16. A frame with a sample text label and a combo box for
17. selecting font faces.
18. */
19. class ComboBoxFrame extends JFrame
20. {
21. public ComboBoxFrame()
22. {
23. setTitle("ComboBoxTest");
24. setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
25.
26. // add the sample text label
27.
28. label = new JLabel("The quick brown fox jumps over the lazy dog.");
29. label.setFont(new Font("Serif", Font.PLAIN, DEFAULT_SIZE));
30. add(label, BorderLayout.CENTER);
31.
32. // make a combo box and add face names
33.
34. faceCombo = new JComboBox();
35. faceCombo.setEditable(true);
36. faceCombo.addItem("Serif");
37. faceCombo.addItem("SansSerif");
38. faceCombo.addItem("Monospaced");
39. faceCombo.addItem("Dialog");
40. faceCombo.addItem("DialogInput");
41.
42. // the combo box listener changes the label font to the selected face name
43.
44. faceCombo.addActionListener(new
45. ActionListener()
46. {
47. public void actionPerformed(ActionEvent event)
48. {
49. label.setFont(new Font(
50. (String) faceCombo.getSelectedItem(),
51. Font.PLAIN,
52. DEFAULT_SIZE));
53. }
54. });
55.
56. // add combo box to a panel at the frame's southern border
57.
58. JPanel comboPanel = new JPanel();
59. comboPanel.add(faceCombo);
60. add(comboPanel, BorderLayout.SOUTH);
61. }
62.
63. public static final int DEFAULT_WIDTH = 300;
64. public static final int DEFAULT_HEIGHT = 200;
65.
66. private JComboBox faceCombo;
67. private JLabel label;
68. private static final int DEFAULT_SIZE = 12;
69. }
javax.swing.JComboBox 1.2
void setEditable(boolean b)
Parameters:
b
true if the combo box field can be edited by the user, false otherwise
void addItem(Object item)
adds an item to the item list.
void insertItemAt(Object item, int index)
inserts an item into the item list at a given index.
void removeItem(Object item)
removes an item from the item list.
void removeItemAt(int index)
removes the item at an index.
void removeAllItems()
removes all items from the item list.
Object getSelectedItem()
returns the currently selected item.
Sliders
Combo boxes let users choose from a discrete set of values. Sliders offer a choice from a continuum of values, for example, any number between 1 and 100.
The most common way of constructing a slider is as follows:
JSlider slider = new JSlider(min, max, initialValue);
If you omit the minimum, maximum, and initial values, they are initialized with 0, 100, and 50, respectively.
Or if you want the slider to be vertical, then use the following constructor call:
JSlider slider = new JSlider(SwingConstants.VERTICAL, min, max, initialValue);
These constructors create a plain slider, such as the top slider in Figure 9-19. You will see presently how to add decorations to a slider.
Figure 9-19. Sliders
As the user slides the slider bar, the value of the slider moves between the minimum and the maximum values. When the value changes, a ChangeEvent is sent to all change listeners. To be notified of the change, you call the addChangeListener method and install an object that implements the ChangeListener interface. That interface has a single method, stateChanged. In that method, you should retrieve the slider value:
public void stateChanged(ChangeEvent event)
{
JSlider slider = (JSlider) event.getSource();
int value = slider.getValue();
. . .
}
You can embellish the slider by showing ticks. For example, in the sample program, the second slider uses the following settings:
slider.setMajorTickSpacing(20);
slider.setMinorTickSpacing(5);
The slider is decorated with large tick marks every 20 units and small tick marks every 5 units. The units refer to slider values, not pixels.
These instructions only set the units for the tick marks. To actually have the tick marks appear, you also call
slider.setPaintTicks(true);
The major and minor tick marks are independent. For example, you can set major tick marks every 20 units and minor tick marks every 7 units, but you'll get a very messy scale.
You can force the slider to snap to ticks. Whenever the user has finished dragging a slider in snap mode, it is immediately moved to the closest tick. You activate this mode with the call
slider.setSnapToTicks(true);
NOTE
The "snap to ticks" behavior doesn't work as well as you might imagine. Until the slider has actually snapped, the change listener still reports slider values that don't correspond to ticks. And if you click next to the slider-an action that normally advances the slider a bit in the direction of the click-a slider with "snap to ticks" does not move to the next tick.
You can ask for tick mark labels for the major tick marks by calling
slider.setPaintLabels(true);
For example, with a slider ranging from 0 to 100 and major tick spacing of 20, the ticks are labeled 0, 20, 40, 60, 80, and 100.
You can also supply other tick marks, such as strings or icons (see Figure 9-19). The process is a bit convoluted. You need to fill a hash table with keys of type Integer and values of type Component. (Autoboxing makes this simple in JDK 5.0 and beyond.) You then call the setLabelTable method. The components are placed under the tick marks. Usually, you use JLabel objects. Here is how you can label ticks as A, B, C, D, E, and F.
Hashtable<Integer, Component> labelTable = new Hashtable<Integer, Component>();
labelTable.put(0, new JLabel("A"));
labelTable.put(20, new JLabel("B"));
. . .
labelTable.put(100, new JLabel("F"));
slider.setLabelTable(labelTable);
See Chapter 2 of Volume 2 for more information about hash tables.
Example 9-9 also shows a slider with icons as tick labels.
TIP
If your tick marks or labels don't show, double-check that you called setPaintTicks(true) and setPaintLabels(true).
To suppress the "track" in which the slider moves, call
slider.setPaintTrack(false);
The fourth slider in Figure 9-19 has no track.
The fifth slider has its direction reversed by a call to
slider.setInverted(true);
The example program shows all these visual effects with a collection of sliders. Each slider has a change event listener installed that places the current slider value into the text field at the bottom of the frame.
Example 9-9. SliderTest.java
1. import java.awt.*;
2. import java.awt.event.*;
3. import java.util.*;
4. import javax.swing.*;
5. import javax.swing.event.*;
6.
7. public class SliderTest
8. {
9. public static void main(String[] args)
10. {
11. SliderTestFrame frame = new SliderTestFrame();
12. frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
13. frame.setVisible(true);
14. }
15. }
16.
17. /**
18. A frame with many sliders and a text field to show slider
19. values.
20. */
21. class SliderTestFrame extends JFrame
22. {
23. public SliderTestFrame()
24. {
25. setTitle("SliderTest");
26. setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
27.
28. sliderPanel = new JPanel();
29. sliderPanel.setLayout(new FlowLayout(FlowLayout.LEFT));
30.
31. // common listener for all sliders
32. listener = new
33. ChangeListener()
34. {
35. public void stateChanged(ChangeEvent event)
36. {
37. // update text field when the slider value changes
38. JSlider source = (JSlider) event.getSource();
39. textField.setText("" + source.getValue());
40. }
41. };
42.
43. // add a plain slider
44.
45. JSlider slider = new JSlider();
46. addSlider(slider, "Plain");
47.
48. // add a slider with major and minor ticks
49.
50. slider = new JSlider();
51. slider.setPaintTicks(true);
52. slider.setMajorTickSpacing(20);
53. slider.setMinorTickSpacing(5);
54. addSlider(slider, "Ticks");
55.
56. // add a slider that snaps to ticks
57.
58. slider = new JSlider();
59. slider.setPaintTicks(true);
60. slider.setSnapToTicks(true);
61. slider.setMajorTickSpacing(20);
62. slider.setMinorTickSpacing(5);
63. addSlider(slider, "Snap to ticks");
64.
65. // add a slider with no track
66.
67. slider = new JSlider();
68. slider.setPaintTicks(true);
69. slider.setMajorTickSpacing(20);
70. slider.setMinorTickSpacing(5);
71. slider.setPaintTrack(false);
72. addSlider(slider, "No track");
73.
74. // add an inverted slider
75.
76. slider = new JSlider();
77. slider.setPaintTicks(true);
78. slider.setMajorTickSpacing(20);
79. slider.setMinorTickSpacing(5);
80. slider.setInverted(true);
81. addSlider(slider, "Inverted");
82.
83. // add a slider with numeric labels
84.
85. slider = new JSlider();
86. slider.setPaintTicks(true);
87. slider.setPaintLabels(true);
88. slider.setMajorTickSpacing(20);
89. slider.setMinorTickSpacing(5);
90. addSlider(slider, "Labels");
91.
92. // add a slider with alphabetic labels
93.
94. slider = new JSlider();
95. slider.setPaintLabels(true);
96. slider.setPaintTicks(true);
97. slider.setMajorTickSpacing(20);
98. slider.setMinorTickSpacing(5);
99.
100. Dictionary<Integer, Component> labelTable = new Hashtable<Integer, Component>();
101. labelTable.put(0, new JLabel("A"));
102. labelTable.put(20, new JLabel("B"));
103. labelTable.put(40, new JLabel("C"));
104. labelTable.put(60, new JLabel("D"));
105. labelTable.put(80, new JLabel("E"));
106. labelTable.put(100, new JLabel("F"));
107.
108. slider.setLabelTable(labelTable);
109. addSlider(slider, "Custom labels");
110.
111. // add a slider with icon labels
112.
113. slider = new JSlider();
114. slider.setPaintTicks(true);
115. slider.setPaintLabels(true);
116. slider.setSnapToTicks(true);
117. slider.setMajorTickSpacing(20);
118. slider.setMinorTickSpacing(20);
119.
120. labelTable = new Hashtable<Integer, Component>();
121.
122. // add card images
123.
124. labelTable.put(0, new JLabel(new ImageIcon("nine.gif")));
125. labelTable.put(20, new JLabel(new ImageIcon("ten.gif")));
126. labelTable.put(40, new JLabel(new ImageIcon("jack.gif")));
127. labelTable.put(60, new JLabel(new ImageIcon("queen.gif")));
128. labelTable.put(80, new JLabel(new ImageIcon("king.gif")));
129. labelTable.put(100, new JLabel(new ImageIcon("ace.gif")));
130.
131. slider.setLabelTable(labelTable);
132. addSlider(slider, "Icon labels");
133.
134. // add the text field that displays the slider value
135.
136. textField = new JTextField();
137. add(sliderPanel, BorderLayout.CENTER);
138. add(textField, BorderLayout.SOUTH);
139. }
140.
141. /**
142. Adds a slider to the slider panel and hooks up the listener
143. @param s the slider
144. @param description the slider description
145. */
146. public void addSlider(JSlider s, String description)
147. {
148. s.addChangeListener(listener);
149. JPanel panel = new JPanel();
150. panel.add(s);
151. panel.add(new JLabel(description));
152. sliderPanel.add(panel);
153. }
154.
155. public static final int DEFAULT_WIDTH = 350;
156. public static final int DEFAULT_HEIGHT = 450;
157.
158. private JPanel sliderPanel;
159. private JTextField textField;
160. private ChangeListener listener;
161. }
javax.swing.JSlider 1.2
JSlider()
JSlider(int direction)
JSlider(int min, int max)
JSlider(int min, int max, int initialValue)
JSlider(int direction, int min, int max, int initialValue)
construct a horizontal slider with the given direction and minimum, maximum, and initial values.
Parameters:
direction
One of SwingConstants.HORIZONTAL or SwingConstants.VERTICAL. The default is horizontal.
min, max
The minimum and maximum for the slider values. Defaults are 0 and 100.
initialValue
The initial value for the slider. The default is 50.
void setPaintTicks(boolean b)
displays ticks if b is true.
void setMajorTickSpacing(int units)
void setMinorTickSpacing(int units)
set major or minor ticks at multiples of the given slider units.
void setPaintLabels(boolean b)
displays tick labels if b is true.
void setLabelTable(Dictionary table)
sets the components to use for the tick labels. Each key/value pair in the table has the form new Integer(value)/component.
void setSnapToTicks(boolean b)
if b is true, then the slider snaps to the closest tick after each adjustment.
void setPaintTrack(boolean b)
if b is true, then a track is displayed in which the slider runs.
The JSpinner Component
A JSpinner is a text field with two small buttons on the side. When the buttons are clicked, the text field value is incremented or decremented (see Figure 9-20).
Figure 9-20. Several variations of the JSpinner component
The values in the spinner can be numbers, dates, values from a list, or, in the most general case, any sequence of values for which predecessors and successors can be determined. The JSpinner class defines standard data models for the first three cases. You can define your own data model to describe arbitrary sequences.
By default, a spinner manages an integer, and the buttons increment or decrement it by 1. You can get the current value by calling the getValue method. That method returns an Object. Cast it to an Integer and retrieve the wrapped value.
JSpinner defaultSpinner = new JSpinner();
. . .
int value = (Integer) defaultSpinner.getValue();
You can change the increment to a value other than 1, and you can also supply lower and upper bounds. Here is a spinner with starting value 5, bounded between 0 and 10, and an increment of 0.5:
JSpinner boundedSpinner = new JSpinner(new SpinnerNumberModel(5, 0, 10, 0.5));
There are two SpinnerNumberModel constructors, one with only int parameters and one with double parameters. If any of the parameters is a floating-point number, then the second constructor is used. It sets the spinner value to a Double object.
Spinners aren't restricted to numeric values. You can have a spinner iterate through any collection of values. Simply pass a SpinnerListModel to the JSpinner constructor. You can construct a SpinnerListModel from an array or a class implementing the List interface (such as an ArrayList). In our sample program, we display a spinner control with all available font names.
String[] fonts = GraphicsEnvironment.getLocalGraphicsEnvironment()
.getAvailableFontFamilyNames();
JSpinner listSpinner = new JSpinner(new SpinnerListModel(fonts));
However, we found that the direction of the iteration was mildly confusing because it is opposite from the user experience with a combo box. In a combo box, higher values are below lower values, so you would expect the downward arrow to navigate toward higher values. But the spinner increments the array index so that the upward arrow yields higher values. There is no provision for reversing the traversal order in the SpinnerListModel, but an impromptu anonymous subclass yields the desired result:
JSpinner reverseListSpinner = new JSpinner(
new SpinnerListModel(fonts)
{
public Object getNextValue()
return super.getPreviousValue();
}
public Object getPreviousValue()
{
return super.getNextValue();
}
});
Try out both versions and see which you find more intuitive.
Another good use for a spinner is for a date that the user can increment or decrement. You get such a spinner, initialized with today's date, with the call
JSpinner dateSpinner = new JSpinner(new SpinnerDateModel());
However, if you look carefully at Figure 9-20, you will see that the spinner text shows both date and time, such as
3/12/02 7:23 PM
The time doesn't make any sense for a date picker. It turns out to be somewhat difficult to make the spinner show just the date. Here is the magic incantation:
JSpinner betterDateSpinner = new JSpinner(new SpinnerDateModel());
String pattern = ((SimpleDateFormat) DateFormat.getDateInstance()).toPattern();
betterDateSpinner.setEditor(new JSpinner.DateEditor(betterDateSpinner, pattern));
Using the same approach, you can also make a time picker. Then use the SpinnerDateModel constructor that lets you specify a Date, the lower and upper bounds (or null if there are no bounds), and the Calendar field (such as Calendar.HOUR) to be modified.
JSpinner timeSpinner = new JSpinner(
new SpinnerDateModel(
new GregorianCalendar(2000, Calendar.JANUARY, 1, 12, 0, 0).getTime(),
null, null, Calendar.HOUR));
However, if you want to update the minutes in 15-minute increments, then you exceed the capabilities of the standard SpinnerDateModel class.
You can display arbitrary sequences in a spinner by defining your own spinner model. In our sample program, we have a spinner that iterates through all permutations of the string "meat". You can get to "mate", "meta", "team", and another 20 permutations by clicking the spinner buttons.
When you define your own model, you should extend the AbstractSpinnerModel class and define the following four methods:
Object getValue()
void setValue(Object value)
Object getNextValue()
Object getPreviousValue()
The getValue method returns the value stored by the model. The setValue method sets a new value. It should throw an IllegalArgumentException if the new value is not appropriate.
CAUTION
The setValue method must call the fireStateChanged method after setting the new value. Otherwise, the spinner field won't be updated.
The getNextValue and getPreviousValue methods return the values that should come after or before the current value, or null if the end of the traversal has been reached.
CAUTION
The getNextValue and getPreviousValue methods should not change the current value. When a user clicks on the upward arrow of the spinner, the getNextValue method is called. If the return value is not null, it is set by a call to setValue.
In the sample program, we use a standard algorithm to determine the next and previous permutations. The details of the algorithm are not important.
Example 9-10 shows how to generate the various spinner types. Click on the Ok button to see the spinner values.
Example 9-10. SpinnerTest.java
1. import java.awt.*;
2. import java.awt.event.*;
3. import java.text.*;
4. import java.util.*;
5. import javax.swing.*;
6.
7. /**
8. A program to test spinners.
9. */
10. public class SpinnerTest
11. {
12. public static void main(String[] args)
13. {
14. SpinnerFrame frame = new SpinnerFrame();
15. frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
16. frame.setVisible(true);
17. }
18. }
19.
20. /**
21. A frame with a panel that contains several spinners and
22. a button that displays the spinner values.
23. */
24. class SpinnerFrame extends JFrame
25. {
26. public SpinnerFrame()
27. {
28. setTitle("SpinnerTest");
29. setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
30. JPanel buttonPanel = new JPanel();
31. okButton = new JButton("Ok");
32. buttonPanel.add(okButton);
33. add(buttonPanel, BorderLayout.SOUTH);
34.
35. mainPanel = new JPanel();
36. mainPanel.setLayout(new GridLayout(0, 3));
37. add(mainPanel, BorderLayout.CENTER);
38.
39. JSpinner defaultSpinner = new JSpinner();
40. addRow("Default", defaultSpinner);
41.
42. JSpinner boundedSpinner = new JSpinner(new SpinnerNumberModel(5, 0, 10, 0.5));
43. addRow("Bounded", boundedSpinner);
44.
45. String[] fonts = GraphicsEnvironment
46. .getLocalGraphicsEnvironment()
47. .getAvailableFontFamilyNames();
48.
49. JSpinner listSpinner = new JSpinner(new SpinnerListModel(fonts));
50. addRow("List", listSpinner);
51.
52. JSpinner reverseListSpinner = new JSpinner(
53. new
54. SpinnerListModel(fonts)
55. {
56. public Object getNextValue()
57. {
58. return super.getPreviousValue();
59. }
60. public Object getPreviousValue()
61. {
62. return super.getNextValue();
63. }
64. });
65. addRow("Reverse List", reverseListSpinner);
66.
67. JSpinner dateSpinner = new JSpinner(new SpinnerDateModel());
68. addRow("Date", dateSpinner);
69.
70. JSpinner betterDateSpinner = new JSpinner(new SpinnerDateModel());
71. String pattern = ((SimpleDateFormat) DateFormat.getDateInstance()).toPattern();
72. betterDateSpinner.setEditor(new JSpinner.DateEditor(betterDateSpinner, pattern));
73. addRow("Better Date", betterDateSpinner);
74.
75. JSpinner timeSpinner = new JSpinner(
76. new SpinnerDateModel(
77. new GregorianCalendar(2000, Calendar.JANUARY, 1, 12, 0, 0).getTime(),
78. null, null, Calendar.HOUR));
79. addRow("Time", timeSpinner);
80.
81. JSpinner permSpinner = new JSpinner(new PermutationSpinnerModel("meat"));
82. addRow("Word permutations", permSpinner);
83. }
84.
85. /**
86. Adds a row to the main panel.
87. @param labelText the label of the spinner
88. @param spinner the sample spinner
89. */
90. public void addRow(String labelText, final JSpinner spinner)
91. {
92. mainPanel.add(new JLabel(labelText));
93. mainPanel.add(spinner);
94. final JLabel valueLabel = new JLabel();
95. mainPanel.add(valueLabel);
96. okButton.addActionListener(new
97. ActionListener()
98. {
99. public void actionPerformed(ActionEvent event)
100. {
101. Object value = spinner.getValue();
102. valueLabel.setText(value.toString());
103. }
104. });
105. }
106.
107. public static final int DEFAULT_WIDTH = 400;
108. public static final int DEFAULT_HEIGHT = 250;
109.
110. private JPanel mainPanel;
111. private JButton okButton;
112. }
113.
114. /**
115. A model that dynamically generates word permutations
116. */
117. class PermutationSpinnerModel extends AbstractSpinnerModel
118. {
119. /**
120. Constructs the model.
121. @param w the word to permute
122. */
123. public PermutationSpinnerModel(String w)
124. {
125. word = w;
126. }
127.
128. public Object getValue()
129. {
130. return word;
131. }
132.
133. public void setValue(Object value)
134. {
135. if (!(value instanceof String))
136. throw new IllegalArgumentException();
137. word = (String) value;
138. fireStateChanged();
139. }
140.
141. public Object getNextValue()
142. {
143. int[] codePoints = toCodePointArray(word);
144. for (int i = codePoints.length - 1; i > 0; i--)
145. {
146. if (codePoints[i - 1] < codePoints[i])
147. {
148. int j = codePoints.length - 1;
149. while (codePoints[i - 1] > codePoints[j]) j--;
150. swap(codePoints, i - 1, j);
151. reverse(codePoints, i, codePoints.length - 1);
152. return new String(codePoints, 0, codePoints.length);
153. }
154. }
155. reverse(codePoints, 0, codePoints.length - 1);
156. return new String(codePoints, 0, codePoints.length);
157. }
158.
159. public Object getPreviousValue()
160. {
161. int[] codePoints = toCodePointArray(word);
162. for (int i = codePoints.length - 1; i > 0; i--)
163. {
164. if (codePoints[i - 1] > codePoints[i])
165. {
166. int j = codePoints.length - 1;
167. while (codePoints[i - 1] < codePoints[j]) j--;
168. swap(codePoints, i - 1, j);
169. reverse(codePoints, i, codePoints.length - 1);
170. return new String(codePoints, 0, codePoints.length);
171. }
172. }
173. reverse(codePoints, 0, codePoints.length - 1);
174. return new String(codePoints, 0, codePoints.length);
175. }
176.
177. private static int[] toCodePointArray(String str)
178. {
179. int[] codePoints = new int[str.codePointCount(0, str.length())];
180. for (int i = 0, j = 0; i < str.length(); i++, j++)
181. {
182. int cp = str.codePointAt(i);
183. if (Character.isSupplementaryCodePoint(cp)) i++;
184. codePoints[j] = cp;
185. }
186. return codePoints;
187. }
188.
189. private static void swap(int[] a, int i, int j)
190. {
191. int temp = a[i];
192. a[i] = a[j];
193. a[j] = temp;
194. }
195.
196. private static void reverse(int[] a, int i, int j)
197. {
198. while (i < j) { swap(a, i, j); i++; j--; }
199. }
200.
201. private String word;
202. }
javax.swing.JSpinner 1.4
JSpinner()
constructs a spinner that edits an integer with starting value 0, increment 1, and no bounds.
JSpinner(SpinnerModel model)
constructs a spinner that uses the given data model.
Object getValue()
gets the current value of the spinner.
void setValue(Object value)
attempts to set the value of the spinner. Throws an IllegalArgumentException if the model does not accept the value.
void setEditor(JComponent editor)
sets the component that is used for editing the spinner value.
javax.swing.SpinnerNumberModel 1.4
SpinnerNumberModel(int initval, int minimum, int maximum, int stepSize)
SpinnerNumberModel(double initval, double minimum, double maximum, double stepSize)
these constructors yield number models that manage an Integer or Double value. Use the MIN_VALUE and MAX_VALUE constants of the Integer and Double classes for unbounded values.
Parameters:
initval
The initial value
minimum
The minimum valid value
maximum
The maximum valid value
stepSize
The increment or decrement of each spin
javax.swing.SpinnerListModel 1.4
SpinnerListModel(Object[] values)
SpinnerListModel(List values)
these constructors yield models that select a value from among the given values.
javax.swing.SpinnerDateModel 1.4
SpinnerDateModel()
constructs a date model with today's date as the initial value, no lower or upper bounds, and an increment of Calendar.DAY_OF_MONTH.
SpinnerDateModel(Date initval, Comparable minimum, Comparable maximum, int step)
Parameters:
initval
The initial value
minimum
The minimum valid value, or null if no lower bound is desired
maximum
The maximum valid value, or null if no lower bound is desired
step
The date field to increment or decrement of each spin. One of the constants ERA, YEAR, MONTH, WEEK_OF_YEAR, WEEK_OF_MONTH, DAY_OF_MONTH, DAY_OF_YEAR, DAY_OF_WEEK, DAY_OF_WEEK_IN_MONTH, AM_PM, HOUR, HOUR_OF_DAY, MINUTE, SECOND, or MILLISECOND of the Calendar class
java.text.SimpleDateFormat 1.1
String toPattern() 1.2
gets the editing pattern for this date formatter. A typical pattern is "yyyy-MM-dd". See the JDK documentation for more details about the pattern.
javax.swing.JSpinner.DateEditor 1.4
DateEditor(JSpinner spinner, String pattern)
constructs a date editor for a spinner.
Parameters:
spinner
The spinner to which this editor belongs
pattern
The format pattern for the associated SimpleDateFormat
javax.swing.AbstractSpinnerModel 1.4
Object getValue()
gets the current value of the model.
void setValue(Object value)
attempts to set a new value for the model. Throws an IllegalArgumentException if the value is not acceptable. When overriding this method, you should call fireStateChanged after setting the new value.
Object getNextValue()
Object getPreviousValue()
compute (but do not set) the next or previous value in the sequence that this model defines.
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
Menus
We started this chapter by introducing the most common components that you might want to place into a window, such as various kinds of buttons, text fields, and combo boxes. Swing also supports another type of user interface element, the pull-down menus that are familiar from GUI applications.
A menu bar on top of the window contains the names of the pull-down menus. Clicking on a name opens the menu containing menu items and submenus. When the user clicks on a menu item, all menus are closed and a message is sent to the program. Figure 9-21 shows a typical menu with a submenu.
Figure 9-21. A menu with a submenu
Menu Building
Building menus is straightforward. You create a menu bar:
JMenuBar menuBar = new JMenuBar();
A menu bar is just a component that you can add anywhere you like. Normally, you want it to appear at the top of a frame. You can add it there with the setJMenuBar method:
frame.setJMenuBar(menuBar);
For each menu, you create a menu object:
JMenu editMenu = new JMenu("Edit");
You add the top-level menus to the menu bar:
menuBar.add(editMenu);
You add menu items, separators, and submenus to the menu object:
JMenuItem pasteItem = new JMenuItem("Paste");
editMenu.add(pasteItem);
editMenu.addSeparator();
JMenu optionsMenu = . . .; // a submenu
editMenu.add(optionsMenu);
You can see separators in Figure 9-21 below the "Paste" and "Read-only" menu items.
When the user selects a menu, an action event is triggered. You need to install an action listener for each menu item.
ActionListener listener = . . .;
pasteItem.addActionListener(listener);
The method JMenu.add(String s) conveniently adds a menu item to the end of a menu, for example:
editMenu.add("Paste");
The add method returns the created menu item, so you can capture it and then add the listener, as follows:
JMenuItem pasteItem = editMenu.add("Paste");
pasteItem.addActionListener(listener);
It often happens that menu items trigger commands that can also be activated through other user interface elements such as toolbar buttons. In Chapter 8, you saw how to specify commands through Action objects. You define a class that implements the Action interface, usually by extending the AbstractAction convenience class. You specify the menu item label in the constructor of the AbstractAction object, and you override the actionPerformed method to hold the menu action handler. For example,
Action exitAction = new
AbstractAction("Exit") // menu item text goes here
{
public void actionPerformed(ActionEvent event)
{
// action code goes here
System.exit(0);
}
};
You can then add the action to the menu:
JMenuItem exitItem = fileMenu.add(exitAction);
This command adds a menu item to the menu, using the action name. The action object becomes its listener. This is just a convenient shortcut for
JMenuItem exitItem = new JMenuItem(exitAction);
fileMenu.add(exitItem);
NOTE
In Windows and Macintosh programs, menus are generally defined in an external resource file and tied to the application with resource identifiers. It is possible to build menus programmatically, but it is not commonly done. In Java, menus are still usually built inside the program because the mechanism for dealing with external resources is far more limited than it is in Windows or Mac OS.
javax.swing.JMenu 1.2
JMenu(String label)
constructs a menu.
Parameters:
label
The label for the menu in the menu bar or parent menu
JMenuItem add(JMenuItem item)
adds a menu item (or a menu).
Parameters:
item
The item or menu to add
JMenuItem add(String label)
adds a menu item to this menu.
Parameters:
label
The label for the menu items
JMenuItem add(Action a)
adds a menu item and associates an action with it.
Parameters:
a
An action encapsulating a name, optional icon, and listener (see Chapter 8)
void addSeparator()
adds a separator line to the menu.
JMenuItem insert(JMenuItem menu, int index)
adds a new menu item (or submenu) to the menu at a specific index.
Parameters:
menu
The menu to be added
index
Where to add the item
JMenuItem insert(Action a, int index)
adds a new menu item at a specific index and associates an action with it.
Parameters:
a
An action encapsulating a name, optional icon, and listener
index
Where to add the item
void insertSeparator(int index)
adds a separator to the menu.
Parameters:
index
Where to add the separator
void remove(int index)
removes a specific item from the menu.
Parameters:
index
The position of the item to remove
void remove(JMenuItem item)
removes a specific item from the menu.
Parameters:
item
The item to remove
javax.swing.JMenuItem 1.2
JMenuItem(String label)
constructs a menu item with a given label.
JMenuItem(Action a) 1.3
constructs a menu item for the given action.
Parameters:
a
An action encapsulating a name, optional icon, and listener
javax.swing.AbstractButton 1.2
void setAction(Action a) 1.3
sets the action for this button or menu item.
Parameters:
a
An action encapsulating a name, optional icon, and listener
javax.swing.JFrame 1.2
void setJMenuBar(JMenuBar menubar)
sets the menu bar for this frame.
Icons in Menu Items
Menu items are very similar to buttons. In fact, the JMenuItem class extends the AbstractButton class. Just like buttons, menus can have just a text label, just an icon, or both. You can specify the icon with the JMenuItem(String, Icon) or JMenuItem(Icon) constructor, or you can set it with the setIcon method that the JMenuItem class inherits from the AbstractButton class. Here is an example:
JMenuItem cutItem = new JMenuItem("Cut", new ImageIcon("cut.gif"));
Figure 9-22 shows a menu with icons next to several menu items. As you can see, by default, the menu item text is placed to the right of the icon. If you prefer the text to be placed on the left, call the setHorizontalTextPosition method that the JMenuItem class inherits from the AbstractButton class. For example, the call
cutItem.setHorizontalTextPosition(SwingConstants.LEFT);
Figure 9-22. Icons in menu items
moves the menu item text to the left of the icon.
You can also add an icon to an action:
cutAction.putValue(Action.SMALL_ICON, new ImageIcon("cut.gif"));
Whenever you construct a menu item out of an action, the Action.NAME value becomes the text of the menu item and the Action.SMALL_ICON value becomes the icon.
Alternatively, you can set the icon in the AbstractAction constructor:
cutAction = new
AbstractAction("Cut", new ImageIcon("cut.gif"))
{
public void actionPerformed(ActionEvent event)
{
// action code goes here
}
};
javax.swing.JMenuItem 1.2
JMenuItem(String label, Icon icon)
constructs a menu item with the given label and icon.
javax.swing.AbstractButton 1.2
void setHorizontalTextPosition(int pos)
sets the horizontal position of the text relative to the icon.
Parameters:
pos
SwingConstants.RIGHT (text is to the right of icon) or SwingConstants.LEFT
javax.swing.AbstractAction 1.2
AbstractAction(String name, Icon smallIcon)
constructs an abstract action with the given name and icon.
Checkbox and Radio Button Menu Items
Checkbox and radio button menu items display a checkbox or radio button next to the name (see Figure 9-23). When the user selects the menu item, the item automatically toggles between checked and unchecked.
Figure 9-23. A checked menu item and menu items with radio buttons
Apart from the button decoration, you treat these menu items just as you would any others. For example, here is how you create a checkbox menu item.
JCheckBoxMenuItem readonlyItem = new JCheckBoxMenuItem("Read-only");
optionsMenu.add(readonlyItem);
The radio button menu items work just like regular radio buttons. You must add them to a button group. When one of the buttons in a group is selected, all others are automatically deselected.
ButtonGroup group = new ButtonGroup();
JRadioButtonMenuItem insertItem = new JRadioButtonMenuItem("Insert");
insertItem.setSelected(true);
JRadioButtonMenuItem overtypeItem = new JRadioButtonMenuItem("Overtype");
group.add(insertItem);
group.add(overtypeItem);
optionsMenu.add(insertItem);
optionsMenu.add(overtypeItem);
With these menu items, you don't necessarily want to be notified at the exact moment the user selects the item. Instead, you can simply use the isSelected method to test the current state of the menu item. (Of course, that means that you should keep a reference to the menu item stored in an instance field.) Use the setSelected method to set the state.
javax.swing.JCheckBoxMenuItem 1.2
JCheckBoxMenuItem(String label)
constructs the checkbox menu item with the given label.
JCheckBoxMenuItem(String label, boolean state)
constructs the checkbox menu item with the given label and the given initial state (TRue is checked).
javax.swing.JRadioButtonMenuItem 1.2
JRadioButtonMenuItem(String label)
constructs the radio button menu item with the given label.
JRadioButtonMenuItem(String label, boolean state)
constructs the radio button menu item with the given label and the given initial state (true is checked).
javax.swing.AbstractButton 1.2
boolean isSelected()
returns the check state of this item (true is checked).
void setSelected(boolean state)
sets the check state of this item.
Pop-Up Menus
A pop-up menu is a menu that is not attached to a menu bar but that floats somewhere (see Figure 9-24).
Figure 9-24. A pop-up menu
You create a pop-up menu similarly to the way you create a regular menu, but a pop-up menu has no title.
JPopupMenu popup = new JPopupMenu();
You then add menu items in the usual way:
JMenuItem item = new JMenuItem("Cut");
item.addActionListener(listener);
popup.add(item);
Unlike the regular menu bar that is always shown at the top of the frame, you must explicitly display a pop-up menu by using the show method. You specify the parent component and the location of the pop-up, using the coordinate system of the parent. For example:
popup.show(panel, x, y);
Usually you write code to pop up a menu when the user clicks a particular mouse button, the so-called pop-up trigger. In Windows and Linux, the pop-up trigger is the nonprimary (usually, the right) mouse button. To pop up a menu when the user clicks on a component, using the pop-up trigger, simply call the method
component.setComponentPopupMenu(popup);
Very occasionally, you may place a component inside another component that has a pop-up menu. The child component can inherit the parent component's pop-up menu by calling
child.setInheritsPopupMenu(true);
These methods were added in JDK 5.0 to insulate programmers from system dependencies with pop-up menus. Before JDK 5.0, you had to install a mouse listener and add the following code to both the mousePressed and the mouseReleased listener methods:
if (popup.isPopupTrigger(event))
popup.show(event.getComponent(), event.getX(), event.getY());
Some systems trigger pop-ups when the mouse button goes down, others when the mouse button goes up.
javax.swing.JPopupMenu 1.2
void show(Component c, int x, int y)
shows the pop-up menu.
Parameters:
c
The component over which the pop-up menu is to appear
x, y
The coordinates (in the coordinate space of c) of the top-left corner of the pop-up menu
boolean isPopupTrigger(MouseEvent event) 1.3
returns true if the mouse event is the pop-up menu trigger.
java.awt.event.MouseEvent 1.1
boolean isPopupTrigger()
returns TRue if this mouse event is the pop-up menu trigger.
javax.swing.JComponent 1.2
void setComponentPopupMenu(JPopupMenu popup) 5.0
JPopup getComponentPopupMenu() 5.0
set or get the pop-up menu for this component.
void setInheritsPopupMenu(boolean b) 5.0
boolean getInheritsPopupMenu() 5.0
set or get the inheritsPopupMenu property. If the property is set and this component's pop-up menu is null, it uses its parent's pop-up menu.
Keyboard Mnemonics and Accelerators
It is a real convenience for the experienced user to select menu items by keyboard mnemonics. You can specify keyboard mnemonics for menu items by specifying a mnemonic letter in the menu item constructor:
JMenuItem cutItem = new JMenuItem("Cut", 'T');
The keyboard mnemonic is displayed automatically in the menu, with the mnemonic letter underlined (see Figure 9-25). For example, in the item defined in the last example, the label will be displayed as "Cut" with an underlined letter 't'. When the menu is displayed, the user just needs to press the T key, and the menu item is selected. (If the mnemonic letter is not part of the menu string, then typing it still selects the item, but the mnemonic is not displayed in the menu. Naturally, such invisible mnemonics are of dubious utility.)
Figure 9-25. Keyboard mnemonics
Sometimes, you don't want to underline the first letter of the menu item that matches the mnemonic. For example, if you have a mnemonic "A" for the menu item "Save As," then it makes more sense to underline the second "A" (Save As). As of JDK 1.4, you can specify which character you want to have underlined; call the setDisplayedMnemonicIndex method.
If you have an Action object, you can add the mnemonic as the value of the Action.MNEMONIC_KEY key, as follows:
cutAction.putValue(Action.MNEMONIC_KEY, new Integer('T'));
You can supply a mnemonic letter only in the constructor of a menu item, not in the constructor for a menu. Instead, to attach a mnemonic to a menu, you call the setMnemonic method:
JMenu helpMenu = new JMenu("Help");
helpMenu.setMnemonic('H');
To select a top-level menu from the menu bar, you press the ALT key together with the mnemonic letter. For example, you press ALT+H to select the Help menu from the menu bar.
Keyboard mnemonics let you select a submenu or menu item from the currently open menu. In contrast, accelerators are keyboard shortcuts that let you select menu items without ever opening a menu. For example, many programs attach the accelerators CTRL+O and CTRL+S to the Open and Save items in the File menu. You use the setAccelerator method to attach an accelerator key to a menu item. The setAccelerator method takes an object of type Keystroke. For example, the following call attaches the accelerator CTRL+O to the openItem menu item.
openItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_O, InputEvent.CTRL_MASK));
When the user presses the accelerator key combination, this automatically selects the menu option and fires an action event, as if the user had selected the menu option manually.
You can attach accelerators only to menu items, not to menus. Accelerator keys don't actually open the menu. Instead, they directly fire the action event that is associated with a menu.
Conceptually, adding an accelerator to a menu item is similar to the technique of adding an accelerator to a Swing component. (We discussed that technique in Chapter 8.) However, when the accelerator is added to a menu item, the key combination is automatically displayed in the menu (see Figure 9-26).
Figure 9-26. Accelerators
NOTE
Under Windows, ALT+F4 closes a window. But this is not an accelerator that was programmed in Java. It is a shortcut defined by the operating system. This key combination will always trigger the WindowClosing event for the active window regardless of whether there is a Close item on the menu.
javax.swing.JMenuItem 1.2
JMenuItem(String label, int mnemonic)
constructs a menu item with a given label and mnemonic.
Parameters:
label
The label for this menu item
mnemonic
The mnemonic character for the item; this character will be underlined in the label
void setAccelerator(KeyStroke k)
sets the keystroke k as accelerator for this menu item. The accelerator key is displayed next to the label.
javax.swing.AbstractButton 1.2
void setMnemonic(int mnemonic)
sets the mnemonic character for the button. This character will be underlined in the label.
Parameters:
mnemonic
The mnemonic character for the button
void setDisplayedMnemonicIndex(int index) 1.4
sets the index of the character to be underlined in the button text. Use this method if you don't want the first occurrence of the mnemonic character to be underlined.
Parameters:
index
The index of the button text character to be underlined
Enabling and Disabling Menu Items
Occasionally, a particular menu item should be selected only in certain contexts. For example, when a document is opened for reading only, then the Save menu item is not meaningful. Of course, we could remove the item from the menu with the JMenu.remove method, but users would react with some surprise to menus whose content keeps changing. Instead, it is better to deactivate the menu items that lead to temporarily inappropriate commands. A deactivated menu item is shown in gray, and it cannot be selected (see Figure 9-27).
Figure 9-27. Disabled menu items
To enable or disable a menu item, use the setEnabled method:
saveItem.setEnabled(false);
There are two strategies for enabling and disabling menu items. Each time circumstances change, you can call setEnabled on the relevant menu items or actions. For example, as soon as a document has been set to read-only mode, you can locate the Save and Save As menu items and disable them. Alternatively, you can disable items just before displaying the menu. To do this, you must register a listener for the "menu selected" event. The javax.swing.event package defines a MenuListener interface with three methods:
void menuSelected(MenuEvent event)
void menuDeselected(MenuEvent event)
void menuCanceled(MenuEvent event)
The menuSelected method is called before the menu is displayed. It can therefore be used to disable or enable menu items. The following code shows how to disable the Save and Save As actions whenever the Read Only checkbox menu item is selected:
public void menuSelected(MenuEvent event)
{
saveAction.setEnabled(!readonlyItem.isSelected());
saveAsAction.setEnabled(!readonlyItem.isSelected());
}
CAUTION
Disabling menu items just before displaying the menu is a clever idea, but it does not work for menu items that also have accelerator keys. Because the menu is never opened when the accelerator key is pressed, the action is never disabled, and it is still triggered by the accelerator key.
javax.swing.JMenuItem 1.2
void setEnabled(boolean b)
enables or disables the menu item.
javax.swing.event.MenuListener 1.2
void menuSelected(MenuEvent e)
is called when the menu has been selected, before it is opened.
void menuDeselected(MenuEvent e)
is called when the menu has been deselected, after it has been closed.
void menuCanceled(MenuEvent e)
is called when the menu has been canceled, for example, by a user clicking outside the menu.
Example 9-11 is a sample program that generates a set of menus. It shows all the features that you saw in this section: nested menus, disabled menu items, checkbox and radio button menu items, a pop-up menu, and keyboard mnemonics and accelerators.
Example 9-11. MenuTest.java
1. import java.awt.*;
2. import java.awt.event.*;
3. import javax.swing.*;
4. import javax.swing.event.*;
5.
6. public class MenuTest
7. {
8. public static void main(String[] args)
9. {
10. MenuFrame frame = new MenuFrame();
11. frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
12. frame.setVisible(true);
13. }
14. }
15.
16. /**
17. A frame with a sample menu bar.
18. */
19. class MenuFrame extends JFrame
20. {
21. public MenuFrame()
22. {
23. setTitle("MenuTest");
24. setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
25.
26. JMenu fileMenu = new JMenu("File");
27. JMenuItem newItem = fileMenu.add(new TestAction("New"));
28.
29. // demonstrate accelerators
30.
31. JMenuItem openItem = fileMenu.add(new TestAction("Open"));
32. openItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_O, InputEvent
.CTRL_MASK));
33.
34. fileMenu.addSeparator();
35.
36. saveAction = new TestAction("Save");
37. JMenuItem saveItem = fileMenu.add(saveAction);
38. saveItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_S, InputEvent
.CTRL_MASK));
39.
40. saveAsAction = new TestAction("Save As");
41. JMenuItem saveAsItem = fileMenu.add(saveAsAction);
42. fileMenu.addSeparator();
43.
44. fileMenu.add(new
45. AbstractAction("Exit")
46. {
47. public void actionPerformed(ActionEvent event)
48. {
49. System.exit(0);
50. }
51. });
52.
53. // demonstrate checkbox and radio button menus
54.
55. readonlyItem = new JCheckBoxMenuItem("Read-only");
56. readonlyItem.addActionListener(new
57. ActionListener()
58. {
59. public void actionPerformed(ActionEvent event)
60. {
61. boolean saveOk = !readonlyItem.isSelected();
62. saveAction.setEnabled(saveOk);
63. saveAsAction.setEnabled(saveOk);
64. }
65. });
66.
67. ButtonGroup group = new ButtonGroup();
68.
69. JRadioButtonMenuItem insertItem = new JRadioButtonMenuItem("Insert");
70. insertItem.setSelected(true);
71. JRadioButtonMenuItem overtypeItem = new JRadioButtonMenuItem("Overtype");
72.
73. group.add(insertItem);
74. group.add(overtypeItem);
75.
76. // demonstrate icons
77.
78. Action cutAction = new TestAction("Cut");
79. cutAction.putValue(Action.SMALL_ICON, new ImageIcon("cut.gif"));
80. Action copyAction = new TestAction("Copy");
81. copyAction.putValue(Action.SMALL_ICON, new ImageIcon("copy.gif"));
82. Action pasteAction = new TestAction("Paste");
83. pasteAction.putValue(Action.SMALL_ICON, new ImageIcon("paste.gif"));
84.
85. JMenu editMenu = new JMenu("Edit");
86. editMenu.add(cutAction);
87. editMenu.add(copyAction);
88. editMenu.add(pasteAction);
89.
90. // demonstrate nested menus
91.
92. JMenu optionMenu = new JMenu("Options");
93.
94. optionMenu.add(readonlyItem);
95. optionMenu.addSeparator();
96. optionMenu.add(insertItem);
97. optionMenu.add(overtypeItem);
98.
99. editMenu.addSeparator();
100. editMenu.add(optionMenu);
101.
102. // demonstrate mnemonics
103.
104. JMenu helpMenu = new JMenu("Help");
105. helpMenu.setMnemonic('H');
106.
107. JMenuItem indexItem = new JMenuItem("Index");
108. indexItem.setMnemonic('I');
109. helpMenu.add(indexItem);
110.
111. // you can also add the mnemonic key to an action
112. Action aboutAction = new TestAction("About");
113. aboutAction.putValue(Action.MNEMONIC_KEY, new Integer('A'));
114. helpMenu.add(aboutAction);
115.
116. // add all top-level menus to menu bar
117.
118. JMenuBar menuBar = new JMenuBar();
119. setJMenuBar(menuBar);
120.
121. menuBar.add(fileMenu);
122. menuBar.add(editMenu);
123. menuBar.add(helpMenu);
124.
125. // demonstrate pop-ups
126.
127. popup = new JPopupMenu();
128. popup.add(cutAction);
129. popup.add(copyAction);
130. popup.add(pasteAction);
131.
132. JPanel panel = new JPanel();
133. panel.setComponentPopupMenu(popup);
134. add(panel);
135.
136. // the following line is a workaround for bug 4966109
137. panel.addMouseListener(new MouseAdapter() {});
138. }
139.
140. public static final int DEFAULT_WIDTH = 300;
141. public static final int DEFAULT_HEIGHT = 200;
142.
143. private Action saveAction;
144. private Action saveAsAction;
145. private JCheckBoxMenuItem readonlyItem;
146. private JPopupMenu popup;
147. }
148.
149. /**
150. A sample action that prints the action name to System.out
151. */
152. class TestAction extends AbstractAction
153. {
154. public TestAction(String name) { super(name); }
155.
156. public void actionPerformed(ActionEvent event)
157. {
158. System.out.println(getValue(Action.NAME) + " selected.");
159. }
160. }
Toolbars
A toolbar is a button bar that gives quick access to the most commonly used commands in a program (see Figure 9-28).
Figure 9-28. A toolbar
What makes toolbars special is that you can move them elsewhere. You can drag the toolbar to one of the four borders of the frame (see Figure 9-29). When you release the mouse button, the toolbar is dropped into the new location (see Figure 9-30).
Figure 9-29. Dragging the toolbar
Figure 9-30. Dragging the toolbar to another border
NOTE
Toolbar dragging works if the toolbar is inside a container with a border layout, or any other layout manager that supports the North, East, South, and West constraints.
The toolbar can even be completely detached from the frame. A detached toolbar is contained in its own frame (see Figure 9-31). When you close the frame containing a detached toolbar, the toolbar jumps back into the original frame.
Figure 9-31. Detaching the toolbar
Toolbars are straightforward to program. You add components into the toolbar:
JToolBar bar = new JToolBar();
bar.add(blueButton);
The JToolBar class also has a method to add an Action object. Simply populate the toolbar with Action objects, like this:
bar.add(blueAction);
The small icon of the action is displayed in the toolbar.
You can separate groups of buttons with a separator:
bar.addSeparator();
For example, the toolbar in Figure 9-28 has a separator between the third and fourth button.
Then, you add the toolbar to the frame.
add(bar, BorderLayout.NORTH);
You can also specify a title for the toolbar that appears when the toolbar is undocked:
bar = new JToolBar(titleString);
By default, toolbars are initially horizontal. To have a toolbar start out as vertical, use
bar = new JToolBar(SwingConstants.VERTICAL)
or
bar = new JToolBar(titleString, SwingConstants.VERTICAL)
Buttons are the most common components inside toolbars. But there is no restriction on the components that you can add to a toolbar. For example, you can add a combo box to a toolbar.
TIP
You can find nice toolbar buttons at http://developer.java.sun.com/developer/techDocs/hi/repository.
Tooltips
A disadvantage of toolbars is that users are often mystified by the meanings of the tiny icons in toolbars. To solve this problem, user interface designers invented tooltips. A tooltip is activated when the cursor rests for a moment over a button. The tooltip text is displayed inside a colored rectangle. When the user moves the mouse away, the tooltip is removed. (See Figure 9-32.)
Figure 9-32. A tooltip
In Swing, you can add tooltips to any JComponent simply by calling the setToolTipText method:
exitButton.setToolTipText("Exit");
Alternatively, if you use Action objects, you associate the tooltip with the SHORT_DESCRIPTION key:
exitAction.putValue(Action.SHORT_DESCRIPTION, "Exit");
Example 9-12 shows how the same Action objects can be added to a menu and a toolbar. Note that the action names show up as the menu item names in the menu, and the short descriptions as the tooltips in the toolbar.
Example 9-12. ToolBarTest.java
1. import java.awt.*;
2. import java.awt.event.*;
3. import java.beans.*;
4. import javax.swing.*;
5.
6. public class ToolBarTest
7. {
8. public static void main(String[] args)
9. {
10. ToolBarFrame frame = new ToolBarFrame();
11. frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
12. frame.setVisible(true);
13. }
14. }
15.
16. /**
17. A frame with a toolbar and menu for color changes.
18. */
19. class ToolBarFrame extends JFrame
20. {
21. public ToolBarFrame()
22. {
23. setTitle("ToolBarTest");
24. setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
25.
26. // add a panel for color change
27.
28. panel = new JPanel();
29.
30. // set up actions
31.
32. Action blueAction = new ColorAction("Blue",
33. new ImageIcon("blue-ball.gif"), Color.BLUE);
34. Action yellowAction = new ColorAction("Yellow",
35. new ImageIcon("yellow-ball.gif"), Color.YELLOW);
36. Action redAction = new ColorAction("Red",
37. new ImageIcon("red-ball.gif"), Color.RED);
38.
39. Action exitAction = new
40. AbstractAction("Exit", new ImageIcon("exit.gif"))
41. {
42. public void actionPerformed(ActionEvent event)
43. {
44. System.exit(0);
45. }
46. };
47. exitAction.putValue(Action.SHORT_DESCRIPTION, "Exit");
48.
49. // populate toolbar
50.
51. JToolBar bar = new JToolBar();
52. bar.add(blueAction);
53. bar.add(yellowAction);
54. bar.add(redAction);
55. bar.addSeparator();
56. bar.add(exitAction);
57. add(bar, BorderLayout.NORTH);
58.
59. // populate menu
60.
61. JMenu menu = new JMenu("Color");
62. menu.add(yellowAction);
63. menu.add(blueAction);
64. menu.add(redAction);
65. menu.add(exitAction);
66. JMenuBar menuBar = new JMenuBar();
67. menuBar.add(menu);
68. setJMenuBar(menuBar);
69. }
70.
71. public static final int DEFAULT_WIDTH = 300;
72. public static final int DEFAULT_HEIGHT = 200;
73.
74. private JPanel panel;
75.
76. /**
77. The color action sets the background of the frame to a
78. given color.
79. */
80. class ColorAction extends AbstractAction
81. {
82. public ColorAction(String name, Icon icon, Color c)
83. {
84. putValue(Action.NAME, name);
85. putValue(Action.SMALL_ICON, icon);
86. putValue(Action.SHORT_DESCRIPTION, name + " background");
87. putValue("Color", c);
88. }
89.
90. public void actionPerformed(ActionEvent event)
91. {
92. Color c = (Color) getValue("Color");
93. panel.setBackground(c);
94. }
95. }
96. }
javax.swing.JToolBar 1.2
JToolBar()
JToolBar(String titleString)
JToolBar(int orientation)
JToolBar(String titleString, int orientation)
construct a toolbar with the given title string and orientation. orientation is one of SwingConstants.HORIZONTAL (the default) and SwingConstants.VERTICAL.
JButton add(Action a)
constructs a new button inside the toolbar with name, icon, short description, and action callback from the given action, and adds the button to the end of the toolbar.
void addSeparator()
adds a separator to the end of the toolbar.
javax.swing.JComponent 1.2
void setToolTipText(String text)
sets the text that should be displayed as a tooltip when the mouse hovers over the component.
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
Sophisticated Layout Management
We have managed to lay out the user interface components of our sample applications so far by using only the border layout, flow layout, and grid layout. For more complex tasks, this is not going to be enough. In this section, we give you a detailed discussion of the advanced layout managers that the standard Java library provides to organize components.
Windows programmers may well wonder why Java makes so much fuss about layout managers. After all, in Windows, layout management is not a big deal: First, you use a dialog editor to drag and drop your components onto the surface of the dialog, and then you use editor tools to line up components, to space them equally, to center them, and so on. If you are working on a big project, you probably don't have to worry about component layout at all-a skilled user interface designer does all this for you.
The problem with this approach is that the resulting layout must be manually updated if the size of the components changes. Why would the component size change? There are two common cases. First, a user may choose a larger font for button labels and other dialog text. If you try this out for yourself in Windows, you will find that many applications deal with this exceedingly poorly. The buttons do not grow, and the larger font is simply crammed into the same space as before. The same problem can occur when the strings in an application are translated to a foreign language. For example, the German word for "Cancel" is "Abbrechen." If a button has been designed with just enough room for the string "Cancel", then the German version will look broken, with a clipped command string.
Why don't Windows buttons simply grow to accommodate the labels? Because the designer of the user interface gave no instructions in which direction they should grow. After the dragging and dropping and arranging, the dialog editor merely remembers the pixel position and size of each component. It does not remember why the components were arranged in this fashion.
The Java layout managers are a much better approach to component layout. With a layout manager, the layout comes with instructions about the relationships among the components. This was particularly important in the original AWT, which used native user interface elements. The size of a button or list box in Motif, Windows, and the Macintosh could vary widely, and an application or applet would not know a priori on which platform it would display its user interface. To some extent, that degree of variability has gone away with Swing. If your application forces a particular look and feel, such as the Metal look and feel, then it looks identical on all platforms. However, if you let users of your application choose their favorite look and feel, then you again need to rely on the flexibility of layout managers to arrange the components.
Of course, to achieve complex layouts, you will need to have more control over the layout than the border layout, flow layout, and grid layout give you. In this section, we discuss the layout managers that the standard Java library has to offer. Using a sophisticated layout manager combined with the appropriate use of multiple panels will give you complete control over how your application will look.
TIP
If none of the layout schemes fit your needs, break the surface of your window into separate panels and lay out each panel separately. Then, use another layout manager to organize the panels.
First, let's review a few basic principles. As you know, in the AWT, components are laid out inside containers. Buttons, text fields, and other user interface elements are components and can be placed inside containers. Therefore, these classes extend the class Component. Containers such as panels can themselves be put inside other containers. Therefore, the class Container derives from Component. Figure 9-33 shows the inheritance hierarchy for Component.
Figure 9-33. Inheritance hierarchy for the Component class
[View full size image]
NOTE
Some objects belong to classes extending Component even though they are not user interface components and cannot be inserted into containers. Top-level windows such as JFrame and JApplet cannot be contained inside another window or panel.
As you have seen, to organize the components in a container, you first specify a layout manager. For example, the statement
panel.setLayout(new GridLayout(4, 4));
will use the GridLayout class to lay out the panels. After you set the layout manager, you add components to the container. The add method of the container passes the component and any placement directions to the layout manager.
With the border layout manager, you give a string to specify component placement:
panel.add(new JTextField(), BorderLayout.SOUTH);
With the grid layout, you need to add components sequentially:
panel.add(new JCheckBox("italic"));
panel.add(new JCheckBox("bold"));
The grid layout is useful for arranging components in a grid, somewhat like the rows and columns of a spreadsheet. However, all rows and columns of the grid have identical size, which is not all that useful in practice.
To overcome the limitations of the grid layout, the AWT supplies the grid bag layout. It, too, lays out components in rows and columns, but the row and column sizes are flexible and components can span multiple rows and columns. This layout manager is very flexible, but it is also very complex. The mere mention of the words "grid bag layout" has been known to strike fear in the hearts of Java programmers. Actually, in most common situations, the grid bag layout is not that hard to use, and we tell you a strategy that should make grid bag layouts relatively painless.
In an (unsuccessful) attempt to design a layout manager that would free programmers from the tyranny of the grid bag layout, the Swing designers came up with the box layout. The box layout simply arranges a sequence of components horizontally or vertically. When arranging components horizontally, the box layout is similar to the flow layout; however, components do not "wrap" to a new row when one row is full. By placing a number of horizontal box layouts inside a vertical box layout (or the other way around), you can give some order to a set of components in a two-dimensional area. However, because each box is laid out independently, you cannot use box layouts to arrange neighboring components both horizontally and vertically.
JDK 1.4 saw yet another attempt to design a replacement for the grid bag layout-the spring layout. We discuss the spring layout on page 440, and we leave it to you to decide whether it succeeds in its goal.
Swing also contains an overlay layout that lets you place components on top of each other. This layout manager is not generally useful, and we don't discuss it.
Finally, there is a card layout that was used in the original AWT to produce tabbed dialogs. Because Swing has a much better tabbed dialog container (called JTabbedPane-see Volume 2), we do not cover the card layout here.
We end the discussion of layout managers by showing you how you can bypass layout management altogether and place components manually and by showing you how you can write your own layout manager.
Box Layout
The box layout lets you lay out a single row or column of components with more flexibility than the grid layout affords. There is even a container-the Box class-whose default layout manager is the BoxLayout (unlike the JPanel class whose default layout manager is the FlowLayout). Of course, you can also set the layout manager of a JPanel to the box layout, but it is simpler to just start with a Box container. The Box class also contains a number of static methods that are useful for managing box layouts.
To create a new container with a box layout, you can simply call
Box b = Box.createHorizontalBox();
or
Box b = Box.createVerticalBox();
You then add components in the usual way:
b.add(okButton);
b.add(cancelButton);
In a horizontal box, the components are arranged left to right. In a vertical box, the components are arranged top to bottom. Let us look at the horizontal layout more closely.
Each component has three sizes:
The preferred size- the width and height at which the component would like to be displayed
The maximum size- the largest width and height at which the component is willing to be displayed
The minimum size- the smallest width and height at which the component is willing to be displayed
Here are details about what the box layout manager does:
It computes the maximum (!) height of the tallest component.
It tries to grow all components vertically to that height.
If a component does not actually grow to that height when requested, then its y-alignment is queried by a call to its getAlignmentY method. That method returns a floating-point number between 0 (align on top) and 1 (align on bottom). The default in the Component class is 0.5 (center). The value is used to align the component vertically.
The preferred width of each component is obtained. All preferred widths are added up.
If the total preferred width is less than the box width, then the components are expanded by being allowed to grow to their maximum width. Components are then placed, from left to right, with no additional space between them. If the total preferred width is greater than the box width, the components are shrunk, potentially down to their minimum width but no further. If the components don't all fit at their minimum width, some of them will not be shown.
For vertical layouts, the process is analogous.
TIP
It is unfortunate that BoxLayout tries to grow components beyond the preferred size. In particular, text fields have maximum width and height set to Integer.MAX_VALUE; that is, they are willing to grow as much as necessary. If you put a text field into a box layout, it will grow to monstrous proportions. Remedy: set the maximum size to the preferred size with
textField.setMaximumSize(textField.getPreferredSize());
Fillers
By default, there is no space between the components in a box layout. (Unlike the flow layout, the box layout does not have a notion of gaps between components.) To space the components out, you add invisible fillers. There are three kinds of fillers:
Struts
Rigid areas
Glue
A strut simply adds some space between components. For example, here is how you can add 10 pixels of space between two components in a horizontal box:
b.add(label);
b.add(Box.createHorizontalStrut(10));
b.add(textField);
You add a horizontal strut into a horizontal box, or a vertical strut into a vertical box, to add space between components. You can also add a vertical strut into a horizontal box, but that does not affect the horizontal layout. Instead, it sets the minimum height of the box.
The rigid area filler is similar to a pair of struts. It separates adjacent components but also adds a height or width minimum in the other direction. For example,
b.add(Box.createRigidArea(new Dimension(5, 20));
adds an invisible area with minimum, preferred, and maximum width of 5 pixels and height of 20 pixels, and centered alignment. If added into a horizontal box, it acts like a strut of width 5 and also forces the minimum height of the box to be 20 pixels.
By adding struts, you separate adjacent components by a fixed amount. Adding glue separates components as much as possible. The (invisible) glue expands to consume all available empty space, pushing the components away from each other. (We don't know why the designers of the box layout came up with the name "glue"-"spring" would have been a more appropriate name.)
For example, here is how you space two buttons in a box as far apart as possible:
b.add(button1);
b.add(Box.createGlue());
b.add(button2);
If the box contains no other components, then button1 is moved all the way to the left and button2 is moved all the way to the right.
The program in Example 9-13 arranges a set of labels, text fields, and buttons, using a set of horizontal and vertical box layouts. Each row is placed in a horizontal box. Struts separate the labels from the text fields. Glue pushes the two buttons away from each other. The three horizontal boxes are placed in a vertical box, with glue pushing the button box to the bottom (see Figure 9-34).
Example 9-13. BoxLayoutTest.java
1. import java.awt.*;
2. import java.awt.event.*;
3. import javax.swing.*;
4.
5. public class BoxLayoutTest
6. {
7. public static void main(String[] args)
8. {
9. BoxLayoutFrame frame = new BoxLayoutFrame();
10. frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
11. frame.setVisible(true);
12. }
13. }
14.
15. /**
16. A frame that uses box layouts to organize various components.
17. */
18. class BoxLayoutFrame extends JFrame
19. {
20. public BoxLayoutFrame()
21. {
22. setTitle("BoxLayoutTest");
23. setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
24.
25. // construct the top horizontal box
26.
27. JLabel label1 = new JLabel("Name:");
28. JTextField textField1 = new JTextField(10);
29. textField1.setMaximumSize(textField1.getPreferredSize());
30.
31. Box hbox1 = Box.createHorizontalBox();
32. hbox1.add(label1);
33. // separate with a 10-pixel strut
34. hbox1.add(Box.createHorizontalStrut(10));
35. hbox1.add(textField1);
36.
37. // construct the middle horizontal box
38.
39. JLabel label2 = new JLabel("Password:");
40. JTextField textField2 = new JTextField(10);
41. textField2.setMaximumSize(textField2.getPreferredSize());
42.
43.
44. Box hbox2 = Box.createHorizontalBox();
45. hbox2.add(label2);
46. // separate with a 10-pixel strut
47. hbox2.add(Box.createHorizontalStrut(10));
48. hbox2.add(textField2);
49.
50. // construct the bottom horizontal box
51.
52. JButton button1 = new JButton("Ok");
53. JButton button2 = new JButton("Cancel");
54.
55. Box hbox3 = Box.createHorizontalBox();
56. hbox3.add(button1);
57. // use "glue" to push the two buttons apart
58. hbox3.add(Box.createGlue());
59. hbox3.add(button2);
60.
61. // add the three horizontal boxes inside a vertical box
62.
63. Box vbox = Box.createVerticalBox();
64. vbox.add(hbox1);
65. vbox.add(hbox2);
66. vbox.add(Box.createGlue());
67. vbox.add(hbox3);
68.
69. add(vbox, BorderLayout.CENTER);
70. }
71.
72. public static final int DEFAULT_WIDTH = 200;
73. public static final int DEFAULT_HEIGHT = 200;
74. }
javax.swing.Box 1.2
static Box createHorizontalBox()
static Box createVerticalBox()
create a container that arranges its content horizontally or vertically.
static Component createHorizontalGlue()
static Component createVerticalGlue()
static Component createGlue()
create an invisible component that can expand infinitely horizontally, vertically, or in both directions.
static Component createHorizontalStrut(int width)
static Component createVerticalStrut(int height)
static Component createRigidArea(Dimension d)
create an invisible component with fixed width, fixed height, or fixed width and height.
java.awt.Component 1.0
float getAlignmentX() 1.1
float getAlignmentY() 1.1
return the alignment along the x- or y-axis, a value between 0 and 1. The value 0 denotes alignment on top or left, 0.5 is centered, 1 is aligned on bottom or right.
Figure 9-34. Box layouts
The Grid Bag Layout
The grid bag layout is the mother of all layout managers. You can think of a grid bag layout as a grid layout without the limitations. In a grid bag layout, the rows and columns can have variable sizes. You can join adjacent cells to make room for larger components. (Many word processors, as well as HTML, have the same capability when tables are edited: you start out with a grid and then merge adjacent cells if need be.) The components need not fill the entire cell area, and you can specify their alignment within cells.
Fair warning: using grid bag layouts can be incredibly complex. The payoff is that they have the most flexibility and will work in the widest variety of situations. Keep in mind that the purpose of layout managers is to keep the arrangement of the components reasonable under different font sizes and operating systems, so it is not surprising that you need to work somewhat harder than when you design a layout just for one environment.
NOTE
According to the JDK documentation of the BoxLayout class: "Nesting multiple panels with different combinations of horizontal and vertical [sic] gives an effect similar to GridBagLayout, without the complexity." However, as you can see from Figure 9-34, the effect that you can achieve from multiple box layouts is plainly not useful in practice. No amount of fussing with boxes, struts, and glue will ensure that the components line up. When you need to arrange components so that they line up horizontally and vertically, you should consider the GridBagLayout class.
Consider the font selection dialog of Figure 9-35. It consists of the following components:
Two combo boxes to specify the font face and size
Labels for these two combo boxes
Two checkboxes to select bold and italic
A text area for the sample string
Figure 9-35. Font dialog box
Now, chop up the dialog box into a grid of cells, as shown in Figure 9-36. (The rows and columns need not have equal size.) Each checkbox spans two columns, and the text area spans four rows.
Figure 9-36. Dialog box grid used in design
To describe the layout to the grid bag manager, you must go through the following convoluted procedure.
1. Create an object of type GridBagLayout. You don't tell it how many rows and columns the underlying grid has. Instead, the layout manager will try to guess it from the information you give it later.
2. Set this GridBagLayout object to be the layout manager for the component.
3. For each component, create an object of type GridBagConstraints. Set field values of the GridBagConstraints object to specify how the components are laid out within the grid bag.
4. Then (finally), add the component with the constraints by using the call:
add(component, constraints);
Here's an example of the code needed. (We go over the various constraints in more detail in the sections that follow-so don't worry if you don't know what some of the constraints do.)
GridBagLayout layout = new GridBagLayout();
panel.setLayout(layout);
GridBagConstraints constraints = new GridBagConstraints();
constraints.weightx = 100;
constraints.weighty = 100;
constraints.gridx = 0;
constraints.gridy = 2;
constraints.gridwidth = 2;
constraints.gridheight = 1;
panel.add(style, bold);
The trick is knowing how to set the state of the GridBagConstraints object. We go over the most important constraints for using this object in the sections that follow.
The gridx, gridy, gridwidth, and gridheight Parameters
These constraints define where the component is located in the grid. The gridx and gridy values specify the column and row positions of the upper-left corner of the component to be added. The gridwidth and gridheight values determine how many columns and rows the component occupies.
The grid coordinates start with 0. In particular, gridx = 0 and gridy = 0 denotes the top-left corner.
For example, the text area in our example has gridx = 2, gridy = 0 because it starts in column 2 (that is, the third column) of row 0. It has gridwidth = 1 and gridheight = 4 because it spans one column and four rows.
Weight Fields
You always need to set the weight fields (weightx and weighty) for each area in a grid bag layout. If you set the weight to 0, then the area never grows or shrinks beyond its initial size in that direction. In the grid bag layout for Figure 9-35, we set the weightx field of the labels to be 0. This allows the labels to remain a constant width when you resize the window. On the other hand, if you set the weights for all areas to 0, the container will huddle in the center of its allotted area rather than stretching to fill it.
Conceptually, the problem with the weight parameters is that weights are properties of rows and columns, not individual cells. But you need to specify them in terms of cells because the grid bag layout does not expose the rows and columns. The row and column weights are computed as the maxima of the cell weights in each row or column. Thus, if you want a row or column to stay at a fixed size, you need to set the weights of all components in it to zero.
Note that the weights don't actually give the relative sizes of the columns. They tell what proportion of the "slack" space should be allocated to each area if the container exceeds its preferred size. This isn't particularly intuitive. We recommend that you set all weights at 100. Then, run the program and see how the layout looks. Resize the dialog to see how the rows and columns adjust. If you find that a particular row or column should not grow, set the weights of all components in it to zero. You can tinker with other weight values, but it is usually not worth the effort.
The fill and anchor Parameters
If you don't want a component to stretch out and fill the entire area, you set the fill constraint. You have four possibilities for this parameter: the valid values are used in the forms GridBagConstraints.NONE, GridBagConstraints.HORIZONTAL, GridBagConstraints.VERTICAL, and GridBagConstraints.BOTH.
If the component does not fill the entire area, you can specify where in the area you want it by setting the anchor field. The valid values are GridBagConstraints.CENTER (the default), GridBagConstraints.NORTH, GridBagConstraints.NORTHEAST, GridBagConstraints.EAST, and so on.
Padding
You can surround a component with additional blank space by setting the insets field of GridBagConstraints. Set the left, top, right and bottom values of the Insets object to the amount of space that you want to have around the component. This is called the external padding.
The ipadx and ipady values set the internal padding. These values are added to the minimum width and height of the component. This ensures that the component does not shrink down to its minimum size.
Alternative Method to Specify the gridx, gridy, gridwidth, and gridheight Parameters
The AWT documentation recommends that instead of setting the gridx and gridy values to absolute positions, you set them to the constant GridBagConstraints.RELATIVE. Then, add the components to the grid bag layout in a standardized order, going from left to right in the first row, then moving along the next row, and so on.
You still specify the number of rows and columns spanned, by giving the appropriate gridheight and gridwidth fields. Except, if the component extends to the last row or column, you aren't supposed to specify the actual number, but the constant GridBagConstraints.REMAINDER. This tells the layout manager that the component is the last one in its row.
This scheme does seem to work. But it sounds really goofy to hide the actual placement information from the layout manager and hope that it will rediscover it.
All this sounds like a lot of trouble and complexity. But in practice, the strategy in the following recipe makes grid bag layouts relatively trouble-free.
Recipe for Making a Grid Bag Layout
Step 1. Sketch out the component layout on a piece of paper.
Step 2. Find a grid such that the small components are each contained in a cell and the larger components span multiple cells.
Step 3. Label the rows and columns of your grid with 0, 1, 2, 3, . . . You can now read off the gridx, gridy, gridwidth, and gridheight values.
Step 4. For each component, ask yourself whether it needs to fill its cell horizontally or vertically. If not, how do you want it aligned? This tells you the fill and anchor parameters.
Step 5. Set all weights to 100. However, if you want a particular row or column to always stay at its default size, set the weightx or weighty to 0 in all components that belong to that row or column.
Step 6. Write the code. Carefully double-check your settings for the GridBagConstraints. One wrong constraint can ruin your whole layout.
Step 7. Compile, run, and enjoy.
A Helper Class to Tame the Grid Bag Constraints
The most tedious aspect of the grid bag layout is writing the code that sets the constraints. Most programmers write helper functions or a small helper class for this purpose. We present such a class after the complete code for the font dialog example. This class has the following features:
Its name is short: GBC instead of GridBagConstraints
It extends GridBagConstraints, so you can use shorter names such as GBC.EAST for the constants.
Use a GBC object when adding a component, such as
add(component, new GBC(1, 2));
There are two constructors to set the most common parameters: gridx and gridy, or gridx, gridy, gridwidth, and gridheight.
add(component, new GBC(1, 2, 1, 4));
There are convenient setters for the fields that come in x/y pairs:
add(component, new GBC(1, 2).setWeight(100, 100));
The setter methods return this, so you can chain them:
add(component, new GBC(1, 2).setAnchor(GBC.EAST).setWeight(100, 100));
The setInsets methods construct the Insets object for you. To get one-pixel insets, simply call
add(component, new GBC(1, 2).setAnchor(GBC.EAST).setInsets(1));
Example 9-14 shows the complete code for the font dialog example. Here is the code that adds the components to the grid bag:
add(faceLabel, new GBC(0, 0).setAnchor(GBC.EAST));
add(face, new GBC(1, 0).setFill(GBC.HORIZONTAL).setWeight(100, 0).setInsets(1));
add(sizeLabel, new GBC(0, 1).setAnchor(GBC.EAST));
add(size, new GBC(1, 1).setFill(GBC.HORIZONTAL).setWeight(100, 0).setInsets(1));
add(bold, new GBC(0, 2, 2, 1).setAnchor(GBC.CENTER).setWeight(100, 100));
add(italic, new GBC(0, 3, 2, 1).setAnchor(GBC.CENTER).setWeight(100, 100));
add(sample, new GBC(2, 0, 1, 4).setFill(GBC.BOTH).setWeight(100, 100));
Once you understand the grid bag constraints, this kind of code is fairly easy to read and debug.
NOTE
The Sun tutorial at http://java.sun.com/docs/books/tutorial/uiswing/layout/gridbag.html suggests that you reuse the same GridBagConstraints object for all components. We find the resulting code hard to read and error prone. For example, look at the demo at http://java.sun.com/docs/books/tutorial/uiswing/events/containerlistener.html. Was it really intended that the buttons are stretched horizontally, or did the programmer just forget to turn off the BOTH setting for the fill constraint?
Example 9-14. FontDialog.java
1. import java.awt.*;
2. import java.awt.event.*;
3. import javax.swing.*;
4. import javax.swing.event.*;
5.
6. public class FontDialog
7. {
8. public static void main(String[] args)
9. {
10. FontDialogFrame frame = new FontDialogFrame();
11. frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
12. frame.setVisible(true);
13. }
14. }
15.
16. /**
17. A frame that uses a grid bag layout to arrange font
18. selection components.
19. */
20. class FontDialogFrame extends JFrame
21. {
22. public FontDialogFrame()
23. {
24. setTitle("FontDialog");
25. setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
26.
27. GridBagLayout layout = new GridBagLayout();
28. setLayout(layout);
29.
30. ActionListener listener = new FontAction();
31.
32. // construct components
33.
34. JLabel faceLabel = new JLabel("Face: ");
35.
36. face = new JComboBox(new String[]
37. {
38. "Serif", "SansSerif", "Monospaced",
39. "Dialog", "DialogInput"
40. });
41.
42. face.addActionListener(listener);
43.
44. JLabel sizeLabel = new JLabel("Size: ");
45.
46. size = new JComboBox(new String[]
47. {
48. "8", "10", "12", "15", "18", "24", "36", "48"
49. });
50.
51. size.addActionListener(listener);
52.
53. bold = new JCheckBox("Bold");
54. bold.addActionListener(listener);
55.
56. italic = new JCheckBox("Italic");
57. italic.addActionListener(listener);
58.
59. sample = new JTextArea();
60. sample.setText("The quick brown fox jumps over the lazy dog");
61. sample.setEditable(false);
62. sample.setLineWrap(true);
63. sample.setBorder(BorderFactory.createEtchedBorder());
64.
65. // add components to grid, using GBC convenience class
66.
67. add(faceLabel, new GBC(0, 0).setAnchor(GBC.EAST));
68. add(face, new GBC(1, 0).setFill(GBC.HORIZONTAL).setWeight(100, 0).setInsets(1));
69. add(sizeLabel, new GBC(0, 1).setAnchor(GBC.EAST));
70. add(size, new GBC(1, 1).setFill(GBC.HORIZONTAL).setWeight(100, 0).setInsets(1));
71. add(bold, new GBC(0, 2, 2, 1).setAnchor(GBC.CENTER).setWeight(100, 100));
72. add(italic, new GBC(0, 3, 2, 1).setAnchor(GBC.CENTER).setWeight(100, 100));
73. add(sample, new GBC(2, 0, 1, 4).setFill(GBC.BOTH).setWeight(100, 100));
74. }
75.
76. public static final int DEFAULT_WIDTH = 300;
77. public static final int DEFAULT_HEIGHT = 200;
78.
79. private JComboBox face;
80. private JComboBox size;
81. private JCheckBox bold;
82. private JCheckBox italic;
83. private JTextArea sample;
84.
85. /**
86. An action listener that changes the font of the
87. sample text.
88. */
89. private class FontAction implements ActionListener
90. {
91. public void actionPerformed(ActionEvent event)
92. {
93. String fontFace = (String) face.getSelectedItem();
94. int fontStyle = (bold.isSelected() ? Font.BOLD : 0)
95. + (italic.isSelected() ? Font.ITALIC : 0);
96. int fontSize = Integer.parseInt((String) size.getSelectedItem());
97. Font font = new Font(fontFace, fontStyle, fontSize);
98. sample.setFont(font);
99. sample.repaint();
100. }
101. }
102. }
Example 9-15 shows the code of the GBC helper class.
Example 9-15. GBC.java
1. /*
2. GBC - A convenience class to tame the GridBagLayout
3.
4. Copyright (C) 2002 Cay S. Horstmann (http://horstmann.com)
5.
6. This program is free software; you can redistribute it and/or modify
7. it under the terms of the GNU General Public License as published by
8. the Free Software Foundation; either version 2 of the License, or
9. (at your option) any later version.
10.
11. This program is distributed in the hope that it will be useful,
12. but WITHOUT ANY WARRANTY; without even the implied warranty of
13. MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14. GNU General Public License for more details.
15.
16. You should have received a copy of the GNU General Public License
17. along with this program; if not, write to the Free Software
18. Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
19. */
20.
21. import java.awt.*;
22.
23. /**
24. This class simplifies the use of the GridBagConstraints
25. class.
26. */
27. public class GBC extends GridBagConstraints
28. {
29. /**
30. Constructs a GBC with a given gridx and gridy position and
31. all other grid bag constraint values set to the default.
32. @param gridx the gridx position
33. @param gridy the gridy position
34. */
35. public GBC(int gridx, int gridy)
36. {
37. this.gridx = gridx;
38. this.gridy = gridy;
39. }
40.
41. /**
42. Constructs a GBC with given gridx, gridy, gridwidth, gridheight
43. and all other grid bag constraint values set to the default.
44. @param gridx the gridx position
45. @param gridy the gridy position
46. @param gridwidth the cell span in x-direction
47. @param gridheight the cell span in y-direction
48. */
49. public GBC(int gridx, int gridy, int gridwidth, int gridheight)
50. {
51. this.gridx = gridx;
52. this.gridy = gridy;
53. this.gridwidth = gridwidth;
54. this.gridheight = gridheight;
55. }
56.
57. /**
58. Sets the anchor.
59. @param anchor the anchor value
60. @return this object for further modification
61. */
62. public GBC setAnchor(int anchor)
63. {
64. this.anchor = anchor;
65. return this;
66. }
67.
68. /**
69. Sets the fill direction.
70. @param fill the fill direction
71. @return this object for further modification
72. */
73. public GBC setFill(int fill)
74. {
75. this.fill = fill;
76. return this;
77. }
78.
79. /**
80. Sets the cell weights.
81. @param weightx the cell weight in x-direction
82. @param weighty the cell weight in y-direction
83. @return this object for further modification
84. */
85. public GBC setWeight(double weightx, double weighty)
86. {
87. this.weightx = weightx;
88. this.weighty = weighty;
89. return this;
90. }
91.
92. /**
93. Sets the insets of this cell.
94. @param distance the spacing to use in all directions
95. @return this object for further modification
96. */
97. public GBC setInsets(int distance)
98. {
99. this.insets = new Insets(distance, distance, distance, distance);
100. return this;
101. }
102.
103. /**
104. Sets the insets of this cell.
105. @param top the spacing to use on top
106. @param left the spacing to use to the left
107. @param bottom the spacing to use on the bottom
108. @param right the spacing to use to the right
109. @return this object for further modification
110. */
111. public GBC setInsets(int top, int left, int bottom, int right)
112. {
113. this.insets = new Insets(top, left, bottom, right);
114. return this;
115. }
116.
117. /**
118. Sets the internal padding
119. @param ipadx the internal padding in x-direction
120. @param ipady the internal padding in y-direction
121. @return this object for further modification
122. */
123. public GBC setIpad(int ipadx, int ipady)
124. {
125. this.ipadx = ipadx;
126. this.ipady = ipady;
127. return this;
128. }
129. }
TIP
Some GUI builders have tools for specifying the constraints visually-see Figure 9-37 for the configuration dialog in NetBeans.
Figure 9-37. Specifying grid bag constraints in NetBeans
[View full size image]
java.awt.GridBagConstraints 1.0
int gridx, gridy
specifies the starting column and row of the cell. The default is 0.
int gridwidth, gridheight
specifies the column and row extent of the cell. The default is 1.
double weightx, weighty
specifies the capacity of the cell to grow. The default is 0.
int anchor
indicates the alignment of the component inside the cell. You can choose between absolute positions-
NORTHWEST
NORTH
NORTHEAST
WEST
CENTER
EAST
SOUTHWEST
SOUTH
SOUTHEAST
-or their orientation-independent counterparts
FIRST_LINE_START
LINE_START
FIRST_LINE_END
PAGE_START
CENTER
PAGE_END
LAST_LINE_START
LINE_END
LAST_LINE_END
Use the latter if your application may be localized for right-to-left or top-to-bottom text. The default is CENTER.
int fill
specifies the fill behavior of the component inside the cell, one of NONE, BOTH, HORIZONTAL, or VERTICAL. The default is NONE.
int ipadx, ipady
specifies the "internal" padding around the component. The default is 0.
Insets insets
specifies the "external" padding along the cell boundaries. The default is no padding.
GridBagConstraints(int gridx, int gridy, int gridwidth, int gridheight, double weightx, double weighty, int anchor, int fill, Insets insets, int ipadx, int ipady) 1.2
constructs a GridBagConstraints with all its fields specified in the arguments. Sun recommends that this constructor be used only by automatic code generators because it makes your source code very hard to read.
The Spring Layout
Ever since programmers met the GridBagLayout, they begged the Java team for a layout manager that is equally flexible but more intuitive. Finally, JDK 1.4 features a contender, the SpringLayout. In this section you will see how it measures up.
With the spring layout, you attach springs to each component. A spring is a device for specifying component positions. As shown in Figure 9-38, each spring has
A minimum value
A preferred value
A maximum value
An actual value
Figure 9-38. A spring
When the spring is compressed or expanded in the layout phase, the actual value is fixed; it falls between the minimum and maximum value and is as close to the preferred value as the other springs allow. Then the actual value determines the position of the component to which it has been attached.
The spring class defines a sum operation that takes two springs and produces a new spring that combines the characteristics of the individual springs. When you lay out a number of components in a row, you attach several springs so that their sum spans the entire container-see Figure 9-39. That sum spring is now compressed or expanded so that its value equals the dimension of the container. This operation exerts a strain on the individual springs. Each spring value is set so that the strain of each spring equals the strain of the sum. Thus, the values of the individual springs are determined, and the layout is fixed. (If you are interested in the details of the strain computations, check the API documentation of the Spring class for more information.)
Figure 9-39. Summing springs
Let's run through a simple example. Suppose you want to lay out three buttons horizontally.
JButton b1 = new JButton("Yellow");
JButton b2 = new JButton("Blue");
JButton b3 = new JButton("Red");
You first set the layout manager of the frame to a SpringLayout and add the components.
SpringLayout layout = new SpringLayout();
panel.setLayout(layout);
panel.add(b1);
panel.add(b2);
panel.add(b3);
Now construct a spring with a good amount of compressibility. The static Spring.constant method produces a spring with given minimum, preferred, and maximum values. (The spring isn't actually constant-it can be compressed or expanded.)
Spring s = Spring.constant(0, 10000, 10000);
Next, attach one copy of the spring from the west side of the container to the west side of b1:
layout.putConstraint(SpringLayout.WEST, b1, s, SpringLayout.WEST, panel);
The putConstraint method adds the given spring so that it ends at the first parameter set (the west side of b1 in our case) and starts from the second parameter set (the west side of the content pane).
Next, you link up the other springs:
layout.putConstraint(SpringLayout.WEST, b2, s, SpringLayout.EAST, b1);
layout.putConstraint(SpringLayout.WEST, b3, s, SpringLayout.EAST, b2);
Finally, you hook up a spring with the east wall of the container:
layout.putConstraint(SpringLayout.EAST, panel, s, SpringLayout.EAST, b3);
The result is that the four springs are compressed to the same size, and the buttons are equally spaced (see Figure 9-40).
Figure 9-40. Equally spaced buttons
Alternatively, you may want to vary the distances. Let's suppose you want to have a fixed distance between the buttons. Use a strut-a spring that can't be expanded or compressed. You get such a spring with the single-parameter version of the Spring.constant method:
Spring strut = Spring.constant(10);
If you add two struts between the buttons, but leave the springs at the ends, the result is a button group that is centered in the container (see Figure 9-41).
Figure 9-41. Springs and struts
Of course, you don't really need the spring layout for such a simple arrangement. Let's look at something more complex, a portion of the font dialog of the preceding example. We have two combo boxes with labels, and we want to have the west sides of both combo boxes start after the longer label (see Figure 9-42).
Figure 9-42. Lining up columns
This calls for another spring operation. You can form the maximum of two springs with the static Spring.max method. The result is a spring that is as long as the longer of the two inputs.
We get the maximum of the two east sides like this:
Spring labelsEast = Spring.max(
layout.getConstraint(SpringLayout.EAST, faceLabel),
layout.getConstraint(SpringLayout.EAST, sizeLabel));
Note that the getConstraint method yields a spring that reaches all the way from the west side of the container to the given sides of the component
Let's add a strut so that there is some space between the labels and the combo boxes:
Spring combosWest = Spring.sum(labelsEast, strut);
Now we attach this spring to the west side of both combo boxes. The starting point is the start of the container because the labelsEast spring starts there.
layout.putConstraint(SpringLayout.WEST, face, combosWest,SpringLayout.WEST, panel);
layout.putConstraint(SpringLayout.WEST, size, combosWest,SpringLayout.WEST, panel);
Now the two combo boxes line up because they are held by the same spring.
However, there is a slight blemish. We'd prefer the labels to be right-aligned. It is possible to achieve this effect as well, but it requires a more precise understanding of spring attachments.
Let's look at the horizontal springs in detail. Vertical springs follow the same logic. Figure 9-43 shows the three ways in which horizontal springs can be attached:
Connecting the west side of the component with the west side of the component;
Traversing the width of the component;
Connecting the west side of the component with the east side of the component.
Figure 9-43. Horizontal springs attached to a component
You get these springs as follows:
Spring west = layout.getConstraints(component).getX();
Spring width = layout.getConstraints(component).getWidth();
Spring east = layout.getConstraint(SpringLayout.EAST, component);
The getConstraints method yields an object of type SpringLayout.Constraints. You can think of such an object as a rectangle, except that the x, y, width, and height values are springs, not numbers. The getConstraint method yields a single spring that reaches to one of the four component boundaries. You can also get the west spring as
Spring west = layout.getConstraint(SpringLayout.WEST, component);
Of course, the three springs are related: The spring sum of west and width must equal east.
When the component constraints are first set, the width is set to a spring whose parameters are the minimum, preferred, and maximum width of the component. The west is set to 0.
CAUTION
If you don't set the west (and north) spring of a component, then the component stays at offset 0 in the container.
If a component has two springs set and you add a third one, then it becomes overconstrained. One of the existing springs is removed and its value is computed as the sum or difference of the other springs. Table 9-3 shows which spring is recomputed.
Table 9-3. Adding a Spring to an Overconstrained Component Added Spring
Removed Spring
Replaced By
West
width
east - west
Width
east
west + width
East
west
east - width
NOTE
The difference between two springs may not be intuitive, but it makes sense in the spring algebra. There is no Java method for spring subtraction. If you need to compute the difference of two springs, use
Spring.sum(s, Spring.minus(t))
Now you know enough about springs to solve the "right alignment" problem. Compute the maximum of the widths of the two labels. Then set the east spring of both labels to that maximum. As you can see from Table 9-3, the label widths don't change, the west springs are recomputed, and the labels become aligned at the eastern boundary.
Spring labelsEast = Spring.sum(strut,
Spring.max(layout.getConstraints(faceLabel).getWidth(),
Spring.max(layout.getConstraints(sizeLabel).getWidth()));
layout.putConstraint(SpringLayout.EAST, faceLabel, labelsEast, SpringLayout.WEST, panel);
layout.putConstraint(SpringLayout.EAST, sizeLabel, labelsEast, SpringLayout.WEST, panel);
Example 9-16 shows how to lay out the font dialog with springs. If you look at the code, you will probably agree that the spring layout is quite a bit less intuitive than the grid bag layout. We hope to someday see tools that make the spring layout more approachable. However, in the meantime we recommend that you stick with the grid bag layout for complex layouts.
Example 9-16. SpringLayoutTest.java
1. import java.awt.*;
2. import java.awt.event.*;
3. import javax.swing.*;
4. import javax.swing.event.*;
5.
6. public class SpringLayoutTest
7. {
8. public static void main(String[] args)
9. {
10. FontDialogFrame frame = new FontDialogFrame();
11. frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
12. frame.setVisible(true);
13. }
14. }
15.
16. /**
17. A frame that uses a spring layout to arrange font
18. selection components.
19. */
20. class FontDialogFrame extends JFrame
21. {
22. public FontDialogFrame()
23. {
24. setTitle("FontDialog");
25. setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
26.
27. JPanel panel = new JPanel();
28. SpringLayout layout = new SpringLayout();
29. panel.setLayout(layout);
30.
31. ActionListener listener = new FontAction();
32.
33. // construct components
34.
35. JLabel faceLabel = new JLabel("Font Face: ");
36.
37. face = new JComboBox(new String[]
38. {
39. "Serif", "SansSerif", "Monospaced",
40. "Dialog", "DialogInput"
41. });
42.
43. face.addActionListener(listener);
44.
45. JLabel sizeLabel = new JLabel("Size: ");
46.
47. size = new JComboBox(new String[]
48. {
49. "8", "10", "12", "15", "18", "24", "36", "48"
50. });
51.
52. size.addActionListener(listener);
53.
54. bold = new JCheckBox("Bold");
55. bold.addActionListener(listener);
56.
57. italic = new JCheckBox("Italic");
58. italic.addActionListener(listener);
59.
60. sample = new JTextArea();
61. sample.setText("The quick brown fox jumps over the lazy dog");
62. sample.setEditable(false);
63. sample.setLineWrap(true);
64. sample.setBorder(BorderFactory.createEtchedBorder());
65.
66. panel.add(faceLabel);
67. panel.add(sizeLabel);
68. panel.add(face);
69. panel.add(size);
70. panel.add(bold);
71. panel.add(italic);
72. panel.add(sample);
73.
74. // add springs to lay out components
75. Spring strut = Spring.constant(10);
76.
77. Spring labelsEast = Spring.sum(strut,
78. Spring.max(
79. layout.getConstraints(faceLabel).getWidth(),
80. layout.getConstraints(sizeLabel).getWidth()));
81.
82. layout.putConstraint(SpringLayout.EAST, faceLabel, labelsEast, SpringLayout
.WEST, panel);
83. layout.putConstraint(SpringLayout.EAST, sizeLabel, labelsEast, SpringLayout
.WEST, panel);
84.
85. layout.putConstraint(SpringLayout.NORTH, faceLabel, strut, SpringLayout.NORTH,
panel);
86. layout.putConstraint(SpringLayout.NORTH, face, strut, SpringLayout.NORTH, panel);
87.
88. Spring secondRowNorth = Spring.sum(strut,
89. Spring.max(
90. layout.getConstraint(SpringLayout.SOUTH, faceLabel),
91. layout.getConstraint(SpringLayout.SOUTH, face)));
92.
93. layout.putConstraint(SpringLayout.NORTH, sizeLabel, secondRowNorth,
SpringLayout.NORTH,
94. panel);
95. layout.putConstraint(SpringLayout.NORTH, size, secondRowNorth, SpringLayout
.NORTH, panel);
96.
97. layout.putConstraint(SpringLayout.WEST, face, strut, SpringLayout.EAST, faceLabel);
98. layout.putConstraint(SpringLayout.WEST, size, strut, SpringLayout.EAST, sizeLabel);
99.
100. layout.putConstraint(SpringLayout.WEST, bold, strut, SpringLayout.WEST, panel);
101. layout.putConstraint(SpringLayout.WEST, italic, strut, SpringLayout.WEST, panel);
102.
103. Spring s = Spring.constant(10, 10000, 10000);
104.
105. Spring thirdRowNorth = Spring.sum(s,
106. Spring.max(
107. layout.getConstraint(SpringLayout.SOUTH, sizeLabel),
108. layout.getConstraint(SpringLayout.SOUTH, size)));
109.
110. layout.putConstraint(SpringLayout.NORTH, bold, thirdRowNorth, SpringLayout
.NORTH, panel);
111. layout.putConstraint(SpringLayout.NORTH, italic, s, SpringLayout.SOUTH, bold);
112. layout.putConstraint(SpringLayout.SOUTH, panel, s, SpringLayout.SOUTH, italic);
113.
114. Spring secondColumnWest = Spring.sum(strut,
115. Spring.max(
116. layout.getConstraint(SpringLayout.EAST, face),
117. layout.getConstraint(SpringLayout.EAST, size)));
118.
119. layout.putConstraint(SpringLayout.WEST, sample, secondColumnWest, SpringLayout
.WEST, panel);
120. layout.putConstraint(SpringLayout.SOUTH, sample, Spring.minus(strut),
SpringLayout.SOUTH,
121. panel);
122. layout.putConstraint(SpringLayout.NORTH, sample, strut, SpringLayout.NORTH, panel);
123. layout.putConstraint(SpringLayout.EAST, panel, strut, SpringLayout.EAST, sample);
124.
125. add(panel);
126. }
127.
128. public static final int DEFAULT_WIDTH = 400;
129. public static final int DEFAULT_HEIGHT = 200;
130.
131. private JComboBox face;
132. private JComboBox size;
133. private JCheckBox bold;
134. private JCheckBox italic;
135. private JTextArea sample;
136.
137. /**
138. An action listener that changes the font of the
139. sample text.
140. */
141. private class FontAction implements ActionListener
142. {
143. public void actionPerformed(ActionEvent event)
144. {
145. String fontFace = (String) face.getSelectedItem();
146. int fontStyle = (bold.isSelected() ? Font.BOLD : 0)
147. + (italic.isSelected() ? Font.ITALIC : 0);
148. int fontSize = Integer.parseInt((String) size.getSelectedItem());
149. Font font = new Font(fontFace, fontStyle, fontSize);
150. sample.setFont(font);
151. sample.repaint();
152. }
153. }
154. }
javax.swing.SpringLayout 1.4
SpringLayout.Constraints getConstraints(Component c)
gets the constraints of the given component.
Parameters:
c
One of the components or the container managed by this layout manager
void putConstraint(String endSide, Component end, Spring s, String startSide, Component start)
void putConstraint(String endSide, Component end, int pad, String startSide, Component start)
set the given side of the end component to a spring that is obtained by adding the spring s, or a strut with size pad, to the spring that reaches from the left end of the container to the given side of the start container.
Parameters:
endSide, startSide
WEST, EAST, NORTH, or SOUTH
end
The component to which a spring is added
s
One of the summands of the added spring
pad
The size of the strut summand
start
The component to which the other summand spring reaches
javax.swing.SpringLayout.Constraints 1.4
Constraints(Component c) 5.0
constructs a Constraints object whose positions, width, and springs match the given component.
Spring getX()
Spring getY()
return the spring reaching from the start of the container to the west or north end of the constrained component.
Spring getWidth()
Spring getHeight()
return the spring spanning the width or height of the constrained component.
Spring getConstraint(String side)
void setConstraint(String edge, Spring s)
get or set a spring reaching from the start of the container to the given side of the constrained component.
Parameters:
side
One of the constants WEST, EAST, NORTH, or SOUTH of the SpringLayout class
s
The spring to set
javax.swing.Spring 1.4
static Spring constant(int preferred)
constructs a strut with the given preferred size. The minimum and maximum sizes are set to the preferred size.
static Spring constant(int minimum, int preferred, int maximum)
constructs a spring with the given minimum, preferred, and maximum sizes.
static Spring sum(Spring s, Spring t)
returns the spring sum of s and t.
static Spring max(Spring s, Spring t)
returns the spring maximum of s and t.
static Spring minus(Spring s)
returns the opposite of the spring s.
static Spring scale(Spring s, float factor) 5.0
scales the minimum, preferred, and maximum sizes of s by the given factor. If the factor is negative, the scaled opposite of s is returned.
static Spring width(Component c) 5.0
static Spring height(Component c) 5.0
return a spring whose minimum, preferred, and maximum sizes equal the minimum, preferred, and maximum width or height of the given component.
int getMinimumValue()
int getPreferredValue()
int getMaximumValue()
return the minimum, preferred, and maximum value of this spring.
int getValue()
void setValue(int newValue)
get and set the spring value. When setting the value of a compound spring, the values of the components are set as well.
Using No Layout Manager
There will be times when you don't want to bother with layout managers but just want to drop a component at a fixed location (sometimes called absolute positioning). This is not a great idea for platform-independent applications, but there is nothing wrong with using it for a quick prototype.
Here is what you do to place a component at a fixed location:
Set the layout manager to null.
Add the component you want to the container.
Then specify the position and size that you want.
setLayout(null);
JButton ok = new JButton("Ok");
add(ok);
ok.setBounds(10, 10, 30, 15);
java.awt.Component 1.0
void setBounds(int x, int y, int width, int height)
moves and resizes a component.
Parameters:
x, y
The new top-left corner of the component
width, height
The new size of the component
Custom Layout Managers
In principle, you can design your own LayoutManager class that manages components in a special way. For example, you could arrange all components in a container to form a circle. This will almost always be a major effort and a real time sink, but as Figure 9-44 shows, the results can be quite dramatic.
Figure 9-44. Circle layout
If you do feel you can't live without your own layout manager, here is what you do. Your own layout manager must implement the LayoutManager interface. You need to override the following five methods.
void addLayoutComponent(String s, Component c);
void removeLayoutComponent(Component c);
Dimension preferredLayoutSize(Container parent);
Dimension minimumLayoutSize(Container parent);
void layoutContainer(Container parent);
The first two functions are called when a component is added or removed. If you don't keep any additional information about the components, you can make them do nothing. The next two functions compute the space required for the minimum and the preferred layout of the components. These are usually the same quantity. The fifth function does the actual work and invokes setBounds on all components.
NOTE
The AWT has a second interface, called LayoutManager2, with 10 methods to implement rather than 5. The main point of the LayoutManager2 interface is to allow the user to use the add method with constraints. For example, the BorderLayout and GridBagLayout implement the LayoutManager2 interface.
Example 9-17 is a simple implementation of the CircleLayout manager, which, amazingly and uselessly enough, lays out the components along a circle inside the parent.
Example 9-17. CircleLayoutTest.java
1. import java.awt.*;
2. import java.awt.event.*;
3. import javax.swing.*;
4.
5. public class CircleLayoutTest
6. {
7. public static void main(String[] args)
8. {
9. CircleLayoutFrame frame = new CircleLayoutFrame();
10. frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
11. frame.pack();
12. frame.setVisible(true);
13. }
14. }
15.
16. /**
17. A frame that shows buttons arranged along a circle.
18. */
19. class CircleLayoutFrame extends JFrame
20. {
21. public CircleLayoutFrame()
22. {
23. setTitle("CircleLayoutTest");
24.
25. setLayout(new CircleLayout());
26. add(new JButton("Yellow"));
27. add(new JButton("Blue"));
28. add(new JButton("Red"));
29. add(new JButton("Green"));
30. add(new JButton("Orange"));
31. add(new JButton("Fuchsia"));
32. add(new JButton("Indigo"));
33. }
34. }
35.
36. /**
37. A layout manager that lays out components along a circle.
38. */
39. class CircleLayout implements LayoutManager
40. {
41. public void addLayoutComponent(String name, Component comp)
42. {}
43.
44. public void removeLayoutComponent(Component comp)
45. {}
46.
47. public void setSizes(Container parent)
48. {
49. if (sizesSet) return;
50. int n = parent.getComponentCount();
51.
52. preferredWidth = 0;
53. preferredHeight = 0;
54. minWidth = 0;
55. minHeight = 0;
56. maxComponentWidth = 0;
57. maxComponentHeight = 0;
58.
59. // compute the maximum component widths and heights
60. // and set the preferred size to the sum of the component sizes.
61. for (int i = 0; i < n; i++)
62. {
63. Component c = parent.getComponent(i);
64. if (c.isVisible())
65. {
66. Dimension d = c.getPreferredSize();
67. maxComponentWidth = Math.max(maxComponentWidth, d.width);
68. maxComponentHeight = Math.max(maxComponentHeight, d.height);
69. preferredWidth += d.width;
70. preferredHeight += d.height;
71. }
72. }
73. minWidth = preferredWidth / 2;
74. minHeight = preferredHeight / 2;
75. sizesSet = true;
76. }
77.
78. public Dimension preferredLayoutSize(Container parent)
79. {
80. setSizes(parent);
81. Insets insets = parent.getInsets();
82. int width = preferredWidth + insets.left + insets.right;
83. int height = preferredHeight + insets.top + insets.bottom;
84. return new Dimension(width, height);
85. }
86.
87. public Dimension minimumLayoutSize(Container parent)
88. {
89. setSizes(parent);
90. Insets insets = parent.getInsets();
91. int width = minWidth + insets.left + insets.right;
92. int height = minHeight + insets.top + insets.bottom;
93. return new Dimension(width, height);
94. }
95.
96. public void layoutContainer(Container parent)
97. {
98. setSizes(parent);
99.
100. // compute center of the circle
101.
102. Insets insets = parent.getInsets();
103. int containerWidth = parent.getSize().width - insets.left - insets.right;
104. int containerHeight = parent.getSize().height - insets.top - insets.bottom;
105.
106. int xcenter = insets.left + containerWidth / 2;
107. int ycenter = insets.top + containerHeight / 2;
108.
109. // compute radius of the circle
110.
111. int xradius = (containerWidth - maxComponentWidth) / 2;
112. int yradius = (containerHeight - maxComponentHeight) / 2;
113. int radius = Math.min(xradius, yradius);
114.
115. // lay out components along the circle
116.
117. int n = parent.getComponentCount();
118. for (int i = 0; i < n; i++)
119. {
120. Component c = parent.getComponent(i);
121. if (c.isVisible())
122. {
123. double angle = 2 * Math.PI * i / n;
124.
125. // center point of component
126. int x = xcenter + (int)(Math.cos(angle) * radius);
127. int y = ycenter + (int)(Math.sin(angle) * radius);
128.
129. // move component so that its center is (x, y)
130. // and its size is its preferred size
131. Dimension d = c.getPreferredSize();
132. c.setBounds(x - d.width / 2, y - d.height / 2, d.width, d.height);
133. }
134. }
135. }
136.
137. private int minWidth = 0;
138. private int minHeight = 0;
139. private int preferredWidth = 0;
140. private int preferredHeight = 0;
141. private boolean sizesSet = false;
142. private int maxComponentWidth = 0;
143. private int maxComponentHeight = 0;
144. }
java.awt.LayoutManager 1.0
void addLayoutComponent(String name, Component comp)
adds a component to the layout.
Parameters:
name
An identifier for the component placement
comp
The component to be added
void removeLayoutComponent(Component comp)
removes a component from the layout.
Parameters:
comp
The component to be removed
Dimension preferredLayoutSize(Container parent)
returns the preferred size dimensions for the container under this layout.
Parameters:
parent
The container whose components are being laid out
Dimension minimumLayoutSize(Container parent)
returns the minimum size dimensions for the container under this layout.
Parameters:
parent
The container whose components are being laid out
void layoutContainer(Container parent)
lays out the components in a container.
Parameters:
parent
The container whose components are being laid out
Traversal Order
When you add many components into a window, you need to give some thought to the traversal order. When a window is first displayed, the first component in the traversal order has the keyboard focus. Each time the user presses the TAB key, the next component gains focus. (Recall that a component that has the keyboard focus can be manipulated with the keyboard. For example, a button can be "clicked" with the space bar when it has focus.) You may not personally care about using the TAB key to navigate through a set of controls, but plenty of users do. Among them are the mouse haters and those who cannot use a mouse, perhaps because of a handicap or because they are navigating the user interface by voice. For that reason, you need to know how Swing handles traversal order.
The traversal order is straightforward, first left to right and then top to bottom. For example, in the font dialog example, the components are traversed in the following order (see Figure 9-45):
Face combo box
Sample text area (press CTRL+TAB to move to the next field; the TAB character is considered text input)
Size combo box
Bold checkbox
Italic checkbox
Figure 9-45. Geometric traversal order
NOTE
In the old AWT, the traversal order was determined by the order in which you inserted components into a container. In Swing, the insertion order does not matter-only the layout of the components is considered.
The situation is more complex if your container contains other containers. When the focus is given to another container, it automatically ends up within the top-left component in that container and then it traverses all other components in that container. Finally, the focus is given to the component following the container.
You can use this to your advantage by grouping related elements in another container such as a panel.
NOTE
As of JDK 1.4, you call
component.setFocusable(false);
to remove a component from the focus traversal. In older versions of the JDK, you had to override the isFocusTraversable method, but that method is now deprecated.
In summary, there are two standard traversal policies in JDK 1.4:
Pure AWT applications use the DefaultFocusTraversalPolicy. Components are included in the focus traversal if they are visible, displayable, enabled, and focusable, and if their native peers are focusable. The components are traversed in the order in which they were inserted in the container.
Swing applications use the LayoutFocusTraversalPolicy. Components are included in the focus traversal if they are visible, displayable, enabled, and focusable. The components are traversed in geometric order: left to right, then top to bottom. However, a container introduces a new "cycle"-its components are traversed first before the successor of the container gains focus.
NOTE
The "cycle" notion is a bit confusing. After reaching the last element in a child container, the focus does not go back to its first element, but instead to the container's successor. The API supports true cycles, including keystrokes that move up and down in a cycle hierarchy. However, the standard traversal policy does not use hierarchical cycles. It flattens the cycle hierarchy into a linear (depth-first) traversal.
NOTE
In JDK 1.3, you could change the default traversal order by calling the setNextFocusableComponent method of the JComponent class. That method is now deprecated. To change the traversal order, try grouping related components into panels so that they form cycles. If that doesn't work, you have to either install a comparator that sorts the components differently or completely replace the traversal policy. Neither operation seems intended for the faint of heart-see the Sun API documentation for details.
--------------------------------------------------------------------------------
--------------------------------------------------------------------------------
Dialog Boxes
So far, all our user interface components have appeared inside a frame window that was created in the application. This is the most common situation if you write applets that run inside a web browser. But if you write applications, you usually want separate dialog boxes to pop up to give information to or get information from the user.
Just as with most windowing systems, AWT distinguishes between modal and modeless dialog boxes. A modal dialog box won't let users interact with the remaining windows of the application until he or she deals with it. You use a modal dialog box when you need information from the user before you can proceed with execution. For example, when the user wants to read a file, a modal file dialog box is the one to pop up. The user must specify a file name before the program can begin the read operation. Only when the user closes the (modal) dialog box can the application proceed.
A modeless dialog box lets the user enter information in both the dialog box and the remainder of the application. One example of a modeless dialog is a toolbar. The toolbar can stay in place as long as needed, and the user can interact with both the application window and the toolbar as needed.
We start this section with the simplest dialogs-modal dialogs with just a single message. Swing has a convenient JOptionPane class that lets you put up a simple dialog without writing any special dialog box code. Next, you see how to write more complex dialogs by implementing your own dialog windows. Finally, you see how to transfer data from your application into a dialog and back.
We conclude this section by looking at two standard dialogs: file dialogs and color dialogs. File dialogs are complex, and you definitely want to be familiar with the Swing JFileChooser for this purpose-it would be a real challenge to write your own. The JColorChooser dialog is useful when you want users to pick colors.
Option Dialogs
Swing has a set of ready-made simple dialogs that suffice when you need to ask the user for a single piece of information. The JOptionPane has four static methods to show these simple dialogs:
showMessageDialog
Show a message and wait for the user to click OK
showConfirmDialog
Show a message and get a confirmation (like OK/Cancel)
showOptionDialog
Show a message and get a user option from a set of options
showInputDialog
Show a message and get one line of user input
Figure 9-46 shows a typical dialog. As you can see, the dialog has the following components:
An icon
A message
One or more option buttons
Figure 9-46. An option dialog
The input dialog has an additional component for user input. This can be a text field into which the user can type an arbitrary string, or a combo box from which the user can select one item.
The exact layout of these dialogs, and the choice of icons for standard message types, depend on the pluggable look and feel.
The icon on the left side depends on one of five message types:
ERROR_MESSAGE
INFORMATION_MESSAGE
WARNING_MESSAGE
QUESTION_MESSAGE
PLAIN_MESSAGE
The PLAIN_MESSAGE type has no icon. Each dialog type also has a method that lets you supply your own icon instead.
For each dialog type, you can specify a message. This message can be a string, an icon, a user interface component, or any other object. Here is how the message object is displayed:
String:
Draw the string
Icon:
Show the icon
Component:
Show the component
Object[]:
Show all objects in the array, stacked on top of each other
any other object:
Apply toString and show the resulting string
You can see these options by running the program in Example 9-18.
Of course, supplying a message string is by far the most common case. Supplying a Component gives you ultimate flexibility because you can make the paintComponent method draw anything you want.
The buttons on the bottom depend on the dialog type and the option type. When calling showMessageDialog and showInputDialog, you get only a standard set of buttons (OK and OK/Cancel, respectively). When calling showConfirmDialog, you can choose among four option types:
DEFAULT_OPTION
YES_NO_OPTION
YES_NO_CANCEL_OPTION
OK_CANCEL_OPTION
With the showOptionDialog you can specify an arbitrary set of options. You supply an array of objects for the options. Each array element is rendered as follows:
String:
Make a button with the string as label
Icon:
Make a button with the icon as label
Component:
Show the component
any other object:
Apply toString and make a button with the resulting string as label
The return values of these functions are as follows:
showMessageDialog
None
showConfirmDialog
An integer representing the chosen option
showOptionDialog
An integer representing the chosen option
showInputDialog
The string that the user supplied or selected
The showConfirmDialog and showOptionDialog return integers to indicate which button the user chose. For the option dialog, this is simply the index of the chosen option or the value CLOSED_OPTION if the user closed the dialog instead of choosing an option. For the confirmation dialog, the return value can be one of the following:
OK_OPTION
CANCEL_OPTION
YES_OPTION
NO_OPTION
CLOSED_OPTION
This all sounds like a bewildering set of choices, but in practice it is simple:
1. Choose the dialog type (message, confirmation, option, or input).
2. Choose the icon (error, information, warning, question, none, or custom).
3. Choose the message (string, icon, custom component, or a stack of them).
4. For a confirmation dialog, choose the option type (default, Yes/No, Yes/No/Cancel, or OK/Cancel).
5. For an option dialog, choose the options (strings, icons, or custom components) and the default option.
6. For an input dialog, choose between a text field and a combo box.
7. Locate the appropriate method to call in the JOptionPane API.
For example, suppose you want to show the dialog in Figure 9-46. The dialog shows a message and asks the user to confirm or cancel. Thus, it is a confirmation dialog. The icon is a warning icon. The message is a string. The option type is OK_CANCEL_OPTION. Here is the call you would make:
int selection = JOptionPane.showConfirmDialog(parent,
"Message", "Title",
JOptionPane.OK_CANCEL_OPTION,
JOptionPane.WARNING_MESSAGE);
if (selection == JOptionPane.OK_OPTION) . . .
TIP
The message string can contain newline ('
') characters. Such a string is displayed in multiple lines.
The program in Example 9-18 lets you make the selections shown in Figure 9-47. It then shows you the resulting dialog.
Example 9-18. OptionDialogTest.java
1. import java.awt.*;
2. import java.awt.event.*;
3. import java.awt.geom.*;
4. import java.util.*;
5. import javax.swing.*;
6. import javax.swing.border.*;
7.
8. public class OptionDialogTest
9. {
10. public static void main(String[] args)
11. {
12. OptionDialogFrame frame = new OptionDialogFrame();
13. frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
14. frame.setVisible(true);
15. }
16. }
17.
18. /**
19. A panel with radio buttons inside a titled border.
20. */
21. class ButtonPanel extends JPanel
22. {
23. /**
24. Constructs a button panel.
25. @param title the title shown in the border
26. @param options an array of radio button labels
27. */
28. public ButtonPanel(String title, String[] options)
29. {
30. setBorder(BorderFactory.createTitledBorder(BorderFactory.createEtchedBorder(),
title));
31. setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
32. group = new ButtonGroup();
33.
34. // make one radio button for each option
35. for (int i = 0; i < options.length; i++)
36. {
37. JRadioButton b = new JRadioButton(options[i]);
38. b.setActionCommand(options[i]);
39. add(b);
40. group.add(b);
41. b.setSelected(i == 0);
42. }
43. }
44.
45. /**
46. Gets the currently selected option.
47. @return the label of the currently selected radio button.
48. */
49. public String getSelection()
50. {
51. return group.getSelection().getActionCommand();
52. }
53.
54. private ButtonGroup group;
55. }
56.
57. /**
58. A frame that contains settings for selecting various option
59. dialogs.
60. */
61. class OptionDialogFrame extends JFrame
62. {
63. public OptionDialogFrame()
64. {
65. setTitle("OptionDialogTest");
66. setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
67.
68. JPanel gridPanel = new JPanel();
69. gridPanel.setLayout(new GridLayout(2, 3));
70.
71. typePanel = new ButtonPanel("Type",
72. new String[]
73. {
74. "Message",
75. "Confirm",
76. "Option",
77. "Input"
78. });
79.
80. messageTypePanel = new ButtonPanel("Message Type",
81. new String[]
82. {
83. "ERROR_MESSAGE",
84. "INFORMATION_MESSAGE",
85. "WARNING_MESSAGE",
86. "QUESTION_MESSAGE",
87. "PLAIN_MESSAGE"
88. });
89.
90. messagePanel = new ButtonPanel("Message",
91. new String[]
92. {
93. "String",
94. "Icon",
95. "Component",
96. "Other",
97. "Object[]"
98. });
99.
100. optionTypePanel = new ButtonPanel("Confirm",
101. new String[]
102. {
103. "DEFAULT_OPTION",
104. "YES_NO_OPTION",
105. "YES_NO_CANCEL_OPTION",
106. "OK_CANCEL_OPTION"
107. });
108.
109. optionsPanel = new ButtonPanel("Option",
110. new String[]
111. {
112. "String[]",
113. "Icon[]",
114. "Object[]"
115. });
116.
117. inputPanel = new ButtonPanel("Input",
118. new String[]
119. {
120. "Text field",
121. "Combo box"
122. });
123.
124. gridPanel.add(typePanel);
125. gridPanel.add(messageTypePanel);
126. gridPanel.add(messagePanel);
127. gridPanel.add(optionTypePanel);
128. gridPanel.add(optionsPanel);
129. gridPanel.add(inputPanel);
130.
131. // add a panel with a Show button
132.
133. JPanel showPanel = new JPanel();
134. JButton showButton = new JButton("Show");
135. showButton.addActionListener(new ShowAction());
136. showPanel.add(showButton);
137.
138. add(gridPanel, BorderLayout.CENTER);
139. add(showPanel, BorderLayout.SOUTH);
140. }
141.
142. /**
143. Gets the currently selected message.
144. @return a string, icon, component, or object array,
145. depending on the Message panel selection
146. */
147. public Object getMessage()
148. {
149. String s = messagePanel.getSelection();
150. if (s.equals("String"))
151. return messageString;
152. else if (s.equals("Icon"))
153. return messageIcon;
154. else if (s.equals("Component"))
155. return messageComponent;
156. else if (s.equals("Object[]"))
157. return new Object[]
158. {
159. messageString,
160. messageIcon,
161. messageComponent,
162. messageObject
163. };
164. else if (s.equals("Other"))
165. return messageObject;
166. else return null;
167. }
168.
169. /**
170. Gets the currently selected options.
171. @return an array of strings, icons, or objects, depending
172. on the Option panel selection
173. */
174. public Object[] getOptions()
175. {
176. String s = optionsPanel.getSelection();
177. if (s.equals("String[]"))
178. return new String[] { "Yellow", "Blue", "Red" };
179. else if (s.equals("Icon[]"))
180. return new Icon[]
181. {
182. new ImageIcon("yellow-ball.gif"),
183. new ImageIcon("blue-ball.gif"),
184. new ImageIcon("red-ball.gif")
185. };
186. else if (s.equals("Object[]"))
187. return new Object[]
188. {
189. messageString,
190. messageIcon,
191. messageComponent,
192. messageObject
193. };
194. else
195. return null;
196. }
197.
198. /**
199. Gets the selected message or option type
200. @param panel the Message Type or Confirm panel
201. @return the selected XXX_MESSAGE or XXX_OPTION constant
202. from the JOptionPane class
203. */
204. public int getType(ButtonPanel panel)
205. {
206. String s = panel.getSelection();
207. try
208. {
209. return JOptionPane.class.getField(s).getInt(null);
210. }
211. catch(Exception e)
212. {
213. return -1;
214. }
215. }
216.
217. /**
218. The action listener for the Show button shows a
219. Confirm, Input, Message, or Option dialog depending
220. on the Type panel selection.
221. */
222. private class ShowAction implements ActionListener
223. {
224. public void actionPerformed(ActionEvent event)
225. {
226. if (typePanel.getSelection().equals("Confirm"))
227. JOptionPane.showConfirmDialog(
228. OptionDialogFrame.this,
229. getMessage(),
230. "Title",
231. getType(optionTypePanel),
232. getType(messageTypePanel));
233. else if (typePanel.getSelection().equals("Input"))
234. {
235. if (inputPanel.getSelection().equals("Text field"))
236. JOptionPane.showInputDialog(
237. OptionDialogFrame.this,
238. getMessage(),
239. "Title",
240. getType(messageTypePanel));
241. else
242. JOptionPane.showInputDialog(
243. OptionDialogFrame.this,
244. getMessage(),
245. "Title",
246. getType(messageTypePanel),
247. null,
248. new String[] { "Yellow", "Blue", "Red" },
249. "Blue");
250. }
251. else if (typePanel.getSelection().equals("Message"))
252. JOptionPane.showMessageDialog(
253. OptionDialogFrame.this,
254. getMessage(),
255. "Title",
256. getType(messageTypePanel));
257. else if (typePanel.getSelection().equals("Option"))
258. JOptionPane.showOptionDialog(
259. OptionDialogFrame.this,
260. getMessage(),
261. "Title",
262. getType(optionTypePanel),
263. getType(messageTypePanel),
264. null,
265. getOptions(),
266. getOptions()[0]);
267. }
268. }
269.
270. public static final int DEFAULT_WIDTH = 600;
271. public static final int DEFAULT_HEIGHT = 400;
272.
273. private ButtonPanel typePanel;
274. private ButtonPanel messagePanel;
275. private ButtonPanel messageTypePanel;
276. private ButtonPanel optionTypePanel;
277. private ButtonPanel optionsPanel;
278. private ButtonPanel inputPanel;
279.
280. private String messageString = "Message";
281. private Icon messageIcon = new ImageIcon("blue-ball.gif");
282. private Object messageObject = new Date();
283. private Component messageComponent = new SamplePanel();
284. }
285.
286. /**
287. A panel with a painted surface
288. */
289.
290. class SamplePanel extends JPanel
291. {
292. public void paintComponent(Graphics g)
293. {
294. super.paintComponent(g);
295. Graphics2D g2 = (Graphics2D) g;
296. Rectangle2D rect = new Rectangle2D.Double(0, 0, getWidth() - 1, getHeight() - 1);
297. g2.setPaint(Color.YELLOW);
298. g2.fill(rect);
299. g2.setPaint(Color.BLUE);
300. g2.draw(rect);
301. }
302.
303. public Dimension getMinimumSize()
304. {
305. return new Dimension(10, 10);
306. }
307. }
javax.swing.JOptionPane 1.2
static void showMessageDialog(Component parent, Object message, String title, int messageType, Icon icon)
static void showMessageDialog(Component parent, Object message, String title, int messageType)
static void showMessageDialog(Component parent, Object message)
static void showInternalMessageDialog(Component parent, Object message, String title, int messageType, Icon icon)
static void showInternalMessageDialog(Component parent, Object message, String title, int messageType)
static void showInternalMessageDialog(Component parent, Object message)
show a message dialog or an internal message dialog. (An internal dialog is rendered entirely within its owner frame.)
Parameters:
parent
The parent component (can be null)
message
The message to show on the dialog (can be a string, icon, component, or an array of them)
title
The string in the title bar of the dialog
messageType
One of ERROR_MESSAGE, INFORMATION_MESSAGE, WARNING_MESSAGE, QUESTION_MESSAGE, PLAIN_MESSAGE
icon
An icon to show instead of one of the standard icons
static int showConfirmDialog(Component parent, Object message, String title, int optionType, int messageType, Icon icon)
static int showConfirmDialog(Component parent, Object message, String title, int optionType, int messageType)
static int showConfirmDialog(Component parent, Object message, String title, int optionType)
static int showConfirmDialog(Component parent, Object message)
static int showInternalConfirmDialog(Component parent, Object message, String title, int optionType, int messageType, Icon icon)
static int showInternalConfirmDialog(Component parent, Object message, String title, int optionType, int messageType)
static int showInternalConfirmDialog(Component parent, Object message, String title, int optionType)
static int showInternalConfirmDialog(Component parent, Object message)
show a confirmation dialog or an internal confirmation dialog. (An internal dialog is rendered entirely within its owner frame.) Returns the option selected by the user (one of OK_OPTION, CANCEL_OPTION, YES_OPTION, NO_OPTION), or CLOSED_OPTION if the user closed the dialog.
Parameters:
parent
The parent component (can be null)
message
The message to show on the dialog (can be a string, icon, component, or an array of them)
title
The string in the title bar of the dialog
messageType
One of ERROR_MESSAGE, INFORMATION_MESSAGE, WARNING_MESSAGE, QUESTION_MESSAGE, PLAIN_MESSAGE
optionType
One of DEFAULT_OPTION, YES_NO_OPTION, YES_NO_CANCEL_OPTION, OK_CANCEL_OPTION
icon
An icon to show instead of one of the standard icons
static int showOptionDialog(Component parent, Object message, String title, int optionType, int messageType, Icon icon, Object[] options, Object default)
static int showInternalOptionDialog(Component parent, Object message, String title, int optionType, int messageType, Icon icon, Object[] options, Object default)
show an option dialog or an internal option dialog. (An internal dialog is rendered entirely within its owner frame.) Returns the index of the option selected by the user, or CLOSED_OPTION if the user canceled the dialog.
Parameters:
parent
The parent component (can be null)
message
The message to show on the dialog (can be a string, icon, component, or an array of them)
title
The string in the title bar of the dialog
messageType
One of ERROR_MESSAGE, INFORMATION_MESSAGE, WARNING_MESSAGE, QUESTION_MESSAGE, PLAIN_MESSAGE
optionType
One of DEFAULT_OPTION, YES_NO_OPTION, YES_NO_CANCEL_OPTION, OK_CANCEL_OPTION
icon
An icon to show instead of one of the standard icons
options
An array of options (can be strings, icons, or components)
default
The default option to present to the user
static Object showInputDialog(Component parent, Object message, String title, int messageType, Icon icon, Object[] values, Object default)
static String showInputDialog(Component parent, Object message, String title, int messageType)
static String showInputDialog(Component parent, Object message)
static String showInputDialog(Object message)
static String showInputDialog(Component parent, Object message, Object default) 1.4
static String showInputDialog(Object message, Object default) 1.4
static Object showInternalInputDialog(Component parent, Object message, String title, int messageType, Icon icon, Object[] values, Object default)
static String showInternalInputDialog(Component parent, Object message, String title, int messageType)
static String showInternalInputDialog(Component parent, Object message)
show an input dialog or an internal input dialog. (An internal dialog is rendered entirely within its owner frame.) Returns the input string typed by the user, or null if the user canceled the dialog.
Parameters:
parent
The parent component (can be null)
message
The message to show on the dialog (can be a string, icon, component, or an array of them)
title
The string in the title bar of the dialog
messageType
One of ERROR_MESSAGE, INFORMATION_MESSAGE, WARNING_MESSAGE, QUESTION_MESSAGE, PLAIN_MESSAGE
icon
An icon to show instead of one of the standard icons
values
An array of values to show in a combo box
default
The default value to present to the user
Figure 9-47. The OptionDialogTest program
[View full size image]
Creating Dialogs
In the last section, you saw how to use the JOptionPane class to show a simple dialog. In this section, you see how to create such a dialog by hand.
Figure 9-48 shows a typical modal dialog box, a program information box that is displayed when the user clicks the About button.
Figure 9-48. An About dialog box
To implement a dialog box, you derive a class from JDialog. This is essentially the same process as deriving the main window for an application from JFrame. More precisely:
1. In the constructor of your dialog box, call the constructor of the superclass JDialog. You will need to tell it the owner frame (the frame window over which the dialog pops up), the title of the dialog frame, and a Boolean flag to indicate if the dialog box is modal or modeless.
You should supply the owner frame so that the dialog can be displayed on top of its owner. Windowing systems typically require that every pop-up window is owned by another frame. You can also supply a null owner, but that is a bit risky-the dialog might be hidden behind other windows. (Dialogs with a null owner are actually owned by a shared hidden frame.)
2. Add the user interface components of the dialog box.
3. Add the event handlers.
4. Set the size for the dialog box.
Here's an example dialog box:
public AboutDialog extends JDialog
{
public AboutDialog(JFrame owner)
{
super(owner, "About DialogTest", true);
add(new JLabel(
"<html><h1>Core Java</h1><hr>By Cay Horstmann and Gary Cornell</html>"),
BorderLayout.CENTER);
JPanel panel = new JPanel();
JButton ok = new JButton("Ok");
ok.addActionListener(new
ActionListener()
{
public void actionPerformed(ActionEvent event)
{
setVisible(false);
}
});
panel.add(ok);
add(panel, BorderLayout.SOUTH);
setSize(250, 150);
}
}
As you can see, the constructor adds user interface elements: in this case, labels and a button. It adds a handler to the button and sets the size of the dialog.
To display the dialog box, you create a new dialog object and make it visible:
JDialog dialog = new AboutDialog(this);
dialog.setVisible(true);
Actually, in the sample code below, we create the dialog box only once, and we can reuse it whenever the user clicks the About button.
if (dialog == null) // first time
dialog = new AboutDialog(this);
dialog.setVisible(true);
When the user clicks the Ok button, the dialog box should close. This is handled in the event handler of the Ok button:
ok.addActionListener(new
ActionListener()
{
public void actionPerformed(ActionEvent event)
{
setVisible(false);
}
});
When the user closes the dialog by clicking on the Close box, then the dialog is also hidden. Just as with a JFrame, you can override this behavior with the setDefaultCloseOperation method.
Example 9-19 is the code for the About dialog box test program.
Example 9-19. DialogTest.java
1. import java.awt.*;
2. import java.awt.event.*;
3. import javax.swing.*;
4.
5. public class DialogTest
6. {
7. public static void main(String[] args)
8. {
9. DialogFrame frame = new DialogFrame();
10. frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
11. frame.setVisible(true);
12. }
13. }
14.
15. /**
16. A frame with a menu whose File->About action shows a dialog.
17. */
18. class DialogFrame extends JFrame
19. {
20. public DialogFrame()
21. {
22. setTitle("DialogTest");
23. setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
24.
25. // construct a File menu
26.
27. JMenuBar menuBar = new JMenuBar();
28. setJMenuBar(menuBar);
29. JMenu fileMenu = new JMenu("File");
30. menuBar.add(fileMenu);
31.
32. // add About and Exit menu items
33.
34. // The About item shows the About dialog
35.
36. JMenuItem aboutItem = new JMenuItem("About");
37. aboutItem.addActionListener(new
38. ActionListener()
39. {
40. public void actionPerformed(ActionEvent event)
41. {
42. if (dialog == null) // first time
43. dialog = new AboutDialog(DialogFrame.this);
44. dialog.setVisible(true); // pop up dialog
45. }
46. });
47. fileMenu.add(aboutItem);
48.
49. // The Exit item exits the program
50.
51. JMenuItem exitItem = new JMenuItem("Exit");
52. exitItem.addActionListener(new
53. ActionListener()
54. {
55. public void actionPerformed(ActionEvent event)
56. {
57. System.exit(0);
58. }
59. });
60. fileMenu.add(exitItem);
61. }
62.
63. public static final int DEFAULT_WIDTH = 300;
64. public static final int DEFAULT_HEIGHT = 200;
65.
66. private AboutDialog dialog;
67. }
68.
69. /**
70. A sample modal dialog that displays a message and
71. waits for the user to click the Ok button.
72. */
73. class AboutDialog extends JDialog
74. {
75. public AboutDialog(JFrame owner)
76. {
77. super(owner, "About DialogTest", true);
78.
79. // add HTML label to center
80.
81. add(new JLabel(
82. "<html><h1>Core Java</h1><hr>By Cay Horstmann and Gary Cornell</html>"),
83. BorderLayout.CENTER);
84.
85. // Ok button closes the dialog
86.
87. JButton ok = new JButton("Ok");
88. ok.addActionListener(new
89. ActionListener()
90. {
91. public void actionPerformed(ActionEvent event)
92. {
93. setVisible(false);
94. }
95. });
96.
97. // add Ok button to southern border
98.
99. JPanel panel = new JPanel();
100. panel.add(ok);
101. add(panel, BorderLayout.SOUTH);
102.
103. setSize(250, 150);
104. }
105. }
javax.swing.JDialog 1.2
public JDialog(Frame parent, String title, boolean modal)
constructs a dialog. The dialog is not visible until it is explicitly shown.
Parameters:
parent
The frame that is the owner of the dialog
title
The title of the dialog
modal
True for modal dialogs (a modal dialog blocks input to other windows)
Data Exchange
The most common reason to put up a dialog box is to get information from the user. You have already seen how easy it is to make a dialog box object: give it initial data and then call setVisible(true) to display the dialog box on the screen. Now let us see how to transfer data in and out of a dialog box.
Consider the dialog box in Figure 9-49 that could be used to obtain a user name and a password to connect to some on-line service.
Figure 9-49. Password dialog box
Your dialog box should provide methods to set default data. For example, the PasswordChooser class of the example program has a method, setUser, to place default values into the next fields:
public void setUser(User u)
{
username.setText(u.getName());
}
Once you set the defaults (if desired), you show the dialog by calling setVisible(true). The dialog is now displayed.
The user then fills in the information and clicks the Ok or Cancel button. The event handlers for both buttons call setVisible(false), which terminates the call to setVisible(true). Alternatively, the user may close the dialog. If you did not install a window listener for the dialog, then the default window closing operation applies: the dialog becomes invisible, which also terminates the call to setVisible(true).
The important issue is that the call to setVisible(true) blocks until the user has dismissed the dialog. This makes it easy to implement modal dialogs.
You want to know whether the user has accepted or canceled the dialog. Our sample code sets the ok flag to false before showing the dialog. Only the event handler for the Ok button sets the ok flag to TRue. In that case, you can retrieve the user input from the dialog.
NOTE
Transferring data out of a modeless dialog is not as simple. When a modeless dialog is displayed, the call to setVisible(true) does not block and the program continues running while the dialog is displayed. If the user selects items on a modeless dialog and then clicks Ok, the dialog needs to send an event to some listener in the program.
The example program contains another useful improvement. When you construct a JDialog object, you need to specify the owner frame. However, quite often you want to show the same dialog with different owner frames. It is better to pick the owner frame when you are ready to show the dialog, not when you construct the PasswordChooser object.
The trick is to have the PasswordChooser extend JPanel instead of JDialog. Build a JDialog object on the fly in the showDialog method:
public boolean showDialog(Frame owner, String title)
{
ok = false;
if (dialog == null || dialog.getOwner() != owner)
{
dialog = new JDialog(owner, true);
dialog.add(this);
dialog.pack();
}
dialog.setTitle(title);
dialog.setVisible(true);
return ok;
}
Note that it is safe to have owner equal to null.
You can do even better. Sometimes, the owner frame isn't readily available. It is easy enough to compute it from any parent component, like this:
Frame owner;
if (parent instanceof Frame)
owner = (Frame) parent;
else
owner = (Frame) SwingUtilities.getAncestorOfClass(Frame.class, parent);
We use this enhancement in our sample program. The JOptionPane class also uses this mechanism.
Many dialogs have a default button, which is automatically selected if the user presses a trigger key (ENTER in most "look and feel" implementations). The default button is specially marked, often with a thick outline.
You set the default button in the root pane of the dialog:
dialog.getRootPane().setDefaultButton(okButton);
If you follow our suggestion of laying out the dialog in a panel, then you must be careful to set the default button only after you wrapped the panel into a dialog. The panel itself has no root pane.
Example 9-20 is the complete code that illustrates the data flow into and out of a dialog box.
Example 9-20. DataExchangeTest.java
1. import java.awt.*;
2. import java.awt.event.*;
3. import javax.swing.*;
4.
5. public class DataExchangeTest
6. {
7. public static void main(String[] args)
8. {
9. DataExchangeFrame frame = new DataExchangeFrame();
10. frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
11. frame.setVisible(true);
12. }
13. }
14.
15. /**
16. A frame with a menu whose File->Connect action shows a
17. password dialog.
18. */
19. class DataExchangeFrame extends JFrame
20. {
21. public DataExchangeFrame()
22. {
23. setTitle("DataExchangeTest");
24. setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
25.
26. // construct a File menu
27.
28. JMenuBar mbar = new JMenuBar();
29. setJMenuBar(mbar);
30. JMenu fileMenu = new JMenu("File");
31. mbar.add(fileMenu);
32.
33. // add Connect and Exit menu items
34.
35. JMenuItem connectItem = new JMenuItem("Connect");
36. connectItem.addActionListener(new ConnectAction());
37. fileMenu.add(connectItem);
38.
39. // The Exit item exits the program
40.
41. JMenuItem exitItem = new JMenuItem("Exit");
42. exitItem.addActionListener(new
43. ActionListener()
44. {
45. public void actionPerformed(ActionEvent event)
46. {
47. System.exit(0);
48. }
49. });
50. fileMenu.add(exitItem);
51.
52. textArea = new JTextArea();
53. add(new JScrollPane(textArea), BorderLayout.CENTER);
54. }
55.
56. public static final int DEFAULT_WIDTH = 300;
57. public static final int DEFAULT_HEIGHT = 200;
58.
59. private PasswordChooser dialog = null;
60. private JTextArea textArea;
61.
62. /**
63. The Connect action pops up the password dialog.
64. */
65.
66. private class ConnectAction implements ActionListener
67. {
68. public void actionPerformed(ActionEvent event)
69. {
70. // if first time, construct dialog
71.
72. if (dialog == null)
73. dialog = new PasswordChooser();
74.
75. // set default values
76. dialog.setUser(new User("yourname", null));
77.
78. // pop up dialog
79. if (dialog.showDialog(DataExchangeFrame.this, "Connect"))
80. {
81. // if accepted, retrieve user input
82. User u = dialog.getUser();
83. textArea.append(
84. "user name = " + u.getName()
85. + ", password = " + (new String(u.getPassword()))
86. + "
");
87. }
88. }
89. }
90. }
91.
92. /**
93. A password chooser that is shown inside a dialog
94. */
95. class PasswordChooser extends JPanel
96. {
97. public PasswordChooser()
98. {
99. setLayout(new BorderLayout());
100.
101. // construct a panel with user name and password fields
102.
103. JPanel panel = new JPanel();
104. panel.setLayout(new GridLayout(2, 2));
105. panel.add(new JLabel("User name:"));
106. panel.add(username = new JTextField(""));
107. panel.add(new JLabel("Password:"));
108. panel.add(password = new JPasswordField(""));
109. add(panel, BorderLayout.CENTER);
110.
111. // create Ok and Cancel buttons that terminate the dialog
112.
113. okButton = new JButton("Ok");
114. okButton.addActionListener(new
115. ActionListener()
116. {
117. public void actionPerformed(ActionEvent event)
118. {
119. ok = true;
120. dialog.setVisible(false);
121. }
122. });
123.
124. JButton cancelButton = new JButton("Cancel");
125. cancelButton.addActionListener(new
126. ActionListener()
127. {
128. public void actionPerformed(ActionEvent event)
129. {
130. dialog.setVisible(false);
131. }
132. });
133.
134. // add buttons to southern border
135.
136. JPanel buttonPanel = new JPanel();
137. buttonPanel.add(okButton);
138. buttonPanel.add(cancelButton);
139. add(buttonPanel, BorderLayout.SOUTH);
140. }
141.
142. /**
143. Sets the dialog defaults.
144. @param u the default user information
145. */
146. public void setUser(User u)
147. {
148. username.setText(u.getName());
149. }
150.
151. /**
152. Gets the dialog entries.
153. @return a User object whose state represents
154. the dialog entries
155. */
156. public User getUser()
157. {
158. return new User(username.getText(), password.getPassword());
159. }
160.
161. /**
162. Show the chooser panel in a dialog
163. @param parent a component in the owner frame or null
164. @param title the dialog window title
165. */
166. public boolean showDialog(Component parent, String title)
167. {
168. ok = false;
169.
170. // locate the owner frame
171.
172. Frame owner = null;
173. if (parent instanceof Frame)
174. owner = (Frame) parent;
175. else
176. owner = (Frame) SwingUtilities.getAncestorOfClass(Frame.class, parent);
177.
178. // if first time, or if owner has changed, make new dialog
179.
180. if (dialog == null || dialog.getOwner() != owner)
181. {
182. dialog = new JDialog(owner, true);
183. dialog.add(this);
184. dialog.getRootPane().setDefaultButton(okButton);
185. dialog.pack();
186. }
187.
188. // set title and show dialog
189.
190. dialog.setTitle(title);
191. dialog.setVisible(true);
192. return ok;
193. }
194.
195. private JTextField username;
196. private JPasswordField password;
197. private JButton okButton;
198. private boolean ok;
199. private JDialog dialog;
200. }
201.
202. /**
203. A user has a name and password. For security reasons, the
204. password is stored as a char[], not a String.
205. */
206. class User
207. {
208. public User(String aName, char[] aPassword)
209. {
210. name = aName;
211. password = aPassword;
212. }
213.
214. public String getName() { return name; }
215. public char[] getPassword() { return password; }
216.
217. public void setName(String aName) { name = aName; }
218. public void setPassword(char[] aPassword) { password = aPassword; }
219.
220. private String name;
221. private char[] password;
222. }
javax.swing.SwingUtilities 1.2
Container getAncestorOfClass(Class c, Component comp)
returns the innermost parent container of the given component that belongs to the given class or one of its subclasses.
javax.swing.JComponent 1.2
JRootPane getRootPane()
gets the root pane enclosing this component, or null if this component does not have an ancestor with a root pane.
javax.swing.JRootPane 1.2
void setDefaultButton(JButton button)
sets the default button for this root pane. To deactivate the default button, call this method with a null parameter.
javax.swing.JButton 1.2
boolean isDefaultButton()
returns true if this button is the default button of its root pane.
File Dialogs
When you write an application, you often want to be able to open and save files. A good file dialog box that shows files and directories and lets the user navigate the file system is hard to write, and you definitely don't want to reinvent that wheel. Fortunately, Swing provides a JFileChooser class that allows you to display a file dialog box similar to the one that most native applications use. JFileChooser dialogs are always modal. Note that the JFileChooser class is not a subclass of JDialog. Instead of calling setVisible(true), you call showOpenDialog to display a dialog for opening a file or you call showSaveDialog to display a dialog for saving a file. The button for accepting a file is then automatically labeled Open or Save. You can also supply your own button label with the showDialog method. Figure 9-50 shows an example of the file chooser dialog box.
Figure 9-50. File chooser dialog box
[View full size image]
Here are the steps needed to put up a file dialog box and recover what the user chooses from the box.
1. Make a JFileChooser object. Unlike the constructor for the JDialog class, you do not supply the parent component. This allows you to reuse a file chooser dialog with multiple frames.
For example:
JFileChooser chooser = new JFileChooser();
TIP
Reusing a file chooser object is a good idea because the JFileChooser constructor can be quite slow, especially on Windows if the user has many mapped network drives.
2. Set the directory by calling the setCurrentDirectory method.
For example, to use the current working directory:
chooser.setCurrentDirectory(new File("."));
You need to supply a File object. File objects are explained in detail in Chapter 12. All you need to know for now is that the constructor File(String filename) turns a file or directory name into a File object.
3. If you have a default file name that you expect the user to choose, supply it with the setSelectedFile method:
chooser.setSelectedFile(new File(filename));
4. To enable the user to select multiple files in the dialog, call the setMultiSelectionEnabled method. This is, of course, entirely optional and not all that common.
chooser.setMultiSelectionEnabled(true);
5. If you want to restrict the display of files in the dialog to those of a particular type (for example, all files with extension .gif), then you need to set a file filter. We discuss file filters later in this section.
6. By default, a user can select only files with a file chooser. If you want the user to select directories, use the setFileSelectionMode method. Call it with JFileChooser.FILES_ONLY (the default), JFileChooser.DIRECTORIES_ONLY, or JFileChooser.FILES_AND_DIRECTORIES.
7. Show the dialog box by calling the showOpenDialog or showSaveDialog method. You must supply the parent component in these calls:
int result = chooser.showOpenDialog(parent);
or
int result = chooser.showSaveDialog(parent);
The only difference between these calls is the label of the "approve button," the button that the user clicks to finish the file selection. You can also call the showDialog method and pass an explicit text for the approve button:
int result = chooser.showDialog(parent, "Select");
These calls return only when the user has approved, canceled, or dismissed the file dialog. The return value is JFileChooser.APPROVE_OPTION, JFileChooser.CANCEL_OPTION, or JFileChooser.ERROR_OPTION
8. You get the selected file or files with the getSelectedFile() or getSelectedFiles() method. These methods return either a single File object or an array of File objects. If you just need the name of the file object, call its getPath method. For example,
String filename = chooser.getSelectedFile().getPath();
For the most part, these steps are simple. The major difficulty with using a file dialog is to specify a subset of files from which the user should choose. For example, suppose the user should choose a GIF image file. Then, the file chooser should only display files with extension .gif. It should also give the user some kind of feedback that the displayed files are of a particular category, such as "GIF Images." But the situation can be more complex. If the user should choose a JPEG image file, then the extension can be either .jpg or .jpeg. Rather than coming up with a mechanism to codify these complexities, the designers of the file chooser supply a more elegant mechanism: to restrict the displayed files, you supply an object that extends the abstract class javax.swing.filechooser.FileFilter. The file chooser passes each file to the file filter and displays only the files that the file filter accepts.
At the time of this writing, only one such subclass is supplied: the default filter that accepts all files. However, it is easy to write ad hoc file filters. You simply implement the two abstract methods of the FileFilter superclass:
public boolean accept(File f);
public String getDescription();
NOTE
An unrelated FileFilter interface in the java.io package has a single method, boolean accept(File f). It is used in the listFiles method of the File class to list files in a directory. We do not know why the designers of Swing didn't extend this interface-perhaps the Java class library has now become so complex that even the programmers at Sun are no longer aware of all the standard classes and interfaces.
You will need to resolve the name conflict between these two identically named types if you import both the java.io and the javax.swing.filechooser package. The simplest remedy is to import javax.swing.filechooser.FileFilter, not javax.swing.filechooser.*.
The first method tests whether a file should be accepted. The second method returns a description of the file type that can be displayed in the file chooser dialog. For example, to filter for GIF files, you might use
public class GifFilter extends FileFilter
{
public boolean accept(File f)
{
return f.getName().toLowerCase().endsWith(".gif") || f.isDirectory();
}
public String getDescription()
{
return "GIF Image";
}
}
Once you have a file filter object, you use the setFileFilter method of the JFileChooser class to install it into the file chooser object:
chooser.setFileFilter(new GifFilter());
In our sample program, we supply a class ExtensionFileFilter, to be used as follows:
ExtensionFileFilter filter = new ExtensionFileFilter();
filter.addExtension("jpg");
filter.addExtension("gif");
filter.setDescription("Image files");
The implementation of the ExtensionFileFilter is a straightforward generalization of the GifFilter class. You may want to use that class in your own programs.
NOTE
The JDK contains a similar class, ExampleFileFilter, in the demo/jfc/FileChooserDemo directory.
You can install multiple filters to the file chooser by calling
chooser.addChoosableFileFilter(new GifFilter());
chooser.addChoosableFileFilter(new JpegFilter());
. . .
The user selects a filter from the combo box at the bottom of the file dialog. By default, the "All files" filter is always present in the combo box. This is a good idea, just in case a user of your program needs to select a file with a nonstandard extension. However, if you want to suppress the "All files" filter, call
chooser.setAcceptAllFileFilterUsed(false)
CAUTION
If you reuse a single file chooser for loading and saving different file types, call
chooser.resetChoosableFilters()
to clear any old file filters before adding new ones.
Finally, you can customize the file chooser by providing special icons and file descriptions for each file that the file chooser displays. You do this by supplying an object of a class extending the FileView class in the javax.swing.filechooser package. This is definitely an advanced technique. Normally, you don't need to supply a file view-the pluggable look and feel supplies one for you. But if you want to show different icons for special file types, you can install your own file view. You need to extend the FileView class and implement five methods:
Icon getIcon(File f);
String getName(File f);
String getDescription(File f);
String getTypeDescription(File f);
Boolean isTraversable(File f);
Then you use the setFileView method to install your file view into the file chooser.
The file chooser calls your methods for each file or directory that it wants to display. If your method returns null for the icon, name, or description, the file chooser then consults the default file view of the look and feel. That is good, because it means you need to deal only with the file types for which you want to do something different.
The file chooser calls the isTraversable method to decide whether to open a directory when a user clicks on it. Note that this method returns a Boolean object, not a boolean value! This seems weird, but it is actually convenient-if you aren't interested in deviating from the default file view, just return null. The file chooser will then consult the default file view. In other words, the method returns a Boolean to let you choose among three options: true (Boolean.TRUE), false (Boolean.FALSE), and don't care (null).
The example program contains a simple file view class. That class shows a particular icon whenever a file matches a file filter. We use it to display a palette icon for all image files.
class FileIconView extends FileView
{
public FileIconView(FileFilter aFilter, Icon anIcon)
{
filter = aFilter;
icon = anIcon;
}
public Icon getIcon(File f)
{
if (!f.isDirectory() && filter.accept(f))
return icon;
else return null;
}
private FileFilter filter;
private Icon icon;
}
CAUTION
In JDK version 1.2, you must define all five methods of your FileView subclass. Simply return null in the methods that you don't need. In JDK version 1.3, the FileView methods are no longer abstract.
You install this file view into your file chooser with the setFileView method:
chooser.setFileView(new FileIconView(filter,
new ImageIcon("palette.gif")));
The file chooser will then show the palette icon next to all files that pass the filter and use the default file view to show all other files. Naturally, we use the same filter that we set in the file chooser.
TIP
You can find a more useful ExampleFileView class in the demo/jfc/FileChooserDemo directory of the JDK. That class lets you associate icons and descriptions with arbitrary extensions.
Finally, you can customize a file dialog by adding an accessory component. For example, Figure 9-51 shows a preview accessory next to the file list. This accessory displays a thumbnail view of the currently selected file.
Figure 9-51. A file dialog with a preview accessory
[View full size image]
An accessory can be any Swing component. In our case, we extend the JLabel class and set its icon to a scaled copy of the graphics image:
class ImagePreviewer extends JLabel
{
public ImagePreviewer(JFileChooser chooser)
{
setPreferredSize(new Dimension(100, 100));
setBorder(BorderFactory.createEtchedBorder());
}
public void loadImage(File f)
{
ImageIcon icon = new ImageIcon(f.getPath());
if(icon.getIconWidth() > getWidth())
icon = new ImageIcon(icon.getImage().getScaledInstance(
getWidth(), -1, Image.SCALE_DEFAULT));
setIcon(icon);
repaint();
}
}
There is just one challenge. We want to update the preview image whenever the user selects a different file. The file chooser uses the "JavaBeans" mechanism of notifying interested listeners whenever one of its properties changes. The selected file is a property that you can monitor by installing a PropertyChangeListener. We discuss this mechanism in greater detail in Volume 2. Here is the code that you need to trap the notifications:
chooser.addPropertyChangeListener(new
PropertyChangeListener()
{
public void propertyChange(PropertyChangeEvent event)
{
if (event.getPropertyName() == JFileChooser.SELECTED_FILE_CHANGED_PROPERTY)
{
File newFile = (File) event.getNewValue()
// update the accessory
. . .
}
}
});
In our example program, we add this code to the ImagePreviewer constructor.
Example 9-21 contains a modification of the ImageViewer program from Chapter 2, in which the file chooser has been enhanced by a custom file view and a preview accessory.
Example 9-21. FileChooserTest.java
1. import java.awt.*;
2. import java.awt.event.*;
3. import java.awt.image.*;
4. import java.beans.*;
5. import java.util.*;
6. import java.io.*;
7. import javax.swing.*;
8. import javax.swing.filechooser.FileFilter;
9. import javax.swing.filechooser.FileView;
10.
11. public class FileChooserTest
12. {
13. public static void main(String[] args)
14. {
15. ImageViewerFrame frame = new ImageViewerFrame();
16. frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
17. frame.setVisible(true);
18. }
19. }
20.
21. /**
22. A frame that has a menu for loading an image and a display
23. area for the loaded image.
24. */
25. class ImageViewerFrame extends JFrame
26. {
27. public ImageViewerFrame()
28. {
29. setTitle("FileChooserTest");
30. setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
31.
32. // set up menu bar
33. JMenuBar menuBar = new JMenuBar();
34. setJMenuBar(menuBar);
35.
36. JMenu menu = new JMenu("File");
37. menuBar.add(menu);
38.
39. JMenuItem openItem = new JMenuItem("Open");
40. menu.add(openItem);
41. openItem.addActionListener(new FileOpenListener());
42.
43. JMenuItem exitItem = new JMenuItem("Exit");
44. menu.add(exitItem);
45. exitItem.addActionListener(new
46. ActionListener()
47. {
48. public void actionPerformed(ActionEvent event)
49. {
50. System.exit(0);
51. }
52. });
53.
54. // use a label to display the images
55. label = new JLabel();
56. add(label);
57.
58. // set up file chooser
59. chooser = new JFileChooser();
60.
61. // accept all image files ending with .jpg, .jpeg, .gif
62. final ExtensionFileFilter filter = new ExtensionFileFilter();
63. filter.addExtension("jpg");
64. filter.addExtension("jpeg");
65. filter.addExtension("gif");
66. filter.setDescription("Image files");
67. chooser.setFileFilter(filter);
68.
69. chooser.setAccessory(new ImagePreviewer(chooser));
70.
71. chooser.setFileView(new FileIconView(filter, new ImageIcon("palette.gif")));
72. }
73.
74. /**
75. This is the listener for the File->Open menu item.
76. */
77. private class FileOpenListener implements ActionListener
78. {
79. public void actionPerformed(ActionEvent event)
80. {
81. chooser.setCurrentDirectory(new File("."));
82.
83. // show file chooser dialog
84. int result = chooser.showOpenDialog(ImageViewerFrame.this);
85.
86. // if image file accepted, set it as icon of the label
87. if(result == JFileChooser.APPROVE_OPTION)
88. {
89. String name = chooser.getSelectedFile().getPath();
90. label.setIcon(new ImageIcon(name));
91. }
92. }
93. }
94.
95. public static final int DEFAULT_WIDTH = 300;
96. public static final int DEFAULT_HEIGHT = 400;
97.
98. private JLabel label;
99. private JFileChooser chooser;
100. }
101.
102. /**
103. This file filter matches all files with a given set of
104. extensions.
105. */
106. class ExtensionFileFilter extends FileFilter
107. {
108. /**
109. Adds an extension that this file filter recognizes.
110. @param extension a file extension (such as ".txt" or "txt")
111. */
112. public void addExtension(String extension)
113. {
114. if (!extension.startsWith("."))
115. extension = "." + extension;
116. extensions.add(extension.toLowerCase());
117. }
118.
119. /**
120. Sets a description for the file set that this file filter
121. recognizes.
122. @param aDescription a description for the file set
123. */
124. public void setDescription(String aDescription)
125. {
126. description = aDescription;
127. }
128.
129. /**
130. Returns a description for the file set that this file
131. filter recognizes.
132. @return a description for the file set
133. */
134. public String getDescription()
135. {
136. return description;
137. }
138.
139. public boolean accept(File f)
140. {
141. if (f.isDirectory()) return true;
142. String name = f.getName().toLowerCase();
143.
144. // check if the file name ends with any of the extensions
145. for (String extension : extensions)
146. if (name.endsWith(extension))
147. return true;
148. return false;
149. }
150.
151. private String description = "";
152. private ArrayList<String> extensions = new ArrayList<String>();
153. }
154.
155. /**
156. A file view that displays an icon for all files that match
157. a file filter.
158. */
159. class FileIconView extends FileView
160. {
161. /**
162. Constructs a FileIconView.
163. @param aFilter a file filter--all files that this filter
164. accepts will be shown with the icon.
165. @param anIcon--the icon shown with all accepted files.
166. */
167. public FileIconView(FileFilter aFilter, Icon anIcon)
168. {
169. filter = aFilter;
170. icon = anIcon;
171. }
172.
173. public Icon getIcon(File f)
174. {
175. if (!f.isDirectory() && filter.accept(f))
176. return icon;
177. else return null;
178. }
179.
180. private FileFilter filter;
181. private Icon icon;
182. }
183.
184. /**
185. A file chooser accessory that previews images.
186. */
187. class ImagePreviewer extends JLabel
188. {
189. /**
190. Constructs an ImagePreviewer.
191. @param chooser the file chooser whose property changes
192. trigger an image change in this previewer
193. */
194. public ImagePreviewer(JFileChooser chooser)
195. {
196. setPreferredSize(new Dimension(100, 100));
197. setBorder(BorderFactory.createEtchedBorder());
198.
199. chooser.addPropertyChangeListener(new
200. PropertyChangeListener()
201. {
202. public void propertyChange(PropertyChangeEvent
203. event)
204. {
205. if (event.getPropertyName() ==
206. JFileChooser.SELECTED_FILE_CHANGED_PROPERTY)
207. {
208. // the user has selected a new file
209. File f = (File) event.getNewValue();
210. if (f == null) { setIcon(null); return; }
211.
212. // read the image into an icon
213. ImageIcon icon = new ImageIcon(f.getPath());
214.
215. // if the icon is too large to fit, scale it
216. if(icon.getIconWidth() > getWidth())
217. icon = new ImageIcon(icon.getImage().getScaledInstance(
218. getWidth(), -1, Image.SCALE_DEFAULT));
219.
220. setIcon(icon);
221. }
222. }
223. });
224. }
225. }
javax.swing.JFileChooser 1.2
JFileChooser()
creates a file chooser dialog box that can be used for multiple frames.
void setCurrentDirectory(File dir)
sets the initial directory for the file dialog box.
void setSelectedFile(File file)
void setSelectedFiles(File[] file)
set the default file choice for the file dialog box.
void setMultiSelectionEnabled(boolean b)
sets or clears multiple selection mode.
void setFileSelectionMode(int mode)
lets the user select files only (the default), directories only, or both files and directories. The mode parameter is one of JFileChooser.FILES_ONLY, JFileChooser.DIRECTORIES_ONLY, and JFileChooser.FILES_AND_DIRECTORIES.
int showOpenDialog(Component parent)
int showSaveDialog(Component parent)
int showDialog(Component parent, String approveButtonText)
show a dialog in which the approve button is labeled "Open", "Save", or with the approveButtonText string. Returns APPROVE_OPTION, CANCEL_OPTION (if the user selected the cancel button or dismissed the dialog), or ERROR_OPTION (if an error occurred).
File getSelectedFile()
File[] getSelectedFiles()
get the file or files that the user selected (or return null if the user didn't select any file).
void setFileFilter(FileFilter filter)
sets the file mask for the file dialog box. All files for which filter.accept returns true will be displayed. Also adds the filter to the list of choosable filters.
void addChoosableFileFilter(FileFilter filter)
adds a file filter to the list of choosable filters.
void setAcceptAllFileFilterUsed(boolean b)
includes or suppresses an "All files" filter in the filter combo box.
void resetChoosableFileFilters()
clears the list of choosable filters. Only the "All files" filter remains unless it is explicitly suppressed.
void setFileView(FileView view)
sets a file view to provide information about the files that the file chooser displays.
void setAccessory(JComponent component)
sets an accessory component.
javax.swing.filechooser.FileFilter 1.2
boolean accept(File f)
returns true if the file chooser should display this file.
String getDescription()
returns a description of this file filter, for example, "Image files (*.gif,*.jpeg)".
javax.swing.filechooser.FileView 1.2
String getName(File f)
returns the name of the file f, or null. Normally, this method simply returns f.getName().
String getDescription(File f)
returns a humanly readable description of the file f, or null. For example, if f is an HTML document, this method might return its title.
String getTypeDescription(File f)
returns a humanly readable description of the type of the file f, or null. For example, if f is an HTML document, this method might return a string "Hypertext document".
Icon getIcon(File f)
returns an icon for the file f, or null. For example, if f is a JPEG file, this method might return a thumbnail icon.
Boolean isTraversable(File f)
returns Boolean.TRUE if f is a directory that the user can open. This method might return false if a directory is conceptually a compound document. Like all FileView methods, this method can return null to signify that the file chooser should consult the default view instead.
Color Choosers
As you saw in the preceding section, a high-quality file chooser is an intricate user interface component that you definitely do not want to implement yourself. Many user interface toolkits provide other common dialogs: to choose a date/time, currency value, font, color, and so on. The benefit is twofold. Programmers can simply use a high-quality implementation rather than rolling their own. And users have a common experience for these selections.
At this point, Swing provides only one additional chooser, the JColorChooser (see Figures 9-52 through 9-54). You use it to let users pick a color value. Like the JFileChooser class, the color chooser is a component, not a dialog, but it contains convenience methods to create dialogs that contain a color chooser component.
Figure 9-52. The "swatches" pane of a color chooser
Figure 9-54. The RGB pane of a color chooser
Here is how you show a modal dialog with a color chooser:
Color selectedColor = JColorChooser.showDialog(parent,title, initialColor);
Alternatively, you can display a modeless color chooser dialog. You supply
A parent component;
The title of the dialog;
A flag to select either a modal or a modeless dialog;
A color chooser; and
Listeners for the OK and Cancel buttons (or null if you don't want a listener).
Figure 9-53. The HSB pane of a color chooser
Here is how you make a modeless dialog that sets the background color when the user clicks the OK button:
chooser = new JColorChooser();
dialog = JColorChooser.createDialog(
parent,
"Background Color",
false /* not modal */,
chooser,
new ActionListener() // OK button listener
{
public void actionPerformed(ActionEvent event)
{
setBackground(chooser.getColor());
}
},
null /* no Cancel button listener */);
You can do even better than that and give the user immediate feedback of the color selection. To monitor the color selections, you need to obtain the selection model of the chooser and add a change listener:
chooser.getSelectionModel().addChangeListener(new
ChangeListener()
{
public void stateChanged(ChangeEvent event)
{
do something with chooser.getColor();
}
});
In this case, there is no benefit to the OK and Cancel buttons that the color chooser dialog provides. You can just add the color chooser component directly into a modeless dialog:
dialog = new JDialog(parent, false /* not modal */);
dialog.add(chooser);
dialog.pack();
The program in Example 9-22 shows the three types of dialogs. If you click on the Modal button, you must select a color before you can do anything else. If you click on the Modeless button, you get a modeless dialog, but the color change only happens when you click the OK button on the dialog. If you click the Immediate button, you get a modeless dialog without buttons. As soon as you pick a different color in the dialog, the background color of the panel is updated.
This ends our discussion of user interface components. The material in Chapters 7 through 9 showed you how to implement simple GUIs in Swing. Turn to Volume 2 for more advanced Swing components and sophisticated graphics techniques.
Example 9-22. ColorChooserTest.java
1. import java.awt.*;
2. import java.awt.event.*;
3. import javax.swing.*;
4. import javax.swing.event.*;
5.
6. public class ColorChooserTest
7. {
8. public static void main(String[] args)
9. {
10. ColorChooserFrame frame = new ColorChooserFrame();
11. frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
12. frame.setVisible(true);
13. }
14. }
15.
16. /**
17. A frame with a color chooser panel
18. */
19. class ColorChooserFrame extends JFrame
20. {
21. public ColorChooserFrame()
22. {
23. setTitle("ColorChooserTest");
24. setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
25.
26. // add color chooser panel to frame
27.
28. ColorChooserPanel panel = new ColorChooserPanel();
29. add(panel);
30. }
31.
32. public static final int DEFAULT_WIDTH = 300;
33. public static final int DEFAULT_HEIGHT = 200;
34. }
35.
36. /**
37. A panel with buttons to pop up three types of color choosers
38. */
39. class ColorChooserPanel extends JPanel
40. {
41. public ColorChooserPanel()
42. {
43. JButton modalButton = new JButton("Modal");
44. modalButton.addActionListener(new ModalListener());
45. add(modalButton);
46.
47. JButton modelessButton = new JButton("Modeless");
48. modelessButton.addActionListener(new ModelessListener());
49. add(modelessButton);
50.
51. JButton immediateButton = new JButton("Immediate");
52. immediateButton.addActionListener(new ImmediateListener());
53. add(immediateButton);
54. }
55.
56. /**
57. This listener pops up a modal color chooser
58. */
59. private class ModalListener implements ActionListener
60. {
61. public void actionPerformed(ActionEvent event)
62. {
63. Color defaultColor = getBackground();
64. Color selected = JColorChooser.showDialog(
65. ColorChooserPanel.this,
66. "Set background",
67. defaultColor);
68. if (selected != null) setBackground(selected);
69. }
70. }
71.
72. /**
73. This listener pops up a modeless color chooser.
74. The panel color is changed when the user clicks the OK
75. button.
76. */
77. private class ModelessListener implements ActionListener
78. {
79. public ModelessListener()
80. {
81. chooser = new JColorChooser();
82. dialog = JColorChooser.createDialog(
83. ColorChooserPanel.this,
84. "Background Color",
85. false /* not modal */,
86. chooser,
87. new ActionListener() // OK button listener
88. {
89. public void actionPerformed(ActionEvent event)
90. {
91. setBackground(chooser.getColor());
92. }
93. },
94. null /* no Cancel button listener */);
95. }
96.
97. public void actionPerformed(ActionEvent event)
98. {
99. chooser.setColor(getBackground());
100. dialog.setVisible(true);
101. }
102.
103. private JDialog dialog;
104. private JColorChooser chooser;
105. }
106.
107. /**
108. This listener pops up a modeless color chooser.
109. The panel color is changed immediately when the
110. user picks a new color.
111. */
112. private class ImmediateListener implements ActionListener
113. {
114. public ImmediateListener()
115. {
116. chooser = new JColorChooser();
117. chooser.getSelectionModel().addChangeListener(new
118. ChangeListener()
119. {
120. public void stateChanged(ChangeEvent event)
121. {
122. setBackground(chooser.getColor());
123. }
124. });
125.
126. dialog = new JDialog(
127. (Frame) null,
128. false /* not modal */);
129. dialog.add(chooser);
130. dialog.pack();
131. }
132.
133. public void actionPerformed(ActionEvent event)
134. {
135. chooser.setColor(getBackground());
136. dialog.setVisible(true);
137. }
138.
139. private JDialog dialog;
140. private JColorChooser chooser;
141. }
142. }
javax.swing.JColorChooser 1.2
JColorChooser()
constructs a color chooser with an initial color of white.
Color getColor()
void setColor(Color c)
get and set the current color of this color chooser.
static Color showDialog(Component parent, String title, Color initialColor)
shows a modal dialog that contains a color chooser.
Parameters:
parent
The component over which to pop up the dialog
title
The title for the dialog box frame
initialColor
The initial color to show in the color chooser
static JDialog createDialog(Component parent, String title, boolean modal, JColorChooser chooser, ActionListener okListener, ActionListener cancelListener)
creates a dialog box that contains a color chooser.
Parameters:
parent
The component over which to pop up the dialog
title
The title for the dialog box frame
modal
TRue if this call should block until the dialog is closed
chooser
The color chooser to add to the dialog
okListener, cancelListener
The listeners of the OK and Cancel buttons
--------------------------------------------------------------------------------
Lists
If you want to present a set of choices to a user, and a radio button or checkbox set consumes too much space, you can use a combo box or a list. Combo boxes were covered in Volume 1 because they are relatively simple. The JList component has many more features, and its design is similar to that of the tree and table components. For that reason, it is our starting point for the discussion of complex Swing components.
Of course, you can have lists of strings, but you can also have lists of arbitrary objects, with full control of how they appear. The internal architecture of the list component that makes this generality possible is rather elegant. Unfortunately, the designers at Sun felt that they needed to show off that elegance, rather than hiding it from the programmer who just wants to use the component. You will find that the list control is somewhat awkward to use for common cases because you need to manipulate some of the machinery that makes the general cases possible. We walk you through the simple and most common case, a list box of strings, and then give a more complex example that shows off the flexibility of the list component.
The JList Component
The JList component is similar to a set of checkboxes or radio buttons, except that the items are placed inside a single box and are selected by clicking on the items themselves, not on buttons. If you permit multiple selection for a list box, the user can select any combination of the items in the box.
Figure 6-1 shows an admittedly silly example. The user can select the attributes for the fox, such as "quick," "brown," "hungry," "wild," and, because we ran out of attributes, "static," "private," and "final." You can thus have the static final fox jump over the lazy dog.
Figure 6-1. A list box
To construct this list component, you first start out with an array of strings, then pass the array to the JList constructor:
String[] words= { "quick", "brown", "hungry", "wild", ... };
JList wordList = new JList(words);
Alternatively, you can use an anonymous array:
JList wordList = new JList(new String[] {"quick", "brown", "hungry", "wild", ... });
List boxes do not scroll automatically. To make a list box scroll, you must insert it into a scroll pane:
JScrollPane scrollPane = new JScrollPane(wordList);
You then add the scroll pane, not the list, into the surrounding panel.
We must admit that the separation of the list display and the scrolling mechanism is elegant in theory, but it is a pain in practice. Essentially all lists that we ever encountered needed scrolling. It seems cruel to force programmers to go through hoops in the default case just so they can appreciate that elegance.
By default, the list component displays eight items; use the setVisibleRowCount method to change that value:
wordList.setVisibleRowCount(4); // display 4 items
You can set the layout orientation to one of three values:
JList.VERTICAL (the default): arrange all items vertically
JList.VERTICAL_WRAP: start new columns if there are more items than the visible row count (see Figure 6-2)
Figure 6-2. Lists with vertical and horizontal wrap
[View full size image]
JList.HORIZONTAL_WRAP: start new columns if there are more items than the visible row count, but fill them horizontally. Look at the placement of the words "quick," "brown," and "hungry" in Figure 6-2 to see the difference between vertical and horizontal wrap.
By default, a user can select multiple items. This requires some knowledge of mouse technique: To add more items to a selection, press the CTRL key while clicking on each item. To select a contiguous range of items, click on the first one, then hold down the SHIFT key and click on the last one.
You can also restrict the user to a more limited selection mode with the setSelectionMode method:
wordList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION); \
// select one item at a time
wordList.setSelectionMode(ListSelectionModel.SINGLE_INTERVAL_SELECTION);
// select one item or one range of items
You may recall from Volume 1 that the basic user interface components send out action events when the user activates them. List boxes use a different notification mechanism. Rather than listening to action events, you need to listen to list selection events. Add a list selection listener to the list component, and implement the method
public void valueChanged(ListSelectionEvent evt)
in the listener.
When the user selects items, a flurry of list selection events is generated. For example, suppose the user clicks on a new item. When the mouse button goes down, an event reports a change in selection. This is a transitional eventthe call
event.isAdjusting()
returns true if the selection is not yet final. Then, when the mouse button goes up, there is another event, this time with isAdjusting returning false. If you are not interested in the transitional events, then you can wait for the event for which isAdjusting is false. However, if you want to give the user instant feedback as soon as the mouse button is clicked, then you need to process all events.
Once you are notified that an event has happened, you will want to find out what items are currently selected. The getSelectedValues method returns an array of objects containing all selected items. Cast each array element to a string.
Object[] values = list.getSelectedValues();
for (Object value : values)
do something with (String) value;
CAUTION
You cannot cast the return value of getSelectedValues from an Object[] array to a String[] array. The return value was not created as an array of strings, but as an array of objects, each of which happens to be a string. To process the return value as an array of strings, use the following code:
int length = values.length;
String[] words = new String[length];
System.arrayCopy(values, 0, words, 0, length);
If your list does not allow multiple selections, you can call the convenience method getSelectedValue. It returns the first selected value (which you know to be the only value if multiple selections are disallowed).
String value = (String) list.getSelectedValue();
NOTE
List components do not react to double clicks from a mouse. As envisioned by the designers of Swing, you use a list to select an item, and then you click a button to make something happen. However, some user interfaces allow a user to double-click on a list item as a shortcut for item selection and acceptance of a default action. If you want to implement this behavior, you have to add a mouse listener to the list box, then trap the mouse event as follows:
public void mouseClicked(MouseEvent evt)
{
if (evt.getClickCount() == 2)
{
JList source = (JList) evt.getSource();
Object[] selection = source.getSelectedValues();
doAction(selection);
}
}
Example 6-1 is the listing of the program that demonstrates a list box filled with strings. Notice how the valueChanged method builds up the message string from the selected items.
Example 6-1. ListTest.java
1. import java.awt.*;
2. import java.awt.event.*;
3. import javax.swing.*;
4. import javax.swing.event.*;
5.
6. /**
7. This program demonstrates a simple fixed list of strings.
8. */
9. public class ListTest
10. {
11. public static void main(String[] args)
12. {
13. JFrame frame = new ListFrame();
14. frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
15. frame.setVisible(true);
16. }
17. }
18.
19. /**
20. This frame contains a word list and a label that shows a
21. sentence made up from the chosen words. Note that you can
22. select multiple words with Ctrl+click and Shift+click.
23. */
24. class ListFrame extends JFrame
25. {
26. public ListFrame()
27. {
28. setTitle("ListTest");
29. setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
30.
31. String[] words =
32. {
33. "quick","brown","hungry","wild","silent",
34. "huge","private","abstract","static","final"
35. };
36.
37. wordList = new JList(words);
38. wordList.setVisibleRowCount(4);
39. JScrollPane scrollPane = new JScrollPane(wordList);
40.
41. listPanel = new JPanel();
42. listPanel.add(scrollPane);
43. wordList.addListSelectionListener(new
44. ListSelectionListener()
45. {
46. public void valueChanged(ListSelectionEvent event)
47. {
48. Object[] values = wordList.getSelectedValues();
49.
50. StringBuilder text = new StringBuilder(prefix);
51. for (int i = 0; i < values.length; i++)
52. {
53. String word = (String) values[i];
54. text.append(word);
55. text.append(" ");
56. }
57. text.append(suffix);
58.
59. label.setText(text.toString());
60. }
61. });
62.
63. buttonPanel = new JPanel();
64. group = new ButtonGroup();
65. makeButton("Vertical", JList.VERTICAL);
66. makeButton("Vertical Wrap", JList.VERTICAL_WRAP);
67. makeButton("Horizontal Wrap", JList.HORIZONTAL_WRAP);
68.
69. add(listPanel, BorderLayout.NORTH);
70. label = new JLabel(prefix + suffix);
71. add(label, BorderLayout.CENTER);
72. add(buttonPanel, BorderLayout.SOUTH);
73. }
74.
75. /**
76. Makes a radio button to set the layout orientation.
77. @param label the button label
78. @param orientation the orientation for the list
79. */
80. private void makeButton(String label, final int orientation)
81. {
82. JRadioButton button = new JRadioButton(label);
83. buttonPanel.add(button);
84. if (group.getButtonCount() == 0) button.setSelected(true);
85. group.add(button);
86. button.addActionListener(new
87. ActionListener()
88. {
89. public void actionPerformed(ActionEvent event)
90. {
91. wordList.setLayoutOrientation(orientation);
92. listPanel.revalidate();
93. }
94. });
95. }
96.
97. private static final int DEFAULT_WIDTH = 400;
98. private static final int DEFAULT_HEIGHT = 300;
99. private JPanel listPanel;
100. private JList wordList;
101. private JLabel label;
102. private JPanel buttonPanel;
103. private ButtonGroup group;
104. private String prefix = "The ";
105. private String suffix = "fox jumps over the lazy dog.";
106. }
javax.swing.JList 1.2
JList(Object[] items)
constructs a list that displays these items.
int getVisibleRowCount()
void setVisibleRowCount(int c)
get or set the preferred number of rows in the list that can be displayed without a scroll bar.
int getLayoutOrientation() 1.4
void setLayoutOrientation(int orientation) 1.4
get or set the layout orientation
Parameters:
orientation
One of VERTICAL, VERTICAL_WRAP, HORIZONTAL_WRAP
int getSelectionMode()
void setSelectionMode(int mode)
get or set the mode that determines whether single-item or multiple-item selections are allowed.
Parameters:
mode
One of SINGLE_SELECTION, SINGLE_INTERVAL_SELECTION, MULTIPLE_INTERVAL_SELECTION
void addListSelectionListener(ListSelectionListener listener)
adds to the list a listener that's notified each time a change to the selection occurs.
Object[] getSelectedValues()
returns the selected values or an empty array if the selection is empty.
Object getSelectedValue()
returns the first selected value or null if the selection is empty.
javax.swing.event.ListSelectionListener 1.2
void valueChanged(ListSelectionEvent e)
is called whenever the list selection changes.
List Models
In the preceding section, you saw the most common method for using a list component:
Specify a fixed set of strings for display in the list,
Place the list inside a scroll pane, and
Trap the list selection events.
In the remainder of the section on lists, we cover more complex situations that require a bit more finesse:
Very long lists
Lists with changing contents
Lists that don't contain strings
In the first example, we constructed a JList component that held a fixed collection of strings. However, the collection of choices in a list box is not always fixed. How do we add or remove items in the list box? Somewhat surprisingly, there are no methods in the JList class to achieve this. Instead, you have to understand a little more about the internal design of the list component. As with text components, the list component uses the model-view-controller design pattern to separate the visual appearance (a column of items that are rendered in some way) from the underlying data (a collection of objects).
The JList class is responsible for the visual appearance of the data. It actually knows very little about how the data are storedall it knows is that it can retrieve the data through some object that implements the ListModel interface:
public interface ListModel
{
int getSize();
Object getElementAt(int i);
void addListDataListener(ListDataListener l);
void removeListDataListener(ListDataListener l);
}
Through this interface, the JList can get a count of elements and retrieve each one of the elements. Also, the JList object can add itself as a list data listener. That way, if the collection of elements changes, JList gets notified so that it can repaint the list.
Why is this generality useful? Why doesn't the JList object simply store a vector of objects?
Note that the interface doesn't specify how the objects are stored. In particular, it doesn't force them to be stored at all! The getElementAt method is free to recompute each value whenever it is called. This is potentially useful if you want to show a very large collection without having to store the values.
Here is a somewhat silly example: We let the user choose among all three-letter words in a list box (see Figure 6-3).
Figure 6-3. Choosing from a very long list of selections
There are 26 x 26 x 26 = 17,576 three-letter combinations. Rather than storing all these combinations, we recompute them as requested when the user scrolls through them.
This turns out to be easy to implement. The tedious part, adding and removing listeners, has been done for us in the AbstractListModel class, which we extend. We only need to supply the getSize and getElementAt methods:
class WordListModel extends AbstractListModel
{
public WordListModel(int n) { length = n; }
public int getSize() { return (int) Math.pow(26, length); }
public Object getElementAt(int n)
{
// compute nth string
. . .
}
. . .
}
The computation of the nth string is a bit technicalyou'll find the details in the code listing in Example 6-2.
Now that we have supplied a model, we can simply build a list that lets the user scroll through the elements supplied by the model:
JList wordList = new JList(new WordListModel(3));
wordList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
JScrollPane scrollPane = new JScrollPane(wordList);
The point is that the strings are never stored. Only those strings that the user actually requests to see are generated.
We must make one other setting. We must tell the list component that all items have a fixed width and height. The easiest way to set the cell dimensions is to specify a prototype cell value:
wordList.setPrototypeCellValue("www");
The prototype cell value is used to determine the size for all cells. Alternatively, you can set a fixed cell size:
wordList.setFixedCellWidth(50);
wordList.setFixedCellHeight(15);
If you don't set a prototype value or a fixed cell size, the list component computes the width and height of each item. That would take a long time.
As a practical matter, very long lists are rarely useful. It is extremely cumbersome for a user to scroll through a huge selection. For that reason, we believe that the list control has been completely overengineered. A selection that a user can comfortably manage on the screen is certainly small enough to be stored directly in the list component. That arrangement would have saved programmers from the pain of having to deal with the list model as a separate entity. On the other hand, the JList class is consistent with the Jtree and JTable class where this generality is useful.
Example 6-2. LongListTest.java
1. import java.awt.*;
2. import java.awt.event.*;
3. import javax.swing.*;
4. import javax.swing.event.*;
5.
6. /**
7. This program demonstrates a list that dynamically computes
8. list entries.
9. */
10. public class LongListTest
11. {
12. public static void main(String[] args)
13. {
14. JFrame frame = new LongListFrame();
15. frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
16. frame.setVisible(true);
17. }
18. }
19.
20. /**
21. This frame contains a long word list and a label that shows a
22. sentence made up from the chosen word.
23. */
24. class LongListFrame extends JFrame
25. {
26. public LongListFrame()
27. {
28. setTitle("LongListTest");
29. setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
30.
31. wordList = new JList(new WordListModel(3));
32. wordList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
33. wordList.setPrototypeCellValue("www");
34. JScrollPane scrollPane = new JScrollPane(wordList);
35.
36. JPanel p = new JPanel();
37. p.add(scrollPane);
38. wordList.addListSelectionListener(new
39. ListSelectionListener()
40. {
41. public void valueChanged(ListSelectionEvent evt)
42. {
43. StringBuilder word = (StringBuilder) wordList.getSelectedValue();
44. setSubject(word.toString());
45. }
46.
47. });
48.
49. Container contentPane = getContentPane();
50. contentPane.add(p, BorderLayout.NORTH);
51. label = new JLabel(prefix + suffix);
52. contentPane.add(label, BorderLayout.CENTER);
53. setSubject("fox");
54. }
55.
56. /**
57. Sets the subject in the label.
58. @param word the new subject that jumps over the lazy dog
59. */
60. public void setSubject(String word)
61. {
62. StringBuilder text = new StringBuilder(prefix);
63. text.append(word);
64. text.append(suffix);
65. label.setText(text.toString());
66. }
67.
68. private static final int DEFAULT_WIDTH = 400;
69. private static final int DEFAULT_HEIGHT = 300;
70. private JList wordList;
71. private JLabel label;
72. private String prefix = "The quick brown ";
73. private String suffix = " jumps over the lazy dog.";
74. }
75.
76. /**
77. A model that dynamically generates n-letter words.
78. */
79. class WordListModel extends AbstractListModel
80. {
81. /**
82. Constructs the model.
83. @param n the word length
84. */
85. public WordListModel(int n) { length = n; }
86.
87. public int getSize()
88. {
89. return (int) Math.pow(LAST - FIRST + 1, length);
90. }
91.
92. public Object getElementAt(int n)
93. {
94. StringBuilder r = new StringBuilder();;
95. for (int i = 0; i < length; i++)
96. {
97. char c = (char)(FIRST + n % (LAST - FIRST + 1));
98. r.insert(0, c);
99. n = n / (LAST - FIRST + 1);
100. }
101. return r;
102. }
103.
104. private int length;
105. public static final char FIRST = 'a';
106. public static final char LAST = 'z';
107. }
javax.swing.JList 1.2
JList(ListModel dataModel)
constructs a list that displays the elements in the specified model.
void setPrototypeCellValue(Object newValue)
Object getPrototypeCellValue()
set or get the prototype cell value that is used to determine the width and height of each cell in the list. The default is null, which forces the size of each cell to be measured.
void setFixedCellWidth(int width)
if the width is greater than zero, specifies the width of every cell in the list. The default value is -1, which forces the size of each cell to be measured.
void setFixedCellHeight(int height)
if the height is greater than zero, specifies the height of every cell in the list. The default value is -1, which forces the size of each cell to be measured.
javax.swing.ListModel 1.2
int getSize()
returns the number of elements of the model.
Object getElementAt(int position)
returns an element of the model at the given position.
Inserting and Removing Values
You cannot directly edit the collection of list values. Instead, you must access the model and then add or remove elements. That, too, is easier said than done. Suppose you want to add more values to a list. You can obtain a reference to the model:
ListModel model = list.getModel();
But that does you no goodas you saw in the preceding section, the ListModel interface has no methods to insert or remove elements because, after all, the whole point of having a list model is that it need not store the elements.
Let's try it the other way around. One of the constructors of JList takes a vector of objects:
Vector<String> values = new Vector<String>();
values.addElement("quick");
values.addElement("brown");
. . .
JList list = new JList(values);
You can now edit the vector and add or remove elements, but the list does not know that this is happening, so it cannot react to the changes. In particular, the list cannot update its view when you add the values. Therefore, this constructor is not very useful.
Instead, you should construct a DefaultListModel object, fill it with the initial values, and associate it with the list.
DefaultListModel model = new DefaultListModel();
model.addElement("quick");
model.addElement("brown");
. . .
JList list = new JList(model);
Now you can add or remove values from the model object. The model object then notifies the list of the changes, and the list repaints itself.
model.removeElement("quick");
model.addElement("slow");
For historical reasons, the DefaultListModel class doesn't use the same method names as the collection classes.
The default list model uses a vector internally to store the values.
CAUTION
There are JList constructors that construct a list from an array or vector of objects or strings. You might think that these constructors use a DefaultListModel to store these values. That is not the casethe constructors build a trivial model that can access the values without any provisions for notification if the content changes. For example, here is the code for the constructor that constructs a JList from a Vector:
public JList(final Vector<?> listData)
{
this (new AbstractListModel()
{
public int getSize() { return listData.size(); }
public Object getElementAt(int i) { return listData.elementAt(i); }
});
}
That means, if you change the contents of the vector after the list is constructed, then the list may show a confusing mix of old and new values until it is completely repainted. (The keyword final in the constructor above does not prevent you from changing the vector elsewhereit only means that the constructor itself won't modify the value of the listData reference; the keyword is required because the listData object is used in the inner class.)
javax.swing.JList 1.2
ListModel getModel()
gets the model of this list.
javax.swing.DefaultListModel 1.2
void addElement(Object obj)
adds the object to the end of the model.
boolean removeElement(Object obj)
removes the first occurrence of the object from the model. Returns true if the object was contained in the model, false otherwise.
Rendering Values
So far, all lists that you saw in this chapter contained only strings. It is actually just as easy to show a list of iconssimply pass an array or vector filled with Icon objects. More interestingly, you can easily represent your list values with any drawing whatsoever.
Although the JList class can display strings and icons automatically, you need to install a list cell renderer into the JList object for all custom drawing. A list cell renderer is any class that implements the following interface:
interface ListCellRenderer
{
Component getListCellRendererComponent(JList list, Object value, int index,
boolean isSelected, boolean cellHasFocus);
}
This method is called for each cell. It returns a component that paints the cell contents. The component is placed at the appropriate location whenever a cell needs to be rendered.
One way to implement a cell renderer is to create a class that extends JPanel, like this:
class MyCellRenderer extends JPanel implements ListCellRenderer
{
public Component getListCellRendererComponent(JList list, Object value, int index,
boolean isSelected, boolean cellHasFocus)
{
// stash away information that is needed for painting and size measurement
return this;
}
public void paintComponent(Graphics g)
{
// paint code goes here
}
public Dimension getPreferredSize()
{
// size measurement code goes here
}
// instance fields
}
In Example 6-3, we display the font choices graphically by showing the actual appearance of each font (see Figure 6-4). In the paintComponent method, we display each name in its own font. We also need to make sure to match the usual colors of the look and feel of the JList class. We obtain these colors by calling the getForeground/getBackground and getSelectionForeground/getSelectionBackground methods of the JList class. In the getPreferredSize method, we need to measure the size of the string, using the techniques that you saw in Volume 1, Chapter 7.
Figure 6-4. A list box with rendered cells
To install the cell renderer, simply call the setCellRenderer method:
fontList.setCellRenderer(new FontCellRenderer());
Now all list cells are drawn with the custom renderer.
Actually, a simpler method for writing custom renderers works in many cases. If the rendered image just contains text, an icon, and possibly a change of color, then you can get by with configuring a JLabel. For example, to show the font name in its own font, we can use the following renderer:
class FontCellRenderer extends JLabel implements ListCellRenderer
{
public Component getListCellRendererComponent(JList list, Object value, int index,
boolean isSelected, boolean cellHasFocus)
{
JLabel label = new JLabel();
Font font = (Font) value;
setText(font.getFamily());
setFont(font);
setOpaque(true);
setBackground(isSelected ? list.getSelectionBackground() : list.getBackground());
setForeground(isSelected ? list.getSelectionForeground() : list.getForeground());
return this;
}
}
Note that here we don't write any paintComponent or getPreferredSize methods; the JLabel class already implements these methods to our satisfaction. All we do is configure the label appropriately by setting its text, font, and color.
This code is a convenient shortcut for those cases in which an existing componentin this case, JLabelalready provides all functionality needed to render a cell value.
CAUTION
It is not a good idea to construct a new component in each call to getListCellRendererComponent. If the user scrolls through many list entries, a new component would be constructed every time. Reconfiguring an existing component is safe and much more efficient.
Example 6-3. ListRenderingTest.java
1. import java.util.*;
2. import java.awt.*;
3. import java.awt.event.*;
4. import javax.swing.*;
5. import javax.swing.event.*;
6.
7. /**
8. This program demonstrates the use of cell renderers in
9. a list box.
10. */
11. public class ListRenderingTest
12. {
13. public static void main(String[] args)
14. {
15. JFrame frame = new ListRenderingFrame();
16. frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
17. frame.setVisible(true);
18. }
19. }
20.
21. /**
22. This frame contains a list with a set of fonts and a text
23. area that is set to the selected font.
24. */
25. class ListRenderingFrame extends JFrame
26. {
27. public ListRenderingFrame()
28. {
29. setTitle("ListRenderingTest");
30. setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
31.
32. ArrayList<Font> fonts = new ArrayList<Font>();
33. final int SIZE = 24;
34. fonts.add(new Font("Serif", Font.PLAIN, SIZE));
35. fonts.add(new Font("SansSerif", Font.PLAIN, SIZE));
36. fonts.add(new Font("Monospaced", Font.PLAIN, SIZE));
37. fonts.add(new Font("Dialog", Font.PLAIN, SIZE));
38. fonts.add(new Font("DialogInput", Font.PLAIN, SIZE));
39. fontList = new JList(fonts.toArray());
40. fontList.setVisibleRowCount(4);
41. fontList.setSelectionMode(ListSelectionModel.SINGLE_SELECTION);
42. fontList.setCellRenderer(new FontCellRenderer());
43. JScrollPane scrollPane = new JScrollPane(fontList);
44.
45. JPanel p = new JPanel();
46. p.add(scrollPane);
47. fontList.addListSelectionListener(new
48. ListSelectionListener()
49. {
50. public void valueChanged(ListSelectionEvent evt)
51. {
52. Font font = (Font) fontList.getSelectedValue();
53. text.setFont(font);
54. }
55.
56. });
57.
58. Container contentPane = getContentPane();
59. contentPane.add(p, BorderLayout.SOUTH);
60. text = new JTextArea("The quick brown fox jumps over the lazy dog");
61. text.setFont((Font) fonts.get(0));
62. text.setLineWrap(true);
63. text.setWrapStyleWord(true);
64. contentPane.add(text, BorderLayout.CENTER);
65. }
66.
67. private JTextArea text;
68. private JList fontList;
69. private static final int DEFAULT_WIDTH = 400;
70. private static final int DEFAULT_HEIGHT = 300;
71. }
72.
73. /**
74. A cell renderer for Font objects that renders the font name in its own font.
75. */
76. class FontCellRenderer extends JPanel implements ListCellRenderer
77. {
78. public Component getListCellRendererComponent(JList list, Object value, int index,
79. boolean isSelected, boolean cellHasFocus)
80. {
81. font = (Font) value;
82. background = isSelected ? list.getSelectionBackground() : list.getBackground();
83. foreground = isSelected ? list.getSelectionForeground() : list.getForeground();
84. return this;
85. }
86.
87. public void paintComponent(Graphics g)
88. {
89. String text = font.getFamily();
90. FontMetrics fm = g.getFontMetrics(font);
91. g.setColor(background);
92. g.fillRect(0, 0, getWidth(), getHeight());
93. g.setColor(foreground);
94. g.setFont(font);
95. g.drawString(text, 0, fm.getAscent());
96. }
97.
98. public Dimension getPreferredSize()
99. {
100. String text = font.getFamily();
101. Graphics g = getGraphics();
102. FontMetrics fm = g.getFontMetrics(font);
103. return new Dimension(fm.stringWidth(text), fm.getHeight());
104. }
105.
106. private Font font;
107. private Color background;
108. private Color foreground;
109. }
javax.swing.JList 1.2
Color getBackground()
returns the background color for unselected cells.
Color getSelectionBackground()
returns the background color for selected cells.
Color getForeground()
returns the foreground color for unselected cells.
Color getSelectionForeground()
returns the foreground color for selected cells.
void setCellRenderer(ListCellRenderer cellRenderer)
sets the renderer that paints the cells in the list.
javax.swing.ListCellRenderer 1.2
Component getListCellRendererComponent(JList list, Object item, int index, boolean isSelected, boolean hasFocus)
returns a component whose paint method draws the cell contents. If the list cells do not have fixed size, that component must also implement getPreferredSize.
Parameters:
list
The list whose cell is being drawn
item
The item to be drawn
index
The index where the item is stored in the model
isSelected
TRue if the specified cell was selected
hasFocus
true if the specified cell has the focus
Trees
Every computer user who uses a hierarchical file system has encountered tree displays such as the one in Figure 6-5. Of course, directories and files form only one of the many examples of treelike organizations. Programmers are familiar with inheritance trees for classes. Many tree structures arise in everyday life, such as the hierarchy of countries, states, and cities shown in Figure 6-6.
Figure 6-5. A directory tree
Figure 6-6. A hierarchy of countries, states, and cities
[View full size image]
As programmers, we often have to display these tree structures. Fortunately, the Swing library has a JTRee class for this purpose. The Jtree class (together with its helper classes) takes care of laying out the tree and processing user requests for expanding and collapsing nodes. In this section, you will learn how to put the Jtree class to use.
As with the other complex Swing components, we must focus on the common and useful cases and cannot cover every nuance. If you want to achieve an unusual effect, we recommend that you consult Graphic Java 2 by David M. Geary [Prentice-Hall 1999], Core Java Foundation Classes by Kim Topley [Prentice-Hall 1998], or Core Swing: Advanced Programming by Kim Topley [Prentice-Hall 1999].
Before going any further, let's settle on some terminology (see Figure 6-7). A tree is composed of nodes. Every node is either a leaf or it has child nodes. Every node, with the exception of the root node, has exactly one parent. A tree has exactly one root node. Sometimes you have a collection of trees, each of which has its own root node. Such a collection is called a forest.
Figure 6-7. Tree terminology
Simple Trees
In our first example program, we simply display a tree with a few nodes (see Figure 6-9 on page 340). As with most other Swing components, the Jtree component follows the model-view-controller pattern. You provide a model of the hierarchical data, and the component displays it for you. To construct a Jtree, you supply the tree model in the constructor:
TreeModel model = . . .;
JTree tree = new JTree(model);
Figure 6-9. A simple tree
NOTE
There are also constructors that construct trees out of a collection of elements:
JTree(Object[] nodes)
JTree(Vector<?> nodes)
JTree(Hashtable<?, ?> nodes) // the values become the nodes
These constructors are not very useful. They merely build a forest of trees, each with a single node. The third constructor seems particularly useless since the nodes appear in the essentially random order given by the hash codes of the keys.
How do you obtain a tree model? You can construct your own model by creating a class that implements the treeModel interface. You see later in this chapter how to do that. For now, we stick with the DefaultTreeModel that the Swing library supplies.
To construct a default tree model, you must supply a root node.
TreeNode root = . . .;
DefaultTreeModel model = new DefaultTreeModel(root);
treeNode is another interface. You populate the default tree model with objects of any class that implements the interface. For now, we use the concrete node class that Swing supplies, namely, DefaultMutableTreeNode. This class implements the MutableTreeNode interface, a subinterface of TReeNode (see Figure 6-8).
Figure 6-8. Tree classes
A default mutable tree node holds an object, the user object. The tree renders the user objects for all nodes. Unless you specify a renderer, the tree simply displays the string that is the result of the toString method.
In our first example, we use strings as user objects. In practice, you would usually populate a tree with more expressive user objects. For example, when displaying a directory tree, it makes sense to use File objects for the nodes.
You can specify the user object in the constructor, or you can set it later with the setUserObject method.
DefaultMutableTreeNode node = new DefaultMutableTreeNode("Texas");
node.setUserObject("California");
Next, you establish the parent/child relationships between the nodes. Start with the root node, and use the add method to add the children:
DefaultMutableTreeNode root = new DefaultMutableTreeNode("World");
DefaultMutableTreeNode country = new DefaultMutableTreeNode("USA");
root.add(country);
DefaultMutableTreeNode state = new DefaultMutableTreeNode("California");
country.add(state);
Figure 6-9 illustrates how the tree will look.
Link up all nodes in this fashion. Then, construct a DefaultTreeModel with the root node. Finally, construct a Jtree with the tree model.
DefaultTreeModel treeModel = new DefaultTreeModel(root);
JTree tree = new JTree(treeModel);
Or, as a shortcut, you can simply pass the root node to the Jtree constructor. Then the tree automatically constructs a default tree model:
JTree tree = new JTree(root);
Example 6-4 contains the complete code.
Example 6-4. SimpleTree.java
1. import java.awt.*;
2. import java.awt.event.*;
3. import javax.swing.*;
4. import javax.swing.tree.*;
5.
6. /**
7. This program shows a simple tree.
8. */
9. public class SimpleTree
10. {
11. public static void main(String[] args)
12. {
13. JFrame frame = new SimpleTreeFrame();
14. frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
15. frame.setVisible(true);
16. }
17. }
18.
19. /**
20. This frame contains a simple tree that displays a
21. manually constructed tree model.
22. */
23. class SimpleTreeFrame extends JFrame
24. {
25. public SimpleTreeFrame()
26. {
27. setTitle("SimpleTree");
28. setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
29.
30. // set up tree model data
31.
32. DefaultMutableTreeNode root
33. = new DefaultMutableTreeNode("World");
34. DefaultMutableTreeNode country
35. = new DefaultMutableTreeNode("USA");
36. root.add(country);
37. DefaultMutableTreeNode state
38. = new DefaultMutableTreeNode("California");
39. country.add(state);
40. DefaultMutableTreeNode city
41. = new DefaultMutableTreeNode("San Jose");
42. state.add(city);
43. city = new DefaultMutableTreeNode("Cupertino");
44. state.add(city);
45. state = new DefaultMutableTreeNode("Michigan");
46. country.add(state);
47. city = new DefaultMutableTreeNode("Ann Arbor");
48. state.add(city);
49. country = new DefaultMutableTreeNode("Germany");
50. root.add(country);
51. state = new DefaultMutableTreeNode("Schleswig-Holstein");
52. country.add(state);
53. city = new DefaultMutableTreeNode("Kiel");
54. state.add(city);
55.
56. // construct tree and put it in a scroll pane
57.
58. JTree tree = new JTree(root);
59. Container contentPane = getContentPane();
60. contentPane.add(new JScrollPane(tree));
61. }
62.
63. private static final int DEFAULT_WIDTH = 300;
64. private static final int DEFAULT_HEIGHT = 200;
65. }
When you run the program, the tree first looks as in Figure 6-10. Only the root node and its children are visible. Click on the circle icons (the handles) to open up the subtrees. The line sticking out from the handle icon points to the right when the subtree is collapsed, and it points down when the subtree is expanded (see Figure 6-11). We don't know what the designers of the Metal look and feel had in mind, but we think of the icon as a door handle. You push down on the handle to open the subtree.
Figure 6-10. The initial tree display
Figure 6-11. Collapsed and expanded subtrees
NOTE
Of course, the display of the tree depends on the selected look and feel. We just described the Metal look and feel. In the Windows and Motif look and feel, the handles have the more familiar looka "-" or "+" in a box (see Figure 6-12).
Figure 6-12. A tree with the Windows look and feel
Up to JDK 1.3, the Metal look and feel does not display the tree outline by default (see Figure 6-13). As of JDK 1.4, the default line style is "angled."
Figure 6-13. A tree with no connecting lines
In JDK 1.4, use the following magic incantation to turn off the lines joining parents and children:
tree.putClientProperty("JTree.lineStyle", "None");
Conversely, to make sure that the lines are shown, use
tree.putClientProperty("JTree.lineStyle", "Angled");
Another line style, "Horizontal", is shown in Figure 6-14. The tree is displayed with horizontal lines separating only the children of the root. We aren't quite sure what it is good for.
Figure 6-14. A tree with the horizontal line style
By default, there is no handle for collapsing the root of the tree. If you like, you can add one with the call
tree.setShowsRootHandles(true);
Figure 6-15 shows the result. Now you can collapse the entire tree into the root node.
Figure 6-15. A tree with a root handle
Conversely, you can hide the root altogether. You do that to display a forest, a set of trees, each of which has its own root. You still must join all trees in the forest to a common root. Then, you hide the root with the instruction
tree.setRootVisible(false);
Look at Figure 6-16. There appear to be two roots, labeled "USA" and "Germany." The actual root that joins the two is made invisible.
Figure 6-16. A forest
Let's turn from the root to the leaves of the tree. Note that the leaves have a different icon from the other nodes (see Figure 6-17).
Figure 6-17. Leaf and folder icons
When the tree is displayed, each node is drawn with an icon. There are actually three kinds of icons: a leaf icon, an opened non-leaf icon, and a closed non-leaf icon. For simplicity, we refer to the last two as folder icons.
The node renderer needs to know which icon to use for each node. By default, the decision process works like this: If the isLeaf method of a node returns true, then the leaf icon is used. Otherwise, a folder icon is used.
The isLeaf method of the DefaultMutableTreeNode class returns TRue if the node has no children. Thus, nodes with children get folder icons, and nodes without children get leaf icons.
Sometimes, that behavior is not appropriate. Suppose we added a node "Montana" to our sample tree, but we're at a loss as to what cities to add. We would not want the state node to get a leaf icon because conceptually only the cities are leaves.
The Jtree class has no idea which nodes should be leaves. It asks the tree model. If a childless node isn't automatically a conceptual leaf, you can ask the tree model to use a different criterion for leafiness, namely, to query the "allows children" node property.
For those nodes that should not have children, call
node.setAllowsChildren(false);
Then, tell the tree model to ask the value of the "allows children" property to determine whether a node should be displayed with a leaf icon. You use the setAsksAllowsChildren method of the DefaultTreeModel class to set this behavior:
model.setAsksAllowsChildren(true);
With this decision criterion, nodes that allow children get folder icons, and nodes that don't allow children get leaf icons.
Alternatively, if you construct the tree by supplying the root node, supply the setting for the "asks allows children" property in the constructor.
JTree tree = new JTree(root, true); // nodes that don't allow children get leaf icons
javax.swing.JTree 1.2
Jtree(TreeModel model)
constructs a tree from a tree model.
Jtree(TreeNode root)
Jtree(TreeNode root, boolean asksAllowChildren)
construct a tree with a default tree model that displays the root and its children.
Parameters:
root
The root node
asksAllowsChildren
true to use the "allows children" node property for determining whether a node is a leaf
void setShowsRootHandles(boolean b)
If b is true, then the root node has a handle for collapsing or expanding its children.
void setRootVisible(boolean b)
If b is true, then the root node is displayed. Otherwise, it is hidden.
javax.swing.tree.TreeNode 1.2
boolean isLeaf()
returns true if this node is conceptually a leaf.
boolean getAllowsChildren()
returns true if this node can have child nodes.
javax.swing.tree.MutableTreeNode 1.2
void setUserObject(Object userObject)
sets the "user object" that the tree node uses for rendering.
javax.swing.tree.TreeModel 1.2
boolean isLeaf(Object node)
returns TRue if node should be displayed as a leaf node.
javax.swing.tree.DefaultTreeModel 1.2
void setAsksAllowsChildren(boolean b)
If b is true, then nodes are displayed as leaves when their getAllowsChildren method returns false. Otherwise, they are displayed as leaves when their isLeaf method returns true.
javax.swing.tree.DefaultMutableTreeNode 1.2
DefaultMutableTreeNode(Object userObject)
constructs a mutable tree node with the given user object.
void add(MutableTreeNode child)
adds a node as the last child of this node.
void setAllowsChildren(boolean b)
If b is true, then children can be added to this node.
javax.swing.JComponent 1.2
void putClientProperty(Object key, Object value)
adds a key/value pair to a small table that each component manages. This is an "escape hatch" mechanism that some Swing components use for storing look-and-feelspecific properties.
Editing Trees and Tree Paths
In the next example program, you see how to edit a tree. Figure 6-18 shows the user interface. If you click the Add Sibling or Add Child button, the program adds a new node (with title New) to the tree. If you click the Delete button, the program deletes the currently selected node.
Figure 6-18. Editing a tree
To implement this behavior, you need to find out which tree node is currently selected. The JTRee class has a surprising way of identifying nodes in a tree. It does not deal with tree nodes, but with paths of objects, called tree paths. A tree path starts at the root and consists of a sequence of child nodessee Figure 6-19.
Figure 6-19. A tree path
You may wonder why the Jtree class needs the whole path. Couldn't it just get a treeNode and keep calling the getParent method? In fact, the Jtree class knows nothing about the TReeNode interface. That interface is never used by the treeModel interface; it is only used by the DefaultTreeModel implementation. You can have other tree models in which the nodes do not implement the treeNode interface at all. If you use a tree model that manages other types of objects, then those objects may not have getParent and getChild methods. They would of course need to have some other connection to each other. It is the job of the tree model to link nodes together. The JTRee class itself has no clue about the nature of their linkage. For that reason, the Jtree class always needs to work with complete paths.
The treePath class manages a sequence of Object (not treeNode!) references. A number of Jtree methods return treePath objects. When you have a tree path, you usually just need to know the terminal node, which you get with the getLastPathComponent method. For example, to find out the currently selected node in a tree, you use the getSelectionPath method of the Jtree class. You get a treePath object back, from which you can retrieve the actual node.
TreePath selectionPath = tree.getSelectionPath();
DefaultMutableTreeNode selectedNode
= (DefaultMutableTreeNode) selectionPath.getLastPathComponent();
Actually, because this particular query is so common, there is a convenience method that gives the selected node immediately.
DefaultMutableTreeNode selectedNode
= (DefaultMutableTreeNode) tree.getLastSelectedPathComponent();
This method is not called getSelectedNode because the tree does not know that it contains nodesits tree model deals only with paths of objects.
NOTE
Tree paths are one of two ways in which the JTRee class describes nodes. Quite a few Jtree methods take or return an integer index, the row position. A row position is simply the row number (starting with 0) of the node in the tree display. Only visible nodes have row numbers, and the row number of a node changes if other nodes before it are expanded, collapsed, or modified. For that reason, you should avoid row positions. All Jtree methods that use rows have equivalents that use tree paths instead.
Once you have the selected node, you can edit it. However, do not simply add children to a tree node:
selectedNode.add(newNode); // NO!
If you change the structure of the nodes, you change the model but the associated view is not notified. You could send out a notification yourself, but if you use the insertNodeInto method of the DefaultTreeModel class, the model class takes care of that. For example, the following call appends a new node as the last child of the selected node and notifies the tree view.
model.insertNodeInto(newNode, selectedNode, selectedNode.getChildCount());
The analogous call removeNodeFromParent removes a node and notifies the view:
model.removeNodeFromParent(selectedNode);
If you keep the node structure in place but you changed the user object, you should call the following method:
model.nodeChanged(changedNode);
The automatic notification is a major advantage of using the DefaultTreeModel. If you supply your own tree model, you have to implement automatic notification by hand. (See Core Java Foundation Classes by Kim Topley for details.)
CAUTION
The DefaultTreeModel class has a reload method that reloads the entire model. However, don't call reload simply to update the tree after making a few changes. When the tree is regenerated, all nodes beyond the root's children are collapsed again. It is quite disconcerting to your users if they have to keep expanding the tree after every change.
When the view is notified of a change in the node structure, it updates the display but it does not automatically expand a node to show newly added children. In particular, if a user in our sample program adds a new child node to a node whose children are currently collapsed, then the new node is silently added to the collapsed subtree. This gives the user no feedback that the command was actually carried out. In such a case, you should make a special effort to expand all parent nodes so that the newly added node becomes visible. You use the makeVisible method of the JTRee class for this purpose. The makeVisible method expects a tree path leading to the node that should become visible.
Thus, you need to construct a tree path from the root to the newly inserted node. To get a tree path, you first call the getPathToRoot method of the DefaultTreeModel class. It returns a TReeNode[] array of all nodes from a node to the root node. You pass that array to a treePath constructor.
For example, here is how you make the new node visible:
TreeNode[] nodes = model.getPathToRoot(newNode);
TreePath path = new TreePath(nodes);
tree.makeVisible(path);
NOTE
It is curious that the DefaultTreeModel class feigns almost complete ignorance about the treePath class, even though its job is to communicate with a Jtree. The Jtree class uses tree paths a lot, and it never uses arrays of node objects.
But now suppose your tree is contained inside a scroll pane. After the tree node expansion, the new node may still not be visible because it falls outside the viewport. To overcome that problem, call
tree.scrollPathToVisible(path);
instead of calling makeVisible. This call expands all nodes along the path, and it tells the ambient scroll pane to scroll the node at the end of the path into view (see Figure 6-20).
Figure 6-20. The scroll pane scrolls to display a new node
By default, tree nodes cannot be edited. However, if you call
tree.setEditable(true);
then the user can edit a node simply by double-clicking, editing the string, and pressing the ENTER key. Double-clicking invokes the default cell editor, which is implemented by the DefaultCellEditor class (see Figure 6-21). It is possible to install other cell editors, but we defer our discussion of cell editors until the section on tables, where cell editors are more commonly used.
Figure 6-21. The default cell editor
Example 6-5 shows the complete source code of the tree editing program. Run the program, add a few nodes, and edit them by double-clicking them. Observe how collapsed nodes expand to show added children and how the scroll pane keeps added nodes in the viewport.
Example 6-5. TreeEditTest.java
1. import java.awt.*;
2. import java.awt.event.*;
3. import javax.swing.*;
4. import javax.swing.tree.*;
5.
6. /**
7. This program demonstrates tree editing.
8. */
9. public class TreeEditTest
10. {
11. public static void main(String[] args)
12. {
13. JFrame frame = new TreeEditFrame();
14. frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
15. frame.setVisible(true);
16. }
17. }
18.
19. /**
20. A frame with a tree and buttons to edit the tree.
21. */
22. class TreeEditFrame extends JFrame
23. {
24. public TreeEditFrame()
25. {
26. setTitle("TreeEditTest");
27. setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
28.
29. // construct tree
30.
31. TreeNode root = makeSampleTree();
32. model = new DefaultTreeModel(root);
33. tree = new JTree(model);
34. tree.setEditable(true);
35.
36. // add scroll pane with tree
37.
38. JScrollPane scrollPane = new JScrollPane(tree);
39. add(scrollPane, BorderLayout.CENTER);
40.
41. makeButtons();
42. }
43.
44. public TreeNode makeSampleTree()
45. {
46. DefaultMutableTreeNode root = new DefaultMutableTreeNode("World");
47. DefaultMutableTreeNode country = new DefaultMutableTreeNode("USA");
48. root.add(country);
49. DefaultMutableTreeNode state = new DefaultMutableTreeNode("California");
50. country.add(state);
51. DefaultMutableTreeNode city = new DefaultMutableTreeNode("San Jose");
52. state.add(city);
53. city = new DefaultMutableTreeNode("San Diego");
54. state.add(city);
55. state = new DefaultMutableTreeNode("Michigan");
56. country.add(state);
57. city = new DefaultMutableTreeNode("Ann Arbor");
58. state.add(city);
59. country = new DefaultMutableTreeNode("Germany");
60. root.add(country);
61. state = new DefaultMutableTreeNode("Schleswig-Holstein");
62. country.add(state);
63. city = new DefaultMutableTreeNode("Kiel");
64. state.add(city);
65. return root;
66. }
67.
68. /**
69. Makes the buttons to add a sibling, add a child, and
70. delete a node.
71. */
72. public void makeButtons()
73. {
74. JPanel panel = new JPanel();
75. JButton addSiblingButton = new JButton("Add Sibling");
76. addSiblingButton.addActionListener(new
77. ActionListener()
78. {
79. public void actionPerformed(ActionEvent event)
80. {
81. DefaultMutableTreeNode selectedNode
82. = (DefaultMutableTreeNode) tree.getLastSelectedPathComponent();
83.
84. if (selectedNode == null) return;
85.
86. DefaultMutableTreeNode parent
87. = (DefaultMutableTreeNode) selectedNode.getParent();
88.
89. if (parent == null) return;
90.
91. DefaultMutableTreeNode newNode = new DefaultMutableTreeNode("New");
92.
93. int selectedIndex = parent.getIndex(selectedNode);
94. model.insertNodeInto(newNode, parent, selectedIndex + 1);
95.
96. // now display new node
97.
98. TreeNode[] nodes = model.getPathToRoot(newNode);
99. TreePath path = new TreePath(nodes);
100. tree.scrollPathToVisible(path);
101. }
102. });
103. panel.add(addSiblingButton);
104.
105. JButton addChildButton = new JButton("Add Child");
106. addChildButton.addActionListener(new
107. ActionListener()
108. {
109. public void actionPerformed(ActionEvent event)
110. {
111. DefaultMutableTreeNode selectedNode = (DefaultMutableTreeNode)
112. tree.getLastSelectedPathComponent();
113.
114. if (selectedNode == null) return;
15.
116. DefaultMutableTreeNode newNode = new DefaultMutableTreeNode("New");
117. model.insertNodeInto(newNode, selectedNode, selectedNode.getChildCount());
118.
119. // now display new node
120.
121. TreeNode[] nodes = model.getPathToRoot(newNode);
122. TreePath path = new TreePath(nodes);
123. tree.scrollPathToVisible(path);
124. }
125. });
126. panel.add(addChildButton);
127.
128. JButton deleteButton = new JButton("Delete");
129. deleteButton.addActionListener(new
130. ActionListener()
131. {
132. public void actionPerformed(ActionEvent event)
133. {
134. DefaultMutableTreeNode selectedNode
135. = (DefaultMutableTreeNode) tree.getLastSelectedPathComponent();
136.
137. if (selectedNode != null && selectedNode.getParent() != null)
138. model.removeNodeFromParent(selectedNode);
139. }
140. });
141. panel.add(deleteButton);
142. add(panel, BorderLayout.SOUTH);
143. }
144.
145. private DefaultTreeModel model;
146. private JTree tree;
147. private static final int DEFAULT_WIDTH = 400;
148. private static final int DEFAULT_HEIGHT = 200;
149. }
javax.swing.JTree 1.2
treePath getSelectionPath()
gets the path to the currently selected node, or the path to the first selected node if multiple nodes are selected. Returns null if no node is selected.
Object getLastSelectedPathComponent()
gets the node object that represents the currently selected node, or the first node if multiple nodes are selected. Returns null if no node is selected.
void makeVisible(TreePath path)
expands all nodes along the path.
void scrollPathToVisible(TreePath path)
expands all nodes along the path and, if the tree is contained in a scroll pane, scrolls to ensure that the last node on the path is visible.
javax.swing.tree.TreePath 1.2
Object getLastPathComponent()
gets the last object on this path, that is, the node object that the path represents.
javax.swing.tree.TreeNode 1.2
treeNode getParent()
returns the parent node of this node.
treeNode getChildAt(int index)
looks up the child node at the given index. The index must be between 0 and getChildCount() - 1.
int getChildCount()
returns the number of children of this node.
Enumeration children()
returns an enumeration object that iterates through all children of this node.
javax.swing.tree.DefaultTreeModel 1.2
void insertNodeInto(MutableTreeNode newChild, MutableTreeNode parent, int index)
inserts newChild as a new child node of parent at the given index and notifies the tree model listeners.
void removeNodeFromParent(MutableTreeNode node)
removes node from this model and notifies the tree model listeners.
void nodeChanged(TreeNode node)
notifies the tree model listeners that node has changed.
void nodesChanged(TreeNode parent, int[] changedChildIndexes)
notifies the tree model listeners that all child nodes of parent with the given indexes have changed.
void reload()
reloads all nodes into the model. This is a drastic operation that you should use only if the nodes have changed completely because of some outside influence.
Node Enumeration
Sometimes you need to find a node in a tree by starting at the root and visiting all children until you have found a match. The DefaultMutableTreeNode class has several convenience methods for iterating through nodes.
The breadthFirstEnumeration and depthFirstEnumeration methods return enumeration objects whose nextElement method visits all children of the current node, using either a breadth-first or depth-first traversal. Figure 6-22 shows the traversals for a sample treethe node labels indicate the order in which the nodes are traversed.
Figure 6-22. Tree traversal orders
Breadth-first enumeration is the easiest to visualize. The tree is traversed in layers. The root is visited first, followed by all of its children, then followed by the grandchildren, and so on.
To visualize depth-first enumeration, imagine a rat trapped in a tree-shaped maze. It rushes along the first path until it comes to a leaf. Then, it backtracks and turns around to the next path, and so on.
Computer scientists also call this postorder traversal because the search process visits the children before visiting the parents. The postOrderTraversal method is a synonym for depthFirstTraversal. For completeness, there is also a preOrderTraversal, a depth-first search that enumerates parents before the children.
Here is the typical usage pattern:
Enumeration breadthFirst = node.breadthFirstEnumeration();
while (breadthFirst.hasMoreElements())
do something with breadthFirst.nextElement();
Finally, a related method, pathFromAncestorEnumeration, finds a path from an ancestor to a given node and then enumerates the nodes along that path. That's no big dealit just keeps calling getParent until the ancestor is found and then presents the path in reverse order.
In our next example program, we put node enumeration to work. The program displays inheritance trees of classes. Type the name of a class into the text field on the bottom of the frame. The class and all of its superclasses are added to the tree (see Figure 6-23).
Figure 6-23. An inheritance tree
In this example, we take advantage of the fact that the user objects of the tree nodes can be objects of any type. Because our nodes describe classes, we store Class objects in the nodes.
Of course, we don't want to add the same class object twice, so we need to check whether a class already exists in the tree. The following method finds the node with a given user object if it exists in the tree.
public DefaultMutableTreeNode findUserObject(Object obj)
{
Enumeration e = root.breadthFirstEnumeration();
while (e.hasMoreElements())
{
DefaultMutableTreeNode node = (DefaultMutableTreeNode) e.nextElement();
if (node.getUserObject().equals(obj))
return node;
}
return null;
}
Rendering Nodes
In your applications, you will often need to change the way in which a tree component draws the nodes. The most common change is, of course, to choose different icons for nodes and leaves. Other changes might involve changing the font of the node labels or drawing images at the nodes. All these changes are made possible by installing a new tree cell renderer into the tree. By default, the Jtree class uses DefaultTreeCellRenderer objects to draw each node. The DefaultTreeCellRenderer class extends the JLabel class. The label contains the node icon and the node label.
NOTE
The cell renderer does not draw the "handles" for expanding and collapsing subtrees. The handles are part of the look and feel, and it is recommended that you not change them.
You can customize the display in three ways.
You can change the icons, font, and background color used by a DefaultTreeCellRenderer. These settings are used for all nodes in the tree.
You can install a renderer that extends the DefaultTreeCellRenderer class and vary the icons, fonts, and background color for each node.
You can install a renderer that implements the treeCellRenderer interface, to draw a custom image for each node.
Let us look at these possibilities one by one. The easiest customization is to construct a DefaultTreeCellRenderer object, change the icons, and install it into the tree:
DefaultTreeCellRenderer renderer = new DefaultTreeCellRenderer();
renderer.setLeafIcon(new ImageIcon("blue-ball.gif")); // used for leaf nodes
renderer.setClosedIcon(new ImageIcon("red-ball.gif")); // used for collapsed nodes
renderer.setOpenIcon(new ImageIcon("yellow-ball.gif")); // used for expanded nodes
tree.setCellRenderer(renderer);
You can see the effect in Figure 6-23. We just use the "ball" icons as placeholderspresumably your user interface designer would supply you with appropriate icons to use for your applications.
We don't recommend that you change the font or background color for an entire treethat is really the job of the look and feel.
However, it can be useful to change the font for individual nodes in a tree to highlight some of them. If you look carefully at Figure 6-23, you will notice that the abstract classes are set in italics.
To change the appearance of individual nodes, you install a tree cell renderer. Tree cell renderers are very similar to the list cell renderers we discussed earlier in this chapter. The treeCellRenderer interface has a single method
Component getTreeCellRendererComponent(JTree tree, Object value, boolean selected,
boolean expanded, boolean leaf, int row, boolean hasFocus)
The gettreeCellRendererComponent method of the DefaultTreeCellRenderer class returns thisin other words, a label. (The DefaultTreeCellRenderer class extends the JLabel class.) To customize the component, extend the DefaultTreeCellRenderer class. Override the getTReeCellRendererComponent method as follows: Call the superclass method, so that it can prepare the label data. Customize the label properties, and finally return this.
class MyTreeCellRenderer extends DefaultTreeCellRenderer
{
public Component getTreeCellRendererComponent(JTree tree, Object value, boolean selected,
boolean expanded, boolean leaf, int row, boolean hasFocus)
{
super.getTreeCellRendererComponent(tree, value, selected, expanded, leaf, row, hasFocus);
DefaultMutableTreeNode node = (DefaultMutableTreeNode) value;
look at node.getUserObject();
Font font = appropriate font;
setFont(font);
return this;
}
};
CAUTION
The value parameter of the gettreeCellRendererComponent method is the node object, not the user object! Recall that the user object is a feature of the DefaultMutableTreeNode, and that a Jtree can contain nodes of an arbitrary type. If your tree uses DefaultMutableTreeNode nodes, then you must retrieve the user object in a second step, as we did in the preceding code sample.
CAUTION
The DefaultTreeCellRenderer uses the same label object for all nodes, only changing the label text for each node. If you change the font for a particular node, you must set it back to its default value when the method is called again. Otherwise, all subsequent nodes will be drawn in the changed font! Look at the code in Example 6-6 to see how to restore the font to the default.
We do not show an example for a tree cell renderer that draws arbitrary graphics. If you need this capability, you can adapt the list cell renderer in Example 6-3; the technique is entirely analogous.
Let's put tree cell renderers to work. Example 6-6 shows the complete source code for the class tree program. The program displays inheritance hierarchies, and it customizes the display to show abstract classes in italics. You can type the name of any class into the text field at the bottom of the frame. Press the ENTER key or click the Add button to add the class and its superclasses to the tree. You must enter the full package name, such as java.util.ArrayList.
This program is a bit tricky because it uses reflection to construct the class tree. This work is contained inside the addClass method. (The details are not that important. We use the class tree in this example because inheritance trees yield a nice supply of trees without laborious coding. If you display trees in your own applications, you will have your own source of hierarchical data.) The method uses the breadth-first search algorithm to find whether the current class is already in the tree by calling the findUserObject method that we implemented in the preceding section. If the class is not already in the tree, we add the superclasses to the tree, then make the new class node a child and make that node visible.
The ClassNameTreeCellRenderer sets the class name in either the normal or italic font, depending on the ABSTRACT modifier of the Class object. We don't want to set a particular font because we don't want to change whatever font the look and feel normally uses for labels. For that reason, we use the font from the label and derive an italic font from it. Recall that only a single shared JLabel object is returned by all calls. We need to hang on to the original font and restore it in the next call to the gettreeCellRendererComponent method.
Finally, note how we change the node icons in the ClassTreeFrame constructor.
Example 6-6. ClassTree.java
[View full width]
1. import java.awt.*;
2. import java.awt.event.*;
3. import java.lang.reflect.*;
4. import java.util.*;
5. import javax.swing.*;
6. import javax.swing.event.*;
7. import javax.swing.tree.*;
8.
9. /**
10. This program demonstrates cell rendering by showing
11. a tree of classes and their superclasses.
12. */
13. public class ClassTree
14. {
15. public static void main(String[] args)
16. {
17. JFrame frame = new ClassTreeFrame();
18. frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
19. frame.setVisible(true);
20. }
21. }
22.
23. /**
24. This frame displays the class tree, a text field and
25. add button to add more classes into the tree.
26. */
27. class ClassTreeFrame extends JFrame
28. {
29. public ClassTreeFrame()
30. {
31. setTitle("ClassTree");
32. setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
33.
34. // the root of the class tree is Object
35. root = new DefaultMutableTreeNode(java.lang.Object.class);
36. model = new DefaultTreeModel(root);
37. tree = new JTree(model);
38.
39. // add this class to populate the tree with some data
40. addClass(getClass());
41.
42. // set up node icons
43. ClassNameTreeCellRenderer renderer = new ClassNameTreeCellRenderer();
44. renderer.setClosedIcon(new ImageIcon("red-ball.gif"));
45. renderer.setOpenIcon(new ImageIcon("yellow-ball.gif"));
46. renderer.setLeafIcon(new ImageIcon("blue-ball.gif"));
47. tree.setCellRenderer(renderer);
48.
49. add(new JScrollPane(tree), BorderLayout.CENTER);
50.
51. addTextField();
52. }
53.
54. /**
55. Add the text field and "Add" button to add a new class.
56. */
57. public void addTextField()
58. {
59. JPanel panel = new JPanel();
60.
61. ActionListener addListener = new
62. ActionListener()
63. {
64. public void actionPerformed(ActionEvent event)
65. {
66. // add the class whose name is in the text field
67. try
68. {
69. String text = textField.getText();
70. addClass(Class.forName(text)); // clear text field to indicate success
71. textField.setText("");
72. }
73. catch (ClassNotFoundException e)
74. {
75. JOptionPane.showMessageDialog(null, "Class not found");
76. }
77. }
78. };
79.
80. // new class names are typed into this text field
81. textField = new JTextField(20);
82. textField.addActionListener(addListener);
83. panel.add(textField);
84.
85. JButton addButton = new JButton("Add");
86. addButton.addActionListener(addListener);
87. panel.add(addButton);
88.
89. add(panel, BorderLayout.SOUTH);
90. }
91.
92. /**
93. Finds an object in the tree.
94. @param obj the object to find
95. @return the node containing the object or null
96. if the object is not present in the tree
97. */
98. public DefaultMutableTreeNode findUserObject(Object obj)
99. {
100. // find the node containing a user object
101. Enumeration e = root.breadthFirstEnumeration();
102. while (e.hasMoreElements())
103. {
104. DefaultMutableTreeNode node = (DefaultMutableTreeNode) e.nextElement();
105. if (node.getUserObject().equals(obj))
106. return node;
107. }
108. return null;
109. }
110.
111. /**
112. Adds a new class and any parent classes that aren't
113. yet part of the tree
114. @param c the class to add
115. @return the newly added node.
116. */
117. public DefaultMutableTreeNode addClass(Class c)
118. {
119. // add a new class to the tree
120.
121. // skip non-class types
122. if (c.isInterface() || c.isPrimitive()) return null;
123.
124. // if the class is already in the tree, return its node
125. DefaultMutableTreeNode node = findUserObject(c);
126. if (node != null) return node;
127.
128. // class isn't present--first add class parent recursively
129.
130. Class s = c.getSuperclass();
131.
132. DefaultMutableTreeNode parent;
133. if (s == null)
134. parent = root;
135. else
136. parent = addClass(s);
137.
138. // add the class as a child to the parent
139. DefaultMutableTreeNode newNode = new DefaultMutableTreeNode(c);
140. model.insertNodeInto(newNode, parent, parent.getChildCount());
141.
142. // make node visible
143. TreePath path = new TreePath(model.getPathToRoot(newNode));
144. tree.makeVisible(path);
145.
146. return newNode;
147. }
148.
149. private DefaultMutableTreeNode root;
150. private DefaultTreeModel model;
151. private JTree tree;
152. private JTextField textField;
153. private static final int DEFAULT_WIDTH = 400;
154. private static final int DEFAULT_HEIGHT = 300;
155. }
156.
157. /**
158. This class renders a class name either in plain or italic.
159. Abstract classes are italic.
160. */
161. class ClassNameTreeCellRenderer extends DefaultTreeCellRenderer
162. {
163. public Component getTreeCellRendererComponent(JTree tree, Object value, boolean
selected,
164. boolean expanded, boolean leaf, int row, boolean hasFocus)
165. {
166. super.getTreeCellRendererComponent(tree, value, selected, expanded, leaf, row,
hasFocus);
167. // get the user object
168. DefaultMutableTreeNode node = (DefaultMutableTreeNode) value;
169. Class c = (Class) node.getUserObject();
170.
171. // the first time, derive italic font from plain font
172. if (plainFont == null)
173. {
174. plainFont = getFont();
175. // the tree cell renderer is sometimes called with a label that has a null font
176. if (plainFont != null) italicFont = plainFont.deriveFont(Font.ITALIC);
177. }
178.
179. // set font to italic if the class is abstract, plain otherwise
180. if ((c.getModifiers() & Modifier.ABSTRACT) == 0)
181. setFont(plainFont);
182. else
183. setFont(italicFont);
184. return this;
185. }
186.
187. private Font plainFont = null;
188. private Font italicFont = null;
189. }
javax.swing.tree.DefaultMutableTreeNode 1.2
Enumeration breadthFirstEnumeration()
Enumeration depthFirstEnumeration()
Enumeration preOrderEnumeration()
Enumeration postOrderEnumeration()
return enumeration objects for visiting all nodes of the tree model in a particular order. In breadth-first traversal, children that are closer to the root are visited before those that are farther away. In depth-first traversal, all children of a node are completely enumerated before its siblings are visited. The postOrderEnumeration method is a synonym for depthFirstEnumeration. The preorder traversal is identical to the postorder traversal except that parents are enumerated before their children.
javax.swing.tree.TreeCellRenderer 1.2
Component getTreeCellRendererComponent(JTree tree, Object value, boolean selected, boolean expanded, boolean leaf, int row, boolean hasFocus)
returns a component whose paint method is invoked to render a tree cell.
Parameters:
tree
The tree containing the node to be rendered
value
The node to be rendered
selected
true if the node is currently selected
expanded
true if the children of the node are visible
leaf
TRue if the node needs to be displayed as a leaf
row
The display row containing the node
hasFocus
true if the node currently has input focus
javax.swing.tree.DefaultTreeCellRenderer 1.2
void setLeafIcon(Icon icon)
void setOpenIcon(Icon icon)
void setClosedIcon(Icon icon)
set the icon to show for a leaf node, an expanded node, and a collapsed node.
Listening to Tree Events
Most commonly, a tree component is paired with some other component. When the user selects tree nodes, some information shows up in another window. See Figure 6-24 for an example. When the user selects a class, the instance and static variables of that class are displayed in the text area to the right.
Figure 6-24. A class browser
To obtain this behavior, you install a tree selection listener. The listener must implement the treeSelectionListener interface, an interface with a single method
void valueChanged(TreeSelectionEvent event)
That method is called whenever the user selects or deselects tree nodes.
You add the listener to the tree in the normal way:
tree.addTreeSelectionListener(listener);
You can specify whether the user is allowed to select a single node, a contiguous range of nodes, or an arbitrary, potentially discontiguous, set of nodes. The JTRee class uses a treeSelectionModel to manage node selection. You need to retrieve the model to set the selection state to one of SINGLE_TREE_SELECTION, CONTIGUOUS_TREE_SELECTION, or DISCONTIGUOUS_TREE_SELECTION. (Discontiguous selection mode is the default.) For example, in our class browser, we want to allow selection of only a single class:
int mode = TreeSelectionModel.SINGLE_TREE_SELECTION;
tree.getSelectionModel().setSelectionMode(mode);
Apart from setting the selection mode, you need not worry about the tree selection model.
NOTE
How the user selects multiple items depends on the look and feel. In the Metal look and feel, hold down the CTRL key while clicking on an item to add the item to the selection, or to remove it if it was currently selected. Hold down the SHIFT key while clicking on an item to select a range of items, extending from the previously selected item to the new item.
To find out the current selection, you query the tree with the getSelectionPaths method:
TreePath[] selectedPaths = tree.getSelectionPaths();
If you restricted the user to a single selection, you can use the convenience method getSelectionPath, which returns the first selected path, or null if no path was selected.
CAUTION
The treeSelectionEvent class has a getPaths method that returns an array of treePath objects, but that array describes selection changes, not the current selection.
Example 6-7 puts tree selection to work. This program builds on Example 6-6; however, to keep the program short, we did not use a custom tree cell renderer. In the frame constructor, we restrict the user to single item selection and add a tree selection listener. When the valueChanged method is called, we ignore its event parameter and simply ask the tree for the current selection path. As always, we need to get the last node of the path and look up its user object. We then call the getFieldDescription method, which uses reflection to assemble a string with all fields of the selected class. Finally, that string is displayed in the text area.
Example 6-7. ClassBrowserTest.java
1. import java.awt.*;
2. import java.awt.event.*;
3. import java.lang.reflect.*;
4. import java.util.*;
5. import javax.swing.*;
6. import javax.swing.event.*;
7. import javax.swing.tree.*;
8.
9. /**
10. This program demonstrates tree selection events.
11. */
12. public class ClassBrowserTest
13. {
14. public static void main(String[] args)
15. {
16. JFrame frame = new ClassBrowserTestFrame();
17. frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
18. frame.setVisible(true);
19. }
20. }
21.
22. /**
23. A frame with a class tree, a text area to show the properties
24. of the selected class, and a text field to add new classes.
25. */
26. class ClassBrowserTestFrame extends JFrame
27. {
28. public ClassBrowserTestFrame()
29. {
30. setTitle("ClassBrowserTest");
31. setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
32.
33. // the root of the class tree is Object
34. root = new DefaultMutableTreeNode(java.lang.Object.class);
35. model = new DefaultTreeModel(root);
36. tree = new JTree(model);
37.
38. // add this class to populate the tree with some data
39. addClass(getClass());
40.
41. // set up selection mode
42. tree.addTreeSelectionListener(new
43. TreeSelectionListener()
44. {
45. public void valueChanged(TreeSelectionEvent event)
46. {
47. // the user selected a different node--update description
48. TreePath path = tree.getSelectionPath();
49. if (path == null) return;
50. DefaultMutableTreeNode selectedNode
51. = (DefaultMutableTreeNode) path.getLastPathComponent();
52. Class c = (Class) selectedNode.getUserObject();
53. String description = getFieldDescription(c);
54. textArea.setText(description);
55. }
56. });
57. int mode = TreeSelectionModel.SINGLE_TREE_SELECTION;
58. tree.getSelectionModel().setSelectionMode(mode);
59.
60. // this text area holds the class description
61. textArea = new JTextArea();
62.
63. // add tree and text area
64. JPanel panel = new JPanel();
65. panel.setLayout(new GridLayout(1, 2));
66. panel.add(new JScrollPane(tree));
67. panel.add(new JScrollPane(textArea));
68.
69. add(panel, BorderLayout.CENTER);
70.
71. addTextField();
72. }
73.
74. /**
75. Add the text field and "Add" button to add a new class.
76. */
77. public void addTextField()
78. {
79. JPanel panel = new JPanel();
80.
81. ActionListener addListener = new
82. ActionListener()
83. {
84. public void actionPerformed(ActionEvent event)
85. {
86. // add the class whose name is in the text field
87. try
88. {
89. String text = textField.getText();
90. addClass(Class.forName(text));
91. // clear text field to indicate success
92. textField.setText("");
93. }
94. catch (ClassNotFoundException e)
95. {
96. JOptionPane.showMessageDialog(null, "Class not found");
97. }
98. }
99. };
100.
101. // new class names are typed into this text field
102. textField = new JTextField(20);
103. textField.addActionListener(addListener);
104. panel.add(textField);
105.
106. JButton addButton = new JButton("Add");
107. addButton.addActionListener(addListener);
108. panel.add(addButton);
109.
110. add(panel, BorderLayout.SOUTH);
111. }
112.
113. /**
114. Finds an object in the tree.
115. @param obj the object to find
116. @return the node containing the object or null
117. if the object is not present in the tree
118. */
119. public DefaultMutableTreeNode findUserObject(Object obj)
120. {
121. // find the node containing a user object
122. Enumeration e = root.breadthFirstEnumeration();
123. while (e.hasMoreElements())
124. {
125. DefaultMutableTreeNode node = (DefaultMutableTreeNode) e.nextElement();
126. if (node.getUserObject().equals(obj))
127. return node;
128. }
129. return null;
130. }
131.
132. /**
133. Adds a new class and any parent classes that aren't
134. yet part of the tree
135. @param c the class to add
136. @return the newly added node.
137. */
138. public DefaultMutableTreeNode addClass(Class c)
139. {
140. // add a new class to the tree
141.
142. // skip non-class types
143. if (c.isInterface() || c.isPrimitive()) return null;
144.
145. // if the class is already in the tree, return its node
146. DefaultMutableTreeNode node = findUserObject(c);
147. if (node != null) return node;
148.
149. // class isn't present--first add class parent recursively
150.
151. Class s = c.getSuperclass();
152.
153. DefaultMutableTreeNode parent;
154. if (s == null)
155. parent = root;
156. else
157. parent = addClass(s);
158.
159. // add the class as a child to the parent
160. DefaultMutableTreeNode newNode = new DefaultMutableTreeNode(c);
161. model.insertNodeInto(newNode, parent, parent.getChildCount());
162.
163. // make node visible
164. TreePath path = new TreePath(model.getPathToRoot(newNode));
165. tree.makeVisible(path);
166.
167. return newNode;
168. }
169.
170. /**
171. Returns a description of the fields of a class.
172. @param the class to be described
173. @return a string containing all field types and names
174. */
175. public static String getFieldDescription(Class c)
176. {
177. // use reflection to find types and names of fields
178. StringBuilder r = new StringBuilder();
179. Field[] fields = c.getDeclaredFields();
180. for (int i = 0; i < fields.length; i++)
181. {
182. Field f = fields[i];
183. if ((f.getModifiers() & Modifier.STATIC) != 0) r.append("static ");
184. r.append(f.getType().getName());
185. r.append(" ");
186. r.append(f.getName());
187. r.append("
");
188. }
189. return r.toString();
190. }
191.
192. private DefaultMutableTreeNode root;
193. private DefaultTreeModel model;
194. private JTree tree;
195. private JTextField textField;
196. private JTextArea textArea;
197. private static final int DEFAULT_WIDTH = 400;
198. private static final int DEFAULT_HEIGHT = 300;
199. }
javax.swing.JTree 1.2
treePath getSelectionPath()
treePath[] getSelectionPaths()
return the first selected path, or an array of paths to all selected nodes. If no paths are selected, both methods return null.
javax.swing.event.TreeSelectionListener 1.2
void valueChanged(TreeSelectionEvent event)
is called whenever nodes are selected or deselected.
javax.swing.event.TreeSelectionEvent 1.2
treePath getPath()
treePath[] getPaths()
get the first path or all paths that have changed in this selection event. If you want to know the current selection, not the selection change, you should call Jtree.getSelectionPaths instead.
Custom Tree Models
In the final example, we implement a program that inspects the contents of a variable, just like a debugger does (see Figure 6-25).
Figure 6-25. An object inspection tree
Before going further, compile and run the example program. Each node corresponds to an instance variable. If the variable is an object, expand it to see its instance variables. The program inspects the contents of the frame window. If you poke around a few of the instance variables, you should be able to find some familiar classes. You'll also gain some respect for how complex the Swing user interface components are under the hood.
What's remarkable about the program is that the tree does not use the DefaultTreeModel. If you already have data that is hierarchically organized, you may not want to build a duplicate tree and worry about keeping both trees synchronized. That is the situation in our casethe inspected objects are already linked to each other through the object references, so there is no need to replicate the linking structure.
The treeModel interface has only a handful of methods. The first group of methods enables the Jtree to find the tree nodes by first getting the root, then the children. The Jtree class calls these methods only when the user actually expands a node.
Object getRoot()
int getChildCount(Object parent)
Object getChild(Object parent, int index)
This example shows why the treeModel interface, like the Jtree class itself, does not need an explicit notion of nodes. The root and its children can be any objects. The TReeModel is responsible for telling the Jtree how they are connected.
The next method of the treeModel interface is the reverse of getChild:
int getIndexOfChild(Object parent, Object child)
Actually, this method can be implemented in terms of the first threesee the code in Example 6-8.
The tree model tells the JTRee which nodes should be displayed as leaves:
boolean isLeaf(Object node)
If your code changes the tree model, then the tree needs to be notified so that it can redraw itself. The tree adds itself as a treeModelListener to the model. Thus, the model must support the usual listener management methods:
void addTreeModelListener(TreeModelListener l)
void removeTreeModelListener(TreeModelListener l)
You can see implementations for these methods in Example 6-8.
When the model modifies the tree contents, it calls one of the four methods of the treeModelListener interface:
void treeNodesChanged(TreeModelEvent e)
void treeNodesInserted(TreeModelEvent e)
void treeNodesRemoved(TreeModelEvent e)
void treeStructureChanged(TreeModelEvent e)
The TReeModelEvent object describes the location of the change. The details of assembling a tree model event that describes an insertion or removal event are quite technical. You only need to worry about firing these events if your tree can actually have nodes added and removed. In Example 6-8, we show you how to fire one event: replacing the root with a new object.
TIP
To simplify the code for event firing, we use the javax.swing.EventListenerList convenience class that collects listeners. See Volume 1, Chapter 8 for more information on this class.
Finally, if the user edits a tree node, your model is called with the change:
void valueForPathChanged(TreePath path, Object newValue)
If you don't allow editing, this method is never called.
If you don't need to support editing, then constructing a tree model is easily done. Implement the three methods
Object getRoot()
int getChildCount(Object parent)
Object getChild(Object parent, int index)
These methods describe the structure of the tree. Supply routine implementations of the other five methods, as in Example 6-8. You are then ready to display your tree.
Now let's turn to the implementation of the example program. Our tree will contain objects of type Variable.
NOTE
Had we used the DefaultTreeModel, our nodes would have been objects of type DefaultMutableTreeNode with user objects of type Variable.
For example, suppose you inspect the variable
Employee joe;
That variable has a type Employee.class, a name "joe", and a value, the value of the object reference joe. We define a class Variable that describes a variable in a program:
Variable v = new Variable(Employee.class, "joe", joe);
If the type of the variable is a primitive type, you must use an object wrapper for the value.
new Variable(double.class, "salary", new Double(salary));
If the type of the variable is a class, then the variable has fields. Using reflection, we enumerate all fields and collect them in an ArrayList. Since the getFields method of the Class class does not return fields of the superclass, we need to call getFields on all superclasses as well. You can find the code in the Variable constructor. The getFields method of our Variable class returns the array of fields. Finally, the toString method of the Variable class formats the node label. The label always contains the variable type and name. If the variable is not a class, the label also contains the value.
NOTE
If the type is an array, then we do not display the elements of the array. This would not be difficult to do; we leave it as the proverbial "exercise for the reader."
Let's move on to the tree model. The first two methods are simple.
public Object getRoot()
{
return root;
}
public int getChildCount(Object parent)
{
return ((Variable) parent).getFields().size();
}
The getChild method returns a new Variable object that describes the field with the given index. The getType and getName method of the Field class yield the field type and name. By using reflection, you can read the field value as f.get(parentValue). That method can throw an IllegalAccessException. However, we made all fields accessible in the Variable constructor, so this won't happen in practice.
Here is the complete code of the getChild method.
public Object getChild(Object parent, int index)
{
ArrayList fields = ((Variable) parent).getFields();
Field f = (Field) fields.get(index);
Object parentValue = ((Variable) parent).getValue();
try
{
return new Variable(f.getType(), f.getName(), f.get(parentValue));
}
catch (IllegalAccessException e)
{
return null;
}
}
These three methods reveal the structure of the object tree to the Jtree component. The remaining methods are routinesee the source code in Example 6-8.
There is one remarkable fact about this tree model: It actually describes an infinite tree. You can verify this by following one of the WeakReference objects. Click on the variable named referent. It leads you right back to the original object. You get an identical subtree, and you can open its WeakReference object again, ad infinitum. Of course, you cannot store an infinite set of nodes. The tree model simply generates the nodes on demand as the user expands the parents.
This example concludes our discussion on trees. We move on to the table component, another complex Swing component. Superficially, trees and tables don't seem to have much in common, but you will find that they both use the same concepts for data models and cell rendering.
Example 6-8. ObjectInspectorTest.java
[View full width]
1. import java.awt.*;
2. import java.awt.event.*;
3. import java.lang.reflect.*;
4. import java.util.*;
5. import javax.swing.*;
6. import javax.swing.event.*;
7. import javax.swing.tree.*;
8.
9. /**
10. This program demonstrates how to use a custom tree
11. model. It displays the fields of an object.
12. */
13. public class ObjectInspectorTest
14. {
15. public static void main(String[] args)
16. {
17. JFrame frame = new ObjectInspectorFrame();
18. frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
19. frame.setVisible(true);
20. }
21. }
22.
23. /**
24. This frame holds the object tree.
25. */
26. class ObjectInspectorFrame extends JFrame
27. {
28. public ObjectInspectorFrame()
29. {
30. setTitle("ObjectInspectorTest");
31. setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
32.
33. // we inspect this frame object
34.
35. Variable v = new Variable(getClass(), "this", this);
36. ObjectTreeModel model = new ObjectTreeModel();
37. model.setRoot(v);
38.
39. // construct and show tree
40.
41. tree = new JTree(model);
42. add(new JScrollPane(tree), BorderLayout.CENTER);
43. }
44.
45. private JTree tree;
46. private static final int DEFAULT_WIDTH = 400;
47. private static final int DEFAULT_HEIGHT = 300;
48. }
49.
50. /**
51. This tree model describes the tree structure of a Java
52. object. Children are the objects that are stored in instance
53. variables.
54. */
55. class ObjectTreeModel implements TreeModel
56. {
57. /**
58. Constructs an empty tree.
59. */
60. public ObjectTreeModel()
61. {
62. root = null;
63. }
64.
65. /**
66. Sets the root to a given variable.
67. @param v the variable that is being described by this tree
68. */
69. public void setRoot(Variable v)
70. {
71. Variable oldRoot = v;
72. root = v;
73. fireTreeStructureChanged(oldRoot);
74. }
75.
76. public Object getRoot()
77. {
78. return root;
79. }
80.
81. public int getChildCount(Object parent)
82. {
83. return ((Variable) parent).getFields().size();
84. }
85.
86. public Object getChild(Object parent, int index)
87. {
88. ArrayList<Field> fields = ((Variable) parent).getFields();
89. Field f = (Field) fields.get(index);
90. Object parentValue = ((Variable) parent).getValue();
91. try
92. {
93. return new Variable(f.getType(), f.getName(), f.get(parentValue));
94. }
95. catch (IllegalAccessException e)
96. {
97. return null;
98. }
99. }
100.
101. public int getIndexOfChild(Object parent, Object child)
102. {
103. int n = getChildCount(parent);
104. for (int i = 0; i < n; i++)
105. if (getChild(parent, i).equals(child))
106. return i;
107. return -1;
108. }
109.
110. public boolean isLeaf(Object node)
111. {
112. return getChildCount(node) == 0;
113. }
114.
115. public void valueForPathChanged(TreePath path,
116. Object newValue)
117. {}
118.
119. public void addTreeModelListener(TreeModelListener l)
120. {
121. listenerList.add(TreeModelListener.class, l);
122. }
123.
124. public void removeTreeModelListener(TreeModelListener l)
125. {
126. listenerList.remove(TreeModelListener.class, l);
127. }
128.
129. protected void fireTreeStructureChanged(Object oldRoot)
130. {
131. TreeModelEvent event = new TreeModelEvent(this, new Object[] {oldRoot});
132. EventListener[] listeners = listenerList.getListeners(TreeModelListener.class);
133. for (int i = 0; i < listeners.length; i++)
134. ((TreeModelListener) listeners[i]).treeStructureChanged(event);
135. }
136.
137. private Variable root;
138. private EventListenerList listenerList = new EventListenerList();
139. }
140.
141. /**
142. A variable with a type, name, and value.
143. */
144. class Variable
145. {
146. /**
147. Construct a variable
148. @param aType the type
149. @param aName the name
150. @param aValue the value
151. */
152. public Variable(Class aType, String aName, Object aValue)
153. {
154. type = aType;
155. name = aName;
156. value = aValue;
157. fields = new ArrayList<Field>();
158.
159. // find all fields if we have a class type except we don't expand strings and
null values
160.
161. if (!type.isPrimitive() && !type.isArray() && !type.equals(String.class) &&
value != null)
162. {
163. // get fields from the class and all superclasses
164. for (Class c = value.getClass(); c != null; c = c.getSuperclass())
165. {
166. Field[] fs = c.getDeclaredFields();
167. AccessibleObject.setAccessible(fs, true);
168.
169. // get all nonstatic fields
170. for (Field f : fs)
171. if ((f.getModifiers() & Modifier.STATIC) == 0)
172. fields.add(f);
173. }
174. }
175. }
176.
177. /**
178. Gets the value of this variable.
179. @return the value
180. */
181. public Object getValue() { return value; }
182.
183. /**
184. Gets all nonstatic fields of this variable.
185. @return an array list of variables describing the fields
186. */
187. public ArrayList<Field> getFields() { return fields; }
188.
189. public String toString()
190. {
191. String r = type + " " + name;
192. if (type.isPrimitive()) r += "=" + value;
193. else if (type.equals(String.class)) r += "=" + value;
194. else if (value == null) r += "=null";
195. return r;
196. }
197.
198. private Class type;
199. private String name;
200. private Object value;
201. private ArrayList<Field> fields;
202. }
javax.swing.tree.TreeModel 1.2
Object getRoot()
returns the root node.
int getChildCount(Object parent)
gets the number of children of the parent node.
Object getChild(Object parent, int index)
gets the child node of the parent node at the given index.
int getIndexOfChild(Object parent, Object child)
gets the index of the child node in the parent node, or -1 if child is not a child of parent in this tree model.
boolean isLeaf(Object node)
returns true if node is conceptually a leaf of the tree.
void addTreeModelListener(TreeModelListener l)
void removeTreeModelListener(TreeModelListener l)
add and remove listeners that are notified when the information in the tree model changes.
void valueForPathChanged(TreePath path, Object newValue)
is called when a cell editor has modified the value of a node.
Parameters:
path
The path to the node that has been edited
newValue
The replacement value returned by the editor
javax.swing.event.TreeModelListener 1.2
void treeNodesChanged(TreeModelEvent e)
void treeNodesInserted(TreeModelEvent e)
void treeNodesRemoved(TreeModelEvent e)
void treeStructureChanged(TreeModelEvent e)
are called by the tree model when the tree has been modified.
javax.swing.event.TreeModelEvent 1.2
treeModelEvent(Object eventSource, TreePath node)
constructs a tree model event.
Parameters:
eventSource
The tree model generating this event
node
The path to the node that is being changed
Tables
The JTable component displays a two-dimensional grid of objects. Of course, tables are common in user interfaces. The Swing team has put a lot of effort into the table control. Tables are inherently complex, butperhaps more successfully than with other Swing classesthe JTable component hides much of that complexity. You can produce fully functional tables with rich behavior by writing a few lines of code. Of course, you can write more code and customize the display and behavior for your specific applications.
In this section, we explain how to make simple tables, how the user interacts with them, and how to make some of the most common adjustments. As with the other complex Swing controls, it is impossible to cover all aspects in complete detail. For more information, look in Graphic Java 2 by David Geary or Core Java Foundation Classes by Kim Topley.
A Simple Table
As with the tree control, a JTable does not store its own data but obtains its data from a table model. The JTable class has a constructor that wraps a two-dimensional array of objects into a default model. That is the strategy that we use in our first example. Later in this chapter, we turn to table models.
Figure 6-26 shows a typical table, describing properties of the planets of the solar system. (A planet is gaseous if it consists mostly of hydrogen and helium. You should take the "Color" entries with a grain of saltthat column was added because it will be useful in a later code example.)
Figure 6-26. A simple table
As you can see from the code in Example 6-9, the data of the table is stored as a two-dimensional array of Object values:
Object[][] cells =
{
{ "Mercury", 2440.0, 0, false, Color.yellow },
{ "Venus", 6052.0, 0, false, Color.yellow },
. . .
}
NOTE
Here, we take advantage of autoboxing. The entries in the second, third, and fourth column are automatically converted into objects of type Double, Integer, and Boolean.
The table simply invokes the toString method on each object to display it. That's why the colors show up as java.awt.Color[r=...,g=...,b=...].
You supply the column names in a separate array of strings:
String[] columnNames = { "Planet", "Radius", "Moons", "Gaseous", "Color" };
Then, you construct a table from the cell and column name arrays. Finally, add scroll bars in the usual way, by wrapping the table in a JScrollPane.
JTable table = new JTable(cells, columnNames);
JScrollPane pane = new JScrollPane(table);
The resulting table already has surprisingly rich behavior. Resize the table vertically until the scroll bar shows up. Then, scroll the table. Note that the column headers don't scroll out of view!
Next, click on one of the column headers and drag it to the left or right. See how the entire column becomes detached (see Figure 6-27). You can drop it to a different location. This rearranges the columns in the view only. The data model is not affected.
Figure 6-27. Moving a column
To resize columns, simply place the cursor between two columns until the cursor shape changes to an arrow. Then, drag the column boundary to the desired place (see Figure 6-28).
Figure 6-28. Resizing columns
Users can select rows by clicking anywhere in a row. The selected rows are highlighted; you will see later how to get selection events. Users can also edit the table entries by clicking on a cell and typing into it. However, in this code example, the edits do not change the underlying data. In your programs, you should either make cells uneditable or handle cell editing events and update your model. We discuss those topics later in this section.
As of JDK 5.0, you can print a table with the print method:
table.print();
A print dialog box appears, and the table is sent to the printer. We discuss custom printing options in Chapter 7.
Example 6-9. PlanetTable.java
1. import java.awt.*;
2. import java.awt.event.*;
3. import javax.swing.*;
4. import javax.swing.table.*;
5.
6. /**
7. This program demonstrates how to show a simple table
8. */
9. public class PlanetTable
10. {
11. public static void main(String[] args)
12. {
13. JFrame frame = new PlanetTableFrame();
14. frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
15. frame.setVisible(true);
16. }
17. }
18.
19. /**
20. This frame contains a table of planet data.
21. */
22. class PlanetTableFrame extends JFrame
23. {
24. public PlanetTableFrame()
25. {
26. setTitle("PlanetTable");
27. setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
28. final JTable table = new JTable(cells, columnNames);
29. add(new JScrollPane(table), BorderLayout.CENTER);
30. JButton printButton = new JButton("Print");
31. printButton.addActionListener(new
32. ActionListener()
33. {
34. public void actionPerformed(ActionEvent event)
35. {
36. try
37. {
38. table.print();
39. }
40. catch (java.awt.print.PrinterException e)
41. {
42. e.printStackTrace();
43. }
44. }
45. });
46. JPanel buttonPanel = new JPanel();
47. buttonPanel.add(printButton);
48. add(buttonPanel, BorderLayout.SOUTH);
49. }
50.
51. private Object[][] cells =
52. {
53. { "Mercury", 2440.0, 0, false, Color.yellow },
54. { "Venus", 6052.0, 0, false, Color.yellow },
55. { "Earth", 6378.0, 1, false, Color.blue },
56. { "Mars", 3397.0, 2, false, Color.red },
57. { "Jupiter", 71492.0, 16, true, Color.orange },
58. { "Saturn", 60268.0, 18, true, Color.orange },
59. { "Uranus", 25559.0, 17, true, Color.blue },
60. { "Neptune", 24766.0, 8, true, Color.blue },
61. { "Pluto", 1137.0, 1, false, Color.black }
62. };
63.
64. private String[] columnNames = { "Planet", "Radius", "Moons", "Gaseous", "Color" };
65.
66. private static final int DEFAULT_WIDTH = 400;
67. private static final int DEFAULT_HEIGHT = 200;
68. }
javax.swing.JTable 1.2
JTable(Object[][] entries, Object[] columnNames)
constructs a table with a default table model.
void print() 5.0
displays a print dialog box and prints the table.
Table Models
In the preceding example, the table-rendered objects were stored in a two-dimensional array. However, you should generally not use that strategy in your own code. If you find yourself dumping data into an array to display it as a table, you should instead think about implementing your own table model.
Table models are particularly simple to implement because you can take advantage of the AbstractTableModel class that implements most of the required methods. You only need to supply three methods:
public int getRowCount();
public int getColumnCount();
public Object getValueAt(int row, int column);
There are many ways for implementing the getValueAt method. You can simply compute the answer. Or you can look up the value from a database or some other repository. Let us look at a couple of examples.
In the first example, we construct a table that simply shows some computed values, namely, the growth of an investment under different interest rate scenarios (see Figure 6-29).
Figure 6-29. Growth of an investment
[View full size image]
The getValueAt method computes the appropriate value and formats it:
public Object getValueAt(int r, int c)
{
double rate = (c + minRate) / 100.0;
int nperiods = r;
double futureBalance = INITIAL_BALANCE * Math.pow(1 + rate, nperiods);
return String.format("%.2f", futureBalance);
}
The getrowCount and getColumnCount methods simply return the number of rows and columns.
public int getRowCount() { return years; }
public int getColumnCount() { return maxRate - minRate + 1; }
If you don't supply column names, the getColumnName method of the AbstractTableModel names the columns A, B, C, and so on. To change column names, override the getColumnName method. You will usually want to override that default behavior. In this example, we simply label each column with the interest rate.
public String getColumnName(int c) { return (c + minRate) + "%"; }
You can find the complete source code in Example 6-10.
Example 6-10. InvestmentTable.java
1. import java.awt.*;
2. import java.awt.event.*;
3. import java.text.*;
4. import javax.swing.*;
5. import javax.swing.table.*;
6.
7. /**
8. This program shows how to build a table from a table model.
9. */
10. public class InvestmentTable
11. {
12. public static void main(String[] args)
13. {
14. JFrame frame = new InvestmentTableFrame();
15. frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
16. frame.setVisible(true);
17. }
18. }
19.
20. /**
21. This frame contains the investment table.
22. */
23. class InvestmentTableFrame extends JFrame
24. {
25. public InvestmentTableFrame()
26. {
27. setTitle("InvestmentTable");
28. setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
29.
30. TableModel model = new InvestmentTableModel(30, 5, 10);
31. JTable table = new JTable(model);
32. add(new JScrollPane(table));
33. }
34.
35. private static final int DEFAULT_WIDTH = 600;
36. private static final int DEFAULT_HEIGHT = 300;
37. }
38.
39. /**
40. This table model computes the cell entries each time they
41. are requested. The table contents shows the growth of
42. an investment for a number of years under different interest
43. rates.
44. */
45. class InvestmentTableModel extends AbstractTableModel
46. {
47. /**
48. Constructs an investment table model.
49. @param y the number of years
50. @param r1 the lowest interest rate to tabulate
51. @param r2 the highest interest rate to tabulate
52. */
53. public InvestmentTableModel(int y, int r1, int r2)
54. {
55. years = y;
56. minRate = r1;
57. maxRate = r2;
58. }
59.
60. public int getRowCount() { return years; }
61.
62. public int getColumnCount() { return maxRate - minRate + 1; }
63.
64. public Object getValueAt(int r, int c)
65. {
66. double rate = (c + minRate) / 100.0;
67. int nperiods = r;
68. double futureBalance = INITIAL_BALANCE * Math.pow(1 + rate, nperiods);
69. return String.format("%.2f", futureBalance);
70. }
71.
72. public String getColumnName(int c) { return (c + minRate) + "%"; }
73.
74. private int years;
75. private int minRate;
76. private int maxRate;
77.
78. private static double INITIAL_BALANCE = 100000.0;
79. }
Displaying Database Records
Probably the most common information to be displayed in a table is a set of records from a database. If you use a professional development environment, it almost certainly includes convenient JavaBeans components (beans) for accessing database information. However, if you don't have database beans or if you are simply curious about what goes on under the hood, you will find the next example interesting. Figure 6-30 shows the outputthe result of a query for all rows in a database table.
Figure 6-30. Displaying a query result in a table
In the example program, we define a ResultSetTableModel that fetches data from the result set of a database query. (See Chapter 4 for more information on Java database access and result sets.)
You can obtain the column count and the column names from the ResultSetMetaData object:
public String getColumnName(int c)
{
try
{
return rsmd.getColumnName(c + 1);
}
catch (SQLException e)
{
. . .
}
}
public int getColumnCount()
{
try
{
return rsmd.getColumnCount();
}
catch (SQLException e)
{
. . .
}
}
If the database supports scrolling cursors, then it is particularly easy to get a cell value: Just move the cursor to the requested row and fetch the column value.
public Object getValueAt(int r, int c)
{
try
{
ResultSet rs = getResultSet();
rs.absolute(r + 1);
return rs.getObject(c + 1);
}
catch (SQLException e)
{
e.printStackTrace();
return null;
}
}
It makes a lot of sense to use this data model instead of the DefaultTableModel. If you created an array of values, then you would duplicate the cache that the database driver is already managing.
If the database does not support scrolling cursors, our example program puts the data in a row set instead.
Example 6-11. ResultSetTable.java
1. import com.sun.rowset.*;
2. import java.awt.*;
3. import java.awt.event.*;
4. import java.io.*;
5. import java.sql.*;
6. import java.util.*;
7. import javax.swing.*;
8. import javax.swing.table.*;
9. import javax.sql.rowset.*;
10.
11. /**
12. This program shows how to display the result of a
13. database query in a table.
14. */
15. public class ResultSetTable
16. {
17. public static void main(String[] args)
18. {
19. JFrame frame = new ResultSetFrame();
20. frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
21. frame.setVisible(true);
22. }
23. }
24.
25. /**
26. This frame contains a combo box to select a database table
27. and a table to show the data stored in the table
28. */
29. class ResultSetFrame extends JFrame
30. {
31. public ResultSetFrame()
32. {
33. setTitle("ResultSet");
34. setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
35.
36. /* find all tables in the database and add them to
37. a combo box
38. */
39.
40. tableNames = new JComboBox();
41. tableNames.addActionListener(new
42. ActionListener()
43. {
44. public void actionPerformed(ActionEvent event)
45. {
46. try
47. {
48. if (scrollPane != null) remove(scrollPane);
49. String tableName = (String) tableNames.getSelectedItem();
50. if (rs != null) rs.close();
51. String query = "SELECT * FROM " + tableName;
52. rs = stat.executeQuery(query);
53. if (scrolling)
54. model = new ResultSetTableModel(rs);
55. else
56. {
57. CachedRowSet crs = new CachedRowSetImpl();
58. crs.populate(rs);
59. model = new ResultSetTableModel(crs);
60. }
61.
62. JTable table = new JTable(model);
63. scrollPane = new JScrollPane(table);
64. add(scrollPane, BorderLayout.CENTER);
65. validate();
66. }
67. catch (SQLException e)
68. {
69. e.printStackTrace();
70. }
71. }
72. });
73. JPanel p = new JPanel();
74. p.add(tableNames);
75. add(p, BorderLayout.NORTH);
76.
77. try
78. {
79. conn = getConnection();
80. DatabaseMetaData meta = conn.getMetaData();
81. if (meta.supportsResultSetType(ResultSet.TYPE_SCROLL_INSENSITIVE))
82. {
83. scrolling = true;
84. stat = conn.createStatement(ResultSet.TYPE_SCROLL_INSENSITIVE,
85. ResultSet.CONCUR_READ_ONLY);
86. }
87. else
88. {
89. stat = conn.createStatement();
90. scrolling = false;
91. }
92. ResultSet tables = meta.getTables(null, null, null, new String[] { "TABLE" });
93. while (tables.next())
94. tableNames.addItem(tables.getString(3));
95. tables.close();
96. }
97. catch (IOException e)
98. {
99. e.printStackTrace();
100. }
101. catch (SQLException e)
102. {
103. e.printStackTrace();
104. }
105.
106. addWindowListener(new
107. WindowAdapter()
108. {
109. public void windowClosing(WindowEvent event)
110. {
111. try
112. {
113. if (conn != null) conn.close();
114. }
115. catch (SQLException e)
116. {
117. e.printStackTrace();
118. }
119. }
120. });
121. }
122.
123. /**
124. Gets a connection from the properties specified in
125. the file database.properties.
126. @return the database connection
127. */
128. public static Connection getConnection()
129. throws SQLException, IOException
130. {
131. Properties props = new Properties();
132. FileInputStream in = new FileInputStream("database.properties");
133. props.load(in);
134. in.close();
135.
136. String drivers = props.getProperty("jdbc.drivers");
137. if (drivers != null) System.setProperty("jdbc.drivers", drivers);
138. String url = props.getProperty("jdbc.url");
139. String username = props.getProperty("jdbc.username");
140. String password = props.getProperty("jdbc.password");
141.
142. return DriverManager.getConnection(url, username, password);
143. }
144.
145. private JScrollPane scrollPane;
146. private ResultSetTableModel model;
147. private JComboBox tableNames;
148. private ResultSet rs;
149. private Connection conn;
150. private Statement stat;
151. private boolean scrolling;
152.
153. private static final int DEFAULT_WIDTH = 400;
154. private static final int DEFAULT_HEIGHT = 300;
155. }
156.
157. /**
158. This class is the superclass for the scrolling and the
159. caching result set table model. It stores the result set
160. and its metadata.
161. */
162. class ResultSetTableModel extends AbstractTableModel
163. {
164. /**
165. Constructs the table model.
166. @param aResultSet the result set to display.
167. */
168. public ResultSetTableModel(ResultSet aResultSet)
169. {
170. rs = aResultSet;
171. try
172. {
173. rsmd = rs.getMetaData();
174. }
175. catch (SQLException e)
176. {
177. e.printStackTrace();
178. }
179. }
180.
181. public String getColumnName(int c)
182. {
183. try
184. {
185. return rsmd.getColumnName(c + 1);
186. }
187. catch (SQLException e)
188. {
189. e.printStackTrace();
190. return "";
191. }
192. }
193.
194. public int getColumnCount()
195. {
196. try
197. {
198. return rsmd.getColumnCount();
199. }
200. catch (SQLException e)
201. {
202. e.printStackTrace();
203. return 0;
204. }
205. }
206.
207. public Object getValueAt(int r, int c)
208. {
209. try
210. {
211. rs.absolute(r + 1);
212. return rs.getObject(c + 1);
213. }
214. catch(SQLException e)
215. {
216. e.printStackTrace();
217. return null;
218. }
219. }
220.
221. public int getRowCount()
222. {
223. try
224. {
225. rs.last();
226. return rs.getRow();
227. }
228. catch(SQLException e)
229. {
230. e.printStackTrace();
231. return 0;
232. }
233. }
234.
235. private ResultSet rs;
236. private ResultSetMetaData rsmd;
237. }
A Sort Filter
The last two examples drove home the point that tables don't store the cell data; they get them from a model. The model need not store the data either. It can compute the cell values or fetch them from somewhere else.
In this section, we introduce another useful technique, a filter model that presents information from another table in a different form. In our example, we sort the rows in a table. Run the program in Example 6-12 and double-click on one of the column headers. You will see how the rows are rearranged so that the column entries are sorted (see Figure 6-31).
Figure 6-31. Sorting the rows of a table
However, we don't physically rearrange the rows in the data model. Instead, we use a filter model that keeps an array with the permuted row indexes.
The filter model stores a reference to the actual table model. When the JTable needs to look up a value, the filter model computes the actual row index and gets the value from the model. For example,
public Object getValueAt(int r, int c) { return model.getValueAt(actual row index, c); }
All other methods are simply passed on to the original model.
public String getColumnName(int c) { return model.getColumnName(c); }
Figure 6-32 shows how the filter sits between the JTable object and the actual table model.
Figure 6-32. A table model filter
There are two complexities when you implement such a sort filter. First, you need to be notified when the user double-clicks on one of the column headers. We don't want to go into too much detail on this technical point. You can find the code in the addMouseListener method of the SortFilterModel in Example 6-12. Here is the idea behind the code. First, get the table header component and attach a mouse listener. When a double click is detected, you need to find out in which table column the mouse click fell. Then, you need to translate the table column to the model columnthey can be different if the user moved the table columns around. Once you know the model column, you can start sorting the table rows.
The second complexity lies in sorting the table rows. We don't want to physically rearrange the rows. What we want is a sequence of row indexes that tells us how we would rearrange them if they were being sorted. However, the sort algorithms in the Arrays and Collections classes don't tell us how they rearrange the elements. Of course, you could reimplement a sorting algorithm and keep track of the object rearrangements, but there is a much smarter way. The trick is to come up with custom objects and a custom comparison method so that the library sorting algorithm can be pressed into service.
We will sort objects of type Row. A Row object contains the index r of a row in the model. Compare two such objects as follows: Find the elements in the model and compare them. In other words, the compareTo method for Row objects computes
model.getValueAt(r1, c).compareTo(model.getValueAt(r2, c))
Here r1 and r2 are the row indexes of the Row objects, and c is the column whose elements should be sorted.
If the entries of a particular column aren't comparable, we simply compare their string representations. That way, the column with Color values can still be sorted. (The Color class does not implement the Comparable interface.)
We make the Row class into an inner class of the SortFilterModel because the compareTo method needs to access the current model and column. Here is the code:
class SortFilterModel extends AbstractTableModel
{
. . .
private class Row implements Comparable<Row>
{
public int index;
public int compareTo(Row other)
{
Object a = model.getValueAt(index, sortColumn);
Object b = model.getValueAt(other.index, sortColumn);
if (a instanceof Comparable)
return ((Comparable) a).compareTo(b);
else
return a.toString().compareTo(b.toString());
}
}
private TableModel model;
private int sortColumn;
private Row[] rows;
}
In the constructor, we build an array rows, initialized such that rows[i].index is set to i:
public SortFilterModel(TableModel m)
{
model = m;
rows = new Row[model.getRowCount()];
for (int i = 0; i < rows.length; i++)
{
rows[i] = new Row();
rows[i].index = i;
}
}
In the sort method, we invoke the Arrays.sort algorithm. It sorts the Row objects. Because the comparison criterion looks at the model elements in the appropriate column, the elements are arranged so that afterward row[0] contains the index of the smallest element in the column, row[1] contains the index of the next-smallest element, and so on.
When the array is sorted, we notify all table model listeners (in particular, the JTable) that the table contents have changed and must be redrawn.
public void sort(int c)
{
sortColumn = c;
Arrays.sort(rows);
fireTableDataChanged();
}
Finally, we can show you the exact computation of the getValueAt method of the filter class. It simply translates a row index r to the model row index rows[r].index:
public Object getValueAt(int r, int c) { return model.getValueAt(rows[r].index, c); }
The sort model filter shows again the power of the model-view-controller pattern. Because the data and the display are separated, we are able to change the mapping between the two.
TIP
You can find a more elaborate version of a table sorter at http://java.sun.com/docs/books/tutorial/uiswing/components/table.html#sorting. That implementation listens to the table model and updates the sorted view when the table model changes.
Example 6-12. TableSortTest.java
[View full width]
1. import java.awt.*;
2. import java.awt.event.*;
3. import java.util.*;
4. import javax.swing.*;
5. import javax.swing.event.*;
6. import javax.swing.table.*;
7.
8. /**
9. This program demonstrates how to sort a table column.
10. Double-click on a table column's header to sort it.
11. */
12. public class TableSortTest
13. {
14. public static void main(String[] args)
15. {
16. JFrame frame = new TableSortFrame();
17. frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
18. frame.setVisible(true);
19. }
20. }
21.
22. /**
23. This frame contains a table of planet data.
24. */
25. class TableSortFrame extends JFrame
26. {
27. public TableSortFrame()
28. {
29. setTitle("TableSortTest");
30. setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
31.
32. // set up table model and interpose sorter
33.
34. DefaultTableModel model = new DefaultTableModel(cells, columnNames);
35. final SortFilterModel sorter = new SortFilterModel(model);
36.
37. // show table
38.
39. final JTable table = new JTable(sorter);
40. add(new JScrollPane(table), BorderLayout.CENTER);
41.
42. // set up double-click handler for column headers
43.
44. table.getTableHeader().addMouseListener(new
45. MouseAdapter()
46. {
47. public void mouseClicked(MouseEvent event)
48. {
49. // check for double click
50. if (event.getClickCount() < 2) return;
51.
52. // find column of click and
53. int tableColumn = table.columnAtPoint(event.getPoint());
54.
55. // translate to table model index and sort
56. int modelColumn = table.convertColumnIndexToModel(tableColumn);
57. sorter.sort(modelColumn);
58. }
59. });
60. }
61.
62. private Object[][] cells =
63. {
64. { "Mercury", 2440.0, 0, false, Color.yellow },
65. { "Venus", 6052.0, 0, false, Color.yellow },
66. { "Earth", 6378.0, 1, false, Color.blue },
67. { "Mars", 3397.0, 2, false, Color.red },
68. { "Jupiter", 71492.0, 16, true, Color.orange },
69. { "Saturn", 60268.0, 18, true, Color.orange },
70. { "Uranus", 25559.0, 17, true, Color.blue },
71. { "Neptune", 24766.0, 8, true, Color.blue },
72. { "Pluto", 1137.0, 1, false, Color.black }
73. };
74.
75. private String[] columnNames = { "Planet", "Radius", "Moons", "Gaseous", "Color" };
76.
77. private static final int DEFAULT_WIDTH = 400;
78. private static final int DEFAULT_HEIGHT = 200;
79. }
80.
81. /**
82. This table model takes an existing table model and produces a new model that sorts
the rows
83. so that the entries in a given column are sorted.
84. */
85. class SortFilterModel extends AbstractTableModel
86. {
87. /**
88. Constructs a sort filter model.
89. @param m the table model whose rows should be sorted
90. */
91. public SortFilterModel(TableModel m)
92. {
93. model = m;
94. rows = new Row[model.getRowCount()];
95. for (int i = 0; i < rows.length; i++)
96. {
97. rows[i] = new Row();
98. rows[i].index = i;
99. }
100. }
101.
102. /**
103. Sorts the rows.
104. @param c the column that should become sorted
105. */
106. public void sort(int c)
107. {
108. sortColumn = c;
109. Arrays.sort(rows);
110. fireTableDataChanged();
111. }
112.
113. // Compute the moved row for the three methods that access model elements
114.
115. public Object getValueAt(int r, int c) { return model.getValueAt(rows[r].index, c); }
116.
117. public boolean isCellEditable(int r, int c) { return model.isCellEditable(rows[r]
.index, c); }
118.
119. public void setValueAt(Object aValue, int r, int c)
120. {
121. model.setValueAt(aValue, rows[r].index, c);
122. }
123.
124. // delegate all remaining methods to the model
125.
126. public int getRowCount() { return model.getRowCount(); }
127. public int getColumnCount() { return model.getColumnCount(); }
128. public String getColumnName(int c) { return model.getColumnName(c); }
129. public Class getColumnClass(int c) { return model.getColumnClass(c); }
130.
131. /**
132. This inner class holds the index of the model row
133. Rows are compared by looking at the model row entries
134. in the sort column.
135. */
136. private class Row implements Comparable<Row>
137. {
138. public int index;
139. public int compareTo(Row other)
140. {
141. Object a = model.getValueAt(index, sortColumn);
142. Object b = model.getValueAt(other.index, sortColumn);
143. if (a instanceof Comparable)
144. return ((Comparable) a).compareTo(b);
145. else
146. return a.toString().compareTo(b.toString());
147. }
148. }
149.
150. private TableModel model;
151. private int sortColumn;
152. private Row[] rows;
153. }
javax.swing.table.TableModel 1.2
int getRowCount()
int getColumnCount()
get the number of rows and columns in the table model.
Object getValueAt(int row, int column)
gets the value at the given row and column.
void setValueAt(Object newValue, int row, int column)
sets a new value at the given row and column.
boolean isCellEditable(int row, int column)
returns true if the cell at the given row and column is editable.
String getColumnName(int column)
gets the column title.
javax.swing.table.AbstractTableModel 1.2
void fireTableDataChanged()
notifies all table model listeners that the table data has changed.
javax.swing.JTable 1.2
JTableHeader getTableHeader()
returns the table header component of this table.
int columnAtPoint(Point p)
returns the number of the table column that falls under the pixel position p.
int convertColumnIndexToModel(int tableColumn)
returns the model index of the column with the given index. This value is different from tableColumn if some of the table columns are moved or hidden.
Cell Rendering and Editing
In the next example, we again display our planet data, but this time, we want to give the table more information about the column types. If you define the method
Class getColumnClass(int columnIndex)
of your table model to return the class that describes the column type, then the JTable class picks an appropriate renderer for the class. Table 6-1 shows how the JTable class renders types by default.
Table 6-1. Default Rendering Actions Type
Rendered As
Icon
Image
Boolean
Checkbox
Object
String
You can see the checkboxes and images in Figure 6-33. (Thanks to Jim Evins, http://www.snaught.com/JimsCoolIcons/Planets, for providing the planet images!)
Figure 6-33. A table with cell renderers
[View full size image]
For other types, you can supply your own cell renderers. Table cell renderers are similar to the tree cell renderers that you saw earlier. They implement the TableCellRenderer interface, which has a single method
Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected,
boolean hasFocus, int row, int column)
That method is called when the table needs to draw a cell. You return a component whose paint method is then invoked to fill the cell area.
To display a cell of type Color, you can simply return a panel with a background color you set to the color object stored in the cell. The color is passed as the value parameter.
[View full width]
class ColorTableCellRenderer extends JPanel implements TableCellRenderer
{
public Component getTableCellRendererComponent(JTable table, Object value, boolean
isSelected,
boolean hasFocus, int row, int column)
{
setBackground((Color) value);
if (hasFocus)
setBorder(UIManager.getBorder("Table.focusCellHighlightBorder"));
else
setBorder(null);
}
}
As you can see, the renderer installs a border when the cell has focus. (We ask the UIManager for the correct border. To find the lookup key, we peeked into the source code of the DefaultTableCellRenderer class.)
Generally, you will also want to set the background color of the cell to indicate whether it is currently selected. We skip this step because it would interfere with the displayed color. The ListRenderingTest example on page 334 shows how to indicate the selection status in a renderer.
TIP
If your renderer simply draws a text string or an icon, you can extend the DefaultTableCellRenderer class. It takes care of rendering the focus and selection status for you.
You need to tell the table to use this renderer with all objects of type Color. The setDefaultRenderer method of the JTable class lets you establish this association. You supply a Class object and the renderer:
table.setDefaultRenderer(Color.class, new ColorTableCellRenderer());
That renderer is now used for all objects of the given type.
Cell Editing
To enable cell editing, the table model must indicate which cells are editable by defining the isCellEditable method. Most commonly, you will want to make certain columns editable. In the example program, we allow editing in four columns.
public boolean isCellEditable(int r, int c)
{
return c == PLANET_COLUMN || c == MOONS_COLUMN || c == GASEOUS_COLUMN || c == COLOR_COLUMN;
}
private static final int PLANET_COLUMN = 0;
private static final int MOONS_COLUMN = 2;
private static final int GASEOUS_COLUMN = 3;
private static final int COLOR_COLUMN = 4;
NOTE
The AbstractTableModel defines the isCellEditable method to always return false. The DefaultTableModel overrides the method to always return TRue.
If you run the program in Example 6-13, note that you can click on the checkboxes in the Gaseous column and turn the check marks on and off. If you click on a cell in the Moons column, a combo box appears (see Figure 6-34). You will shortly see how to install such a combo box as a cell editor.
Figure 6-34. A cell editor
[View full size image]
Finally, click on a cell in the first column. The cell gains focus. You can start typing and the cell contents change.
What you just saw in action are the three variations of the DefaultCellEditor class. A DefaultCellEditor can be constructed with a JTextField, a JCheckBox, or a JComboBox. The JTable class automatically installs a checkbox editor for Boolean cells and a text field editor for all editable cells that don't supply their own renderer. The text fields let the user edit the strings that result from applying toString to the return value of the getValueAt method of the table model.
When the edit is complete, the edited value is retrieved by calling the getCellEditorValue method of your editor. That method should return a value of the correct type (that is, the type returned by the getColumnType method of the model).
To get a combo box editor, you set a cell editor manuallythe JTable component has no idea what values might be appropriate for a particular type. For the Moons column, we wanted to enable the user to pick any value between 0 and 20. Here is the code for initializing the combo box.
JComboBox moonCombo = new JComboBox();
for (int i = 0; i <= 20; i++)
moonCombo.addItem(i);
To construct a DefaultCellEditor, supply the combo box in the constructor:
TableCellEditor moonEditor = new DefaultCellEditor(moonCombo);
Next, we need to install the editor. Unlike the color cell renderer, this editor does not depend on the object typewe don't necessarily want to use it for all objects of type Integer. Instead, we need to install it into a particular column.
The JTable class stores information about table columns in objects of type TableColumn. A TableColumnModel object manages the columns. (Figure 6-35 shows the relationships among the most important table classes.) If you don't want to insert or remove columns dynamically, you won't use the table column model much. However, to get a particular TableColumn object, you need to get the column model to ask it for the column object:
TableColumnModel columnModel = table.getColumnModel()
TableColumn moonColumn = columnModel.getColumn(PlanetTableModel.MOONS_COLUMN);
Figure 6-35. Relationship between Table classes
Finally, you can install the cell editor:
moonColumn.setCellEditor(moonEditor);
If your cells are taller than the default, you also want to set the row height:
table.setRowHeight(height);
By default, all rows of the table have the same height. You can set the heights of individual rows with the call
table.setRowHeight(row, height);
The actual row height equals the row height that has been set with these methods, reduced by the row margin. The default row margin is 1, but you can change it with the call
table.setRowMargin(margin);
To display an icon in the header, set the header value:
moonColumn.setHeaderValue(new ImageIcon("Moons.gif"));
However, the table header isn't smart enough to choose an appropriate renderer for the header value. You have to install the renderer manually. For example, to show an image icon in a column header, call
moonColumn.setHeaderRenderer(table.getDefaultRenderer(ImageIcon.class));
Custom Editors
Run the example program again and click on a color. A color chooser pops up to let you pick a new color for the planet. Select a color and click OK. The cell color is updated (see Figure 6-36).
Figure 6-36. Editing the cell color with a color chooser
[View full size image]
The color cell editor is not a standard table cell editor but a custom implementation. To create a custom cell editor, you implement the TableCellEditor interface. That interface is a bit tedious, and as of JDK 1.3, an AbstractCellEditor class is provided to take care of the event handling details.
The getTableCellEditorComponent method of the TableCellEditor interface requests a component to render the cell. It is exactly the same as the getTableCellRendererComponent method of the TableCellRenderer interface, except that there is no focus parameter. Because the cell is being edited, it is presumed to have focus. The editor component temporarily replaces the renderer when the editing is in progress. In our example, we return a blank panel that is not colored. This is an indication to the user that the cell is currently being edited.
Next, you want to have your editor pop up when the user clicks on the cell.
The JTable class calls your editor with an event (such as a mouse click) to find out if that event is acceptable to initiate the editing process. The AbstractCellEditor class defines the method to accept all events.
public boolean isCellEditable(EventObject anEvent)
{
return true;
}
However, if you override this method to false, then the table would not go through the trouble of inserting the editor component.
Once the editor component is installed, the shouldSelectCell method is called, presumably with the same event. You should initiate editing in this method, for example, by popping up an external edit dialog box.
public boolean shouldSelectCell(EventObject anEvent)
{
colorDialog.setVisible(true);
return true;
}
If the user cancels the edit, the table calls the cancelCellEditing method. If the user has clicked on another table cell, the table calls the stopCellEditing method. In both cases, you should hide the dialog box. When your stopCellEditing method is called, the table would like to use the partially edited value. You should return TRue if the current value is valid. In the color chooser, any value is valid. But if you edit other data, you can ensure that only valid data is retrieved from the editor.
Also, you should call the superclass methods that take care of event firingotherwise, the editing won't be properly canceled.
public void cancelCellEditing()
{
colorDialog.setVisible(false);
super.cancelCellEditing();
}
Finally, you need to supply a method that yields the value that the user supplied in the editing process:
public Object getCellEditorValue()
{
return colorChooser.getColor();
}
To summarize, your custom editor should do the following:
Extend the AbstractCellEditor class and implement the TableCellEditor interface.
Define the getTableCellEditorComponent method to supply a component. This can either be a dummy component (if you pop up a dialog box) or a component for in-place editing such as a combo box or text field.
Define the shouldSelectCell, stopCellEditing, and cancelCellEditing methods to handle the start, completion, and cancellation of the editing process. The stopCellEditing and cancelCellEditing should call the superclass methods to ensure that listeners are notified.
Define the getCellEditorValue method to return the value that is the result of the editing process.
Finally, you indicate when the user is finished editing by calling the stopCellEditing and cancelCellEditing methods. When constructing the color dialog box, we install accept and cancel callbacks that fire these events.
colorDialog = JColorChooser.createDialog(null, "Planet Color", false, colorChooser,
new
ActionListener() // OK button listener
{
public void actionPerformed(ActionEvent event)
{
stopCellEditing();
}
},
new
ActionListener() // Cancel button listener
{
public void actionPerformed(ActionEvent event)
{
cancelCellEditing();
}
});
Also, when the user closes the dialog box, editing should be canceled. This is achieved by installation of a window listener:
colorDialog.addWindowListener(new
WindowAdapter()
{
public void windowClosing(WindowEvent event)
{
cancelCellEditing();
}
});
This completes the implementation of the custom editor.
You now know how to make a cell editable and how to install an editor. There is one remaining issuehow to update the model with the value that the user edited. When editing is complete, the JTable class calls the following method of the table model:
void setValueAt(Object value, int r, int c)
You need to override the method to store the new value. The value parameter is the object that was returned by the cell editor. If you implemented the cell editor, then you know the type of the object that you return from the getCellEditorValue method. In the case of the DefaultCellEditor, there are three possibilities for that value. It is a Boolean if the cell editor is a checkbox, a string if it is a text field. If the value comes from a combo box, then it is the object that the user selected.
If the value object does not have the appropriate type, you need to convert it. That happens most commonly when a number is edited in a text field. In our example, we populated the combo box with Integer objects so that no conversion is necessary.
Example 6-13. TableCellRenderTest.java
[View full width]
1. import java.awt.*;
2. import java.awt.event.*;
3. import java.util.*;
4. import javax.swing.*;
5. import javax.swing.border.*;
6. import javax.swing.event.*;
7. import javax.swing.table.*;
8.
9. /**
10. This program demonstrates cell rendering and editing
11. in a table.
12. */
13. public class TableCellRenderTest
14. {
15. public static void main(String[] args)
16. {
17. JFrame frame = new TableCellRenderFrame();
18. frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
19. frame.setVisible(true);
20. }
21. }
22.
23. /**
24. This frame contains a table of planet data.
25. */
26. class TableCellRenderFrame extends JFrame
27. {
28. public TableCellRenderFrame()
29. {
30. setTitle("TableCellRenderTest");
31. setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
32.
33. TableModel model = new PlanetTableModel();
34. JTable table = new JTable(model);
35. table.setRowSelectionAllowed(false);
36.
37. // set up renderers and editors
38.
39. table.setDefaultRenderer(Color.class, new ColorTableCellRenderer());
40. table.setDefaultEditor(Color.class, new ColorTableCellEditor());
41.
42. JComboBox moonCombo = new JComboBox();
43. for (int i = 0; i <= 20; i++)
44. moonCombo.addItem(i);
45.
46. TableColumnModel columnModel = table.getColumnModel();
47. TableColumn moonColumn = columnModel.getColumn(PlanetTableModel.MOONS_COLUMN);
48. moonColumn.setCellEditor(new DefaultCellEditor(moonCombo));
49. moonColumn.setHeaderRenderer(table.getDefaultRenderer(ImageIcon.class));
50. moonColumn.setHeaderValue(new ImageIcon("Moons.gif"));
51.
52. // show table
53.
54. table.setRowHeight(100);
55. add(new JScrollPane(table), BorderLayout.CENTER);
56. }
57.
58. private static final int DEFAULT_WIDTH = 600;
59. private static final int DEFAULT_HEIGHT = 400;
60. }
61.
62. /**
63. The planet table model specifies the values, rendering
64. and editing properties for the planet data.
65. */
66. class PlanetTableModel extends AbstractTableModel
67. {
68. public String getColumnName(int c) { return columnNames[c]; }
69. public Class getColumnClass(int c) { return cells[0][c].getClass(); }
70. public int getColumnCount() { return cells[0].length; }
71. public int getRowCount() { return cells.length; }
72. public Object getValueAt(int r, int c) { return cells[r][c]; }
73. public void setValueAt(Object obj, int r, int c) { cells[r][c] = obj; }
74. public boolean isCellEditable(int r, int c)
75. {
76. return c == PLANET_COLUMN || c == MOONS_COLUMN || c == GASEOUS_COLUMN || c ==
COLOR_COLUMN;
77. }
78.
79. public static final int PLANET_COLUMN = 0;
80. public static final int MOONS_COLUMN = 2;
81. public static final int GASEOUS_COLUMN = 3;
82. public static final int COLOR_COLUMN = 4;
83.
84. private Object[][] cells =
85. {
86. { "Mercury", 2440.0, 0, false, Color.yellow, new ImageIcon("Mercury.gif") },
87. { "Venus", 6052.0, 0, false, Color.yellow, new ImageIcon("Venus.gif") },
88. { "Earth", 6378.0, 1, false, Color.blue, new ImageIcon("Earth.gif") },
89. { "Mars", 3397.0, 2, false, Color.red, new ImageIcon("Mars.gif") },
90. { "Jupiter", 71492.0, 16, true, Color.orange, new ImageIcon("Jupiter.gif") },
91. { "Saturn", 60268.0, 18, true, Color.orange, new ImageIcon("Saturn.gif") },
92. { "Uranus", 25559.0, 17, true, Color.blue, new ImageIcon("Uranus.gif") },
93. { "Neptune", 24766.0, 8, true, Color.blue, new ImageIcon("Neptune.gif") },
94. { "Pluto", 1137.0, 1, false, Color.black, new ImageIcon("Pluto.gif") }
95. };
96.
97. private String[] columnNames = { "Planet", "Radius", "Moons", "Gaseous", "Color",
"Image" };
98. }
99.
100. /**
101. This renderer renders a color value as a panel with the
102. given color.
103. */
104. class ColorTableCellRenderer extends JPanel implements TableCellRenderer
105. {
106. public Component getTableCellRendererComponent(JTable table, Object value, boolean
isSelected,
107. boolean hasFocus, int row, int column)
108. {
109. setBackground((Color) value);
110. if (hasFocus)
111. setBorder(UIManager.getBorder("Table.focusCellHighlightBorder"));
112. else
113. setBorder(null);
114. return this;
115. }
116. }
117.
118. /**
119. This editor pops up a color dialog to edit a cell value
120. */
121. class ColorTableCellEditor extends AbstractCellEditor implements TableCellEditor
122. {
123. public ColorTableCellEditor()
124. {
125. panel = new JPanel();
126. // prepare color dialog
127.
128. colorChooser = new JColorChooser();
129. colorDialog = JColorChooser.createDialog(null, "Planet Color", false, colorChooser,
130. new
131. ActionListener() // OK button listener
132. {
133. public void actionPerformed(ActionEvent event) { stopCellEditing(); }
134. },
135. new
136. ActionListener() // Cancel button listener
137. {
138. public void actionPerformed(ActionEvent event) { cancelCellEditing(); }
139. });
140. colorDialog.addWindowListener(new
141. WindowAdapter()
142. {
143. public void windowClosing(WindowEvent event) { cancelCellEditing(); }
144. });
145. }
146.
147. public Component getTableCellEditorComponent(JTable table,
148. Object value, boolean isSelected, int row, int column)
149. {
150. // this is where we get the current Color value. We store it in the dialog in
case the user
151. // starts editing
152. colorChooser.setColor((Color) value);
153. return panel;
154. }
155.
156. public boolean shouldSelectCell(EventObject anEvent)
157. {
158. // start editing
159. colorDialog.setVisible(true);
160.
161. // tell caller it is ok to select this cell
162. return true;
163. }
164.
165. public void cancelCellEditing()
166. {
167. // editing is canceled--hide dialog
168. colorDialog.setVisible(false);
169. super.cancelCellEditing();
170. }
171.
172. public boolean stopCellEditing()
173. {
174. // editing is complete--hide dialog
175. colorDialog.setVisible(false);
176. super.stopCellEditing();
177.
178. // tell caller is is ok to use color value
179. return true;
180. }
181.
182. public Object getCellEditorValue()
183. {
184. return colorChooser.getColor();
185. }
186.
187. private Color color;
188. private JColorChooser colorChooser;
189. private JDialog colorDialog;
190. private JPanel panel;
191. }
javax.swing.JTable 1.2
void setRowHeight(int height)
sets the height of all rows of the table to height pixels.
void setRowHeight(int row, int height)
sets the height of the given row of the table to height pixels.
void setRowMargin(int margin)
sets the amount of empty space between cells in adjacent rows.
int getRowHeight()
gets the default height of all rows of the table.
int getRowHeight(int row)
gets the height of the given row of the table.
int getRowMargin()
gets the amount of empty space between cells in adjacent rows.
Rectangle getCellRect(int row, int column, boolean includeSpacing)
returns the bounding rectangle of a table cell.
Parameters:
row, column
The row and column of the cell
includeSpacing
true if the space around the cell should be included
Color getSelectionBackground()
Color getSelectionForeground()
return the background and foreground colors to use for selected cells.
TableCellRenderer getDefaultRenderer(Class<?> type)
gets the default renderer for the given type.
TableCellEditor getDefaultEditor(Class<?> type)
gets the default editor for the given type.
javax.swing.table.TableModel 1.2
Class getColumnClass(int columnIndex)
gets the class for the values in this column. This information is used by the cell renderer and editor.
javax.swing.table.TableCellRenderer 1.2
Component getTableCellRendererComponent(JTable table, Object value, boolean selected, boolean hasFocus, int row, int column)
returns a component whose paint method is invoked to render a table cell.
Parameters:
table
The table containing the cell to be rendered
value
The cell to be rendered
selected
true if the cell is currently selected
hasFocus
true if the cell currently has focus
row, column
The row and column of the cell
javax.swing.table.TableColumnModel 1.2
TableColumn getColumn(int index)
gets the table column object that describes the column with the given index.
javax.swing.table.TableColumn 1.2
void setCellEditor(TableCellEditor editor)
void setCellRenderer(TableCellRenderer renderer)
set the cell editor or renderer for all cells in this column.
void setHeaderRenderer(TableCellRenderer renderer)
sets the cell renderer for the header cell in this column.
void setHeaderValue(Object value)
sets the value to be displayed for the header in this column.
javax.swing.DefaultCellEditor 1.2
DefaultCellEditor(JComboBox comboBox)
constructs a cell editor that presents the combo box for selecting cell values.
javax.swing.CellEditor 1.2
boolean isCellEditable(EventObject event)
returns true if the event is suitable for initiating the editing process for this cell.
boolean shouldSelectCell(EventObject anEvent)
starts the editing process. Returns TRue if the edited cell should be selected. Normally, you want to return true, but you can return false if you don't want the editing process to change the cell selection.
void cancelCellEditing()
cancels the editing process. You can abandon partial edits.
boolean stopCellEditing()
stops the editing process, with the intent of using the result. Returns TRue if the edited value is in a proper state for retrieval.
Object getCellEditorValue()
returns the edited result.
void addCellEditorListener(CellEditorListener l)
void removeCellEditorListener(CellEditorListener l)
add and remove the obligatory cell editor listener.
javax.swing.table.TableCellEditor 1.2
Component getTableCellEditorComponent(JTable table, Object value, boolean selected, int row, int column)
returns a component whose paint method renders a table cell.
Parameters:
table
The table containing the cell to be rendered
value
The cell to be rendered
selected
TRue if the cell is currently selected
row, column
The row and column of the cell
Working with Rows and Columns
In this subsection, you will see how to manipulate the rows and columns in a table. As you read through this material, keep in mind that a Swing table is quite asymmetricthere are different operations that you can carry out on rows and columns. The table component was optimized to display rows of information with the same structure, such as the result of a database query, not an arbitrary two-dimensional grid of objects. You will see this asymmetry throughout this subsection.
Resizing Columns
The TableColumn class gives you control over the resizing behavior of columns. You can set the preferred, minimum, and maximum width with the methods
void setPreferredWidth(int width)
void setMinWidth(int width)
void setMaxWidth(int width)
This information is used by the table component to lay out the columns.
Use the method
void setResizable(boolean resizable)
to control whether the user is allowed to resize the column.
You can programmatically resize a column with the method
void setWidth(int width)
When a column is resized, the default is to leave the total size of the table unchanged. Of course, the width increase or decrease of the resized column must then be distributed over other columns. The default behavior is to change the size of all columns to the right of the resized column. That's a good default because it allows a user to adjust all columns to a desired width, moving from left to right.
You can set another behavior from Table 6-2 by using the method
void setAutoResizeMode(int mode)
Table 6-2. Resize Modes Mode
Behavior
AUTO_RESIZE_OFF
Don't resize other columns; change the table size
AUTO_RESIZE_NEXT_COLUMN
Resize the next column only
AUTO_RESIZE_SUBSEQUENT_COLUMNS
Resize all subsequent columns equally; this is the default behavior
AUTO_RESIZE_LAST_COLUMN
Resize the last column only
AUTO_RESIZE_ALL_COLUMNS
Resize all columns in the table; this is not a good choice because it poses challenges to the user to adjust multiple columns to a desired size
of the JTable class.
Selecting Rows, Columns, and Cells
Depending on the selection mode, the user can select rows, columns, or individual cells in the table. By default, row selection is enabled. Clicking inside a cell selects the entire row (see Figure 6-37). Call
table.setRowSelectionAllowed(false)
Figure 6-37. Selecting a row
to disable row selection.
When row selection is enabled, you can control whether the user is allowed to select a single row, a contiguous set of rows, or any set of rows. You need to retrieve the selection model and use its setSelectionMode method:
table.getSelectionModel().setSelectionMode(mode);
Here, mode is one of the three values:
ListSelectionModel.SINGLE_SELECTION
ListSelectionModel.SINGLE_INTERVAL_SELECTION
ListSelectionModel.MULTIPLE_INTERVAL_SELECTION
Column selection is disabled by default. You turn it on with the call
table.setColumnSelectionAllowed(true)
Enabling both row and column selection is equivalent to enabling cell selection. The user then selects ranges of cells (see Figure 6-38). You can also enable that setting with the call
table.setCellSelectionEnabled(true)
Figure 6-38. Selecting a range of cells
NOTE
In early versions of the Swing toolkit, when you set both row and column selections, every mouse click selected a "+" shaped area consisting of both the row and the column containing the cursor.
You can find out which rows and columns are selected by calling the getSelectedRows and getSelectedColumns methods. Both return an int[] array of the indexes of the selected items.
You can run the program in Example 6-14 to watch cell selection in action. Enable row, column, or cell selection in the Selection menu and watch how the selection behavior changes.
Hiding and Displaying Columns
The removeColumn method of the JTable class removes a column from the table view. The column data is not actually removed from the modelit is just hidden from view. The removeColumn method takes a TableColumn argument. If you have the column number (for example, from a call to getSelectedColumns), you need to ask the table model for the actual table column object:
TableColumnModel columnModel = table.getColumnModel();
TableColumn column = columnModel.getColumn(i);
table.removeColumn(column);
If you remember the column, you can later add it back in:
table.addColumn(column);
This method adds the column to the end. If you want it to appear elsewhere, you call the moveColumn method.
You can also add a new column that corresponds to a column index in the table model, by adding a new TableColumn object:
table.addColumn(new TableColumn(modelColumnIndex));
You can have multiple table columns that view the same column of the model.
There are no JTable methods for hiding or showing rows. If you want to hide rows, you can create a filter model similar to the sort filter that you saw earlier.
Adding and Removing Rows in the Default Table Model
The DefaultTableModel class is a concrete class that implements the TableModel interface. It stores a two-dimensional grid of objects. If your data are already in a tabular arrangement, then there is no point in copying all the data into a DefaultTableModel, but the class is handy if you quickly need to make a table from a small data set. The DefaultTableModel class has methods for adding rows and columns, and for removing rows.
The addRow and addColumn methods add a row or column of new data. You supply an Object[] array or a vector that holds the new data. With the addColumn method, you also supply a name for the new column. These methods add the new data to the end of the grid. To insert a row in the middle, use the insertRow method. There is no method for inserting a column in the middle of the grid.
Conversely, the removeRow method removes a row from the model. There is no method for removing a column.
Because the JTable object registers itself as a table model listener, the model notifies the table when data are inserted or removed. At that time, the table refreshes the display.
The program in Example 6-14 shows both selection and editing at work. A default table model contains a simple data set (a multiplication table). The Edit menu contains these commands:
Hide all selected columns.
Show all columns that you've ever hidden.
Remove selected rows from the model.
Add a row of data to the end of the model.
This example concludes the discussion of Swing tables. Tables are conceptually a bit easier to grasp than trees because the underlying data modela grid of objectsis easy to visualize. However, under the hood, the table component is actually quite a bit more complex than the tree component. Column headers, resizable columns, and column-specific renderers and editors all add to the complexity. In this section, we focused on those topics that you are most likely to encounter in practice: displaying database information, sorting, and custom cell rendering and editing. If you have special advanced needs, we once again refer you to Core Java Foundation Classes by Kim Topley and Graphic Java 2 by David Geary.
Example 6-14. TableSelectionTest.java
[View full width]
1. import java.awt.*;
2. import java.awt.event.*;
3. import java.util.*;
4. import java.text.*;
5. import javax.swing.*;
6. import javax.swing.table.*;
7.
8. /**
9. This program demonstrates selection, addition, and removal of rows and columns.
10. */
11. public class TableSelectionTest
12. {
13. public static void main(String[] args)
14. {
15. JFrame frame = new TableSelectionFrame();
16. frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
17. frame.setVisible(true);
18. }
19. }
20.
21. /**
22. This frame shows a multiplication table and has menus for setting the row/column
/cell selection
23. modes, and for adding and removing rows and columns.
24. */
25. class TableSelectionFrame extends JFrame
26. {
27. public TableSelectionFrame()
28. {
29. setTitle("TableSelectionTest");
30. setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
31.
32. // set up multiplication table
33.
34. model = new DefaultTableModel(10, 10);
35.
36. for (int i = 0; i < model.getRowCount(); i++)
37. for (int j = 0; j < model.getColumnCount(); j++)
38. model.setValueAt((i + 1) * (j + 1), i, j);
39.
40. table = new JTable(model);
41.
42. add(new JScrollPane(table), "Center");
43.
44. removedColumns = new ArrayList<TableColumn>();
45.
46. // create menu
47.
48. JMenuBar menuBar = new JMenuBar();
49. setJMenuBar(menuBar);
50.
51. JMenu selectionMenu = new JMenu("Selection");
52. menuBar.add(selectionMenu);
53.
54. final JCheckBoxMenuItem rowsItem = new JCheckBoxMenuItem("Rows");
55. final JCheckBoxMenuItem columnsItem = new JCheckBoxMenuItem("Columns");
56. final JCheckBoxMenuItem cellsItem = new JCheckBoxMenuItem("Cells");
57.
58. rowsItem.setSelected(table.getRowSelectionAllowed());
59. columnsItem.setSelected(table.getColumnSelectionAllowed());
60. cellsItem.setSelected(table.getCellSelectionEnabled());
61.
62. rowsItem.addActionListener(new
63. ActionListener()
64. {
65. public void actionPerformed(ActionEvent event)
66. {
67. table.clearSelection();
68. table.setRowSelectionAllowed(rowsItem.isSelected());
69. cellsItem.setSelected(table.getCellSelectionEnabled());
70. }
71. });
72. selectionMenu.add(rowsItem);
73.
74. columnsItem.addActionListener(new
75. ActionListener()
76. {
77. public void actionPerformed(ActionEvent event)
78. {
79. table.clearSelection();
80. table.setColumnSelectionAllowed(columnsItem.isSelected());
81. cellsItem.setSelected(table.getCellSelectionEnabled());
82. }
83. });
84. selectionMenu.add(columnsItem);
85.
86. cellsItem.addActionListener(new
87. ActionListener()
88. {
89. public void actionPerformed(ActionEvent event)
90. {
91. table.clearSelection();
92. table.setCellSelectionEnabled(cellsItem.isSelected());
93. rowsItem.setSelected(table.getRowSelectionAllowed());
94. columnsItem.setSelected(table.getColumnSelectionAllowed());
95. }
96. });
97. selectionMenu.add(cellsItem);
98.
99. JMenu tableMenu = new JMenu("Edit");
100. menuBar.add(tableMenu);
101.
102. JMenuItem hideColumnsItem = new JMenuItem("Hide Columns");
103. hideColumnsItem.addActionListener(new
104. ActionListener()
105. {
106. public void actionPerformed(ActionEvent event)
107. {
108. int[] selected = table.getSelectedColumns();
109. TableColumnModel columnModel = table.getColumnModel();
110.
111. // remove columns from view, starting at the last
112. // index so that column numbers aren't affected
113.
114.
115. for (int i = selected.length - 1; i >= 0; i--)
116. {
117. TableColumn column = columnModel.getColumn(selected[i]);
118. table.removeColumn(column);
119.
120. // store removed columns for "show columns" command
121.
122. removedColumns.add(column);
123. }
124. }
125. });
126. tableMenu.add(hideColumnsItem);
127.
128. JMenuItem showColumnsItem = new JMenuItem("Show Columns");
129. showColumnsItem.addActionListener(new
130. ActionListener()
131. {
132. public void actionPerformed(ActionEvent event)
133. {
134. // restore all removed columns
135. for (TableColumn tc : removedColumns)
136. table.addColumn(tc);
137. removedColumns.clear();
138. }
139. });
140. tableMenu.add(showColumnsItem);
141.
142. JMenuItem addRowItem = new JMenuItem("Add Row");
143. addRowItem.addActionListener(new
144. ActionListener()
145. {
146. public void actionPerformed(ActionEvent event)
147. {
148. // add a new row to the multiplication table in
149. // the model
150.
151. Integer[] newCells = new Integer[model.getColumnCount()];
152. for (int i = 0; i < newCells.length; i++)
153. newCells[i] = (i + 1) * (model.getRowCount() + 1);
154. model.addRow(newCells);
155. }
156. });
157. tableMenu.add(addRowItem);
158.
159. JMenuItem removeRowsItem = new JMenuItem("Remove Rows");
160. removeRowsItem.addActionListener(new
161. ActionListener()
162. {
163. public void actionPerformed(ActionEvent event)
164. {
165. int[] selected = table.getSelectedRows();
166.
167. for (int i = selected.length - 1; i >= 0; i--)
168. model.removeRow(selected[i]);
169. }
170. });
171. tableMenu.add(removeRowsItem);
172.
173. JMenuItem clearCellsItem = new JMenuItem("Clear Cells");
174. clearCellsItem.addActionListener(new
175. ActionListener()
176. {
177. public void actionPerformed(ActionEvent event)
178. {
179. for (int i = 0; i < table.getRowCount(); i++)
180. for (int j = 0; j < table.getColumnCount(); j++)
181. if (table.isCellSelected(i, j))
182. table.setValueAt(0, i, j);
183. }
184. });
185. tableMenu.add(clearCellsItem);
186. }
187.
188. private DefaultTableModel model;
189. private JTable table;
190. private ArrayList<TableColumn> removedColumns;
191.
192. private static final int DEFAULT_WIDTH = 400;
193. private static final int DEFAULT_HEIGHT = 300;
194. }
javax.swing.JTable 1.2
void setAutoResizeMode(int mode)
sets the mode for automatic resizing of table columns.
Parameters:
mode
One of AUTO_RESIZE_OFF, AUTO_RESIZE_NEXT_COLUMN, AUTO_RESIZE_SUBSEQUENT_COLUMNS, AUTO_RESIZE_LAST_COLUMN, AUTO_RESIZE_ALL_COLUMNS
ListSelectionModel getSelectionModel()
returns the list selection model. You need that model to choose between row, column, and cell selection.
void setRowSelectionAllowed(boolean b)
If b is true, then rows can be selected when the user clicks on cells.
void setColumnSelectionAllowed(boolean b)
If b is TRue, then columns can be selected when the user clicks on cells.
void setCellSelectionEnabled(boolean b)
If b is true, then individual cells are selected. This is equivalent to calling both setRowSelectionAllowed(b) and setColumnSelectionAllowed(b).
boolean getRowSelectionAllowed()
returns TRue if row selection is allowed.
boolean getColumnSelectionAllowed()
returns TRue if column selection is allowed.
boolean getCellSelectionEnabled()
returns TRue if both row and column selection are allowed.
void clearSelection()
unselects all selected rows and columns.
void addColumn(TableColumn column)
adds a column to the table view.
void moveColumn(int from, int to)
moves the column at table index from so that its index becomes to. Only the view is affected.
void removeColumn(TableColumn column)
removes the given column from the view.
javax.swing.table.TableColumn 1.2
TableColumn(int modelColumnIndex)
constructs a table column for viewing the model column with the given index.
void setPreferredWidth(int width)
void setMinWidth(int width)
void setMaxWidth(int width)
set the preferred, minimum, and maximum width of this table column to width.
void setWidth(int width)
sets the actual width of this column to width.
void setResizable(boolean b)
If b is TRue, this column is resizable.
javax.swing.ListSelectionModel 1.2
void setSelectionMode(int mode)
Parameters:
mode
One of SINGLE_SELECTION, SINGLE_INTERVAL_SELECTION, and MULTIPLE_INTERVAL_SELECTION
javax.swing.table.DefaultTableModel 1.2
void addRow(Object[] rowData)
void addColumn(Object columnName, Object[] columnData)
add a row or column of data to the end of the table model.
void insertRow(int row, Object[] rowData)
adds a row of data at index row.
void removeRow(int row)
removes the given row from the model.
void moveRow(int start, int end, int to)
moves all rows with indexes between start and end to a new location starting at to.
Styled Text Components
In Volume 1, we discussed the basic text component classes JTextField and JTextArea. Of course, these classes are very useful for obtaining text input from the user. Another useful class, JEditorPane, displays and edits text in HTML and RTF format. (RTF is the "rich text format" that is used by a number of Microsoft applications for document interchange. It is a poorly documented format that doesn't work well even between Microsoft's own applications. We do not cover the RTF capabilities in this book.)
Frankly, at this point, the JEditorPane is limited. The HTML renderer can display simple files, but it chokes at many complex pages that you typically find on the Web. The HTML editor is limited and unstable.
We think that the perfect application for the JEditorPane is to display program help in HTML format. Because you have control over the help files that you provide, you can stay away from features that the JEditorPane does not display well.
NOTE
For more information on an industrial-strength help system, check out JavaHelp at http://java.sun.com/products/javahelp/index.html.
NOTE
The subclass JTextPane of JEditorPane can hold styled text with special fonts and text formats, as well as embedded components. We do not cover that component in this book. If you need to implement a component that allows users of your program to enter styled text, look at the implementation of the StylePad demo that is included in the JDK.
The program in Example 6-15 contains an editor pane that shows the contents of an HTML page. Type a URL into the text field. The URL must start with http: or file:. Then, click the Load button. The selected HTML page is displayed in the editor pane (see Figure 6-39).
Figure 6-39. The editor pane displaying an HTML page
[View full size image]
The hyperlinks are active: If you click on a link, the application loads it. The Back button returns to the previous page.
This program is in fact a very simple browser. Of course, it does not have any of the comfort features, such as page caching or bookmark lists, that you expect from a commercial browser. The editor pane does not even display applets!
If you click on the Editable checkbox, then the editor pane becomes editable. You can type in text and use the BACKSPACE key to delete text. The component also understands the CTRL+X, CTRL+C, and CTRL+V shortcuts for cut, copy, and paste. However, you would have to do quite a bit of programming to add support for fonts and formatting.
When the component is editable, then hyperlinks are not active. Also, with some web pages you can see JavaScript commands, comments, and other tags when edit mode is turned on (see Figure 6-40). The example program lets you investigate the editing feature, but we recommend that you omit that feature in your programs.
Figure 6-40. The editor pane in edit mode
[View full size image]
TIP
By default, the JEditorPane is in edit mode. You should call editorPane.setEditable(false) to turn it off.
The features of the editor pane that you saw in the example program are easy to use. You use the setPage method to load a new document. For example,
JEditorPane editorPane = new JEditorPane();
editorPane.setPage(url);
The parameter is either a string or a URL object. The JEditorPane class extends the JTextComponent class. Therefore, you can call the setText method as wellit simply displays plain text.
TIP
The API documentation is unclear about whether setPage loads the new document in a separate thread (which is generally what you wantthe JEditorPane is no speed demon). However, you can force loading in a separate thread with the following incantation:
AbstractDocument doc = (AbstractDocument) editorPane.getDocument();
doc.setAsynchronousLoadPriority(0);
To listen to hyperlink clicks, you add a HyperlinkListener. The HyperlinkListener interface has a single method, hyperlinkUpdate, that is called when the user moves over or clicks on a link. The method has a parameter of type HyperlinkEvent.
You need to call the getEventType method to find out what kind of event occurred. There are three possible return values:
HyperlinkEvent.EventType.ACTIVATED
HyperlinkEvent.EventType.ENTERED
HyperlinkEvent.EventType.EXITED
The first value indicates that the user clicked on the hyperlink. In that case, you typically want to open the new link. You can use the second and third values to give some visual feedback, such as a tooltip, when the mouse hovers over the link.
NOTE
It is a complete mystery why there aren't three separate methods to handle activation, entry, and exit in the HyperlinkListener interface.
The getURL method of the HyperlinkEvent class returns the URL of the hyperlink. For example, here is how you can install a hyperlink listener that follows the links that a user activates:
editorPane.addHyperlinkListener(new
HyperlinkListener()
{
public void hyperlinkUpdate(HyperlinkEvent event)
{
if (event.getEventType()
== HyperlinkEvent.EventType.ACTIVATED)
{
try
{
editorPane.setPage(event.getURL());
}
catch (IOException e)
{
editorPane.setText("Exception: " + e);
}
}
}
});
The event handler simply gets the URL and updates the editor pane. The setPage method can throw an IOException. In that case, we display an error message as plain text.
The program in Example 6-15 shows all the features that you need to put together an HTML help system. Under the hood, the JEditorPane is even more complex than the tree and table components. However, if you don't need to write a text editor or a renderer of a custom text format, that complexity is hidden from you.
Example 6-15. EditorPaneTest.java
1. import java.awt.*;
2. import java.awt.event.*;
3. import java.io.*;
4. import java.net.*;
5. import java.util.*;
6. import javax.swing.*;
7. import javax.swing.event.*;
8.
9. /**
10. This program demonstrates how to display HTML documents
11. in an editor pane.
12. */
13. public class EditorPaneTest
14. {
15. public static void main(String[] args)
16. {
17. JFrame frame = new EditorPaneFrame();
18. frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
19. frame.setVisible(true);
20. }
21. }
22.
23. /**
24. This frame contains an editor pane, a text field and button
25. to enter a URL and load a document, and a Back button to
26. return to a previously loaded document.
27. */
28. class EditorPaneFrame extends JFrame
29. {
30. public EditorPaneFrame()
31. {
32. setTitle("EditorPaneTest");
33. setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
34.
35. final Stack<String> urlStack = new Stack<String>();
36. final JEditorPane editorPane = new JEditorPane();
37. final JTextField url = new JTextField(30);
38.
39. // set up hyperlink listener
40.
41. editorPane.setEditable(false);
42. editorPane.addHyperlinkListener(new
43. HyperlinkListener()
44. {
45. public void hyperlinkUpdate(HyperlinkEvent event)
46. {
47. if (event.getEventType() == HyperlinkEvent.EventType.ACTIVATED)
48. {
49. try
50. {
51. // remember URL for back button
52. urlStack.push(event.getURL().toString());
53. // show URL in text field
54. url.setText(event.getURL().toString());
55. editorPane.setPage(event.getURL());
56. }
57. catch (IOException e)
58. {
59. editorPane.setText("Exception: " + e);
60. }
61. }
62. }
63. });
64.
65. // set up checkbox for toggling edit mode
66.
67. final JCheckBox editable = new JCheckBox();
68. editable.addActionListener(new
69. ActionListener()
70. {
71. public void actionPerformed(ActionEvent event)
72. {
73. editorPane.setEditable(editable.isSelected());
74. }
75. });
76.
77. // set up load button for loading URL
78.
79. ActionListener listener = new
80. ActionListener()
81. {
82. public void actionPerformed(ActionEvent event)
83. {
84. try
85. {
86. // remember URL for back button
87. urlStack.push(url.getText());
88. editorPane.setPage(url.getText());
89. }
90. catch (IOException e)
91. {
92. editorPane.setText("Exception: " + e);
93. }
94. }
95. };
96.
97. JButton loadButton = new JButton("Load");
98. loadButton.addActionListener(listener);
99. url.addActionListener(listener);
100.
101. // set up back button and button action
102.
103. JButton backButton = new JButton("Back");
104. backButton.addActionListener(new
105. ActionListener()
106. {
107. public void actionPerformed(ActionEvent event)
108. {
109. if (urlStack.size() <= 1) return;
110. try
111. {
112. // get URL from back button
113. urlStack.pop();
114. // show URL in text field
115. String urlString = urlStack.peek();
116. url.setText(urlString);
117. editorPane.setPage(urlString);
118. }
119. catch (IOException e)
120. {
121. editorPane.setText("Exception: " + e);
122. }
123. }
124. });
125.
126. add(new JScrollPane(editorPane), BorderLayout.CENTER);
127.
128. // put all control components in a panel
129.
130. JPanel panel = new JPanel();
131. panel.add(new JLabel("URL"));
132. panel.add(url);
133. panel.add(loadButton);
134. panel.add(backButton);
135. panel.add(new JLabel("Editable"));
136. panel.add(editable);
137.
138. add(panel, BorderLayout.SOUTH);
139. }
140.
141. private static final int DEFAULT_WIDTH = 600;
142. private static final int DEFAULT_HEIGHT = 400;
143. }
javax.swing.JEditorPane 1.2
void setPage(URL url)
loads the page from url into the editor pane.
void addHyperlinkListener(HyperLinkListener listener)
adds a hyperlink listener to this editor pane.
javax.swing.event.HyperlinkListener 1.2
void hyperlinkUpdate(HyperlinkEvent event)
is called whenever a hyperlink was selected.
javax.swing.HyperlinkEvent 1.2
URL getURL()
returns the URL of the selected hyperlink.
Progress Indicators
In the following sections, we discuss three classes for indicating the progress of a slow activity. A JProgressBar is a Swing component that indicates progress. A ProgressMonitor is a dialog box that contains a progress bar. A ProgressMonitorInputStream displays a progress monitor dialog box while the stream is read.
Progress Bars
A progress bar is a simple componentjust a rectangle that is partially filled with color to indicate the progress of an operation. By default, progress is indicated by a string "n%". You can see a progress bar in the bottom right of Figure 6-41.
Figure 6-41. A progress bar
You construct a progress bar much as you construct a slider, by supplying the minimum and maximum value and an optional orientation:
progressBar = new JProgressBar(0, 1000);
progressBar = new JProgressBar(SwingConstants.VERTICAL, 0, 1000);
You can also set the minimum and maximum with the setMinimum and setMaximum methods.
Unlike a slider, the progress bar cannot be adjusted by the user. Your program needs to call setValue to update it.
If you call
progressBar.setStringPainted(true);
the progress bar computes the completion percentage and displays a string "n%". If you want to show a different string, you can supply it with the setString method:
if (progressBar.getValue() > 900)
progressBar.setString("Almost Done");
The program in Example 6-16 shows a progress bar that monitors a simulated time-consuming activity.
The SimulatedActivity class implements a thread that increments a value current 10 times per second. When it reaches a target value, the thread finishes. To terminate the thread before it has reached its target, you interrupt it.
class SimulatedActivity implements Runnable
{
. . .
public void run()
{
try
{
while (current < target && !interrupted())
{
Thread.sleep(100);
current++;
}
}
catch (InterruptedException e)
{
}
}
int current;
int target;
}
When you click the Start button, a new SimulatedActivity tHRead is started. Updating the progress bar would appear to be an easy matterthe simulated activity thread calls the setValue method. But that is not thread safe: Recall that you should call Swing methods only from the event dispatch thread. In practice, the apparent approach is also unrealistic: In general, a worker thread is not aware of the existence of the progress bar. Instead, the example program shows how to launch a timer that periodically polls the thread for a progress status and updates the progress bar.
CAUTION
Remember that a worker thread cannot set the progress bar value directly. A worker thread must use the SwingUtilities.invokeLater method to set the progress bar value in the event dispatch thread.
Recall that a Swing timer calls the actionPerformed method of its listeners and that these calls occur in the event dispatch thread. That means it is safe to update Swing components in the timer callback. Here is the timer callback from the example program. The current value of the simulated activity is displayed both in the text area and the progress bar. If the end of the simulation has been reached, the timer is stopped and the Start button is reenabled.
public void actionPerformed(ActionEvent event)
{
int current = activity.getCurrent();
// show progress
textArea.append(current + "
");
progressBar.setValue(current);
// check if task is completed
if (current == activity.getTarget())
{
activityMonitor.stop();
startButton.setEnabled(true);
}
}
JDK 1.4 adds support for an indeterminate progress bar that shows an animation indicating some kind of progress, without giving an indication of the percentage of completion. That is the kind of progress bar that you see in your browserit indicates that the browser is waiting for the server and has no idea how long the wait may be. To display the "indeterminate wait" animation, call the setIndeterminate method.
Example 6-16 shows the full program code.
Example 6-16. ProgressBarTest.java
1. import java.awt.*;
2. import java.awt.event.*;
3. import java.util.*;
4. import javax.swing.*;
5. import javax.swing.event.*;
6. import javax.swing.Timer;
7.
8. /**
9. This program demonstrates the use of a progress bar
10. to monitor the progress of a thread.
11. */
12. public class ProgressBarTest
13. {
14. public static void main(String[] args)
15. {
16. JFrame frame = new ProgressBarFrame();
17. frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
18. frame.setVisible(true);
19. }
20. }
21.
22. /**
23. A frame that contains a button to launch a simulated activity,
24. a progress bar, and a text area for the activity output.
25. */
26. class ProgressBarFrame extends JFrame
27. {
28. public ProgressBarFrame()
29. {
30. setTitle("ProgressBarTest");
31. setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
32.
33. // this text area holds the activity output
34. textArea = new JTextArea();
35.
36. // set up panel with button and progress bar
37.
38. JPanel panel = new JPanel();
39. startButton = new JButton("Start");
40. progressBar = new JProgressBar();
41. progressBar.setStringPainted(true);
42. panel.add(startButton);
43. panel.add(progressBar);
44.
45. checkBox = new JCheckBox("indeterminate");
46. checkBox.addActionListener(new
47. ActionListener()
48. {
49. public void actionPerformed(ActionEvent event)
50. {
51. progressBar.setIndeterminate(checkBox.isSelected());
52. }
53. });
54. panel.add(checkBox);
55. add(new JScrollPane(textArea), BorderLayout.CENTER);
56. add(panel, BorderLayout.SOUTH);
57.
58. // set up the button action
59.
60. startButton.addActionListener(new
61. ActionListener()
62. {
63. public void actionPerformed(ActionEvent event)
64. {
65. progressBar.setMaximum(1000);
66. activity = new SimulatedActivity(1000);
67. new Thread(activity).start();
68. activityMonitor.start();
69. startButton.setEnabled(false);
70. }
71. });
72.
73.
74. // set up the timer action
75.
76. activityMonitor = new Timer(500, new
77. ActionListener()
78. {
79. public void actionPerformed(ActionEvent event)
80. {
81. int current = activity.getCurrent();
82.
83. // show progress
84. textArea.append(current + "
");
85. progressBar.setStringPainted(!progressBar.isIndeterminate());
86. progressBar.setValue(current);
87.
88. // check if task is completed
89. if (current == activity.getTarget())
90. {
91. activityMonitor.stop();
92. startButton.setEnabled(true);
93. }
94. }
95. });
96. }
97.
98. private Timer activityMonitor;
99. private JButton startButton;
100. private JProgressBar progressBar;
101. private JCheckBox checkBox;
102. private JTextArea textArea;
103. private SimulatedActivity activity;
104.
105. public static final int DEFAULT_WIDTH = 400;
106. public static final int DEFAULT_HEIGHT = 200;
107. }
108.
109. /**
110. A simulated activity runnable.
111. */
112. class SimulatedActivity implements Runnable
113. {
114. /**
115. Constructs the simulated activity thread object. The
116. thread increments a counter from 0 to a given target.
117. @param t the target value of the counter.
118. */
119. public SimulatedActivity(int t)
120. {
121. current = 0;
122. target = t;
123. }
124.
125. public int getTarget()
126. {
127. return target;
128. }
129.
130. public int getCurrent()
131. {
132. return current;
133. }
134.
135. public void run()
136. {
137. try
138. {
139. while (current < target)
140. {
141. Thread.sleep(100);
142. current++;
143. }
144. }
145. catch(InterruptedException e)
146. {
147. }
148. }
149.
150. private volatile int current;
151. private int target;
152. }
Progress Monitors
A progress bar is a simple component that can be placed inside a window. In contrast, a ProgressMonitor is a complete dialog box that contains a progress bar (see Figure 6-42). The dialog box contains a Cancel button. If you click it, the monitor dialog box is closed. In addition, your program can query whether the user has canceled the dialog box and terminate the monitored action. (Note that the class name does not start with a "J".)
Figure 6-42. A progress monitor dialog box
You construct a progress monitor by supplying the following:
The parent component over which the dialog box should pop up;
An object (which should be a string, icon, or component) that is displayed on the dialog box;
An optional note to display below the object;
The minimum and maximum values.
However, the progress monitor cannot measure progress or cancel an activity by itself. You still need to periodically set the progress value by calling the setProgress method. (This is the equivalent of the setValue method of the JProgressBar class.) As you update the progress value, you should also call the isCanceled method to see if the program user has clicked the Cancel button.
When the monitored activity has concluded, call the close method to dismiss the dialog box. You can reuse the same dialog box by calling start again.
The example program looks very similar to that of the preceding section. We still need to launch a timer to watch over the progress of the simulated activity and update the progress monitor. Here is the timer callback.
public void actionPerformed(ActionEvent event)
{
int current = activity.getCurrent();
// show progress
textArea.append(current + "
");
progressDialog.setProgress(current);
// check if task is completed or canceled
if (current == activity.getTarget() || progressDialog.isCanceled())
{
activityMonitor.stop();
progressDialog.close();
activity.interrupt();
startButton.setEnabled(true);
}
}
Note that there are two conditions for termination. The activity might have completed, or the user might have canceled it. In each of these cases, we close down
the timer that monitored the activity;
the progress dialog box;
the activity itself (by interrupting the thread).
If you run the program in Example 6-17, you can observe an interesting feature of the progress monitor dialog box. The dialog box doesn't come up immediately. Instead, it waits for a short interval to see if the activity has already been completed or is likely to complete in less time than it would take for the dialog box to appear.
You control the timing as follows. Use the setMillisToDecideToPopup method to set the number of milliseconds to wait between the construction of the dialog object and the decision whether to show the pop-up at all. The default value is 500 milliseconds. The setMillisToPopup is your estimation of the time the dialog box needs to pop up. The Swing designers set this value to a default of 2 seconds. Clearly they were mindful of the fact that Swing dialogs don't always come up as snappily as we all would like. You should probably not touch this value.
Example 6-17 shows the progress monitor in action, again measuring the progress of a simulated activity. As you can see, the progress monitor is convenient to use and only requires that you periodically query the thread that you want to monitor.
Example 6-17. ProgressMonitorTest.java
1. import java.awt.*;
2. import java.awt.event.*;
3. import java.util.*;
4. import javax.swing.*;
5. import javax.swing.event.*;
6. import javax.swing.Timer;
7.
8. /**
9. A program to test a progress monitor dialog.
10. */
11. public class ProgressMonitorTest
12. {
13. public static void main(String[] args)
14. {
15. JFrame frame = new ProgressMonitorFrame();
16. frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
17. frame.setVisible(true);
18. }
19. }
20.
21. /**
22. A frame that contains a button to launch a simulated activity
23. and a text area for the activity output.
24. */
25. class ProgressMonitorFrame extends JFrame
26. {
27. public ProgressMonitorFrame()
28. {
29. setTitle("ProgressMonitorTest");
30. setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
31.
32. // this text area holds the activity output
33. textArea = new JTextArea();
34.
35. // set up a button panel
36. JPanel panel = new JPanel();
37. startButton = new JButton("Start");
38. panel.add(startButton);
39.
40. add(new JScrollPane(textArea), BorderLayout.CENTER);
41. add(panel, BorderLayout.SOUTH);
42.
43. // set up the button action
44.
45. startButton.addActionListener(new
46. ActionListener()
47. {
48. public void actionPerformed(ActionEvent event)
49. {
50. // start activity
51. activity = new SimulatedActivity(1000);
52. activityThread = new Thread(activity);
53. activityThread.start();
54.
55. // launch progress dialog
56. progressDialog = new ProgressMonitor(ProgressMonitorFrame.this,
57. "Waiting for Simulated Activity", null, 0, activity.getTarget());
58.
59. // start timer
60. activityMonitor.start();
61.
62. startButton.setEnabled(false);
63. }
64. });
65.
66. // set up the timer action
67.
68. activityMonitor = new Timer(500, new
69. ActionListener()
70. {
71. public void actionPerformed(ActionEvent event)
72. {
73. int current = activity.getCurrent();
74.
75. // show progress
76. textArea.append(current + "
");
77. progressDialog.setProgress(current);
78.
79. // check if task is completed or canceled
80. if (current == activity.getTarget() || progressDialog.isCanceled())
81. {
82. activityMonitor.stop();
83. progressDialog.close();
84. activityThread.interrupt();
85. startButton.setEnabled(true);
86. }
87. }
88. });
89. }
90.
91. private Timer activityMonitor;
92. private JButton startButton;
93. private ProgressMonitor progressDialog;
94. private JTextArea textArea;
95. private Thread activityThread;
96. private SimulatedActivity activity;
97.
98. public static final int DEFAULT_WIDTH = 300;
99. public static final int DEFAULT_HEIGHT = 200;
100. }
101.
102. /**
103. A simulated activity runnable.
104. */
105. class SimulatedActivity implements Runnable
106. {
107. /**
108. Constructs the simulated activity thread object. The
109. thread increments a counter from 0 to a given target.
110. @param t the target value of the counter.
111. */
112. public SimulatedActivity(int t)
113. {
114. current = 0;
115. target = t;
116. }
117.
118. public int getTarget()
119. {
120. return target;
121. }
122.
123. public int getCurrent()
124. {
125. return current;
126. }
127.
128. public void run()
129. {
130. try
131. {
132. while (current < target)
133. {
134. Thread.sleep(100);
135. current++;
136. }
137. }
138. catch(InterruptedException e)
139. {
140. }
141. }
142.
143. private volatile int current;
144. private int target;
145. }
Monitoring the Progress of Input Streams
The Swing package contains a useful stream filter, ProgressMonitorInputStream, that automatically pops up a dialog box that monitors how much of the stream has been read.
This filter is extremely easy to use. You sandwich in a ProgressMonitorInputStream between your usual sequence of filtered streams. (See Volume 1, Chapter 12 for more information on streams.)
For example, suppose you read text from a file. You start out with a FileInputStream:
FileInputStream in = new FileInputStream(f);
Normally, you would convert in to an InputStreamReader:
InputStreamReader reader = new InputStreamReader(in);
However, to monitor the stream, first turn the file input stream into a stream with a progress monitor:
ProgressMonitorInputStream progressIn = new ProgressMonitorInputStream(parent, caption, in);
You supply the parent component, a caption, and, of course, the stream to monitor. The read method of the progress monitor stream simply passes along the bytes and updates the progress dialog box.
You now go on building your filter sequence:
InputStreamReader reader = new InputStreamReader(progressIn);
That's all there is to it. When the file is read, the progress monitor automatically pops up (see Figure 6-43). This is a very nice application of stream filtering.
Figure 6-43. A progress monitor for an input stream
CAUTION
The progress monitor stream uses the available method of the InputStream class to determine the total number of bytes in the stream. However, the available method only reports the number of bytes in the stream that are available without blocking. Progress monitors work well for files and HTTP URLs because their length is known in advance, but they don't work with all streams.
The program in Example 6-18 counts the lines in a file. If you read in a large file (such as "The Count of Monte Cristo" on the CD), then the progress dialog box pops up.
Note that the program doesn't use a very efficient way of filling up the text area. It would be faster to first read the file into a StringBuffer and then set the text of the text area to the string buffer contents. However, in this example program, we actually like this slow approachit gives you more time to admire the progress dialog box.
To avoid flicker, we do not display the text area while it is filling up.
Example 6-18. ProgressMonitorInputStreamTest.java
1. import java.awt.*;
2. import java.awt.event.*;
3. import java.io.*;
4. import java.util.*;
5. import javax.swing.*;
6. import javax.swing.event.*;
7.
8. /**
9. A program to test a progress monitor input stream.
10. */
11. public class ProgressMonitorInputStreamTest
12. {
13. public static void main(String[] args)
14. {
15. JFrame frame = new TextFrame();
16. frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
17. frame.setVisible(true);
18. }
19. }
20.
21. /**
22. A frame with a menu to load a text file and a text area
23. to display its contents. The text area is constructed
24. when the file is loaded and set as the content pane of
25. the frame when the loading is complete. That avoids flicker
26. during loading.
27. */
28. class TextFrame extends JFrame
29. {
30. public TextFrame()
31. {
32. setTitle("ProgressMonitorInputStreamTest");
33. setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
34.
35. // set up menu
36.
37. JMenuBar menuBar = new JMenuBar();
38. setJMenuBar(menuBar);
39. JMenu fileMenu = new JMenu("File");
40. menuBar.add(fileMenu);
41. openItem = new JMenuItem("Open");
42. openItem.addActionListener(new
43. ActionListener()
44. {
45. public void actionPerformed(ActionEvent event)
46. {
47. try
48. {
49. openFile();
50. }
51. catch(IOException exception)
52. {
53. exception.printStackTrace();
54. }
55. }
56. });
57.
58. fileMenu.add(openItem);
59. exitItem = new JMenuItem("Exit");
60. exitItem.addActionListener(new
61. ActionListener()
62. {
63. public void actionPerformed(ActionEvent event)
64. {
65. System.exit(0);
66. }
67. });
68. fileMenu.add(exitItem);
69. }
70.
71. /**
72. Prompts the user to select a file, loads the file into
73. a text area, and sets it as the content pane of the frame.
74. */
75. public void openFile() throws IOException
76. {
77. JFileChooser chooser = new JFileChooser();
78. chooser.setCurrentDirectory(new File("."));
79. chooser.setFileFilter(
80. new javax.swing.filechooser.FileFilter()
81. {
82. public boolean accept(File f)
83. {
84. String fname = f.getName().toLowerCase();
85. return fname.endsWith(".txt") || f.isDirectory();
86. }
87. public String getDescription()
88. {
89. return "Text Files";
90. }
91. });
92.
93. int r = chooser.showOpenDialog(this);
94. if (r != JFileChooser.APPROVE_OPTION) return;
95. final File f = chooser.getSelectedFile();
96.
97. // set up stream and reader filter sequence
98.
99. FileInputStream fileIn = new FileInputStream(f);
100. ProgressMonitorInputStream progressIn
101. = new ProgressMonitorInputStream(this, "Reading " + f.getName(), fileIn);
102. final Scanner in = new Scanner(progressIn);
103.
104. // the monitored activity must be in a new thread.
105.
106. Runnable readRunnable = new
107. Runnable()
108. {
109. public void run()
110. {
111. final JTextArea textArea = new JTextArea();
112.
113. while (in.hasNextLine())
114. {
115. String line = in.nextLine();
116. textArea.append(line);
117. textArea.append("
");
118. }
119. in.close();
120.
121. // set content pane in the event dispatch thread
122. EventQueue.invokeLater(new
123. Runnable()
124. {
125. public void run()
126. {
127. setContentPane(new JScrollPane(textArea));
128. validate();
129. }
130. });
131.
132. }
133. };
134.
135. Thread readThread = new Thread(readRunnable);
136. readThread.start();
137. }
138.
139. private JMenuItem openItem;
140. private JMenuItem exitItem;
141.
142. public static final int DEFAULT_WIDTH = 300;
143. public static final int DEFAULT_HEIGHT = 200;
144. }
javax.swing.JProgressBar 1.2
JProgressBar()
JProgressBar(int direction)
JProgressBar(int min, int max)
JProgressBar(int direction, int min, int max)
construct a slider with the given direction, minimum, and maximum.
Parameters:
direction
One of SwingConstants.HORIZONTAL or SwingConstants.VERTICAL. The default is horizontal
min, max
The minimum and maximum for the progress bar values. Defaults are 0 and 100
int getMinimum()
int getMaximum()
void setMinimum(int value)
void setMaximum(int value)
get and set the minimum and maximum values.
int getValue()
void setValue(int value)
get and set the current value.
String getString()
void setString(String s)
get and set the string to be displayed in the progress bar. If the string is null, then a default string "n%" is displayed.
boolean isStringPainted()
void setStringPainted(boolean b)
get and set the "string painted" property. If this property is true, then a string is painted on top of the progress bar. The default is false; no string is painted.
boolean isIndeterminate() 1.4
void setIndeterminate(boolean b) 1.4
get and set the "indeterminate" property. If this property is TRue, then the progress bar becomes a block that moves backward and forward, indicating a wait of unknown duration. The default is false.
javax.swing.ProgressMonitor 1.2
ProgressMonitor(Component parent, Object message, String note, int min, int max)
constructs a progress monitor dialog box.
Parameters:
parent
The parent component over which this dialog box pops up
message
The message object to display in the dialog box
note
The optional string to display under the message. If this value is null, then no space is set aside for the note, and a later call to setNote has no effect
min, max
The minimum and maximum values of the progress bar
void setNote(String note)
changes the note text.
void setProgress(int value)
sets the progress bar value to the given value.
void close()
closes this dialog box.
boolean isCanceled()
returns true if the user canceled this dialog box.
javax.swing.ProgressMonitorInputStream 1.2
ProgressMonitorInputStream(Component parent, Object message, InputStream in)
constructs an input stream filter with an associated progress monitor dialog box.
Parameters:
parent
The parent component over which this dialog box pops up
message
The message object to display in the dialog box
in
The input stream that is being monitored
Component Organizers
We conclude the discussion of advanced Swing features with a presentation of components that help organize other components. These include the split pane, a mechanism for splitting an area into multiple parts whose boundaries can be adjusted, the tabbed pane, which uses tab dividers to allow a user to flip through multiple panels, and the desktop pane, which can be used to implement applications that display multiple internal frames.
Split Panes
Split panes split a component into two parts, with an adjustable boundary in between. Figure 6-44 shows a frame with two split panes. The outer pane is split vertically, with a text area on the bottom and another split pane on the top. That pane is split horizontally, with a list on the left and a label containing an image on the right.
Figure 6-44. A frame with two nested split panes
You construct a split pane by specifying the orientation, one of JSplitPane.HORIZONTAL_SPLIT or JSplitPane.VERTICAL_SPLIT, followed by the two components. For example,
JSplitPane innerPane = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, planetList, planetImage);
That's all you have to do. If you like, you can add "one-touch expand" icons to the splitter bar. You see those icons in the top pane in Figure 6-44. In the Metal look and feel, they are small triangles. If you click one of them, the splitter moves all the way in the direction to which the triangle is pointing, expanding one of the panes completely.
To add this capability, call
innerPane.setOneTouchExpandable(true);
The "continuous layout" feature continuously repaints the contents of both components as the user adjusts the splitter. That looks classier, but it can be slow. You turn on that feature with the call
innerPane.setContinuousLayout(true);
In the example program, we left the bottom splitter at the default (no continuous layout). When you drag it, you only move a black outline. When you release the mouse, the components are repainted.
The straightforward program in Example 6-19 populates a list box with planets. When the user makes a selection, the planet image is displayed to the right and a description is placed in the text area on the bottom. When you run the program, adjust the splitters and try out the one-touch expansion and continuous layout features.
Example 6-19. SplitPaneTest.java
1. import java.awt.*;
2. import java.awt.event.*;
3. import java.util.*;
4. import javax.swing.*;
5. import javax.swing.event.*;
6.
7. /**
8. This program demonstrates the split pane component
9. organizer.
10. */
11. public class SplitPaneTest
12. {
13. public static void main(String[] args)
14. {
15. JFrame frame = new SplitPaneFrame();
16. frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
17. frame.setVisible(true);
18. }
19. }
20.
21. /**
22. This frame consists of two nested split panes to demonstrate
23. planet images and data.
24. */
25. class SplitPaneFrame extends JFrame
26. {
27. public SplitPaneFrame()
28. {
29. setTitle("SplitPaneTest");
30. setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
31.
32. // set up components for planet names, images, descriptions
33.
34. final JList planetList = new JList(planets);
35. final JLabel planetImage = new JLabel();
36. final JTextArea planetDescription = new JTextArea();
37.
38. planetList.addListSelectionListener(new
39. ListSelectionListener()
40. {
41. public void valueChanged(ListSelectionEvent event)
42. {
43. Planet value = (Planet) planetList.getSelectedValue();
44.
45. // update image and description
46.
47. planetImage.setIcon(value.getImage());
48. planetDescription.setText(value.getDescription());
49. }
50. });
51.
52. // set up split panes
53.
54. JSplitPane innerPane
55. = new JSplitPane(JSplitPane.HORIZONTAL_SPLIT, planetList, planetImage);
56.
57. innerPane.setContinuousLayout(true);
58. innerPane.setOneTouchExpandable(true);
59.
60. JSplitPane outerPane
61. = new JSplitPane(JSplitPane.VERTICAL_SPLIT, innerPane, planetDescription);
62.
63. add(outerPane, BorderLayout.CENTER);
64. }
65.
66. private Planet[] planets =
67. {
68. new Planet("Mercury", 2440, 0),
69. new Planet("Venus", 6052, 0),
70. new Planet("Earth", 6378, 1),
71. new Planet("Mars", 3397, 2),
72. new Planet("Jupiter", 71492, 16),
73. new Planet("Saturn", 60268, 18),
74. new Planet("Uranus", 25559, 17),
75. new Planet("Neptune", 24766, 8),
76. new Planet("Pluto", 1137, 1),
77. };
78. private static final int DEFAULT_WIDTH = 300;
79. private static final int DEFAULT_HEIGHT = 300;
80. }
81.
82. /**
83. Describes a planet.
84. */
85. class Planet
86. {
87. /**
88. Constructs a planet.
89. @param n the planet name
90. @param r the planet radius
91. @param m the number of moons
92. */
93. public Planet(String n, double r, int m)
94. {
95. name = n;
96. radius = r;
97. moons = m;
98. image = new ImageIcon(name + ".gif");
99. }
100.
101. public String toString()
102. {
103. return name;
104. }
105.
106. /**
107. Gets a description of the planet.
108. @return the description
109. */
110. public String getDescription()
111. {
112. return "Radius: " + radius + "
Moons: " + moons + "
";
113. }
114.
115. /**
116. Gets an image of the planet.
117. @return the image
118. */
119. public ImageIcon getImage()
120. {
121. return image;
122. }
123.
124. private String name;
125. private double radius;
126. private int moons;
127. private ImageIcon image;
128. }
javax.swing.JSplitPane 1.2
JSplitPane()
JSplitPane(int direction)
JSplitPane(int direction, boolean continuousLayout)
JSplitPane(int direction, Component first, Component second)
JSplitPane(int direction, boolean continuousLayout, Component first, Component second)
construct a new split pane.
Parameters:
direction
One of HORIZONTAL_SPLIT or VERTICAL_SPLIT
continousLayout
true if the components are continuously updated when the splitter is moved
first, second
The components to add
boolean isOneTouchExpandable()
void setOneTouchExpandable(boolean b)
get and set the "one-touch expandable" property. When this property is set, the splitter has two icons to completely expand one or the other component.
boolean isContinuousLayout()
void setContinuousLayout(boolean b)
get and set the "continuous layout" property. When this property is set, then the components are continuously updated when the splitter is moved.
void setLeftComponent(Component c)
void setTopComponent(Component c)
These operations have the same effect, to set c as the first component in the split pane.
void setRightComponent(Component c)
void setBottomComponent(Component c)
These operations have the same effect, to set c as the second component in the split pane.
Tabbed Panes
Tabbed panes are a familiar user interface device to break up a complex dialog box into subsets of related options. You can also use tabs to let a user flip through a set of documents or images (see Figure 6-45). That is what we do in our sample program.
Figure 6-45. A tabbed pane
To create a tabbed pane, you first construct a JTabbedPane object, then you add tabs to it.
JTabbedPane tabbedPane = new JTabbedPane();
tabbedPane.addTab(title, icon, component);
The last parameter of the addTab method has type Component. To add multiple components into the same tab, you first pack them up in a container, such as a JPanel.
The icon is optional; for example, the addTab method does not require an icon:
tabbedPane.addTab(title, component);
You can also add a tab in the middle of the tab collection with the insertTab method:
tabbedPane.insertTab(title, icon, component, tooltip, index);
To remove a tab from the tab collection, use
tabPane.removeTabAt(index);
When you add a new tab to the tab collection, it is not automatically displayed. You must select it with the setSelectedIndex method. For example, here is how you show a tab that you just added to the end:
tabbedPane.setSelectedIndex(tabbedPane.getTabCount() - 1);
If you have a lot of tabs, then they can take up quite a bit of space. Starting with JDK 1.4, you can display the tabs in scrolling mode, in which only one row of tabs is displayed, together with a set of arrow buttons that allow the user to scroll through the tab set (see Figure 6-46).
Figure 6-46. A tabbed pane with scrolling tabs
You set the tab layout to wrapped or scrolling mode by calling
tabbedPane.setTabLayoutPolicy(JTabbedPane.WRAP_TAB_LAYOUT);
or
tabbedPane.setTabLayoutPolicy(JTabbedPane.SCROLL_TAB_LAYOUT);
The example program shows a useful technique with tabbed panes. Sometimes, you want to update a component just before it is displayed. In our example program, we load the planet image only when the user actually clicks on a tab.
To be notified whenever the user clicks on a new tab, you install a ChangeListener with the tabbed pane. Note that you must install the listener with the tabbed pane itself, not with any of the components.
tabbedPane.addChangeListener(listener);
When the user selects a tab, the stateChanged method of the change listener is called. You retrieve the tabbed pane as the source of the event. Call the getSelectedIndex method to find out which pane is about to be displayed.
public void stateChanged(ChangeEvent event)
{
int n = tabbedPane.getSelectedIndex();
loadTab(n);
}
In Example 6-20, we first set all tab components to null. When a new tab is selected, we test whether its component is still null. If so, we replace it with the image. (This happens instantaneously when you click on the tab. You will not see an empty pane.) Just for fun, we also change the icon from a yellow ball to a red ball to indicate which panes have been visited.
Example 6-20. TabbedPaneTest.java
1. import java.awt.*;
2. import java.awt.event.*;
3. import java.util.*;
4. import javax.swing.*;
5. import javax.swing.event.*;
6.
7. /**
8. This program demonstrates the tabbed pane component organizer.
9. */
10. public class TabbedPaneTest
11. {
12. public static void main(String[] args)
13. {
14. JFrame frame = new TabbedPaneFrame();
15. frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
16. frame.setVisible(true);
17. }
18. }
19.
20. /**
21. This frame shows a tabbed pane and radio buttons to
22. switch between wrapped and scrolling tab layout.
23. */
24. class TabbedPaneFrame extends JFrame
25. {
26. public TabbedPaneFrame()
27. {
28. setTitle("TabbedPaneTest");
29. setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
30.
31. tabbedPane = new JTabbedPane();
32. // we set the components to null and delay their loading until the tab is shown
33. // for the first time
34.
35. ImageIcon icon = new ImageIcon("yellow-ball.gif");
36.
37. tabbedPane.addTab("Mercury", icon, null);
38. tabbedPane.addTab("Venus", icon, null);
39. tabbedPane.addTab("Earth", icon, null);
40. tabbedPane.addTab("Mars", icon, null);
41. tabbedPane.addTab("Jupiter", icon, null);
42. tabbedPane.addTab("Saturn", icon, null);
43. tabbedPane.addTab("Uranus", icon, null);
44. tabbedPane.addTab("Neptune", icon, null);
45. tabbedPane.addTab("Pluto", icon, null);
46.
47. add(tabbedPane, "Center");
48.
49. tabbedPane.addChangeListener(new
50. ChangeListener()
51. {
52. public void stateChanged(ChangeEvent event)
53. {
54.
55. // check if this tab still has a null component
56.
57. if (tabbedPane.getSelectedComponent() == null)
58. {
59. // set the component to the image icon
60.
61. int n = tabbedPane.getSelectedIndex();
62. loadTab(n);
63. }
64. }
65. });
66.
67. loadTab(0);
68.
69. JPanel buttonPanel = new JPanel();
70. ButtonGroup buttonGroup = new ButtonGroup();
71. JRadioButton wrapButton = new JRadioButton("Wrap tabs");
72. wrapButton.addActionListener(new
73. ActionListener()
74. {
75. public void actionPerformed(ActionEvent event)
76. {
77. tabbedPane.setTabLayoutPolicy(JTabbedPane.WRAP_TAB_LAYOUT);
78. }
79. });
80. buttonPanel.add(wrapButton);
81. buttonGroup.add(wrapButton);
82. wrapButton.setSelected(true);
83. JRadioButton scrollButton = new JRadioButton("Scroll tabs");
84. scrollButton.addActionListener(new
85. ActionListener()
86. {
87. public void actionPerformed(ActionEvent event)
88. {
89. tabbedPane.setTabLayoutPolicy(JTabbedPane.SCROLL_TAB_LAYOUT);
90. }
91. });
92. buttonPanel.add(scrollButton);
93. buttonGroup.add(scrollButton);
94. add(buttonPanel, BorderLayout.SOUTH);
95. }
96.
97. /**
98. Loads the tab with the given index.
99. @param n the index of the tab to load
100. */
101. private void loadTab(int n)
102. {
103. String title = tabbedPane.getTitleAt(n);
104. ImageIcon planetIcon = new ImageIcon(title + ".gif");
105. tabbedPane.setComponentAt(n, new JLabel(planetIcon));
106.
107. // indicate that this tab has been visited--just for fun
108.
109. tabbedPane.setIconAt(n, new ImageIcon("red-ball.gif"));
110. }
111.
112. private JTabbedPane tabbedPane;
113.
114. private static final int DEFAULT_WIDTH = 400;
115. private static final int DEFAULT_HEIGHT = 300;
116. }
javax.swing.JTabbedPane 1.2
JTabbedPane()
JTabbedPane(int placement)
construct a tabbed pane.
Parameters:
placement
One of SwingConstants.TOP, SwingConstants.LEFT, SwingConstants.RIGHT, or SwingConstants.BOTTOM
void addTab(String title, Component c)
void addTab(String title, Icon icon, Component c)
void addTab(String title, Icon icon, Component c, String tooltip)
add a tab to the end of the tabbed pane.
void insertTab(String title, Icon icon, Component c, String tooltip, int index)
inserts a tab to the tabbed pane at the given index.
void removeTabAt(int index)
removes the tab at the given index.
void setSelectedIndex(int index)
selects the tab at the given index.
int getSelectedIndex()
returns the index of the selected tab.
Component getSelectedComponent()
returns the component of the selected tab.
String getTitleAt(int index)
void setTitleAt(int index, String title)
Icon getIconAt(int index)
void setIconAt(int index, Icon icon)
Component getComponentAt(int index)
void setComponentAt(int index, Component c)
get or set the title, icon, or component at the given index.
int indexOfTab(String title)
int indexOfTab(Icon icon)
int indexOfComponent(Component c)
return the index of the tab with the given title, icon, or component.
int getTabCount()
returns the total number of tabs in this tabbed pane.
int getTabLayoutPolicy()
void setTabLayoutPolicy(int policy) 1.4
get or set the tab layout policy. Tabs can be wrapped or scrolling.
Parameters:
policy
One of JTabbedPane.WRAP_TAB_LAYOUT or JTabbedPane.SCROLL_TAB_LAYOUT
void addChangeListener(ChangeListener listener)
adds a change listener that is notified when the user selects a different tab.
Desktop Panes and Internal Frames
Many applications present information in multiple windows that are all contained inside a large frame. If you minimize the application frame, then all of its windows are hidden at the same time. In the Windows environment, this user interface is sometimes called the multiple document interface or MDI. Figure 6-47 shows a typical application that uses this interface.
Figure 6-47. A multiple document interface application
[View full size image]
For some time, this user interface style was popular, but it has become less prevalent in recent years. Nowadays, many applications simply display a separate top-level frame for each document. Which is better? MDI reduces window clutter, but having separate top-level windows means that you can use the buttons and hotkeys of the host windowing system to flip through your windows.
In the world of Java, where you can't rely on a rich host windowing system, it makes a lot of sense to have your application manage its frames.
Figure 6-48 shows a Java application with three internal frames. Two of them have decorations on the border to maximize and iconify them. The third is in its iconified state.
Figure 6-48. A Java application with three internal frames
[View full size image]
In the Metal look and feel, the internal frames have distinctive "grabber" areas that you use to move the frames around. You can resize the windows by dragging the resize corners.
To achieve this capability, follow these steps:
1. Use a regular JFrame window for the application.
2. Add the JDesktopPane to the JFrame.
desktop = new JDesktopPane();
add(desktop, BorderLayout.CENTER);
3. Construct JInternalFrame windows. You can specify whether you want the icons for resizing or closing the frame. Normally, you want all icons.
JInternalFrame iframe = new JInternalFrame(title,
true, // resizable
true, // closable
true, // maximizable
true); // iconifiable
4. Add components to the frame.
iframe.add(c, BorderLayout.CENTER);
5. Set a frame icon. The icon is shown in the top-left corner of the frame.
iframe.setFrameIcon(icon);
NOTE
In the current version of the Metal look and feel, the frame icon is not displayed in iconized frames.
6. Set the size of the internal frame. As with regular frames, internal frames initially have a size of 0 by 0 pixels. Because you don't want internal frames to be displayed on top of each other, use a variable position for the next frame. Use the reshape method to set both the position and size of the frame:
iframe.reshape(nextFrameX, nextFrameY, width, height);
7. As with JFrames, you need to make the frame visible.
iframe.setVisible(true);
NOTE
In earlier versions of Swing, internal frames were automatically visible and this call was not necessary.
8. Add the frame to the JDesktopPane:
desktop.add(iframe);
9. You probably want to make the new frame the selected frame. Of the internal frames on the desktop, only the selected frame receives keyboard focus. In the Metal look and feel, the selected frame has a blue title bar, whereas the other frames have a gray title bar. You use the setSelected method to select a frame. However, the "selected" property can be vetoedthe currently selected frame can refuse to give up focus. In that case, the setSelected method throws a PropertyVetoException that you need to handle.
try
{
iframe.setSelected(true);
}
catch (PropertyVetoException e)
{
// attempt was vetoed
}
10. You probably want to move the position for the next internal frame down so that it won't overlay the existing frame. A good distance between frames is the height of the title bar, which you can obtain as
int frameDistance = iframe.getHeight() - iframe.getContentPane().getHeight()
11. Use that distance to determine the next internal frame position.
nextFrameX += frameDistance;
nextFrameY += frameDistance;
if (nextFrameX + width > desktop.getWidth())
nextFrameX = 0;
if (nextFrameY + height > desktop.getHeight())
nextFrameY = 0;
Cascading and Tiling
In Windows, there are standard commands for cascading and tiling windows (see Figures 6-49 and 6-50). The Java JDesktopPane and JInternalFrame classes have no built-in support for these operations. In Example 6-21, we show you how to implement these operations yourself.
Figure 6-49. Cascaded internal frames
[View full size image]
Figure 6-50. Tiled internal frames
[View full size image]
To cascade all windows, you reshape windows to the same size and stagger their positions. The getAllFrames method of the JDesktopPane class returns an array of all internal frames.
JInternalFrame[] frames = desktop.getAllFrames();
However, you need to pay attention to the frame state. An internal frame can be in one of three states:
Icon
Resizable
Maximum
You use the isIcon method to find out which internal frames are currently icons and should be skipped. However, if a frame is in the maximum state, you first set it to be resizable by calling setMaximum(false). This is another property that can be vetoed, so you must catch the PropertyVetoException.
The following loop cascades all internal frames on the desktop:
for (JInternalFrame frame : desktop.getAllFrames())
{
if (!frame.isIcon())
{
try
{
// try to make maximized frames resizable; this might be vetoed
frame.setMaximum(false);
frame.reshape(x, y, width, height);
x += frameDistance;
y += frameDistance;
// wrap around at the desktop edge
if (x + width > desktop.getWidth()) x = 0;
if (y + height > desktop.getHeight()) y = 0;
}
catch (PropertyVetoException e)
{}
}
}
Tiling frames is trickier, particularly if the number of frames is not a perfect square. First, count the number of frames that are not icons. Then, compute the number of rows as
int rows = (int) Math.sqrt(frameCount);
Then the number of columns is
int cols = frameCount / rows;
except that the last
int extra = frameCount % rows
columns have rows + 1 rows.
Here is the loop for tiling all frames on the desktop.
int width = desktop.getWidth() / cols;
int height = desktop.getHeight() / rows;
int r = 0;
int c = 0;
for (JInternalFrame frame : desktop.getAllFrames())
{
if (!frame.isIcon())
{
try
{
frame.setMaximum(false);
frame.reshape(c * width, r * height, width, height);
r++;
if (r == rows)
{
r = 0;
c++;
if (c == cols - extra)
{
// start adding an extra row
rows++;
height = desktop.getHeight() / rows;
}
}
}
catch (PropertyVetoException e)
{}
}
}
The example program shows another common frame operation: moving the selection from the current frame to the next frame that isn't an icon. The JDesktopPane class has no method to return the selected frame. Instead, you must traverse all frames and call isSelected until you find the currently selected frame. Then, look for the next frame in the sequence that isn't an icon, and try to select it by calling
frames[next].setSelected(true);
As before, that method can throw a PropertyVetoException, in which case you keep looking. If you come back to the original frame, then no other frame was selectable, and you give up. Here is the complete loop:
JInternalFrame[] frames = desktop.getAllFrames();
for (int i = 0; i < frames.length; i++)
{
if (frames[i].isSelected())
{
// find next frame that isn't an icon and can be selected
int next = (i + 1) % frames.length;
while (next != i)
{
if (!frames[next].isIcon())
{
try
{
// all other frames are icons or veto selection
frames[next].setSelected(true);
frames[next].toFront();
frames[i].toBack();
return;
}
catch (PropertyVetoException e)
{}
}
next = (next + 1) % frames.length;
}
}
}
Vetoing Property Settings
Now that you have seen all these veto exceptions, you may wonder how your frames can issue a veto. The JInternalFrame class uses a general JavaBeans mechanism for monitoring the setting of properties. We discuss this mechanism in full detail in Chapter 8. For now, we just want to show you how your frames can veto requests for property changes.
Frames don't usually want to use a veto to protest iconization or loss of focus, but it is very common for frames to check whether it is okay to close them. You close a frame with the setClosed method of the JInternalFrame class. Because the method is vetoable, it calls all registered vetoable change listeners before proceeding to make the change. That gives each of the listeners the opportunity to throw a PropertyVetoException and thereby terminate the call to setClosed before it changed any settings.
In our example program, we put up a dialog box to ask the user whether it is okay to close the window (see Figure 6-51). If the user doesn't agree, the window stays open.
Figure 6-51. The user can veto the close property
[View full size image]
Here is how you achieve such a notification.
Add a listener object to each frame. The object must belong to some class that implements the VetoableChangeListener interface. It is best to add the listener right after constructing the frame. In our example, we use the frame class that constructs the internal frames. Another option would be to use an anonymous inner class.
iframe.addVetoableChangeListener(listener);
Implement the vetoableChange method, the only method required by the VetoableChangeListener interface. The method receives a PropertyChangeEvent object. Use the getName method to find the name of the property that is about to be changed (such as "closed" if the method call to veto is setClosed(true)). As you see in Chapter 8, you obtain the property name by removing the "set" prefix from the method name and changing the next letter to lowercase.
Use the getNewValue method to get the proposed new value.
String name = event.getPropertyName();
Object value = event.getNewValue();
if (name.equals("closed") && value.equals(true))
{
ask user for confirmation
}
Simply throw a PropertyVetoException to block the property change. Return normally if you don't want to veto the change.
class DesktopFrame extends JFrame
implements VetoableChangeListener
{
. . .
public void vetoableChange(PropertyChangeEvent event)
throws PropertyVetoException
{
. . .
if (not ok)
tHRow new PropertyVetoException(reason, event);
// return normally if ok
}
}
Dialogs in Internal Frames
If you use internal frames, you should not use the JDialog class for dialog boxes. Those dialog boxes have two disadvantages:
They are heavyweight because they create a new frame in the windowing system.
The windowing system does not know how to position them relative to the internal frame that spawned them.
Instead, for simple dialog boxes, use the showInternalXxxDialog methods of the JOptionPane class. They work exactly like the showXxxDialog methods, except they position a lightweight window over an internal frame.
As for more complex dialog boxes, construct them with a JInternalFrame. Unfortunately, you then have no built-in support for modal dialog boxes.
In our sample program, we use an internal dialog box to ask the user whether it is okay to close a frame.
int result = JOptionPane.showInternalConfirmDialog(
iframe, "OK to close?", "Select an Option", JOptionPane.YES_NO_OPTION);;
NOTE
If you simply want to be notified when a frame is closed, then you should not use the veto mechanism. Instead, install an InternalFrameListener. An internal frame listener works just like a WindowListener. When the internal frame is closing, the internalFrameClosing method is called instead of the familiar windowClosing method. The other six internal frame notifications (opened/closed, iconified/deiconified, activated/deactivated) also correspond to the window listener methods.
Outline Dragging
One criticism that developers have leveled against internal frames is that performance has not been great. By far the slowest operation is to drag a frame with complex content across the desktop. The desktop manager keeps asking the frame to repaint itself as it is being dragged, which is quite slow.
Actually, if you use Windows or X Windows with a poorly written video driver, you'll experience the same problem. Window dragging appears to be fast on most systems because the video hardware supports the dragging operation by mapping the image inside the frame to a different screen location during the dragging process.
To improve performance without greatly degrading the user experience, you can set "outline dragging" on. When the user drags the frame, only the outline of the frame is continuously updated. The inside is repainted only when the user drops the frame to its final resting place.
To turn on outline dragging, call
desktop.setDragMode(JDesktopPane.OUTLINE_DRAG_MODE);
This setting is the equivalent of "continuous layout" in the JSplitPane class.
NOTE
In early versions of Swing, you had to use the magic incantation
desktop.putClientProperty("JDesktopPane.dragMode", "outline");
to turn on outline dragging.
In the sample program, you can use the Window -> Drag Outline checkbox menu selection to toggle outline dragging on or off.
NOTE
The internal frames on the desktop are managed by a DesktopManager class. You don't need to know about this class for normal programming. It is possible to implement different desktop behavior by installing a new desktop manager, but we don't cover that.
Example 6-21 populates a desktop with internal frames that show HTML pages. The File -> Open menu option pops up a file dialog box for reading a local HTML file into a new internal frame. If you click on any link, the linked document is displayed in another internal frame. Try out the Window -> Cascade and Window -> Tile commands. This example concludes our discussion of advanced Swing features.
Example 6-21. InternalFrameTest.java
[View full width]
1. import java.awt.*;
2. import java.awt.event.*;
3. import java.beans.*;
4. import java.io.*;
5. import java.net.*;
6. import java.util.*;
7. import javax.swing.*;
8. import javax.swing.event.*;
9.
10. /**
11. This program demonstrates the use of internal frames.
12. */
13. public class InternalFrameTest
14. {
15. public static void main(String[] args)
16. {
17. JFrame frame = new DesktopFrame();
18. frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
19. frame.setVisible(true);
20. }
21. }
22.
23. /**
24. This desktop frame contains editor panes that show HTML
25. documents.
26. */
27. class DesktopFrame extends JFrame
28. {
29. public DesktopFrame()
30. {
31. setTitle("InternalFrameTest");
32. setSize(DEFAULT_WIDTH, DEFAULT_HEIGHT);
33.
34. desktop = new JDesktopPane();
35. add(desktop, BorderLayout.CENTER);
36.
37. // set up menus
38.
39. JMenuBar menuBar = new JMenuBar();
40. setJMenuBar(menuBar);
41. JMenu fileMenu = new JMenu("File");
42. menuBar.add(fileMenu);
43. JMenuItem openItem = new JMenuItem("New");
44. openItem.addActionListener(new
45. ActionListener()
46. {
47. public void actionPerformed(ActionEvent event)
48. {
49. createInternalFrame(
50. new JLabel(new ImageIcon(planets[counter] + ".gif")),
51. planets[counter]);
52. counter = (counter + 1) % planets.length;
53. }
54. });
55. fileMenu.add(openItem);
56. JMenuItem exitItem = new JMenuItem("Exit");
57. exitItem.addActionListener(new
58. ActionListener()
59. {
60. public void actionPerformed(ActionEvent event)
61. {
62. System.exit(0);
63. }
64. });
65. fileMenu.add(exitItem);
66. JMenu windowMenu = new JMenu("Window");
67. menuBar.add(windowMenu);
68. JMenuItem nextItem = new JMenuItem("Next");
69. nextItem.addActionListener(new
70. ActionListener()
71. {
72. public void actionPerformed(ActionEvent event)
73. {
74. selectNextWindow();
75. }
76. });
77. windowMenu.add(nextItem);
78. JMenuItem cascadeItem = new JMenuItem("Cascade");
79. cascadeItem.addActionListener(new
80. ActionListener()
81. {
82. public void actionPerformed(ActionEvent event)
83. {
84. cascadeWindows();
85. }
86. });
87. windowMenu.add(cascadeItem);
88. JMenuItem tileItem = new JMenuItem("Tile");
89. tileItem.addActionListener(new
90. ActionListener()
91. {
92. public void actionPerformed(ActionEvent event)
93. {
94. tileWindows();
95. }
96. });
97. windowMenu.add(tileItem);
98. final JCheckBoxMenuItem dragOutlineItem = new JCheckBoxMenuItem("Drag Outline");
99. dragOutlineItem.addActionListener(new
100. ActionListener()
101. {
102. public void actionPerformed(ActionEvent event)
103. {
104. desktop.setDragMode(dragOutlineItem.isSelected()
105. ? JDesktopPane.OUTLINE_DRAG_MODE
106. : JDesktopPane.LIVE_DRAG_MODE);
107. }
108. });
109. windowMenu.add(dragOutlineItem);
110. }
111.
112. /**
113. Creates an internal frame on the desktop.
114. @param c the component to display in the internal frame
115. @param t the title of the internal frame.
116. */
117. public void createInternalFrame(Component c, String t)
118. {
119. final JInternalFrame iframe = new JInternalFrame(t,
120. true, // resizable
121. true, // closable
122. true, // maximizable
123. true); // iconifiable
124.
125. iframe.add(c, BorderLayout.CENTER);
126. desktop.add(iframe);
127.
128. iframe.setFrameIcon(new ImageIcon("document.gif"));
129.
130. // add listener to confirm frame closing
131. iframe.addVetoableChangeListener(new
132. VetoableChangeListener()
133. {
134. public void vetoableChange(PropertyChangeEvent event)
135. throws PropertyVetoException
136. {
137. String name = event.getPropertyName();
138. Object value = event.getNewValue();
139.
140. // we only want to check attempts to close a frame
141. if (name.equals("closed") && value.equals(true))
142. {
143. // ask user if it is ok to close
144. int result = JOptionPane.showInternalConfirmDialog(
145. iframe, "OK to close?", "Select an Option", JOptionPane
.YES_NO_OPTION);
146.
147. // if the user doesn't agree, veto the close
148. if (result != JOptionPane.YES_OPTION)
149. throw new PropertyVetoException("User canceled close", event);
150. }
151. }
152. });
153.
154. // position frame
155. int width = desktop.getWidth() / 2;
156. int height = desktop.getHeight() / 2;
157. iframe.reshape(nextFrameX, nextFrameY, width, height);
158.
159. iframe.show();
160.
161. // select the frame--might be vetoed
162. try
163. {
164. iframe.setSelected(true);
165. }
166. catch (PropertyVetoException e)
167. {}
168.
169. frameDistance = iframe.getHeight() - iframe.getContentPane().getHeight();
170.
171. // compute placement for next frame
172.
173. nextFrameX += frameDistance;
174. nextFrameY += frameDistance;
175. if (nextFrameX + width > desktop.getWidth()) nextFrameX = 0;
176. if (nextFrameY + height > desktop.getHeight()) nextFrameY = 0;
177. }
178.
179. /**
180. Cascades the non-iconified internal frames of the desktop.
181. */
182. public void cascadeWindows()
183. {
184. int x = 0;
185. int y = 0;
186. int width = desktop.getWidth() / 2;
187. int height = desktop.getHeight() / 2;
188.
189. for (JInternalFrame frame : desktop.getAllFrames())
190. {
191. if (!frame.isIcon())
192. {
193. try
194. {
195. // try to make maximized frames resizable; this might be vetoed
196. frame.setMaximum(false);
197. frame.reshape(x, y, width, height);
198.
199. x += frameDistance;
200. y += frameDistance;
201. // wrap around at the desktop edge
202. if (x + width > desktop.getWidth()) x = 0;
203. if (y + height > desktop.getHeight()) y = 0;
204. }
205. catch (PropertyVetoException e)
206. {}
207. }
208. }
209. }
210.
211. /**
212. Tiles the non-iconified internal frames of the desktop.
213. */
214. public void tileWindows()
215. {
216. // count frames that aren't iconized
217. int frameCount = 0;
218. for (JInternalFrame frame : desktop.getAllFrames())
219. if (!frame.isIcon()) frameCount++;
220. if (frameCount == 0) return;
221.
222. int rows = (int) Math.sqrt(frameCount);
223. int cols = frameCount / rows;
224. int extra = frameCount % rows;
225. // number of columns with an extra row
226.
227. int width = desktop.getWidth() / cols;
228. int height = desktop.getHeight() / rows;
229. int r = 0;
230. int c = 0;
231. for (JInternalFrame frame : desktop.getAllFrames())
232. {
233. if (!frame.isIcon())
234. {
235. try
236. {
237. frame.setMaximum(false);
238. frame.reshape(c * width, r * height, width, height);
239. r++;
240. if (r == rows)
241. {
242. r = 0;
243. c++;
244. if (c == cols - extra)
245. {
246. // start adding an extra row
247. rows++;
248. height = desktop.getHeight() / rows;
249. }
250. }
251. }
252. catch (PropertyVetoException e)
253. {}
254. }
255. }
256. }
257.
258. /**
259. Brings the next non-iconified internal frame to the front.
260. */
261. public void selectNextWindow()
262. {
263. JInternalFrame[] frames = desktop.getAllFrames();
264. for (int i = 0; i < frames.length; i++)
265. {
266. if (frames[i].isSelected())
267. {
268. // find next frame that isn't an icon and can be selected
269. int next = (i + 1) % frames.length;
270. while (next != i)
271. {
272. if (!frames[next].isIcon())
273. {
274. try
275. {
276. // all other frames are icons or veto selection
277. frames[next].setSelected(true);
278. frames[next].toFront();
279. frames[i].toBack();
280. return;
281. }
282. catch (PropertyVetoException e)
283. {}
284. }
285. next = (next + 1) % frames.length;
286. }
287. }
288. }
289. }
290.
291. private JDesktopPane desktop;
292. private int nextFrameX;
293. private int nextFrameY;
294. private int frameDistance;
295. private int counter;
296. private static final String[] planets =
297. {
298. "Mercury",
299. "Venus",
300. "Earth",
301. "Mars",
302. "Jupiter",
303. "Saturn",
304. "Uranus",
305. "Neptune",
306. "Pluto",
307. };
308.
309. private static final int DEFAULT_WIDTH = 600;
310. private static final int DEFAULT_HEIGHT = 400;
311. }
javax.swing.JDesktopPane 1.2
JInternalFrame[] getAllFrames()
gets all internal frames in this desktop pane.
void setDragMode(int mode)
sets the drag mode to live or outline drag mode.
Parameters:
mode
One of JDesktopPane.LIVE_DRAG_MODE or JDesktopPane.OUTLINE_DRAG_MODE
javax.swing.JInternalFrame 1.2
JInternalFrame()
JInternalFrame(String title)
JInternalFrame(String title, boolean resizable)
JInternalFrame(String title, boolean resizable, boolean closable)
JInternalFrame(String title, boolean resizable, boolean closable, boolean maximizable)
JInternalFrame(String title, boolean resizable, boolean closable, boolean maximizable, boolean iconifiable)
construct a new internal frame.
Parameters:
title
The string to display in the title bar
resizable
TRue if the frame can be resized
closable
true if the frame can be closed
maximizable
true if the frame can be maximized
iconifiable
true if the frame can be iconified
boolean isResizable()
void setResizable(boolean b)
boolean isClosable()
void setClosable(boolean b)
boolean isMaximizable()
void setMaximizable(boolean b)
boolean isIconifiable()
void setIconifiable(boolean b)
get and set the resizable, closable, maximizable, and iconifiable properties. When the property is true, an icon appears in the frame title to resize, close, maximize, or iconify the internal frame.
boolean isIcon()
void setIcon(boolean b)
boolean isMaximum()
void setMaximum(boolean b)
boolean isClosed()
void setClosed(boolean b)
get or set the icon, maximum, or closed property. When this property is TRue, the internal frame is iconified, maximized, or closed.
boolean isSelected()
void setSelected(boolean b)
get or set the selected property. When this property is true, the current internal frame becomes the selected frame on the desktop.
void moveToFront()
void moveToBack()
move this internal frame to the front or the back of the desktop.
void reshape(int x, int y, int width, int height)
moves and resizes this internal frame.
Parameters:
x, y
The top-left corner of the frame
width, height
The width and height of the frame
Container getContentPane()
void setContentPane(Container c)
get and set the content pane of this internal frame.
JDesktopPane getDesktopPane()
gets the desktop pane of this internal frame.
Icon getFrameIcon()
void setFrameIcon(Icon anIcon)
get and set the frame icon that is displayed in the title bar.
boolean isVisible()
void setVisible(boolean b)
get and set the "visible" property.
void show()
makes this internal frame visible and brings it to the front.
javax.swing.JComponent 1.2
void addVetoableChangeListener(VetoableChangeListener listener)
adds a vetoable change listener that is notified when an attempt is made to change a constrained property.
java.beans.VetoableChangeListener 1.1
void vetoableChange(PropertyChangeEvent event)
is called when the set method of a constrained property notifies the vetoable change listeners.
java.beans.PropertyChangeEvent 1.1
String getPropertyName()
returns the name of the property that is about to be changed.
Object getNewValue()
returns the proposed new value for the property.
java.beans.PropertyVetoException 1.1
PropertyVetoException(String reason, PropertyChangeEvent event)
constructs a property veto exception.
Parameters:
reason
The reason for the veto
event
The vetoed event
Bạn đang đọc truyện trên: AzTruyen.Top