New to Java? We'll help you get started with our revised beginner's tutorial, or our free online textbook.
|
Get the latest Java books |
|
h t t p : / /w w w . j a v a c o f f e e b r e a k . c
o m /
|
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 drag events. Furthermore, on many platforms, a button can receive the input 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 Button class. A Button object draws itself and processes mouse, mouse dragging, 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 Button object creates an event object belonging to the class ActionEvent. That event 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 is an example of a container. An independent window is another type of container. Java also has a class of container called Panel. Because a Panel object is a container, it can hold other components. But a Panel is itself meant to be placed inside another container. This allows complex nesting of components. Panels 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. As a first example, here is a new version of the ColoredHelloWorldApplet from the Section 1. Click the buttons to change the color of the message:
It is possible to draw directly on an applet, as I've done previously in this chapter. However, it is not a good idea to do so when the applet contains components that will be laid out by a layout manager. The reason is that it's hard to be sure exactly where the components will be placed by the layout manager and how big they will be (If you knew that, you wouldn't be using the layout manager! It's supposed to make such computations, so you don't have to.) A better idea is to use a special-purpose component to display the drawing. This drawing component or "canvas" will be just one of the components contained in the applet. The white rectangle in the above applet, where the "Hello World" message is displayed, is an example of such a component. This component is a member of a class ColoredHelloWorldCanvas, which I have defined as a subclass of the standard class, java.awt.Canvas. The Canvas class exists precisely for creating such drawing areas. An object that belongs to the Canvas class itself is nothing but a patch of color. To create a canvas with content, you have to define a subclass of Canvas and write a paint() method for your subclass to draw the content you want.
When you use a canvas in this way, it's a good idea to put all the information necessary to do the drawing in the canvas object, rather than in the main applet object. The original ColoredHelloWorldApplet used an instance variable, textColor, to keep track of the color of the displayed message. In the new version, the textColor variable is moved to the ColoredHelloWorldCanvas class. This class also contains a method, setTextColor(), which can be called to tell the canvas to change the color of the message. When the user clicks one of the buttons, the applet responds by calling this method. This is good object-oriented program design: The ColoredHelloWorldCanvas class is responsible for displaying a colored greeting, so it should contain all the data and behaviors associated with its role. On the other hand, this class doesn't need to know anything about buttons, layouts, and events. Those are the job of the main applet class. This separation of responsibility helps reduce the overall complexity of the program.
So, here's the ColoredHelloWorldCanvas class:
class ColoredHelloWorldCanvas extends Canvas { // A canvas that displays the message "Hello World" on // a white background in a big, bold font. A method is // provided for changing the color of the message. Color textColor; // Color in which "Hello World" is displayed. Font textFont; // The font in which the message is displayed. ColoredHelloWorldCanvas() { // Constructor. setBackground(Color.white); textColor = Color.red; textFont = new Font("Serif",Font.BOLD,24); } public void paint(Graphics g) { // Show the message in the set color and font. g.setColor(textColor); g.setFont(textFont); g.drawString("Hello World!", 20,40); } void setTextColor(Color color) { // Set the text color and tell the system to repaint // the canvas so the message will be in the new color. textColor = color; repaint(); } } // end class ColoredHelloWorldCanvasYou should be able to understand this. Just keep in mind that all this applies to the canvas, not to the whole applet. When the constructor calls the method setBackground(), it's only the background of the canvas that is set to white. The background color of the applet is not affected. Remember that every component has its own background color, foreground color, and font. The setTextColor() method is called by the applet when it wants to change the color of the displayed message. Note that setTextColor() calls repaint(). Since this repaint() method is in the ColoredHelloWorldCanvas class, it causes just the canvas to be repainted, not the whole applet. Every component has its own paint() and repaint() methods, and every component is responsible for drawing itself. This is another example of the way responsibilities are distributed in an object-oriented system.
The main applet class, ColoredHelloWorldApplet2, is responsible for managing all the components in the applet and the events they generate. The components include the drawing canvas and three buttons. These components are created and added to the applet in the applet's init() method. The applet will listen for ActionEvents from the buttons, so the applet class implements the interface, ActionListener. The applet's init() method sets up the applet to listen for ActionEvents from each button. It does this by calling the button's addActionListener method.
The buttons are not contained directly in the applet. Instead, they are added to a Panel, and that panel is added to the applet. The Panel is the gray strip across the bottom of the applet. Every container object, including applets and panels, include several add() methods that are used to add components to the container. For example, if bttn is a button, and panel is a container, then the command "panel.add(bttn);" adds the button to the panel. This means that the button will appear in the panel. Exactly where it shows up depends on the panel's layout manager.
Once the canvas and panel have been created, it's time to lay out the applet as a whole. This is done by the last three lines of the init() method. I use a "BorderLayout," as the layout manager for the applet. A BorderLayout displays one big component in the "Center" of the applet and, optionally, up to four other components along the edges of the applet in the "North", "South", "East", and "West" positions. The component in the center gets any space that is left over after the other components are drawn. In this example, the canvas is added in the "Center" position, with the button bar below it, to the "South". When a component is added to a container that uses a BorderLayout, the add() method has to specify which of the five possible positions should be used for the component:
Here is the complete init() method from the applet class:
public void init() { // This routine is called by the system to initialize the // applet. It creates the canvas and lays out the applet // to consist of a bar of control buttons below the canvas. setBackground(Color.lightGray); canvas = new ColoredHelloWorldCanvas(); Panel buttonBar = new Panel(); // panel to hold control buttons Button redBttn = new Button("Red"); // Create buttons, add them redBttn.addActionListener(this); // to the button bar. The buttonBar.add(redBttn); // parameter to the Button // constructor is the text // that appears on Button. Button greenBttn = new Button("Green"); greenBttn.addActionListener(this); buttonBar.add(greenBttn); Button blueBttn = new Button("Blue"); blueBttn.addActionListener(this); buttonBar.add(blueBttn); setLayout(new BorderLayout(3,3)); // Set layout for applet. add(buttonBar, BorderLayout.SOUTH); // Put panel at the bottom. add(canvas, BorderLayout.CENTER); // Canvas will fill any // remaining space. } // end init()You might not understand this completely just yet, but if you want to write an applet using a similar layout, you can follow the pattern in this sample init() method.
In order to handle the ActionEvents from the buttons, the applet must define an actionPerformed() method. This is the only method specified by the ActionListener interface. This method has a parameter, evt, of type ActionEvent. This parameter can be used to find out which button is responsible for the action event. The function evt.getActionCommand() returns a String that gives the text that the button displays. The actionPerformed() method in the sample applet checks the value returned by evt.getActionCommand() and sets the text color of the canvas to the appropriate value. (Remember that canvas is an object belonging to the class ColoredHelloWorldCanvas, which is shown above.)
public void actionPerformed(ActionEvent evt) { String command = evt.getActionCommand(); if (command.equals("Red")) canvas.setTextColor(Color.red); else if (command.equals("Green")) canvas.setTextColor(Color.green); else if (command.equals("Blue")) canvas.setTextColor(Color.blue); } // end init()There is just one more method in the applet, and it requires a little explanation:
public Insets getInsets() { return new Insets(3,3,3,3); }This method is called by the layout manager to decide how much space to leave between the edges of the applet and the components that the applet contains. The background color of the applet will show though in this border. The object of type Insets that is returned by this method specifies a 3-pixel border along each edge of the applet. There is also, by the way, a 3-pixel boundary between components in the applet. This is specified in the constructor of the layout manager, "new BorderLayout(3,3)", in the applet's init() method.
You can see how all this is put together in the source code for the applet, ColoredHelloWorldApplet2.java. (Note that the source code for the canvas class is in a separate file, ColoredHelloWorldCanvas.java, and you need both classes in order to use the applet.)
As 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 ColoredHelloWorldApplet2: It has three buttons in a panel at the bottom of the applet and a large canvas for drawing. Of course, in this case the canvas does more interesting things. The canvas class includes all the programming for the game. Since that was true, I decided to let the canvas class act as ActionListener and respond to the buttons directly. The applet class, which just sets everything up, is fairly simple:
import java.awt.*; import java.awt.event.*; import java.applet.*; public class HighLowGUI extends Applet { public void init() { // The init() method lays out the applet using a BorderLayout. // 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) ); setLayout( new BorderLayout(3,3) ); HighLowCanvas board = new HighLowCanvas(); add(board, BorderLayout.CENTER); // Add canvas to the applet. Panel buttonPanel = new Panel(); buttonPanel.setBackground( new Color(220,200,180) ); add(buttonPanel, BorderLayout.SOUTH); // Add panel to applet. Button higher = new Button( "Higher" ); higher.addActionListener(board); // BOARD LISTENS, NOT APPLET! higher.setBackground(Color.lightGray); buttonPanel.add(higher); Button lower = new Button( "Lower" ); lower.addActionListener(board); lower.setBackground(Color.lightGray); buttonPanel.add(lower); Button newGame = new Button( "New Game" ); newGame.addActionListener(board); newGame.setBackground(Color.lightGray); buttonPanel.add(newGame); } // end init() public Insets getInsets() { return new Insets(3,3,3,3); } } // end class HighLowGUIIn programming the canvas 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 thing 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 that are displayed. The cards are stored in an object of type Hand. The message is a String. These values are stored in instance variables in the canvas class. 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 canvas 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 canvas 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() // if 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; repaint(); }The doHigher() and doLower() methods are almost identical (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 applet 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 paint() method of the applet 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 paint() 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. (This file contains the source for both the applet class, HighLowGUI and the canvas class HighLowCanvas.)
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 Choice menu at the bottom 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 Choice menu, and then 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 class SimplePaintCanvas. I wrote this class as a sub-class of Canvas and programmed it to listen for mouse events and respond by drawing a curve. As in the HighLowGUI applet, all the action takes place in the canvas class. The main applet class just does the set up. One new feature of interest is the Choice menu. This component is an object belonging to the standard class, Choice. 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 Choice menu.
You'll find the source code for this example in the file SimplePaint2.java. The file contains both classes, the applet class SimplePaint2 and the canvas class SimplePaintCanvas.
[ Next Section | Previous Section | Chapter Index | Main Index ]