Text I/O for GUIs
Polonius: What do you read, my lord? Hamlet: Words, words, words.
William Shakespeare, Hamlet
So far, we have made pretty window displays, but we do not yet have very versatile techniques for program input and output via GUIs, and input/output is the reason programmers want GUIs. As a programmer, you will want to use a GUI as the input/output part of your programs. Buttons give you some input and labels give you some output, but they are not as versatile as text input and output. In this section, we show you how to add text input and text output to your Swing GUIs.
Text Areas and Text Fields
The GUI in Display 10 has some text in the window but it is static text. Neither the user nor the program can change the text. If we are going to have text input from the user as part of a GUI, then we need some way for the user to enter text into the GUI. Display 14 contains a program that produces a GUI with a text area in which the user can write any text she or he wishes. That text is then saved as a memo that can be recalled later. In this simple example, the user is only allowed two memos, but that is good enough to illustrate how a text area is created and used. The area in the center, colored white, is an object of the class JTextArea. The user can type any sort of text into this JTextArea. If the user clicks the "Save Memo 1" button, the text is saved as memo 1. If the user clicks the "Save Memo 2" button, the text is saved as memo 2. Either memo can be called back up to the JTextArea by clicking one of the buttons "Get Memo 1" or "Get Memo 2". The JTextArea can be cleared by clicking the "Clear" button.
Display 14GUI with Text Area
import javax.swing.*; import java.awt.*; import java.awt.event.*; public class MemoSaver extends JFrame implements ActionListener { public static final int WIDTH = 600; public static final int HEIGHT = 300; public static final int LINES = 10; public static final int CHAR_PER_LINE = 40; private JTextArea theText; private String memo1 = "No Memo 1."; private String memo2 = "No Memo 2."; public MemoSaver() { setSize(WIDTH, HEIGHT); addWindowListener(new WindowDestroyer()); setTitle("Memo Saver"); Container contentPane = getContentPane(); contentPane.setLayout(new BorderLayout()); JPanel buttonPanel = new JPanel(); buttonPanel.setBackground(Color.white); buttonPanel.setLayout(new FlowLayout()); JButton memo1Button = new JButton("Save Memo 1"); memo1Button.addActionListener(this); buttonPanel.add(memo1Button); JButton memo2Button = new JButton("Save Memo 2"); memo2Button.addActionListener(this); buttonPanel.add(memo2Button); JButton clearButton = new JButton("Clear"); clearButton.addActionListener(this); buttonPanel.add(clearButton); JButton get1Button = new JButton("Get Memo 1"); get1Button.addActionListener(this); buttonPanel.add(get1Button); JButton get2Button = new JButton("Get Memo 2"); get2Button.addActionListener(this); buttonPanel.add(get2Button); contentPane.add(buttonPanel, BorderLayout.SOUTH); JPanel textPanel = new JPanel(); textPanel.setBackground(Color.blue); theText = new JTextArea(LINES, CHAR_PER_LINE); theText.setBackground(Color.white); textPanel.add(theText); contentPane.add(textPanel, BorderLayout.CENTER); } public void actionPerformed(ActionEvent e) { String actionCommand = e.getActionCommand(); if (actionCommand.equals("Save Memo 1")) memo1 = theText.getText(); else if (actionCommand.equals("Save Memo 2")) memo2 = theText.getText(); else if (actionCommand.equals("Clear")) theText.setText(""); else if (actionCommand.equals("Get Memo 1")) theText.setText(memo1); else if (actionCommand.equals("Get Memo 2")) theText.setText(memo2); else theText.setText("Error in memo interface"); } public static void main(String[] args) { MemoSaver guiMemo = new MemoSaver(); guiMemo.setVisible(true); } }
Now, let's look at how the program does this.
The buttons in Display 14 are put in a JPanel and then that JPanel is put in the JFrame just as they were in Display 13. The TextArea is set up with the following code, which appears in the constructor definition in Display 14:
JPanel textPanel = new JPanel(); textPanel.setBackground(Color.blue); theText = new JTextArea(LINES, CHAR_PER_LINE); theText.setBackground(Color.white); textPanel.add(theText); contentPane.add(textPanel, BorderLayout.CENTER);
The object theText is a member of the class JTextArea. Note the arguments in the constructor for JTextArea. The arguments LINES, which is a defined constant for 10, and CHAR_PER_LINE, which is a defined constant for 40, say that the text area will be 10 lines from top to bottom and will allow at least 40 characters per line. If you type in more text than will fit in a text area of the size specified by the two arguments to the constructor, then what happens can sometimes be unpredictable. So, it is best to make these numbers large enough to accommodate the largest expected text. (Problems of too much text can be handled by using the method setLineWrap, as described in the box at the end of this section.)
We are using a BorderLayout manager. The textPanel is added to the JFrame in the BorderLayout.CENTER position. The BorderLayout.CENTER position always takes up all the JFrame room that is left. This much of our program sets up the look of the GUI, but we still need to discuss how writing in the text area is handled by the GUI.
The two memos are stored in the two instance variables memo1 and memo2, both of type String. When the user clicks the "Save Memo 1" button, the text in the text area is saved as the value of the instance variable memo1. This is done by the following code, which consists of the first three lines in the definition of the actionPerformed method:
String actionCommand = e.getActionCommand(); if (actionCommand.equals("Save Memo 1")) memo1 = theText.getText();
Recall that the getActionCommand method returns the label of the button that is clicked. So, this says that when the button with label "Save Memo 1" is clicked, the following happens:
memo1 = theText.getText();
The method getText() returns the text that is written in the object theText of the class JTextArea. In other words, it returns what the user typed into the text area. If you read further in the definition of the method actionPerformed, you see that the second memo is stored in a similar way.
The memos are displayed in the text area and the text area is cleared with the following clauses from the definition of the method actionPerformed:
else if (actionCommand.equals("Clear")) theText.setText(""); else if (actionCommand.equals("Get Memo 1")) theText.setText(memo1); else if (actionCommand.equals("Get Memo 2")) theText.setText(memo2);
The method setText of the class JTextArea changes the text in the text area into what is given as the argument to setText. Two quotes with nothing in-between are given as the argument to setText in the second line of the preceding code. These two quotes denote the empty string and so produce a blank text area.
The class JTextField (which is not used in Display 14) is very similar to the class JtextArea, except that it displays only one line of text. It is useful for interfaces where the user gives only a few characters of text, like a single number, the name of a file, or the name of a person. The constructor for JTextField takes one argument, which is the number of characters visible in the text field. You can enter more characters, but only the specified number will be shown. So, be sure to make the text field one or two characters longer than the longest string you expect to have entered into the text field.
Both the classes JTextArea and JTextField can have some initial text contents specified when you create an object using new. The initial text is a string given as the first argument to the constructor. For example,
JTextField inputOutputField = new JTextField("Hello User.", 20);
The text field inputOutputField will have room for 20 visible characters and will start out containing the text "Hello User".
Both the classes JTextArea and JTextField have a constructor with no argument that sets the various parameters to some default values.
Programming Example: Labeling a Text Field
Sometimes you want a label for a text field. For example, suppose the GUI asks for a name and an identification number and expects the user to enter these in two text fields. In this case, the GUI needs to label the two text fields so that the user knows in which field to write the name and in which field to write the number. You can use an object of the class JLabel to label a text field or any other component in a Swing GUI.
Display 15 contains a program that shows how you can attach a label to a text field (or any other component). You put both the text field and the label in a panel. You can then add the entire panel to a container. In Display 15, the code shown in color does just that.
The program in Display 15 is just a demonstration program and does not do very much. If the user enters a name in the text field and clicks the "Test" button, then the GUI gives an evaluation of the name. However, all names receive the same evaluation, namely, "A very good name!" So, if the user enters a name and then clicks the "Test" button, the display will look like the GUI shown in Display 15.
Display 15Labeling a Text Field
import javax.swing.*; import java.awt.*; import java.awt.event.*; /************************************************* *Class to demonstrate placing a label on a text field. *************************************************/ public class LabelDemo extends JFrame implements ActionListener { public static final int WIDTH = 300; public static final int HEIGHT = 200; private JTextField name; public LabelDemo() { setTitle("Name Tester"); setSize(WIDTH, HEIGHT); addWindowListener(new WindowDestroyer()); Container content = getContentPane(); content.setLayout(new GridLayout(2, 1)); JPanel namePanel = new JPanel(); namePanel.setLayout(new BorderLayout()); namePanel.setBackground(Color.lightGray); name = new JTextField(20); namePanel.add(name, BorderLayout.SOUTH); JLabel nameLabel = new JLabel("Enter your name here:"); namePanel.add(nameLabel, BorderLayout.CENTER); content.add(namePanel); JPanel buttonPanel = new JPanel(); buttonPanel.setLayout(new FlowLayout()); JButton b = new JButton("Test"); b.addActionListener(this); buttonPanel.add(b); b = new JButton("Clear"); b.addActionListener(this); buttonPanel.add(b); content.add(buttonPanel); } public void actionPerformed(ActionEvent e) { if (e.getActionCommand().equals("Test")) name.setText("A very good name!"); else if (e.getActionCommand().equals("Clear")) name.setText(""); else name.setText("Error in window interface."); } public static void main(String[] args) { LabelDemo w = new LabelDemo(); w.setVisible(true); } }
Inputting and Outputting Numbers
When you want to input numbers using a GUI, your GUI must convert input text to numbers. For example, when you input the number 42 in a JTextArea or JTextField, your program will receive the string "42", not the number 42. Your program must convert the input string value "42" to the integer value 42. When you want to output numbers using a GUI constructed with Swing, you must convert numbers to a string and then output that string. For example, if you want to output the number 43, your program would convert the integer value 43 to the string value "43". With Swing, all typed input is string input, and all written output is string output.
Let's consider inputting numbers. Your program will need to convert a string, such as "42", to a number. The static method parseInt in the class Integer can accomplish the conversion. For example,
Integer.parseInt("42")
returns the integer 42.
If the number were written in a JTextField named inputOutputField, then you can recover the input string with the method getText. So inputOutputField.getText() would produce the input string. To change the input string to a number, you can use the following expression:
Integer.parseInt(inputOutputField.getText())
If there is any chance that the user might add extra white space before or after the input, you should add an invocation of the method trim to the string object inputOutputField.getText(). Thus, a more robust way to obtain the number that was input would be the following (which adds an invocation of the method trim):
Integer.parseInt(inputOutputField.getText().trim())
Once your program has this number, it can use it just like any other number. For example, to store this number in an int variable named n, the following assignment statement will work fine:
int n = Integer.parseInt(inputOutputField.getText().trim());
If you want to input numbers of type double, just use the class Double in place of the class Integer and the method parseDouble in place of the method parseInt. For example, to store the number in inputOutputField in a double variable named x, the following assignment statement will work fine:
double x = Double.parseDouble(inputOutputField.getText().trim());
You can also do the analogous thing with the classes Long and Float.
You should be able to understand and even write long expressions like
Integer.parseInt(stringObject.trim())
However, your code will be easier to read and easier to write if you define a method to express this as a simple method invocation. Here is one such method:
private static int stringToInt(String stringObject) { return Integer.parseInt(stringObject.trim()); }
Then an expression like
n = Integer.parseInt(inputOutputField.getText().trim());
can be expressed more clearly as
n = stringToInt(inputOutputField.getText());
If the method is to be merely a tool in some GUI class, then it should be declared as private, because it has no use outside of the class. Alternatively, it could be part of a utility class with a number of different useful functions. In that case, it would make more sense to make it public.
To send an output number to a JTextField or JTextArea of a GUI, you use the static method toString. For example, suppose you have the number 43 stored in the int variable sum. You can convert this 43 to the string "43" as follows:
Integer.toString(sum)
We used the class Integer when invoking toString because we were dealing with integers. If on the other hand, you have a variable total of type double and you want to convert the number in total to a string, you would use
Double.toString(total)
If you want the number in the int variable sum to appear in the JTextField named inputOutputField, then you use setText as follows:
inputOutputField.setText(Integer.toString(sum));
This produces the string for the integer. If you wanted a string for the value in the variable total of type double, you would instead use
inputOutputField.setText(Double.toString(total));
These techniques for inputting and outputting numbers are illustrated in the next case study.
Case Study: A GUI Adding Machine
In this case study, you will design a program that uses Swing to produce an adding machine program. The white text field initially contains the text "Numbers go here". The user can enter a number by dragging the mouse over the text in the white text field and then typing in the number so that the number typed in becomes the contents of the text field. When the user clicks the "Add" button, the number is added to a running total and the new total is displayed in the text field. The user can continue to add in more numbers.
long as she or he wants. To start over, the user clicks the "Reset" button, which makes the running sum equal to zero.
You decide that the adding machine will be an object of a class named Adder. You begin by making a list of the data needed to accomplish the adding machine computation and the Swing objects you will need to construct your class Adder. The user will enter a number in a text field, so you will need one instance variable of type JTextField. You also need a number to keep a running sum of the numbers entered so far. Because these numbers might contain a decimal point, you make this an instance variable of type double. Thus, you come up with the following instance variables for your class:
private JTextField inputOutputField; private double sum = 0;
You next make the following list of additional objects needed to construct an object of the class Adder:
As always, you need to be able to close the window and end your program. So, you decide to use an object of the class WindowDestroyer in the usual way.
textPanel: A panel to hold the text field inputOutputField.
addButton and resetButton: The two buttons.
buttonPanel: A panel to hold the two buttons.
A layout manager to arrange the buttons in their panel. You decide that this should be a FlowLayout manager.
A layout manager to place the text field in its panel. You decide that this should also be a FlowLayout manager.
A layout manager to arrange the panels in the window. You decide that this should be a BorderLayout manager
You need listeners to listen to the buttons. You decide to use the window itself as the only listener and have it respond to button events.
The class Adder itself will be a window class and so will be a derived class of the class JFrame. So, you know the class definition begins like the following:
public class Adder extends JFrame
When listing the objects you need, you decided that the window itself would be the listener for the button events. Button events are action events, so you need to make the window (that is, any object of the class Adder) an action listener. You do that as follows:
public class Adder extends JFrame implements ActionListener
Placing components in containers and initializing the components, colors, and such is best done in the constructor. So, you begin by doing the default constructor, which will be the only constructor. First, there are the routine details, namely, setting up the mechanisms for closing the window and placing a title in the title bar and setting the initial size of the window. So, you know the constructor starts something like
setTitle("Adding Machine"); addWindowListener(new WindowDestroyer()); setSize(WIDTH, HEIGHT);
You add the buttons to a button panel in the usual way:
JPanel buttonPanel = new JPanel(); buttonPanel.setBackground(Color.gray); buttonPanel.setLayout(new FlowLayout()); JButton addButton = new JButton("Add"); addButton.addActionListener(this); buttonPanel.add(addButton); JButton resetButton = new JButton("Reset"); resetButton.addActionListener(this); buttonPanel.add(resetButton);
In your outline of objects, you said that the window containing this panel would use the BorderLayout manager, and the specifications for the GUI say the buttons go on the bottom of the window, so you add the button panel as follows:
contentPane.add(buttonPanel, BorderLayout.SOUTH);In order to add a panel in this way, you need to give the content pane a BorderLayout manager as follows:
contentPane.setLayout(new BorderLayout());This should be executed before any components are added. So, you decide to place this line near the start of the constructor definition along with the other actions that set things like the initial size of the window.
You are tempted to add the text field directly to the content pane, but you try it and find that the text field is much larger than you would like. You then recall that with a border layout manager, a component is stretched to fill the entire region. So you decide to instead place the text field in a panel, and then place the panel in the content pane.
The text field is inserted into its panel in a manner similar to the way you added buttons to the button panel, and then the panel with the text field is inserted in the content pane. The only differences from the button case are that the text panel goes in the BorderLayout.CENTER, and you need to decide how many characters you will allow in the text field. Thirty characters seem plenty long enough for a number and some extra white space. So, you produce the following code:
JPanel textPanel = new JPanel(); textPanel.setBackground(Color.blue); textPanel.setLayout(new FlowLayout()); inputOutputField = new JTextField("Numbers go here.", 30); inputOutputField.setBackground(Color.white); textPanel.add(inputOutputField); contentPane.add(textPanel, BorderLayout.CENTER);
This completes your definition of the default constructor. The full definition is given in Display 16.
Display 16An Addition GUI
import javax.swing.*; import java.awt.*; import java.awt.event.*; /************************************** *GUI for totaling a series of numbers. **************************************/ public class Adder extends JFrame implements ActionListener { public static final int WIDTH = 400; public static final int HEIGHT = 200; private JTextField inputOutputField; private double sum = 0; public static void main(String[] args) { Adder guiAdder = new Adder(); guiAdder.setVisible(true); } public Adder() { setTitle("Adding Machine"); addWindowListener(new WindowDestroyer()); setSize(WIDTH, HEIGHT); Container contentPane = getContentPane(); contentPane.setLayout(new BorderLayout()); JPanel buttonPanel = new JPanel(); buttonPanel.setBackground(Color.gray); buttonPanel.setLayout(new FlowLayout()); JButton addButton = new JButton("Add"); addButton.addActionListener(this); buttonPanel.add(addButton); JButton resetButton = new JButton("Reset"); resetButton.addActionListener(this); buttonPanel.add(resetButton); contentPane.add(buttonPanel, BorderLayout.SOUTH); JPanel textPanel = new JPanel(); textPanel.setBackground(Color.blue); textPanel.setLayout(new FlowLayout()); inputOutputField = new JTextField("Numbers go here.", 30); inputOutputField.setBackground(Color.white); textPanel.add(inputOutputField); contentPane.add(textPanel, BorderLayout.CENTER); } public void actionPerformed(ActionEvent e) { if (e.getActionCommand().equals("Add")) { sum = sum + stringToDouble(inputOutputField.getText()); inputOutputField.setText(Double.toString(sum)); } else if (e.getActionCommand().equals("Reset")) { sum = 0; inputOutputField.setText("0.0"); } else inputOutputField.setText("Error in adder code."); } private static double stringToDouble(String stringObject) { return Double.parseDouble(stringObject.trim()); } }
As you can see from your work on the constructor, placing components in containers is fairly routine. Often, the best choice is the window itself, which is what you decided to do this time.
Handling events is not as routine as adding components to containers, but it does follow from a careful analysis of the problem specification (and perhaps a bit of inspiration). For this GUI, there are four basic things that can happen:
The user can click the close-window button to end the program.
The user can write a number in the text field.
The user can push the add button.
The user can push the reset button.
Closing the window by clicking the close-window button is handled by an object of the class WindowDestroyer, as in all our examples. That is routine.
When the user types some number in the text field, no action is required. In fact, you do not want any action as a result of just writing a number. After all, the user may decide the input was entered incorrectly and change it. You do not want any action until one of the two buttons is pushed. So, the two button-pushing events are the only events that still need to be handled.
Pushing a button is an action event, and an action event is handled by the method actionPerformed of the ActionListener. The window itself is the ActionListener, so the method actionPerformed is a method of the class Adder. The header of the method actionPerformed is determined for you:
public void actionPerformed(ActionEvent e)
You have no choice on the header, but you must decide what the method does. You produce the following pseudocode for the method actionPerformed:
if (e.getActionCommand().equals("Add")) { sum = sum + the number written in inputOutputField. Display the value of sum in inputOutputField. } else if (e.getActionCommand().equals("Reset")) { sum = 0; Display "0.0" in inputOutputField. }
You decide to use a private method to convert strings to numbers. You use the method stringToDouble given elsewhere in this book. That means the method stringToDouble will be a private helping method, and the pseudocode for the method actionPerformed can be refined to the following:
if (e.getActionCommand().equals("Add")) { sum = sum + stringToDouble(inputOutputField.getText()); Display the value of sum in inputOutputField. } else if (e.getActionCommand().equals("Reset")) { sum = 0; Display "0.0" in inputOutputField. }
The rest of the translation from pseudocode to Java is straightforward. You decide to add a final else clause to the nested if-else statement, just in case of an unexpected error. The final code you produce and the final definition of the class Adder is given in Display 16.
This is only a very simple adding machine, but it has the elements you need to create a GUI for a display equivalent to a complete hand-held calculator.