> ### 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`.

> Sorry, but your browser
> doesn't support Java.

> 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, answer

> 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. 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:

> Sorry, but your browser
> doesn't support Java.

> 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 adjustmentValueChanged

> The 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 StopWatch

> Don'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 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. 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`!)

> Sorry, but your browser
> doesn't support Java.

> 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.

> Sorry, but your browser
> doesn't support Java.

> 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 ]