Section 7.4
Programming with Components


THE TWO PREVIOUS SECTIONS described some raw materials that are available in the form of layout managers and standard GUI components. This section presents some programming examples that make use of those raw materials.


An Example with Text Input Boxes

As a first example, let's look at a simple calculator applet. This example demonstrates typical uses of JTextFields, JButtons, and JLabels, and it uses several layout managers. In the applet, you can enter two real numbers in the text-input boxes and click on one of the buttons labeled "+", "-", "*", and "/". The corresponding operation is performed on the numbers, and the result is displayed in a JLabel at the bottom of the applet. If one of the input boxes contains an illegal entry -- a word instead of a number, for example -- an error message is displayed in the JLabel.

(Applet "SimpleCalculator" would be displayed here
if Java were available.)

When designing an applet such as this one, you should start by asking yourself questions like: How will the user interact with the applet? What components will I need in order to support that interaction? What events can be generated by user actions, and what will the applet do in response? What data will I have to keep in instance variables to keep track of the state of the applet? What information do I want to display to the user? Once you have answered these questions, you can decide how to lay out the components. You might want to draw the layout on paper. At that point, you are ready to begin writing the program.

In the simple calculator applet, the user types in two numbers and clicks a button. The computer responds by doing a computation with the user's numbers and displaying the result. The program uses two JTextField components to get the user's input. The JTextFields do a lot of work on their own. They respond to mouse, focus, and keyboard events. They show blinking cursors when they are active. They collect and display the characters that the user types. The program only has to do three things with each JTextField: Create it, add it to the applet, and get the text that the user has input by calling its getText() method. The first two things are done in the applet's init() method. The third -- getting the user's input from the input boxes -- is done in an actionPerformed() method, which responds when the user clicks on one of the buttons. When a component is created in one method and used in another, as the input boxes are in this case, we need an instance variable to refer to it. In this case, I use two instance variables, xInput and yInput, of type JTextField to refer to the input boxes. The JLabel that is used to display the result is treated similarly: A JLabel is created and added to the applet in the init() method. When an answer is computed in the actionPerformed() method, the JLabel's setText() method is used to display the answer in the label. I use an instance variable named answer, of type JLabel, to refer to the label.

The applet also has four JButtons and two more JLabels. (The two extra labels display the strings "x =" and "y =".) I use local variables rather than instance variables for these components because I don't need to refer to them outside the init() method.

The applet as a whole uses a GridLayout with four rows and one column. The bottom row is occupied by the JLabel, answer. The other three rows each contain several components. Each of the first three rows is filled by a JPanel, which has its own layout manager and contains several components. The row that contains the four buttons is a JPanel which uses a GridLayout with one row and four columns. The JPanels that contain the input boxes use BorderLayouts. The input box occupies the Center position of the BorderLayout, with a JLabel on the West. (This example shows that BorderLayouts are more versatile than it might appear at first.) All the work of setting up the applet is done in its init() method:

     public void init() {

        /* Since I will be using the content pane several times,
           declare a variable to represent it.  Note that the
           return type of getContentPane() is Container. */

        Container content = getContentPane();

        /* Assign a background color to the applet and its
           content panel.  This color will show through between
           components and around the edges of the applet. */

        setBackground(Color.gray);
        content.setBackground(Color.gray);

        /* Create the input boxes, and make sure that their background
           color is white.  (They are likely to be white by default.) */

        xInput = new JTextField("0");
        xInput.setBackground(Color.white);
        yInput = new JTextField("0");
        yInput.setBackground(Color.white);

        /* Create panels to hold the input boxes and labels "x =" and
           "y = ".  By using a BorderLayout with the JTextField in the
           Center position, the JTextField will take up all the space
           left after the label is given its preferred size. */

        JPanel xPanel = new JPanel();
        xPanel.setLayout(new BorderLayout());
        xPanel.add( new Label(" x = "), BorderLayout.WEST );
        xPanel.add(xInput, BorderLayout.CENTER);

        JPanel yPanel = new JPanel();
        yPanel.setLayout(new BorderLayout());
        yPanel.add( new Label(" y = "), BorderLayout.WEST );
        yPanel.add(yInput, BorderLayout.CENTER);

        /* Create a panel to hold the four buttons for the four
           operations.  A GridLayout is used so that the buttons
           will all have the same size and will fill the panel. 
           The applet serves as ActionListener for the buttons. */

        JPanel buttonPanel = new JPanel();
        buttonPanel.setLayout(new GridLayout(1,4));

        JButton plus = new JButton("+");
        plus.addActionListener(this);
        buttonPanel.add(plus);

        JButton minus = new JButton("-");
        minus.addActionListener(this);
        buttonPanel.add(minus);

        JButton times = new JButton("*");
        times.addActionListener(this);
        buttonPanel.add(times);

        JButton divide = new JButton("/");
        divide.addActionListener(this);
        buttonPanel.add(divide);

        /* Create the label for displaying the answer in red
           on a white background.  The label is set to be
           "opaque" to make sure that the white background
           is painted. */

        answer = new JLabel("x + y = 0", JLabel.CENTER);
        answer.setForeground(Color.red);
        answer.setBackground(Color.white);
        answer.setOpaque(true);

        /* Set up the layout for the applet, using a GridLayout,
            and add all the components that have been created. */

        content.setLayout(new GridLayout(4,1,2,2));
        content.add(xPanel);
        content.add(yPanel);
        content.add(buttonPanel);
        content.add(answer);

        /* Try to give the input focus to xInput, which is the natural
           place for the user to start. */

        xInput.requestFocus();

     }  // end init()

