[ Exercises | Chapter Index | Main Index ]

Solution for Programming Exercise 6.7


This page contains a sample solution to one of the exercises from Introduction to Programming Using Java.


Exercise 6.7:

Exercise 5.2 involved a class, StatCalc.java, that could compute some statistics of a set of numbers. Write a GUI program that uses the StatCalc class to compute and display statistics of numbers entered by the user. The panel will have an instance variable of type StatCalc that does the computations. The panel should include a JTextField where the user enters a number. It should have four labels that display four statistics for the numbers that have been entered: the number of numbers, the sum, the mean, and the standard deviation. Every time the user enters a new number, the statistics displayed on the labels should change. The user enters a number by typing it into the JTextField and pressing return. There should be a "Clear" button that clears out all the data. This means creating a new StatCalc object and resetting the displays on the labels. My panel also has an "Enter" button that does the same thing as pressing the return key in the JTextField. (Recall that a JTextField generates an ActionEvent when the user presses return, so your panel should register itself to listen for ActionEvents from the JTextField as well as the buttons.) Here is a picture of my solution to this problem:

screenshot from StatCalcGUI program


Discussion

In my solution, I used four labels to display results and another label at the top of the panel to display a message to the user. Aside from these labels, one row of the panel holds three other components: a JTextField and two JButtons. The panel uses a GridLayout with six rows. Five of the rows hold JLabels. The other row contains a JPanel that holds the JTextField and JButtons. This JPanel uses a GridLayout with three columns and just one row.

The constructor creates and lays out the components. Since I want the program to look nice, I set a background color and a foreground color for most of the components. I set the labels to be opaque, to make sure that the background of each label will actually be filled in with the label's background color. After looking at my first attempt, I decided to use a Monospaced font for the display labels. In a Monospaced font, all the characters are the same size. This makes it possible to line up the output values vertically by putting the same number of characters in each label. To make it easy to play with the colors and fonts, I declared three named constants

final static Color labelBG = new Color(240,225,200);  // beige
final static Color labelFG = new Color(180,0,0);  // dark red
final static Font labelFont = new Font("Monospaced", Font.PLAIN, 12);

I could then make one of the labels, such as countLabel, with the commands:

countLabel = new JLabel("Number of Entries:   0");
countLabel.setBackground(labelBG);
countLabel.setForeground(labelFG);
countLabel.setOpaque(true);
countLabel.setFont(labelFont);

However, since there are four labels to create, I wrote a subroutine to create a display label to show a given string:

/**
 * A utility routine for creating the labels that are used
 * for display.  This routine is called by the constructor.
 * @param text The text to show on the label.
 */
private JLabel makeLabel(String text) {
   JLabel label = new JLabel(text);
   label.setBackground(labelBG);
   label.setForeground(labelFG);
   label.setOpaque(true);
   label.setFont(labelFont);
   return label;
}

Then in the constructor, the labels can be created with four lines, instead of 16:

countLabel =   makeLabel(" Number of Entries:  0");
sumLabel =     makeLabel(" Sum:                0.0");
meanLabel =    makeLabel(" Average:            undefined");
standevLabel = makeLabel(" Standard Deviation: undefined");

Utility routines like makeLabel() are very commonly used when there are a lot of similar components to create. Note that when the labels are first created, the text on the labels is appropriate for a dataset that contains zero elements. In particular, if there are no data, the average and standard deviation are undefined.

