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 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.
As a first example, let's look at a simple calculator applet. This example demonstrates typical uses of TextFields, Buttons, and Labels, 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 Label 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 Label.
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 TextField components to get the user's input. The TextFields 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 TextField: 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 text from the input box is used in an actionPerformed() method, when the user clicks on one of the buttons. When a component is created in one method and used in another, we need an instance variable to refer to it. In this case, I use two instance variables, xInput and yInput, of type TextField to refer to the input boxes. The Label that is used to display the result is treated similarly: A Label is created and added to the applet in the init() method. When an answer is computed in the actionPerformed method, the Label's setText() method is used to display the answer in the label. I use an instance variable named answer, of type Label, to refer to the label.
The applet also has four Buttons and two more Labels. (The two extra labels display the strings "x =" and "y =".) I don't use 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 Label, answer. The other three rows each contain several components. Each of these rows is occupied by a Panel that has its own layout manager. The row that contains the four buttons is a Panel that uses a GridLayout with one row and four columns. The Panels that contain the input boxes use BorderLayouts. The input box occupies the Center position of the BoarderLayout, with a Label 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() { setBackground(Color.lightGray); /* Create the input boxes, and make sure that the background color is white. (On some platforms, it is automatically.) */ xInput = new TextField("0"); xInput.setBackground(Color.white); yInput = new TextField("0"); yInput.setBackground(Color.white); /* Create panels to hold the input boxes and labels "x = " and "y = ". By using a BorderLayout with the TextField in the Center position, the TextField will take up all the space left after the label is given its preferred size. */ Panel xPanel = new Panel(); xPanel.setLayout(new BorderLayout(2,2)); xPanel.add( new Label(" x = "), BorderLayout.WEST ); xPanel.add(xInput, BorderLayout.CENTER); Panel yPanel = new Panel(); yPanel.setLayout(new BorderLayout(2,2)); 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. */ Panel buttonPanel = new Panel(); buttonPanel.setLayout(new GridLayout(1,4)); Button plus = new Button("+"); plus.addActionListener(this); // Applet will listen for buttonPanel.add(plus); // events from the buttons. Button minus = new Button("-"); minus.addActionListener(this); buttonPanel.add(minus); Button times = new Button("*"); times.addActionListener(this); buttonPanel.add(times); Button divide = new Button("/"); divide.addActionListener(this); buttonPanel.add(divide); /* Create the label for displaying the answer (in red). */ answer = new Label("x + y = 0", Label.CENTER); answer.setForeground(Color.red); /* Set up the layout for the applet, using a GridLayout, and add all the components that have been created. */ setLayout(new GridLayout(4,1,2,2)); add(xPanel); // (Calls the add() method of the applet itself.) add(yPanel); add(buttonPanel); 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 label, answer else if the command is "-" subtract the numbers and display the result in the label, answer else if the command is "*" multiply the numbers and display the result in the label, answer else if the command is "/" divide the numbers and display the result in the label, answerThere 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. It's possible to convert a String to a number. Unfortunately, Java doesn't make it easy. The following code will get the contents of the input box, xInput, and convert it to a value of type double:
String xStr = xInput.getText(); Double d = new Double(xStr); x = d.doubleValue(); // where x is a variable of type double.An object belonging to the standard class, Double, contains a value of type double. (Double is called a wrapper class. An object of type Double is a wrapper for a value of type double. This class exists because a value of type double is not an object, and some contexts require objects. If you want to use a double value in such a context, you have to wrap it in an object of type Double.) The constructor "new Double(xStr)" converts xStr to a double value and puts it in an object of type Double. The function d.doubleValue() gets the numerical value from that object. This is really unnecessarily complicated, isn't it? (Things are a little easier for integers. If you want to convert a String, str, to an int value, you can say "int N = Integer.parseInt(str)". Integer is the wrapper class for values of type int, and parseInt() is a static method in that class. For some reason, there is no parseDouble() method in the Double class.)
The complications are not over. If the string xStr does not contain a legal number, then the constructor "new Double(xStr)" generates an error. It is good style to catch this error and display an error message to the user. Catching the error requires the try...catch statement, which will be covered in Chapter 9. In the meantime, you can see how it's done in the actionPerformed() method from the applet. Aside from the difficulty of getting numerical values from the input boxes, the method is straightforward:
public void actionPerformed(ActionEvent evt) { double x, y; // The numbers from the input boxes. /* Get a number from the xInput TextField. 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(); Double d = new Double(xStr); x = d.doubleValue(); } catch (NumberFormatException e) { answer.setText("Illegal data for x."); return; // Break out of the actionPerformed method. } /* Get a number from yInput in the same way. */ try { String yStr = yInput.getText(); Double d = new Double(yStr); y = d.doubleValue(); } catch (NumberFormatException e) { answer.setText("Illegal data for y."); return; // Break out of the actionPerformed method. } /* 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 this applet can be found in the file SimpleCalculator.java. (It contains very little in addition to the two methods shown above.)
As a second example, let's look more briefly at another applet. In this example, the user manipulates three scrollbars to set the red, green, and blue levels of a color. The value of each color level is displayed in a label, 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 Panel that uses another GridLayout, which contains three Scrollbars. The second also uses a GridLayout to contain three Labels. The third column contains the colored rectangle. The component in this column is a Canvas. The displayed color is the background color of the canvas. When the user changes the color, the background color of the canvas is changed and the canvas is repainted. This is one of the few cases where an object of type Canvas is used, rather than an object belonging to a subclass of the Canvas class.
When the user changes the value on a scrollbar, an event of type AdjustmentEvent is generated. In order to respond to such events, the applet implements the AdjustmentListener interface, which specifies the method "public void adjustmentValueChanged(AdjustmentEvent evt)". The applet registers itself to listen for adjustment events from each scrollbar. The applet has instance variables to refer to the scrollbars, the labels, and the canvas. Let's look at the code from the init() method for setting up one of the scrollbars, redScroll:
redScroll = new Scrollbar(Scrollbar.HORIZONTAL, 0, 10, 0, 265); redScroll.setBackground(Color.lightGray); redScroll.addAdjustmentListener(this);The first line constructs a horizontal scrollbar whose initial value is 0. The entire length of the scroll bar represents numbers between 0 and 265, as specified by the last two parameters in the constructor. However, the tab of the scrollbar takes up 10 units, as specified in the third parameter, so the value of the scrollbar is actually restricted to the range from 0 to 255. These are the possible values of a color level. In the second line, the background color of the scrollbar is set. On some platforms, all scrollbars are the same color and this command is ignored. On other platforms, every component inherits its color from its container, and this can look unattractive. The third line registers the applet ("this") to listen for adjustment events from the scrollbar.
In the adjustmentValueChanged() method, the applet must respond to the fact that the user has changed the value of one of the scroll bars. The response is to read the values of all the scrollbars, set the labels to display those values, and change the color displayed by the canvas. (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 adjustmentValueChanged(AdjustmentEvent evt) { int r = redScroll.getValue(); int g = greenScroll.getValue(); int b = blueScroll.getValue(); redLabel.setText(" R = " + r); greenLabel.setText(" G = " + g); blueLabel.setText(" B = " + b); colorCanvas.setBackground(new Color(r,g,b)); colorCanvas.repaint(); // Redraw the canvas in its new color. } // end adjustmentValueChangedThe complete source code can be found in the file RGBColorChooser.java.
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 Canvas class. A Canvas 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 display can be done with a Label, and I can define my StopWatch component as a subclass of the Label 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 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.*; import java.awt.event.*; public class StopWatch extends Label 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. First, call the constructor from // the superclass, Label. Then, set the component to // listen for mouse clicks on itself. super(" Click to start timer. ", Label.CENTER); addMouseListener(this); } public void mousePressed(MouseEvent evt) { // React when user presses the mouse. // Start the timer or stop it if it is already running. 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 StopWatchDon't forget that since StopWatch is a subclass of Label, you can do anything with a StopWatch that you can do with a Label. You can add it to a container. You can set its font, foreground color, and background color. You can even set the text that it displays (although this would interfere with its stopwatch function).
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 Label 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 Canvas. It has a constructor that specifies the text to be displayed and a setText() method that changes the displayed text. The paint() 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 canvas. The text is drawn to the canvas, in the usual way. Then the canvas is copied to the screen with the command:
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.*; public class MirrorLabel extends Canvas { // Constructor and methods meant for use public use. public MirrorLabel(String text) { // Construct a MirrorLabel to display the specified text. this.text = text; } public void setText(String text) { // Change the displayed text. Call invalidate so that // its size will be computed if its container is validated. this.text = text; invalidate(); 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 canvas holding // the non-reversed text. private int widthOfOSC, heightOfOSC; // Size of off-screen canvas. public void update(Graphics g) { // Redefine update so that it calls paint without erasing. paint(g); } public void paint(Graphics g) { // The paint method makes a new OSC, if necessary. It writes // a non-reversed copy of the string to the OSC, then reverses // the OSC as it copies it to the screen. 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, this); } public Dimension getMinimumSize() { return getPreferredSize(); } 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 MirrorLabelThis 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. The size is not always respected. For example, in a GridLayout, every component is sized to fit the available space. However, in some cases the preferred size is essential. For example, the height of a component in the North or South position of a BorderLayout is given by the component's preferred size. Java's standard GUI components already define a getPreferredSize() method. But when you define a component as a subclass of Canvas, you should include a preferred size method. (You are also supposed to include a getMinimumSize() method, but I don't know of any case where the minimum size is actually respected.)
Here is an applet that demonstrates a MirrorLabel and a StopWatch component. The applet uses a FlowLayout, so the components are not arranged very neatly. The applet also contains two buttons, which are there to illustrate another fine point of programming with components. (Don't forget to try the StopWatch!)
If you click the button labeled "Change Text in this Applet", the text in all the components will be changed. However, you will notice that the components don't change size. That won't happen until you click the other button. Here's how it works: When you click the button labeled "Validate" or "Do Validation", the applet's validate() method is called. The validate() method computes new sizes for components in the applet and lays out the applet again. However, it only computes a new size for a component if that component has been declared to be "invalid." This is done by calling the component's invalidate() method. If you look at the source code for MirrorLabel, you'll see that the setText() method calls invalidate(). This means that when the text in a MirrorLabel is changed, the MirrorLable is marked as being invalid. The next time the applet is validated, the size of the MirrorLabel will be changed. The situation for Java's standard components is not completely clear. On my computer, the Buttons change size, but the StopWatch does not. The best programming style requires that when you make a change to a component that might require a change in size, you should call the component's invalidate() method and call the validate() method of the container that contains the component. (In practice, I rarely do this, and only after I see a problem when I run the program.)
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. If comp is a component, then the statement
comp.setBounds(x, y, width, height);puts the top left corner 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 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 canvas 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() { setLayout(null); // I will do the layout myself! setBackground(new Color(0,150,0)); // Dark green background. /* Create the components and add them to the applet. If you don't add them to the applet, they won't appear, even if you set their bounds! */ board = new SimpleCheckerboardCanvas(); add(board); newGameButton = new Button("New Game"); newGameButton.setBackground(Color.lightGray); newGameButton.addActionListener(this); add(newGameButton); resignButton = new Button("Resign"); resignButton.setBackground(Color.lightGray); resignButton.addActionListener(this); add(resignButton); message = new Label("Click \"New Game\" to begin a game.", Label.CENTER); message.setForeground(Color.green); message.setFont(new Font("Serif", Font.BOLD, 14)); add(message); /* Set the position and size of each component by calling its setBounds() method. It is assumed that this applet is 330 pixels wide and 240 pixels high. */ board.setBounds(20,20,164,164); newGameButton.setBounds(210, 60, 100, 30); resignButton.setBounds(210, 120, 100, 30); message.setBounds(0, 200, 330, 30); }It's 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. One way to do this is by listening for ComponentEvents from the container and doing the computations in the componentResized() method. 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.
[ Next Section | Previous Section | Chapter Index | Main Index ]