The action of the applet takes place in the actionPerformed() method. The algorithm for this method is simple:

      get the number from the input box xInput
      get the number from the input box yInput
      get the action command (the name of the button)
      if the command is "+"
         add the numbers and display the result in the answer label
      else if the command is "-"
         subtract the numbers and display the result in the label
      else if the command is "*"
         multiply the numbers and display the result in the label
      else if the command is "/"
         divide the numbers and display the result in the label

There is only one problem with this. When we call xInput.getText() and yInput.getText() to get the contents of the input boxes, the results are Strings, not numbers. We need a method to convert a string such as "42.17" into the number that it represents. The standard class Double contains a static method, Double.parseDouble(String) for doing just that. So we can get the first number entered by the user with the commands: f

         String xStr = xInput.getText();
         x = Double.parseDouble(xStr);

where x is a variable of type double. Similarly, if we wanted to get an integer value from the string, xStr, we could use a static method in the standard Integer class: x = Integer.parseInt(xStr). This makes it easy to get numerical values from a JTextField, but one problem remains: We can't be sure that the user has entered a string that represents a legal real number. We could ignore this problem and assume that a user who doesn't enter a valid input shouldn't expect to get an answer. However, a more friendly program would notice the error and display an error message to the user. This requires using a "try...catch" statement, which is not covered until Chapter 9 of this book. My program does in fact use a try...catch statement to handle errors, so you can get a preview of how it works. Here is the actionPerformed() method that responds when the user clicks on one of the buttons in the applet:

     public void actionPerformed(ActionEvent evt) {
             // When the user clicks a button, get the numbers
             // from the input boxes and perform the operation
             // indicated by the button.  Put the result in
             // the answer label.  If an error occurs, an
             // error message is put in the label.

        double x, y;  // The numbers from the input boxes.

        /* Get a number from the xInput JTextField.  Use 
           xInput.getText() to get its contents as a String.
           Convert this String to a double.  The try...catch
           statement will check for errors in the String.  If 
           the string is not a legal number, the error message
           "Illegal data for x." is put into the answer and
           the actionPerformed() method ends. */

        try {
           String xStr = xInput.getText();
           x = Double.parseDouble(xStr);
        }
        catch (NumberFormatException e) {
              // The string xStr is not a legal number.
           answer.setText("Illegal data for x.");
           return;
        }

        /* Get a number from yInput in the same way. */

        try {
           String yStr = yInput.getText();
           y = Double.parseDouble(yStr);
        }
        catch (NumberFormatException e) {
           answer.setText("Illegal data for y.");
           return;
        }

        /* Perform the operation based on the action command
           from the button.  Note that division by zero produces
           an error message. */

        String op = evt.getActionCommand();
        if (op.equals("+"))
           answer.setText( "x + y = " + (x+y) );
        else if (op.equals("-"))
           answer.setText( "x - y = " + (x-y) );
        else if (op.equals("*"))
           answer.setText( "x * y = " + (x*y) );
        else if (op.equals("/")) {
           if (y == 0)
              answer.setText("Can't divide by zero!");
           else
              answer.setText( "x / y = " + (x/y) );
        }

     } // end actionPerformed()

