Section 7.4
Programming with Components
THE TWO PREVIOUS SECTIONS described some raw materials
that are available in the form of layout managers and standard GUI components. This
section presents some programming examples that make use of those raw materials.
An Example with Text Input Boxes
As a first example, let's look at a simple calculator applet. This example
demonstrates typical uses of JTextFields, JButtons, and JLabels,
and it uses several layout managers. In the applet, you can enter two real
numbers in the text-input boxes and click on one of the buttons labeled "+",
"-", "*", and "/". The corresponding operation is performed on the
numbers, and the result is displayed in a JLabel at the bottom of the
applet. If one of the input boxes contains an illegal entry -- a word instead
of a number, for example -- an error message is displayed in the JLabel.
When designing an applet such as this one, you should start by asking yourself
questions like: How will the user interact with the applet? What components will
I need in order to support that interaction? What events can be generated by user actions,
and what will the applet do in response? What data will I have to keep in
instance variables to keep track of the state of the applet?
What information do I want to display
to the user? Once you have answered these questions, you can decide how to lay out
the components. You might want to draw the layout on paper. At that point, you are
ready to begin writing the program.
In the simple calculator applet, the user types in two numbers and clicks a button.
The computer responds by doing a computation with the user's numbers and displaying
the result. The program uses two JTextField components to get the user's
input. The JTextFields do a lot of work on their own. They respond to
mouse, focus, and keyboard events. They show blinking cursors when they
are active. They collect and display the characters that the user types. The
program only has to do three things with each JTextField: Create it,
add it to the applet, and get the text that the user has input by calling its
getText() method. The first two things are done in the applet's
init() method. The third -- getting the user's input from the input boxes -- is done in an
actionPerformed() method, which responds when the user clicks on one of the buttons.
When a component is created in one method and used in another, as the input boxes are in this case, we need an
instance variable to refer to it. In this case, I use two instance variables,
xInput and yInput, of type JTextField to refer to the
input boxes. The JLabel that is used to display the result is treated
similarly: A JLabel is created and added to the applet in the init()
method. When an answer is computed in the actionPerformed() method,
the JLabel's setText() method is used to display the answer
in the label. I use an instance variable named answer, of
type JLabel, to refer to the label.
The applet also has four JButtons and two more JLabels. (The
two extra labels display the strings "x =" and "y =".) I use
local variables rather than
instance variables for these components because I don't need to refer to them
outside the init() method.
The applet as a whole uses a GridLayout with four rows and one column.
The bottom row is occupied by the JLabel, answer.
The other three rows each contain several components. Each of the first three rows
is filled by a JPanel, which has its own layout manager and
contains several components. The row
that contains the four buttons is a JPanel which
uses a GridLayout with one row and four
columns. The JPanels that contain the input boxes use BorderLayouts.
The input box occupies the Center position of the BorderLayout, with
a JLabel on the West. (This example shows that BorderLayouts
are more versatile than it might appear at first.) All the work of setting up
the applet is done in its init() method:
public void init() {
/* Since I will be using the content pane several times,
declare a variable to represent it. Note that the
return type of getContentPane() is Container. */
Container content = getContentPane();
/* Assign a background color to the applet and its
content panel. This color will show through between
components and around the edges of the applet. */
setBackground(Color.gray);
content.setBackground(Color.gray);
/* Create the input boxes, and make sure that their background
color is white. (They are likely to be white by default.) */
xInput = new JTextField("0");
xInput.setBackground(Color.white);
yInput = new JTextField("0");
yInput.setBackground(Color.white);
/* Create panels to hold the input boxes and labels "x =" and
"y = ". By using a BorderLayout with the JTextField in the
Center position, the JTextField will take up all the space
left after the label is given its preferred size. */
JPanel xPanel = new JPanel();
xPanel.setLayout(new BorderLayout());
xPanel.add( new Label(" x = "), BorderLayout.WEST );
xPanel.add(xInput, BorderLayout.CENTER);
JPanel yPanel = new JPanel();
yPanel.setLayout(new BorderLayout());
yPanel.add( new Label(" y = "), BorderLayout.WEST );
yPanel.add(yInput, BorderLayout.CENTER);
/* Create a panel to hold the four buttons for the four
operations. A GridLayout is used so that the buttons
will all have the same size and will fill the panel.
The applet serves as ActionListener for the buttons. */
JPanel buttonPanel = new JPanel();
buttonPanel.setLayout(new GridLayout(1,4));
JButton plus = new JButton("+");
plus.addActionListener(this);
buttonPanel.add(plus);
JButton minus = new JButton("-");
minus.addActionListener(this);
buttonPanel.add(minus);
JButton times = new JButton("*");
times.addActionListener(this);
buttonPanel.add(times);
JButton divide = new JButton("/");
divide.addActionListener(this);
buttonPanel.add(divide);
/* Create the label for displaying the answer in red
on a white background. The label is set to be
"opaque" to make sure that the white background
is painted. */
answer = new JLabel("x + y = 0", JLabel.CENTER);
answer.setForeground(Color.red);
answer.setBackground(Color.white);
answer.setOpaque(true);
/* Set up the layout for the applet, using a GridLayout,
and add all the components that have been created. */
content.setLayout(new GridLayout(4,1,2,2));
content.add(xPanel);
content.add(yPanel);
content.add(buttonPanel);
content.add(answer);
/* Try to give the input focus to xInput, which is the natural
place for the user to start. */
xInput.requestFocus();
} // end init()
The action of the applet takes place in the actionPerformed()
method. The algorithm for this method is simple:
get the number from the input box xInput
get the number from the input box yInput
get the action command (the name of the button)
if the command is "+"
add the numbers and display the result in the answer label
else if the command is "-"
subtract the numbers and display the result in the label
else if the command is "*"
multiply the numbers and display the result in the label
else if the command is "/"
divide the numbers and display the result in the label
There is only one problem with this. When we call xInput.getText()
and yInput.getText() to get the contents of the input boxes, the
results are Strings, not numbers. We need a method to convert
a string such as "42.17" into the number that it represents. The standard
class Double contains a static method, Double.parseDouble(String)
for doing just that. So we can get the first number entered by the user
with the commands:
f
String xStr = xInput.getText();
x = Double.parseDouble(xStr);
where x is a variable of type double. Similarly,
if we wanted to get an integer value from the string, xStr,
we could use a static method in the standard Integer class:
x = Integer.parseInt(xStr). This makes it easy to
get numerical values from a JTextField, but one problem
remains: We can't be sure that the user has entered a string
that represents a legal real number. We could ignore this problem
and assume that a user who doesn't enter a valid input shouldn't
expect to get an answer. However, a more friendly program would
notice the error and display an error message to the user.
This requires using a "try...catch" statement, which is
not covered until Chapter 9 of this book. My
program does in fact use a try...catch statement to
handle errors, so you can get a preview of how it works.
Here is the actionPerformed() method that responds
when the user clicks on one of the buttons in the applet:
public void actionPerformed(ActionEvent evt) {
// When the user clicks a button, get the numbers
// from the input boxes and perform the operation
// indicated by the button. Put the result in
// the answer label. If an error occurs, an
// error message is put in the label.
double x, y; // The numbers from the input boxes.
/* Get a number from the xInput JTextField. Use
xInput.getText() to get its contents as a String.
Convert this String to a double. The try...catch
statement will check for errors in the String. If
the string is not a legal number, the error message
"Illegal data for x." is put into the answer and
the actionPerformed() method ends. */
try {
String xStr = xInput.getText();
x = Double.parseDouble(xStr);
}
catch (NumberFormatException e) {
// The string xStr is not a legal number.
answer.setText("Illegal data for x.");
return;
}
/* Get a number from yInput in the same way. */
try {
String yStr = yInput.getText();
y = Double.parseDouble(yStr);
}
catch (NumberFormatException e) {
answer.setText("Illegal data for y.");
return;
}
/* Perform the operation based on the action command
from the button. Note that division by zero produces
an error message. */
String op = evt.getActionCommand();
if (op.equals("+"))
answer.setText( "x + y = " + (x+y) );
else if (op.equals("-"))
answer.setText( "x - y = " + (x-y) );
else if (op.equals("*"))
answer.setText( "x * y = " + (x*y) );
else if (op.equals("/")) {
if (y == 0)
answer.setText("Can't divide by zero!");
else
answer.setText( "x / y = " + (x/y) );
}
} // end actionPerformed()
The complete source code for the applet can be found in the
file SimpleCalculator.java. (It contains
very little in addition to the two methods shown above.)
An Example with Sliders
As a second example, let's look more briefly at another applet.
In this example, the user manipulates three JSliders to set the
red, green, and blue levels of a color. The value of each
color level is displayed in a JLabel, and the color itself is
displayed in a large rectangle:
The layout manager for the applet is a GridLayout with one row
and three columns. The first column contains a JPanel, which
in turn contains the JSliders. This panel uses
another GridLayout, with three rows and one column. The second column,
which contains the JLabels, is similar. The
third column contains the colored rectangle. The component in this column
is a JPanel which contains no components. The displayed color is the background color of the
JPanel. When the user changes the color, the background color of the panel is
changed and the panel is repainted to show the new color. This is one of the few cases where an
object of type JPanel is used without either making a subclass or adding
components to it.
When the user changes the value on a JSlider, an event of type
ChangeEvent is generated. In order to respond to such events,
the applet implements the ChangeListener interface, which
specifies the method "public void stateChanged(ChangeEvent evt)".
The applet registers itself to listen for change events from each slider.
The applet has instance variables to refer to the sliders, the labels, and
the color patch. Note that since the ChangeEvent and ChangeListener
classes are defined in the package javax.swing.event, the command
"import javax.swing.event.*;" is added to the beginning of the
program.
Let's look at the code from the init() method for
setting up one of the JSliders, redSlider:
redSlider = new JSlider(0, 255, 0);
redSlider.addChangeListener(this);
The first line constructs a horizontal slider whose value can range
from 0 to 255. These are the possible values of the red level in a color.
The initial value of the slider, which is specified by the third parameter
to the constructor, is 0. The second line registers the applet ("this")
to listen for change events from the slider. The other two sliders
are initialized in a similar way.
In the stateChanged() method, the applet must respond to
the fact that the user has changed the value of one of the sliders. The
response is to read the values of all the sliders, set the labels to display
those values, and change the color displayed on the color patch. (This is slightly
lazy programming, since only one of the labels actually needs to be changed.
However, there is no rule against setting the text of a label to the same
text that it is already displaying.)
public void stateChanged(ChangeEvent evt) {
// This is called when the user has changed the value on
// one of the sliders. All the sliders are checked,
// the labels are set to display the correct values, and
// the color patch is set to correspond to the new color.
int r = redSlider.getValue();
int g = greenSlider.getValue();
int b = blueSlider.getValue();
redLabel.setText(" R = " + r);
greenLabel.setText(" G = " + g);
blueLabel.setText(" B = " + b);
colorPatch.setBackground(new Color(r,g,b));
} // end stateChanged()
The complete source code can be found in the file
RGBColorChooser.java.
Custom Component Examples
Java's standard component classes are often all you need to construct a
user interface. Sometimes, however, you need a component that Java doesn't
provide. In that case, you can write your own component class, building on
one of the components that Java does provide. We've already done this,
actually, every time we've written a subclass of the JPanel class
to use as a drawing surface.
A JPanel is a blank slate. By defining a subclass, you can make
it show any picture you like, and you can program it to respond in
any way to mouse and keyboard events. Sometimes, if you are lucky, you
don't need such freedom, and you can build on one of Java's more
sophisticated component classes.
For example, suppose I have a need for a "stopwatch" component. When the
user clicks on the stopwatch, I want it to start timing. When the user clicks
again, I want it to display the elapsed time since the first click. The
textual display can be done with a JLabel, but we want a JLabel
that can respond to mouse clicks. We can get this behavior by defining
a StopWatch component as a subclass of the JLabel class.
A StopWatch object will listen for mouse clicks on itself.
The first time the user clicks, it will change its display to
"Timing..." and remember the time when the click occurred. When the
user clicks again, it will check the time again, and it will compute and display the elapsed time.
(Of course, I don't necessarily have to define a subclass. I could
use a regular label in my applet, set the applet to listen for mouse events
on the label, and let the applet do the work of keeping track of the
time and changing the text displayed on the label. However, by writing
a new class, I have something that is reusable in other projects.
I also have all the code involved in the stopwatch function collected
together neatly in one place. For more complicated components, both
of these considerations are very important.)
The StopWatch class is not very hard to write. I need an instance
variable to record the time when the user started the stopwatch. Times in Java
are measured in milliseconds and are stored in variables of type long (to
allow for very large values). In the mousePressed() method, I need to
know whether the timer is being started or stopped, so I need another instance
variable to keep track of this aspect of the component's state. There is
one more item of interest: How do I know what time the mouse was clicked?
The method System.currentTimeMillis() returns the current time.
But there can be some delay between the time the user clicks the mouse and
the time when the mousePressed() routine is called. I don't want to
know the current time. I want to know the exact time when the mouse was pressed.
When I wrote the StopWatch class, this need sent me on a search in
the Java documentation. I found that if evt is an object of
type MouseEvent(), then the function evt.getWhen() returns the
time when the event occurred. I call this function in the mousePressed()
routine.
The complete StopWatch class is rather short:
import java.awt.event.*;
import javax.swing.*;
public class StopWatch extends JLabel implements MouseListener {
private long startTime; // Start time of timer.
// (Time is measured in milliseconds.)
private boolean running; // True when the timer is running.
public StopWatch() {
// Constructor.
super(" Click to start timer. ", JLabel.CENTER);
addMouseListener(this);
}
public void mousePressed(MouseEvent evt) {
// React when user presses the mouse by
// starting or stopping the timer.
if (running == false) {
// Record the time and start the timer.
running = true;
startTime = evt.getWhen(); // Time when mouse was clicked.
setText("Timing....");
}
else {
// Stop the timer. Compute the elapsed time since the
// timer was started and display it.
running = false;
long endTime = evt.getWhen();
double seconds = (endTime - startTime) / 1000.0;
setText("Time: " + seconds + " sec.");
}
}
public void mouseReleased(MouseEvent evt) { }
public void mouseClicked(MouseEvent evt) { }
public void mouseEntered(MouseEvent evt) { }
public void mouseExited(MouseEvent evt) { }
} // end StopWatch
Don't forget that since StopWatch is a subclass of JLabel, you
can do anything with a StopWatch that you can do with a JLabel.
You can add it to a container. You can set its font, foreground color, and background
color. You can set the text that it displays (although this would interfere with
its stopwatch function). You can even add a Border if you want.
Let's look at one more example of defining a custom component. Suppose
that -- for no good reason whatsoever -- I want a component that acts like
a JLabel except that it displays its text in mirror-reversed form.
Since no standard component does anything like this, the MirrorLabel
class is defined as a subclass of JPanel. It has a constructor
that specifies the text to be displayed and a setText() method that
changes the displayed text. The paintComponent() method draws the text
mirror-reversed, in the center of the component. This uses techniques
discussed in Section 1. Information from a
FontMetrics
object is used to center the text in the component. The reversal is achieved
by using an off-screen image. The text is drawn to the off-screen image, in the
usual way. Then the image is copied to the screen with the following command,
where OSC is the variable that refers to the off-screen image:
g.drawImage(OSC, widthOfOSC, 0, 0, heightOfOSC,
0, 0, widthOfOSC, heightOfOSC, this);
This is the version of drawImage() that specifies corners of
destination and source rectangles. The corner (0,0) in OSC
is matched to the corner (widthOfOSC,0) on the screen, while
(widthOfOSC,heightOfOSC) is matched to (0,heightOfOSC).
This reverses the image left-to-right. Here is the complete class:
import java.awt.*;
import javax.swing.*;
public class MirrorLabel extends JPanel {
// Constructor and methods meant for use public use.
public MirrorLabel(String text) {
// Construct a MirrorLable to display the specified text.
this.text = text;
}
public void setText(String text) {
// Change the displayed text. Call revalidate
// so that the layout of its container can be
// recomputed.
this.text = text;
revalidate(); // Tells container that size might have changed.
repaint();
}
public String getText() {
// Return the string that is displayed by this component.
return text;
}
// Implementation. Not meant for public use.
private String text; // The text displayed by this component.
private Image OSC;
// An off-screen image holding the non-reversed text.
private int widthOfOSC, heightOfOSC;
// Current size of the off-screen image, if one exists.
public void paintComponent(Graphics g) {
// The paint method makes a new OSC, if necessary. It writes
// a non-reversed copy of the string to the the OSC, then
// reverses the OSC as it copies it to the screen.
// (Note: color or font might have changed since the
// last time paintComponent() was called, so I can't just
// reuse the old image in the OSC.)
if (OSC == null || getSize().width != widthOfOSC
|| getSize().height != heightOfOSC) {
OSC = createImage(getSize().width, getSize().height);
widthOfOSC = getSize().width;
heightOfOSC = getSize().height;
}
Graphics OSG = OSC.getGraphics();
OSG.setColor(getBackground());
OSG.fillRect(0, 0, widthOfOSC, heightOfOSC);
OSG.setColor(getForeground());
OSG.setFont(getFont());
FontMetrics fm = OSG.getFontMetrics(getFont());
int x = (widthOfOSC - fm.stringWidth(text)) / 2;
int y = (heightOfOSC + fm.getAscent() - fm.getDescent()) / 2;
OSG.drawString(text, x, y);
OSG.dispose();
g.drawImage(OSC, widthOfOSC, 0, 0, heightOfOSC,
0, 0, widthOfOSC, heightOfOSC, null);
} // end paintComponent()
public Dimension getPreferredSize() {
// Compute a preferred size that will hold the string plus
// a border of 5 pixels.
FontMetrics fm = getFontMetrics(getFont());
return new Dimension(fm.stringWidth(text) + 10,
fm.getAscent() + fm.getDescent() + 10);
}
} // end class MirrorLabel
This class defines the method "public Dimension getPreferredSize()".
This method is called by a layout manager when it wants to know how big the
component would like to be. Standard components come with a way of
computing a preferred size. For a custom component based on a JPanel,
it's a good idea to provide a custom preferred size.
As I mentioned in Section 1, every
component has a method setPrefferedSize() that can be used
to set the preferred size of the component. For our MirrorLabel
component, however, the preferred size depends the font
and the text of the component, and these can change from time to time.
We need a way to compute a preferred size on demand, based on the current
font and text. That's what we do by defining a getPreferredSize()
method. The system calls this method when it wants to know the preferred
size of the component. In response, we can compute the preferred size
based on the current font and text.
The StopWatch and MirrorLabel class define components.
Components don't stand on their own. You have to add them to an applet or other container.
Here is an applet that demonstrates a MirrorLabel and a StopWatch
component:
The source code for this applet is in the file ComponentTest.java.
The applet uses a FlowLayout, so the components are not
arranged very neatly. The applet also contains a button, which is there to illustrate
another fine point of programming with components.
If you click the button labeled "Change Text in this Applet", the text
in all the components will be changed. You can also click on the "Timing..."
label to start and stop the StopWatch. When you do any of these things,
you will notice that the components will be rearranged to take the
new sizes into account. This is known as "validating" the container.
This is done automatically when a standard component
changes in some way that requires a change in preferred size or location.
This may or may not be the behavior that you want. (Validation doesn't
always cause as much disruption as it does in this applet. For example,
in a GridLayout, where all the components are displayed at the same size,
it will have no effect at all. I've chosen a FlowLayout for this example
to make the effect more obvious.) A custom component
such as MirrorLabel can call the revalidate() method
to indicate that the container that contains the component should be
validated. In the MirrorLabel class, revalidate()
is called in the setText() method.
A Null Layout Example
As a final example, we'll look at an applet that does not use a layout
manager. If you set the layout manager of a container to be null,
then you assume complete responsibility for positioning and sizing the
components in that container. For an applet, you can remove the layout manager
with the command:
getContentPane().setLayout(null);
If comp is any component, then
the statement
comp.setBounds(x, y, width, height);
puts the top left corner of the component at the point (x,y), measured in the
coordinated system of the container that contains the component, and it
sets the width and height of the component to the specified values.
You should only set the bounds of a component if the container that contains
it has a null layout manager. In a container that has a non-null layout manager,
the layout manager is responsible for setting the bounds, and you should not
interfere with its job.
Assuming that you have set the layout manager to null,
you can call the setBounds() method any time you like. (You can
even make a component that moves or changes size while the user is watching.)
If you are writing an applet that has a known, fixed size, then you can
set the bounds of each component in the applet's init() method.
That's what done in the following applet, which contains four components:
two buttons, a label, and a panel that displays a checkerboard pattern.
This applet doesn't do anything useful. The buttons just change the text
in the label.
In the init() method of this applet, the components are created
and added to the applet. Then the setBounds() method of each component
is called to set the size and position of the component:
public void init() {
getContentPane().setLayout(null); // I will do the layout myself!
getContentPane().setBackground(new Color(0,150,0));
// Set a dark green background.
/* Create the components and add them to the content pane. If you
don't add them to the a container, they won't appear, even if
you set their bounds! */
board = new Checkerboard();
// (Checkerboard is defined later in this class.)
getContentPane().add(board);
newGameButton = new JButton("New Game");
newGameButton.addActionListener(this);
getContentPane().add(newGameButton);
resignButton = new JButton("Resign");
resignButton.addActionListener(this);
getContentPane().add(resignButton);
message = new JLabel("Click \"New Game\" to begin a game.",
JLabel.CENTER);
message.setForeground( new Color(100,255,100) );
message.setFont(new Font("Serif", Font.BOLD, 14));
getContentPane().add(message);
/* Set the position and size of each component by calling
its setBounds() method. */
board.setBounds(20,20,164,164);
newGameButton.setBounds(210, 60, 120, 30);
resignButton.setBounds(210, 120, 120, 30);
message.setBounds(0, 200, 330, 30);
/* Add a border to the content pane. Since the return
type of getContentPane() is Container, not JComponent,
getContentPane() must be type-cast to a JComponent
in order to call the setBorder() method. Although I
think the content pane is always, in fact, a JPanel,
to be safe I test that the return value really is
a JComponent. */
if (getContentPane() instanceof JComponent) {
((JComponent)getContentPane()).setBorder(
BorderFactory.createEtchedBorder());
}
} // end init();
It's reasonably easy, in this case, to get an attractive layout.
It's much more difficult to do your own layout
if you want to allow for changes of size. In that case, you have
to respond to changes in the container's size by recomputing the sizes
and positions of all the components that it contains. If you want to
respond to changes in a container's size, you can register an
appropriate listener with the container. Any component generates
an event of type ComponentEvent when its size changes
(and also when it is moved, hidden, or shown). You can
register a ComponentListener with the container and
respond to size change events by recomputing the sizes and
positions of all the components in the container. Consult
a Java reference for more information about ComponentEvents.
However, my real advice is that if you want to allow for changes
in the container's size, try to find a layout manager to do the work
for you.