Section 6.6
Introduction to Layouts and Components
IN PRECEDING SECTIONS, YOU'VE SEEN how to use a
graphics context to draw on the screen and how to handle mouse events
and keyboard events. In one sense, that's all there is to GUI programming.
If you're willing to program all the drawing and handle all the mouse
and keyboard events, you have nothing more to learn. However, you would
either be doing a lot more work than you need to do, or you would be
limiting yourself to very simple user interfaces. A typical user interface
uses standard GUI components such as buttons, scroll bars, text-input boxes,
and menus. These components have already been written for you, so
you don't have to duplicate the work involved in developing them.
They know how to draw themselves, and they can handle the details of
processing the mouse and keyboard events that concern them.
Consider one of the simplest user interface components, a push button.
The button has a border, and it displays some text. This text can be
changed. Sometimes the button is disabled, so that clicking on it doesn't have
any effect. When it is disabled, its appearance changes. When the user
clicks on the push button, the button changes appearance while the mouse
button is pressed and changes back when the mouse button is released. In
fact, it's more complicated than that. If the user moves the mouse outside
the push button before releasing the mouse button, the button changes
to its regular appearance. To implement this, it is necessary to
respond to mouse exit or mouse drag events. Furthermore, on many platforms, a
button can receive the input focus. The button changes appearance when
it has the focus. If the button has the focus and
the user presses the space bar, the button is triggered. This means
that the button must respond to keyboard and focus events as well.
Fortunately, you don't have to program any of this, provided you use
an object belonging to the standard class javax.swing.JButton. A JButton
object draws itself and processes mouse, keyboard,
and focus events on its own. You only hear from the Button when
the user triggers it by clicking on it or pressing the space bar while
the button has the input focus. When this happens, the JButton
object creates an event object belonging to the class java.awt.event.ActionEvent.
The event object is sent to any registered listeners to tell them that the
button has been pushed. Your program gets only the information it needs --
the fact that a button was pushed.
Another aspect of GUI programming is laying out components on the
screen, that is, deciding where they are drawn and how big they are.
You have probably noticed that computing coordinates can be a difficult
problem, especially if you don't assume a fixed size for the applet.
Java has a solution for this, as well.
Components are the visible objects that make up a GUI.
Some components are containers, which
can hold other components. An applet's content pane is an example
of a container. The standard class JPanel, which we have
only used as a drawing surface up till now, is another example of
a container. Because a JPanel object is a container, it
can hold other components. So JPanels are dual purpose: You
can draw on them, and you can add other components to them.
Because a JPanel is itself a component, you can add a JPanel
to an applet's content pane or even to another JPanel.
This makes complex nesting of components possible. JPanels
can be used to organize complicated user interfaces.
The components in a container must be "laid out,"
which means setting their sizes and positions.
It's possible to program the layout yourself, but ordinarily layout is
done by a layout manager.
A layout manager is an object associated
with a container that implements some policy for laying out the
components in that container. Different types of layout manager
implement different policies.
In this section, we'll look at a few examples of using components
and layout managers, leaving the details until Section 7.2
and Section 7.3. The applets that we look at
in this section have a large drawing area with a row of controls below it.
Our first example is rather simple. It's another "Hello World" applet,
in which the color of the message can be changed by clicking one of the
buttons at the bottom of the applet:
In the previous JApplets that we've looked at, the entire applet was
filled with a JPanel that served as a drawing surface. In this example,
there are two JPanels: the large black area at the top that displays the
message and the smaller area at the bottom that holds the three buttons.
Let's first consider the panel that contains the buttons. This panel
is created in the applet's init() method as a variable named buttonBar,
of type JPanel:
JPanel buttonBar = new JPanel();
When a panel is to be used as a drawing surface, it is necessary
to create a subclass of the JPanel class and include a
paintComponent() method to do the drawing. However, when a
JPanel is just being used as a container, there is no need to create
a subclass. A standard JPanel is already capable of holding components
of any type.
Once the panel has been created, the three buttons are created and
are added to the panel. A button is just an object belonging to
the class javax.swing.JButton. When a button is created,
the text that will be shown on the button is provided as a parameter
to the constructor. The first button in the panel is created with
the command:
JButton redButton = new JButton("Red");
This button is added to the buttonBar panel with the command:
buttonBar.add(redButton);
Every JPanel comes automatically with a layout manager. This default layout
manager will simply line up the components that are added to it in a row.
That's exactly the behavior we want here, so there is nothing more to do.
If we wanted a different kind of layout, it's possible to change the panel's
layout manager.
One more step is required to make the button useful: an object must
be registered with the button to listen for ActionEvents. The button
will generate an ActionEvent when the user clicks on it. ActionEvents
are similar to MouseEvents or KeyEvents. To use them, a class should
import java.awt.event.*. The object that is to do the listening
must implement an interface named ActionListener. This interface
requires a definition for the method "public void actionPerformed(ActionEvent evt);".
Finally, the listener must be registered with the button by calling the
button's addActionListener() method. In this case, the applet
itself will act as listener, and the registration is done with the command:
redButton.addActionListener(this);
After doing the same three commands for each of the other two buttons -- and
setting the background color for the sake of aesthetics -- the buttonBar
panel is ready to use. It just has to be added to the applet.
As we have seen, components are not added directly to an applet.
Instead, they are added to the applet's content pane, which is itself
a container. The content pane comes with a default layout manager
that is capable of displaying up to five components. Four of these
components are placed along the edges of the applet, in the so-called
"North", "South", "East", and "West" positions. A component in the
"Center" position fills in all the remaining space. This type of
layout is called a BorderLayout. In our example,
the button bar occupies the "South" position and the drawing area
fills the "Center" position. When you add a component to a BorderLayout,
you have to specify its position using a constant such as
BorderLayout.SOUTH or BorderLayout.CENTER.
In this example, buttonBar is added
to the applet with the command:
getContentPane().add(buttonBar, BorderLayout.SOUTH);
The display area of the applet is a drawing surface like those we
have seen in other examples. A nested class named Display is
created as a subclass of JPanel, and the display area is
created as an object belonging to that class. The applet class has
an instance variable named display of type Display
to represent the drawing surface. The display object is simply
created and added to the applet with the commands:
display = new Display();
getContentPane().add(display, BorderLayout.CENTER);
Putting this all together, the complete init() method
for the applet becomes:
public void init() {
display = new Display();
// The component that displays "Hello World".
getContentPane().add(display, BorderLayout.CENTER);
// Adds the display panel to the CENTER position of the
// JApplet's content pane.
JPanel buttonBar = new JPanel();
// This panel will hold three buttons and will appear
// at the bottom of the applet.
buttonBar.setBackground(Color.gray);
// Change the background color of the button panel
// so that the buttons will stand out better.
JButton redButton = new JButton("Red");
// Create a new button. "Red" is the text
// displayed on the button.
redButton.addActionListener(this);
// Set up the button to send an "action event" to this applet
// when the user clicks the button. The parameter, this,
// is a name for the applet object that we are creating,
// so action events from the button will be handled by
// calling the actionPerformed() method in this class.
buttonBar.add(redButton);
// Add the button to the buttonBar panel.
JButton greenButton = new JButton("Green"); // the second button
greenButton.addActionListener(this);
buttonBar.add(greenButton);
JButton blueButton = new JButton("Blue"); // the third button
blueButton.addActionListener(this);
buttonBar.add(blueButton);
getContentPane().add(buttonBar, BorderLayout.SOUTH);
// Add button panel to the bottom of the content pane.
} // end init()
Notice that the variables buttonBar, redButton,
greenButton, and blueButton are local to the init()
method. This is because once the buttons and panel have been added to the
applet, the variables are no longer needed. The objects continue to exist,
since they have been added to the applet. But they will take care of themselves,
and there is no need to manipulate them elsewhere in the applet. The
display variable, on the other hand, is an instance variable that
can be used throughout the applet. This is because we are not
finished with the display object after adding it to the applet. When the
user clicks a button, we have to change the color of the display. We
need a way to keep the variable around so that we can refer to it
in the actionPerformed() method. In general, you don't need
an instance variable for every component in an applet -- just for the
components that will be referred to outside the init() method.
The drawing surface in our example is defined by a nested class
named Display which is a subclass of JPanel.
The class contains a
paintComponent() method that is responsible for drawing
the message "Hello World" on a black background. The Display
class also contains a variable that it uses to remember the current
color of the message and a method that can be called to change the
color. This class is more self-contained than than most of
the drawing surface classes that we have looked at, and in fact it
could have been defined as an independent class instead of as
a nested class. Here is the
definition of the nested class, Display:
class Display extends JPanel {
// This nested class defines a component that displays
// the string "Hello World". The color and font for
// the string are recorded in the variables colorNum
// and textFont.
int colorNum; // Keeps track of which color is displayed;
// 1 for red, 2 for green, 3 for blue.
Font textFont; // The font in which the message is displayed.
// A font object represents a certain size and
// style of text drawn on the screen.
Display() {
// Constructor for the Display class. Set the background
// color and assign initial values to the instance
// variables, colorNum and textFont.
setBackground(Color.black);
colorNum = 1; // The color of the message is set to red.
textFont = new Font("Serif",Font.BOLD,36);
// Create a font object representing a big, bold font.
}
void setColor(int code) {
// This method is provided to be called by the
// main class when it wants to set the color of the
// message. The parameter value should be 1, 2, or 3
// to indicate the desired color.
colorNum = code;
repaint(); // Tell the system to repaint this component.
}
public void paintComponent(Graphics g){
// This routine is called by the system whenever this
// panel needs to be drawn or redrawn. It first calls
// super.paintComponent() to fill the panel with the
// background color. It then displays the message
// "Hello World" in the proper color and font.
super.paintComponent(g);
switch (colorNum) { // Set the color.
case 1:
g.setColor(Color.red);
break;
case 2:
g.setColor(Color.green);
break;
case 3:
g.setColor(Color.blue);
break;
}
g.setFont(textFont); // Set the font.
g.drawString("Hello World!", 25,50); // Draw the message.
} // end paintComponent
} // end nested class Display
The main class has an instance variable named display of
type Display. When the user clicks one of the buttons
in the applet, this variable is used to call the setColor()
method in the drawing surface object. This is done in the applet's
actionPerformed() method. This method is called when
the user clicks any one of the three buttons, so it needs some
way to tell which button was pressed. This information is provided in the parameter
to the actionPerformed() method. This parameter contains
an "action command," which in the case of a button is just the
string that is displayed on the button:
public void actionPerformed(ActionEvent evt) {
// This routine is called by the system when the user clicks
// on one of the buttons. The response is to set the display's
// color accordingly.
String command = evt.getActionCommand();
// The "action command" associated with the event
// is the text on the button that was clicked.
if (command.equals("Red")) // Set the color.
display.setColor(1);
else if (command.equals("Green"))
display.setColor(2);
else if (command.equals("Blue"))
display.setColor(3);
} // end actionPerformed()
We have now looked at all the pieces of the sample applet.
You can find the entire source code in the file HelloWorldJApplet.java.
For a second example, let's look at something a little more interesting.
Here's a simple card game in which you look at a playing card and try to
predict whether the next card will be higher or lower in value. (Aces have
the lowest value in this game.) You've seen a text-oriented version of
the same game in Section 5.3. That section
also defined Deck, Hand, and Card classes
that are used in this applet. In this GUI version of the game, you
click on a button to make your prediction. If you predict wrong, you lose.
If you make three correct predictions, you win. After completing one
game, you can click the "New Game" button to start a new game.
Try it! See what happens if you click on one of the buttons at a time
when it doesn't make sense to do so.
The overall form of this applet is the same as that of
the previous example: It has three buttons in a panel
at the bottom of the applet and a large drawing surface that
displays the cards and a message. However, I've organized the code
a little differently in this example. In this case, it's the
drawing surface object, rather than the applet, that listens
for events from the buttons, and I've put almost all the
programming into the display surface class. The applet
object is only responsible for creating the components
and adding them to the applet. This is done in the following
init() method, which has almost the same form
as the init() method in the previous example:
public void init() {
// The init() method lays out the applet. A HighLowCanvas
// occupies the CENTER position of the layout. On the
// bottom is a panel that holds three buttons. The
// HighLowCanvas object listens for ActionEvents from the
// buttons and does all the real work of the program.
setBackground( new Color(130,50,40) );
HighLowCanvas board = new HighLowCanvas();
getContentPane().add(board, BorderLayout.CENTER);
JPanel buttonPanel = new JPanel();
buttonPanel.setBackground( new Color(220,200,180) );
getContentPane().add(buttonPanel, BorderLayout.SOUTH);
JButton higher = new JButton( "Higher" );
higher.addActionListener(board);
buttonPanel.add(higher);
JButton lower = new JButton( "Lower" );
lower.addActionListener(board);
buttonPanel.add(lower);
JButton newGame = new JButton( "New Game" );
newGame.addActionListener(board);
buttonPanel.add(newGame);
} // end init()
In programming the drawing surface class, HighLowCanvas, it is important
to think in terms of the states that the game can be in, how the state
can change, and how the response to events can depend on the state.
The approach that produced the original, text-oriented game in Section 5.3
is not appropriate here. Trying to think about the game in terms of a process that
goes step-by-step from beginning to end is more likely to confuse you than to
help you.
The state of the game includes the cards and the message.
The cards are stored in an object of type Hand. The message is a String.
These values are stored in instance variables.
There is also another,
less obvious aspect of the state: Sometimes a game is in progress,
and the user is supposed to make a prediction about the next card. Sometimes
we are between games, and the user is supposed to click the "New Game"
button. It's a good idea to keep track of this basic difference in state.
The canvas class uses a boolean variable named gameInProgress
for this purpose.
The state of the applet can change whenever the user clicks on a button.
The HighLowCanvas class implements the ActionListener
interface and defines an actionPerformed() method to respond to
the user's clicks. This method
simply calls one of three other methods, doHigher(), doLower(),
or newGame(), depending on which button was pressed. It's in these
three event-handling methods that the action of the game takes place.
We don't want to let the user start a new game if a game is currently
in progress. That would be cheating. So, the response in the
newGame() method is different depending on whether the
state variable gameInProgress is true or false. If a game is
in progress, the message instance variable should be set to
show an error message. If a game is not in progress, then all the state
variables should be set to appropriate values for the beginning of a
new game. In any case, the board must be repainted so that the user
can see that the state has changed. The complete newGame()
method is as follows:
void doNewGame() {
// Called by the constructor, and called by actionPerformed()
// when the user clicks the "New Game" button. Start a new game.
if (gameInProgress) {
// If the current game is not over, it is an error to try
// to start a new game.
message = "You still have to finish this game!";
repaint();
return;
}
deck = new Deck(); // Create a deck and hand to use for this game.
hand = new Hand();
deck.shuffle();
hand.addCard( deck.dealCard() ); // Deal the first card.
message = "Is the next card higher or lower?";
gameInProgress = true; // State changes! A game has started.
repaint();
}
The doHigher() and doLower() methods are almost identical to each other
(and could probably have been combined into one method with a parameter, if
I were more clever). Let's look at the doHigher() routine.
This is called when the user clicks the "Higher" button.
This only makes sense if a game is in progress, so the first thing
doHigher() should do is check the value of the state variable
gameInProgress. If the value is false, then
doHigher() should just set up an error message. If a game
is in progress, a new card should be added to the hand and the user's
prediction should be tested. The user might win or lose at this time.
If so, the value of the state variable gameInProgress must
be set to false because the game is over. In any case,
the board is repainted to show the new state. Here is the
doHigher() method:
void doHigher() {
// Called by actionPerformed() when user clicks "Higher".
// Check the user's prediction. Game ends if user guessed
// wrong or if the user has made three correct predictions.
if (gameInProgress == false) {
// If the game has ended, it was an error to click "Higher",
// so set up an error message and abort processing.
message = "Click \"New Game\" to start a new game!";
repaint();
return;
}
hand.addCard( deck.dealCard() ); // Deal a card to the hand.
int cardCt = hand.getCardCount(); // How many cards in the hand?
Card thisCard = hand.getCard( cardCt - 1 ); // Card just dealt.
Card prevCard = hand.getCard( cardCt - 2 ); // The previous card.
if ( thisCard.getValue() < prevCard.getValue() ) {
gameInProgress = false;
message = "Too bad! You lose.";
}
else if ( thisCard.getValue() == prevCard.getValue() ) {
gameInProgress = false;
message = "Too bad! You lose on ties.";
}
else if ( cardCt == 4) {
gameInProgress = false;
message = "You win! You made three correct guesses.";
}
else {
message = "Got it right! Try for " + cardCt + ".";
}
repaint();
}
The paintComponent() method of the HighLowCanvas class uses the values in the
state variables to decide what to show. It displays the string
stored in the message variable. It draws each of the
cards in the hand. There is one little tricky bit:
If a game is in progress, it draws an extra face-down card,
which is not in the hand, to represent the next card in the deck.
Drawing the cards requires some care and computation. I wrote
a method, "void drawCard(Graphics g, Card card, int x, int y)",
which draws a card with its upper left corner at the point (x,y).
The paintComponent() routine decides where to draw each card and calls this
routine to do the drawing. You can check out all the details in the
source code, HighLowGUI.java.
As a final example, let's look quickly at an improved paint program,
similar to the one from Section 4. The user can draw
on the large white area. In this version, the user selects the drawing
color from the pop-up menu at the bottom-left of the applet. If the user
hits the "Clear" button, the drawing area is filled with
the background color. I've added one feature: If the user hits
the "Set Background" button, the background color
of the drawing area is set to the color currently selected in the
pop-up menu, and the drawing area is cleared. This lets you
draw in cyan on a magenta background if you have a mind to.
The drawing area in this applet is a component, belonging to the nested
class SimplePaintCanvas. I wrote this class, as usual, as a sub-class of
JPanel and programmed it to listen for mouse events and to
respond by drawing a curve. As in the HighLowGUI applet,
all the action takes place in the nested class. The main applet
class just does the set up. One new feature of interest is the
pop-up menu. This component is an object belonging to the standard
class, JComboBox. We'll cover this component class in Chapter 7.
What you should note about this version of the paint applet is
that in many ways, it was easier to write than the original. There are no computations
about where to draw things and how to decode user mouse clicks. We don't
have to worry about the user drawing outside the drawing area. The graphics
context that is used for drawing on the canvas can only
draw on the canvas. If the user tries to extend a curve outside the
canvas, the part that lies outside the canvas is automatically ignored.
We don't have to worry about giving the user visual feedback about which
color is selected. That is handled by the text displayed on the pop-up
menu.
You'll find the source code for this example in the file
SimplePaint2.java.
After struggling through this chapter, you should be equipped
to understand it almost in its entirety!
End of Chapter 6