The complete source code for the applet can be found in the file SimpleCalculator.java. (It contains very little in addition to the two methods shown above.)


An Example with Sliders

As a second example, let's look more briefly at another applet. In this example, the user manipulates three JSliders to set the red, green, and blue levels of a color. The value of each color level is displayed in a JLabel, and the color itself is displayed in a large rectangle:

(Applet "RGBColorChooser" would be displayed here
if Java were available.)

The layout manager for the applet is a GridLayout with one row and three columns. The first column contains a JPanel, which in turn contains the JSliders. This panel uses another GridLayout, with three rows and one column. The second column, which contains the JLabels, is similar. The third column contains the colored rectangle. The component in this column is a JPanel which contains no components. The displayed color is the background color of the JPanel. When the user changes the color, the background color of the panel is changed and the panel is repainted to show the new color. This is one of the few cases where an object of type JPanel is used without either making a subclass or adding components to it.

When the user changes the value on a JSlider, an event of type ChangeEvent is generated. In order to respond to such events, the applet implements the ChangeListener interface, which specifies the method "public void stateChanged(ChangeEvent evt)". The applet registers itself to listen for change events from each slider. The applet has instance variables to refer to the sliders, the labels, and the color patch. Note that since the ChangeEvent and ChangeListener classes are defined in the package javax.swing.event, the command "import javax.swing.event.*;" is added to the beginning of the program.

Let's look at the code from the init() method for setting up one of the JSliders, redSlider:

            redSlider = new JSlider(0, 255, 0);
            redSlider.addChangeListener(this);

The first line constructs a horizontal slider whose value can range from 0 to 255. These are the possible values of the red level in a color. The initial value of the slider, which is specified by the third parameter to the constructor, is 0. The second line registers the applet ("this") to listen for change events from the slider. The other two sliders are initialized in a similar way.

In the stateChanged() method, the applet must respond to the fact that the user has changed the value of one of the sliders. The response is to read the values of all the sliders, set the labels to display those values, and change the color displayed on the color patch. (This is slightly lazy programming, since only one of the labels actually needs to be changed. However, there is no rule against setting the text of a label to the same text that it is already displaying.)

     public void stateChanged(ChangeEvent evt) {
             // This is called when the user has changed the value on
             // one of the sliders.  All the sliders are checked,
             // the labels are set to display the correct values, and
             // the color patch is set to correspond to the new color.
         int r = redSlider.getValue();
         int g = greenSlider.getValue();
         int b = blueSlider.getValue();
         redLabel.setText(" R = " + r);
         greenLabel.setText(" G = " + g);
         blueLabel.setText(" B = " + b);
         colorPatch.setBackground(new Color(r,g,b));
     } // end stateChanged()

The complete source code can be found in the file RGBColorChooser.java.


Custom Component Examples

Java's standard component classes are often all you need to construct a user interface. Sometimes, however, you need a component that Java doesn't provide. In that case, you can write your own component class, building on one of the components that Java does provide. We've already done this, actually, every time we've written a subclass of the JPanel class to use as a drawing surface. A JPanel is a blank slate. By defining a subclass, you can make it show any picture you like, and you can program it to respond in any way to mouse and keyboard events. Sometimes, if you are lucky, you don't need such freedom, and you can build on one of Java's more sophisticated component classes.