The panel registers itself to listen for action events from the JTextField and from the JButtons. In the actionPerformed() method, the function evt.getSource() is called to find the Object that generated the event. This will be either the numberInput box, the enterButton, or the clearButton. The source of the event is checked to decide how to respond. (This is an alternative to checking the event's action command.)

If the user clicked the "Clear" button, the response is to create a new StatCalc object and to reset the display labels to reflect the fact that there is no data in the dataset. It's important to understand the effect of the command "stats = new StatCalc();". The panel will continue to use the same StatCalc variable, stats. However, now the variable refers to a new StatCalc object. The new object does not yet have any data in its dataset. The next time the user enters a number, the dataset will get its first value. Always keep in mind the difference between variables and objects. Also, keep in mind that you have to think in terms of changing the state of the panel in response to events. I change the panel's state by starting to use a new StatCalc object, and the display labels are changed to keep them consistent with the new state.

When the user clicks the "Enter" button or presses return in the JTextField, we have to get the user's input and add it to the StatCalc object. This will cause the values of the four statistics to change. Since the state has changed, we have to change the display labels to match the new state. The code for getting the user's number from the input box includes a check to make sure that the user's input is a legal number. If the input is not legal, then I show an error message in the JLabel named message and return from the actionPerformed() method without entering any new data:

double num;  // The user's number.
try {
   num = Double.parseDouble(numberInput.getText());
}
catch (NumberFormatException e) {
       // The user's entry is not a legal number.  
       // Put an error message in the message label
       // and return without entering a number.
   message.setText("\"" + numberInput.getText() + 
                       "\" is not a legal number.");
   numberInput.selectAll();
   numberInput.requestFocus();
   return;
}

The commands "numberInput.selectAll();" and "numberInput.requestFocus();" are there as a convenience for the user. The first command, which was not covered in this chapter, selects all the text in the number input box. The second command gives the input focus to the input box. That way, the user can just start typing the next number, without having to click on the input box or erase the content of the box. (Since the contents of the box are selected, they will disappear automatically when the user starts typing, to be replaced with the new input. A surprising number of people have never learned that text selections work this way.)

Once we have the user's number, the command "stats.enter(num);" adds the number num into the dataset. The statistics about the data set can be obtained by calling the functions stats.getCount(), stats.getSum(), stats.getMean(), and stats.getStandardDeviation(). This information can be found by reading the source code for the StatCalc class. These values are used on the labels that display the statistics. For example,

countLabel.setText(" Number of Entries:  " + stats.getCount());

Returning to the topic of variables versus objects, a common novice mistake would be to try to change the text that is displayed on the label by saying

countLabel = new JLabel(" Number of Entries:  " + stats.getCount());  // WRONG

since that type of command is what was used to set the text in the first place. But this statement has no effect on what is displayed on the screen. The reason why this doesn't work is instructive: The assignment command creates a new JLabel and sets countLabel to refer to that new object. The new label does indeed have the desired text. But the new label has nothing to do with what's on the screen! The original label was created in the constructor and added to the panel. The panel has a reference to the original label, and that original label continues to appear on the screen even if countLabel now refers to a new label. The correct command, using countLabel.setText(), modifies the original label, which is what is shown on the screen.

As a point of interest, I will mention another technique that has not been covered elsewhere in the book. You might want to limit the number of decimal places that are displayed in the statistics. You know how to do this in output to the command line, using System.out.printf or TextIO.putf, but we are not outputting the data here, just putting it into a String. The String class has a static method String.format() that solves this problem. String.format(fmtString,val,val,val,...) works similarly to System.out.prinf(fmtString,val,val,val,...), but instead of producing output, String.format just creates a string and returns it. To set the text of the label that displays the average, for example, you Might say:

meanLabel.setText( String.format(" Average:            %1.9g", stats.getMean()) );

I don't do this in my solution, but String.format can be very useful, especially in GUI programs.


The Solution

 
import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
 
/**
 * In this panel, the user enters numbers in a text field box.
 * After entering each number, the user presses return (or clicks
 * on a button).  Some statistics are displayed about all the
 * numbers that the user has entered.
 */
public class StatCalcGUI extends JPanel implements ActionListener {
 
    /**
     * A main routine allows this class to be run as an application.
     */
    public static void main(String[] args) {
        JFrame window = new JFrame("Stat Calc");
        StatCalcGUI content = new StatCalcGUI();
        window.setContentPane(content);
        window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        window.setLocation(120,70);
        window.setSize(350,200);
        window.setVisible(true);
    }

    //---------------------------------------------------------------------

    final static Color labelBG = new Color(240,225,200);  // For creating labels
    final static Color labelFG = new Color(180,0,0);
    final static Font labelFont = new Font("Monospaced", Font.PLAIN, 12);
    
    private JLabel countLabel;    // A label for displaying the number of numbers.
    private JLabel sumLabel;      // A label for displaying the sum of the numbers.
    private JLabel meanLabel;     // A label for displaying the average.
    private JLabel standevLabel;  // A label for displaying the standard deviation.
    
    private JLabel message;  // A message at the top of the panel.  It will
                             //   show an error message if the user's input is
                             //   not a legal number.  Otherwise, it just tells
                             //   the user to enter a number and press return.
    
    private JButton enterButton;   // A button the user can press to enter a number.
                                   //    This is an alternative to pressing return.
    private JButton clearButton;   // A button that clears all the data that the
                                   //    user has entered.
    
    private JTextField numberInput;  // The input box where the user enters numbers.
    
    private StatCalc stats;  // An object that keeps track of the statistics
                             //   for all the numbers that have been entered.
 
    /**
     * The constructor creates the objects used by the panel.  The panel
     * will listen for action events from the buttons and from the text
     * field.  (A JTextField generates an ActionEvent when the user presses 
     * return while typing in the text field.)
     */
    public StatCalcGUI() {
    
       stats = new StatCalc();
       
       numberInput = new JTextField();
       numberInput.setBackground(Color.WHITE);
       numberInput.addActionListener(this);
       
       enterButton = new JButton("Enter");
       enterButton.addActionListener(this);
       
       clearButton = new JButton("Clear");
       clearButton.addActionListener(this);
       
       JPanel inputPanel = new JPanel();  // A panel that will hold the
                                          //   JTextField and JButtons.
       inputPanel.setLayout( new GridLayout(1,3) );
       inputPanel.add(numberInput);
       inputPanel.add(enterButton);
       inputPanel.add(clearButton);
       
       countLabel =   makeLabel(" Number of Entries:  0");
       sumLabel =     makeLabel(" Sum:                0.0");
       meanLabel =    makeLabel(" Average:            undefined");
       standevLabel = makeLabel(" Standard Deviation: undefined");
       
       message = new JLabel("Enter a number, press return:",
                                      JLabel.CENTER);
       message.setBackground(labelBG);
       message.setForeground(Color.BLUE);
       message.setOpaque(true);
       message.setFont(new Font("SansSerif", Font.BOLD, 12));
       
       /* Use a GridLayout with 6 rows and 1 column, and add all the
          components that have been created to the panel. */
       
       setBackground(Color.BLUE);
       setLayout( new GridLayout(6,1,2,2) );
       add(message);
       add(inputPanel);
       add(countLabel);
       add(sumLabel);
       add(meanLabel);
       add(standevLabel);

       /* Add a blue border around the panel. */

       setBorder( BorderFactory.createLineBorder(Color.BLUE, 2) );
       
    } // end constructor

    
    /**
     * A utility routine for creating the labels that are used
     * for display.  This routine is called by the constructor.
     * @param text The text to show on the label.
     */
    private JLabel makeLabel(String text) {
       JLabel label = new JLabel(text);
       label.setBackground(labelBG);
       label.setForeground(labelFG);
       label.setFont(labelFont);
       label.setOpaque(true);
       return label;
    }   
    
    
    /**
     * This is called when the user clicks one of the buttons or
     * presses return in the input box.  The response to clicking
     * on the Enter button is the same as the response to pressing
     * return in the JTextField.
     */
    public void actionPerformed(ActionEvent evt) {
    
       Object source = evt.getSource();  // Object that generated 
                                         //   the action event.
       
       if (source == clearButton) {
             // Handle the clear button by starting with a new,
             // empty StatCalc object and resetting the display
             // labels to show no data entered.  The TextField
             // is also made empty.
          stats = new StatCalc();
          countLabel.setText(" Number of Entries:  0");
          sumLabel.setText(" Sum:                0.0");
          meanLabel.setText(" Average:            undefined");
          standevLabel.setText(" Standard Deviation: undefined");
          numberInput.setText("");
       }
       else if (source == enterButton || source == numberInput) {
             // Get the user's number, enter it into the StatCalc
             // object, and set the display on the display labels
             // to reflect the new data.
          double num;  // The user's number.
          try {
             num = Double.parseDouble(numberInput.getText());
          }
          catch (NumberFormatException e) {
                 // The user's entry is not a legal number.  
                 // Put an error message in the message label 
                 // and return without entering a number.
             message.setText("\"" + numberInput.getText() + 
                                  "\" is not a legal number.");
             numberInput.selectAll();
             numberInput.requestFocus();
             return;
          }
          stats.enter(num);
          countLabel.setText(" Number of Entries:  " + stats.getCount());
          sumLabel.setText(" Sum:                " + stats.getSum());
          meanLabel.setText(" Average:            " + stats.getMean());
          standevLabel.setText(" Standard Deviation: " 
                                                   + stats.getStandardDeviation());
       }
       
       /* Set the message label back to its normal text, in case it has
          been showing an error message.  For the user's convenience,
          select the text in the TextField and give the input focus
          to the text field.  That way the user can just start typing
          the next number. */
       
       message.setText("Enter a number, press return:");
       numberInput.selectAll();
       numberInput.requestFocus();
       
    }  // end ActionPerformed
 
}  // end StatsCalcGUI

[ Exercises | Chapter Index | Main Index ]