For example, suppose I have a need for a "stopwatch" component. When the user clicks on the stopwatch, I want it to start timing. When the user clicks again, I want it to display the elapsed time since the first click. The textual display can be done with a JLabel, but we want a JLabel that can respond to mouse clicks. We can get this behavior by defining a StopWatch component as a subclass of the JLabel class. A StopWatch object will listen for mouse clicks on itself. The first time the user clicks, it will change its display to "Timing..." and remember the time when the click occurred. When the user clicks again, it will check the time again, and it will compute and display the elapsed time. (Of course, I don't necessarily have to define a subclass. I could use a regular label in my applet, set the applet to listen for mouse events on the label, and let the applet do the work of keeping track of the time and changing the text displayed on the label. However, by writing a new class, I have something that is reusable in other projects. I also have all the code involved in the stopwatch function collected together neatly in one place. For more complicated components, both of these considerations are very important.)

The StopWatch class is not very hard to write. I need an instance variable to record the time when the user started the stopwatch. Times in Java are measured in milliseconds and are stored in variables of type long (to allow for very large values). In the mousePressed() method, I need to know whether the timer is being started or stopped, so I need another instance variable to keep track of this aspect of the component's state. There is one more item of interest: How do I know what time the mouse was clicked? The method System.currentTimeMillis() returns the current time. But there can be some delay between the time the user clicks the mouse and the time when the mousePressed() routine is called. I don't want to know the current time. I want to know the exact time when the mouse was pressed. When I wrote the StopWatch class, this need sent me on a search in the Java documentation. I found that if evt is an object of type MouseEvent(), then the function evt.getWhen() returns the time when the event occurred. I call this function in the mousePressed() routine.

The complete StopWatch class is rather short:

     import java.awt.event.*;
     import javax.swing.*;

     public class StopWatch extends JLabel implements MouseListener {

        private long startTime;   // Start time of timer.
                                  //   (Time is measured in milliseconds.)

        private boolean running;  // True when the timer is running.

        public StopWatch() {
              // Constructor.
           super("  Click to start timer.  ", JLabel.CENTER);
           addMouseListener(this);
        }

        public void mousePressed(MouseEvent evt) {
               // React when user presses the mouse by
               // starting or stopping the timer.
           if (running == false) {
                 // Record the time and start the timer.
              running = true;
              startTime = evt.getWhen();  // Time when mouse was clicked.
              setText("Timing....");
           }
           else {
                 // Stop the timer.  Compute the elapsed time since the
                 // timer was started and display it.
              running = false;
              long endTime = evt.getWhen();
              double seconds = (endTime - startTime) / 1000.0;
              setText("Time: " + seconds + " sec.");
           }
        }

        public void mouseReleased(MouseEvent evt) { }
        public void mouseClicked(MouseEvent evt) { }
        public void mouseEntered(MouseEvent evt) { }
        public void mouseExited(MouseEvent evt) { }

     }  // end StopWatch

Don't forget that since StopWatch is a subclass of JLabel, you can do anything with a StopWatch that you can do with a JLabel. You can add it to a container. You can set its font, foreground color, and background color. You can set the text that it displays (although this would interfere with its stopwatch function). You can even add a Border if you want.

Let's look at one more example of defining a custom component. Suppose that -- for no good reason whatsoever -- I want a component that acts like a JLabel except that it displays its text in mirror-reversed form. Since no standard component does anything like this, the MirrorLabel class is defined as a subclass of JPanel. It has a constructor that specifies the text to be displayed and a setText() method that changes the displayed text. The paintComponent() method draws the text mirror-reversed, in the center of the component. This uses techniques discussed in Section 1. Information from a FontMetrics object is used to center the text in the component. The reversal is achieved by using an off-screen image. The text is drawn to the off-screen image, in the usual way. Then the image is copied to the screen with the following command, where OSC is the variable that refers to the off-screen image:

       g.drawImage(OSC, widthOfOSC, 0, 0, heightOfOSC,
                       0, 0, widthOfOSC, heightOfOSC, this);

This is the version of drawImage() that specifies corners of destination and source rectangles. The corner (0,0) in OSC is matched to the corner (widthOfOSC,0) on the screen, while (widthOfOSC,heightOfOSC) is matched to (0,heightOfOSC). This reverses the image left-to-right. Here is the complete class:

     import java.awt.*;
     import javax.swing.*;

     public class MirrorLabel extends JPanel {

        // Constructor and methods meant for use public use.

        public MirrorLabel(String text) {
              // Construct a MirrorLable to display the specified text.
           this.text = text;
        }

        public void setText(String text) {
              // Change the displayed text.  Call revalidate
              // so that the layout of its container can be
              // recomputed.
           this.text = text;
           revalidate();  // Tells container that size might have changed.
           repaint();
        }

        public String getText() {
              // Return the string that is displayed by this component.
           return text;
        }

        // Implementation.  Not meant for public use.

        private String text; // The text displayed by this component.

        private Image OSC;  
             // An off-screen image holding the non-reversed text.
             
        private int widthOfOSC, heightOfOSC;  
             // Current size of the off-screen image, if one exists.

        public void paintComponent(Graphics g) {
              // The paint method makes a new OSC, if necessary.  It writes
              // a non-reversed copy of the string to the the OSC, then
              // reverses the OSC as it copies it to the screen.  
              // (Note:  color or font might have changed since the 
              // last time paintComponent() was called, so I can't just
              // reuse the old image in the OSC.)
           if (OSC == null || getSize().width != widthOfOSC 
                                || getSize().height != heightOfOSC) {
               OSC = createImage(getSize().width, getSize().height);
               widthOfOSC = getSize().width;
               heightOfOSC = getSize().height;
            }
            Graphics OSG = OSC.getGraphics();
            OSG.setColor(getBackground());
            OSG.fillRect(0, 0, widthOfOSC, heightOfOSC);
            OSG.setColor(getForeground()); 
            OSG.setFont(getFont());
            FontMetrics fm = OSG.getFontMetrics(getFont());
            int x = (widthOfOSC - fm.stringWidth(text)) / 2;
            int y = (heightOfOSC + fm.getAscent() - fm.getDescent()) / 2;
            OSG.drawString(text, x, y);
            OSG.dispose();
            g.drawImage(OSC, widthOfOSC, 0, 0, heightOfOSC,
                            0, 0, widthOfOSC, heightOfOSC, null);
        } // end paintComponent()
        
        public Dimension getPreferredSize() {
               // Compute a preferred size that will hold the string plus 
               // a border of 5 pixels.
           FontMetrics fm = getFontMetrics(getFont());
           return new Dimension(fm.stringWidth(text) + 10, 
                                    fm.getAscent() + fm.getDescent() + 10);
        }

     }  // end class MirrorLabel

This class defines the method "public Dimension getPreferredSize()". This method is called by a layout manager when it wants to know how big the component would like to be. Standard components come with a way of computing a preferred size. For a custom component based on a JPanel, it's a good idea to provide a custom preferred size. As I mentioned in Section 1, every component has a method setPrefferedSize() that can be used to set the preferred size of the component. For our MirrorLabel component, however, the preferred size depends the font and the text of the component, and these can change from time to time. We need a way to compute a preferred size on demand, based on the current font and text. That's what we do by defining a getPreferredSize() method. The system calls this method when it wants to know the preferred size of the component. In response, we can compute the preferred size based on the current font and text.

The StopWatch and MirrorLabel class define components. Components don't stand on their own. You have to add them to an applet or other container. Here is an applet that demonstrates a MirrorLabel and a StopWatch component:

(Applet "ComponentTest" would be displayed here
if Java were available.)

The source code for this applet is in the file ComponentTest.java. The applet uses a FlowLayout, so the components are not arranged very neatly. The applet also contains a button, which is there to illustrate another fine point of programming with components. If you click the button labeled "Change Text in this Applet", the text in all the components will be changed. You can also click on the "Timing..." label to start and stop the StopWatch. When you do any of these things, you will notice that the components will be rearranged to take the new sizes into account. This is known as "validating" the container. This is done automatically when a standard component changes in some way that requires a change in preferred size or location. This may or may not be the behavior that you want. (Validation doesn't always cause as much disruption as it does in this applet. For example, in a GridLayout, where all the components are displayed at the same size, it will have no effect at all. I've chosen a FlowLayout for this example to make the effect more obvious.) A custom component such as MirrorLabel can call the revalidate() method to indicate that the container that contains the component should be validated. In the MirrorLabel class, revalidate() is called in the setText() method.


A Null Layout Example

As a final example, we'll look at an applet that does not use a layout manager. If you set the layout manager of a container to be null, then you assume complete responsibility for positioning and sizing the components in that container. For an applet, you can remove the layout manager with the command:

            getContentPane().setLayout(null);

If comp is any component, then the statement

            comp.setBounds(x, y, width, height);

puts the top left corner of the component at the point (x,y), measured in the coordinated system of the container that contains the component, and it sets the width and height of the component to the specified values. You should only set the bounds of a component if the container that contains it has a null layout manager. In a container that has a non-null layout manager, the layout manager is responsible for setting the bounds, and you should not interfere with its job.

Assuming that you have set the layout manager to null, you can call the setBounds() method any time you like. (You can even make a component that moves or changes size while the user is watching.) If you are writing an applet that has a known, fixed size, then you can set the bounds of each component in the applet's init() method. That's what done in the following applet, which contains four components: two buttons, a label, and a panel that displays a checkerboard pattern. This applet doesn't do anything useful. The buttons just change the text in the label.

(Applet "NullLayoutDemo" would be displayed here
if Java were available.)

In the init() method of this applet, the components are created and added to the applet. Then the setBounds() method of each component is called to set the size and position of the component:

     public void init() {

        getContentPane().setLayout(null);  // I will do the layout myself!

        getContentPane().setBackground(new Color(0,150,0));  
                                 // Set a dark green background.

        /* Create the components and add them to the content pane.  If you
           don't add them to the a container, they won't appear, even if
           you set their bounds! */

        board = new Checkerboard();  
                         // (Checkerboard is defined later in this class.)
        getContentPane().add(board);

        newGameButton = new JButton("New Game");
        newGameButton.addActionListener(this);
        getContentPane().add(newGameButton);

        resignButton = new JButton("Resign");
        resignButton.addActionListener(this);
        getContentPane().add(resignButton);

        message = new JLabel("Click \"New Game\" to begin a game.", 
                                                           JLabel.CENTER);
        message.setForeground( new Color(100,255,100) );
        message.setFont(new Font("Serif", Font.BOLD, 14));
        getContentPane().add(message);

        /* Set the position and size of each component by calling
           its setBounds() method. */

        board.setBounds(20,20,164,164);
        newGameButton.setBounds(210, 60, 120, 30);
        resignButton.setBounds(210, 120, 120, 30);
        message.setBounds(0, 200, 330, 30);

        /* Add a border to the content pane.  Since the return
           type of getContentPane() is Container, not JComponent,
           getContentPane() must be type-cast to a JComponent
           in order to call the setBorder() method.  Although I
           think the content pane is always, in fact, a JPanel,
           to be safe I test that the return value really is
           a JComponent. */

        if (getContentPane() instanceof JComponent) {
           ((JComponent)getContentPane()).setBorder(
                            BorderFactory.createEtchedBorder());
        }
     } // end init();
     

It's reasonably easy, in this case, to get an attractive layout. It's much more difficult to do your own layout if you want to allow for changes of size. In that case, you have to respond to changes in the container's size by recomputing the sizes and positions of all the components that it contains. If you want to respond to changes in a container's size, you can register an appropriate listener with the container. Any component generates an event of type ComponentEvent when its size changes (and also when it is moved, hidden, or shown). You can register a ComponentListener with the container and respond to size change events by recomputing the sizes and positions of all the components in the container. Consult a Java reference for more information about ComponentEvents. However, my real advice is that if you want to allow for changes in the container's size, try to find a layout manager to do the work for you.


[ Next Section | Previous Section | Chapter Index | Main Index ]