Java GUIs II

Adding a JButton and the wonderful world of LayoutManagers

One of the big lessons of last class is that GUI components are just objects - i.e. they are simply instances of classes, just like any other. For windows we used the class JFrame. The class for buttons is JButton. So you can create a new button with label "Button" with the statement:

JButton b = new JButton("Button");

Note that the label could have been "George" instead of "Button" and it would all be the same. There is a big difference between a JFrame and a JButton, however. A JFrame is a window that sits on the screen all by itself. But a JButton is a component that must be placed inside some other GUI container - like a JFrame or some other component that can act as a container for other GUI elements. So, for starters we'd like to add the JButton to a JFrame. However, now things get a bit complicated. Where in the JFrame window do we want the JButton to go? This question is outrageously subtle because there might be lots of components sitting in a container like a JFrame, and because windows can get resized or, perhaps, were never given a set size at all, and the programmer wants it to be "big enough".

To deal with this complication of where to place components that get added to containers like JFrames, the designers of Java's Swing library turned to OOP principles: let's make the positioner of components within the container be ... an object. Specifically, there is an interface named LayoutManager, and objects implementing this interface are used to position components. By default, JFrame's have LayoutManagers of type BorderLayout, which thinks of the screen being divided up into regions: North, East, South, West and Center. When you add a component to a JFrame, you specify which BorderLayout region you want it in. So if f is a JFrame you might say:

f.add(new JButton("Button"), BorderLayout.CENTER);
if you want a new button with label "Button" added to the Center region, and
f.add(new JButton("Button"), BorderLayout.EAST);
if you want it added to the East region. These regions automatically expand or shrink to fit the various components into their various regions.

BorderLayout's regions
add button BorderLayout.CENTER
add button BorderLayout.EAST

The funny thing is that by default if you only add one component, all other regions shrink to nothing, and the one component grows to fill the whole window. You can get a big button that way!

Adding several components: JLabel, JButton, JTextField

Now that we know about LayoutManagers - or at least BorderLayout - we can add several components to our GUI. Here we will use the following classes:

We'll add these classes to make the GUI pictured to the right, which will show up, but which won't react to user input. So the user can click the button and add text in the box, but nothing will happen. We'll do that next.

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

public class Ex0 {
  public static void main(String[] args) {
    // Create the Jframe (window) for our GUI
    JFrame f = new JFrame();

    f.setTitle("IC211 GUI Ex0");
    f.setLocation(100, 100);
    f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

    // Create GUI components
    JLabel  l    = new JLabel("hello");
    JButton b    = new JButton("change");
    JTextField t = new JTextField(20);

    // Add GUI components at specified locations
    f.add(l, BorderLayout.CENTER);
    f.add(b, BorderLayout.EAST);
    f.add(t, BorderLayout.SOUTH);

    // Adjust sizes to fit everything & make visilbe
    f.pack();
    f.setVisible(true);
  }
}

Reacting to user interactions with components

As we saw last class, the general model for reacting to user interactions with GUI components is that there is an interface with methods corresponding to the different kinds of actions that might occur involving a given kind of component (WindowListener in the case of JFrame), and you define a class that implements that interface (a "listener") where the code you give for each of the interface's methods defines what you want done in case that action occurs on the given component. You then "add" an instance of this class you've created to the component's list of listeners. Should an action occur on that component, it goes through its list of listener's and calls on each of them the method corresponding to the action that occurred.

In our simple GUI, we have a button that the user can click and a text field the user can type something in. A click is considered "the" action for a button, and pressing enter while the box has the focus is considered "the" action for a text field. So both components use a simple ActionListener interface, which looks like this basically:

public interface ActionListener {
  public void actionPerformed(ActionEvent e);
}
So a class that implements ActionListener is what you want, whether you want to react to a button being clicked or text being entered. In this example, we'll simply react to the button being clicked. When that happens, we'll take any text in the text field, and make it the new text in the label, erasing the text field in the process.

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

public class Ex1 {
  public static void main(String[] args) {
    // Create the Jframe (window) for our GUI
    JFrame f = new JFrame();

    f.setTitle("IC211 GUI Ex1");
    f.setLocation(100, 100);
    f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

    // Create GUI components
    JLabel  l    = new JLabel("hello");
    JButton b    = new JButton("change");
    JTextField t = new JTextField(20);

    // Create a new ButtonClickListener that ties the
    // text field t and the label l together, and add
    // it the the button b's list of listeners.
    b.addActionListener(new ButtonClickListener(t, l));

    // Add GUI components at specified locations
    f.add(l, BorderLayout.CENTER);
    f.add(b, BorderLayout.EAST);
    f.add(t, BorderLayout.SOUTH);

    // Adjust sizes to fit everything & make visilbe
    f.pack();
    f.setVisible(true);
  }
}
ButtonClickListener.java
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;

class ButtonClickListener implements ActionListener {
  private JTextField tf;
  private JLabel     lb;
  public ButtonClickListener(JTextField t,
                             JLabel     l) {
    tf = t;
    lb = l;
  }

  public void actionPerformed(ActionEvent e) {
    if (!tf.getText().equals("")) {
      lb.setText(tf.getText());
    }
    tf.setText("");
  }
}

Now, if a user hits "enter" while in the text field, that should change the label just like clicking the button would. So let's make that happen. We could actually add the same listener object to both the button and the text field and be done with it. However, let's do it in a slightly roundabout (but cool) way by using the JButton method doClick() which, when called, simulates the user having clicked the button. We'll make the ActionListener we add to the text field do that.

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

public class Ex2 {
  public static void main(String[] args) {
    // Create the Jframe (window) for our GUI
    JFrame f = new JFrame();

    f.setTitle("IC211 GUI Ex2");
    f.setLocation(100, 100);
    f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

    // Create GUI components
    JLabel  l    = new JLabel("hello");
    JButton b    = new JButton("change");
    JTextField t = new JTextField(20);

    // create and add listeners for both button and
    // text field
    b.addActionListener(new ButtonClickListener(t, l));
    t.addActionListener(new TFActionListener(b));

    // Add GUI components at specified locations
    f.add(l, BorderLayout.CENTER);
    f.add(b, BorderLayout.EAST);
    f.add(t, BorderLayout.SOUTH);

    // Adjust sizes to fit everything & make visilbe
    f.pack();
    f.setVisible(true);
  }
}
ButtonClickListener.java
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;

class ButtonClickListener implements ActionListener {
  JTextField tf;
  JLabel     lb;
  public ButtonClickListener(JTextField t,
                             JLabel     l) {
    tf = t;
    lb = l;
  }

  public void actionPerformed(ActionEvent e) {
    if (!tf.getText().equals("")) {
      lb.setText(tf.getText());
    }
    tf.setText("");
  }
}
TFActionListener.java
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;

class TFActionListener implements ActionListener {
  private JButton b;
  public TFActionListener(JButton b) {
    this.b = b;
  }

  public void actionPerformed(ActionEvent e) {
    b.doClick(); // "fakes" a button click on b
  }
}
Cute, eh?

A simple GUI application: Units Conversion

As a goal for the rest of this lesson, let's create a GUI that actually solves a problem. Let's make a units converter! We'd like it to look like this:

Now, an immediate problem is that the BorderLayout LayoutManager doesn't allow us to do this. So, moving forward, we're going to have to learn about a new LayoutManager (FlowLayout). Also, we're going to learn about a new component, JComboBox<T>, for drop-down list. Notice that this class uses generics, so that different kinds of objects can appear in the drop-down.

JPanel and the FlowLayout

JPanel is a class that provides a container that is not itself a window, but just a container that can be added to other containers (including JFrames). When you create a JPanel, you pass the constructor the LayoutManager to use, which means you can choose from among the many Java LayoutManagers. (See A Visual Guide to LayoutMangers.) If you add a JPanel with, for example, a FlowLayout LayoutManager, and it is the only component added to the JFrame, the JPanel expands to fill the whole frame, and you've effectively changed the LayoutManager from BorderLayout to FlowLayout.

JFrame f = new JFrame();
JPanel p = new JPanel(new FlowLayout());
f.add(p,BorderLayout.CENTER);

BorderLayout's regions
add JPanel BorderLayout.CENTER
after the add (assuming the JPanel is the only component added)

FlowLayout is nice sometimes. It just packs the components you add into the container in the order they are added. For out problem above, we'll just add the components and they'll be placed left-to-right in a single row, since they'll all fit nicely that way.

The nice, systematic solution

Creating and placing the GUI components is easy. The meat of the problem is reacting to user events: entering data into the "from" text field, or choosing a value from either of the combo boxes. For all three of these events we want to do the same thing: get the numerical value from the text field and the conversion factors for the units chosen in the combo boxes, do the conversion, and set the "to" text field to the newly computed value. There are different ways to do this, but the cleanest (though not shortest or easiest) is to create a new class, ConversionWindow, that is a JFrame with a little added functionality - specifically, the methods:

public double getFromValue();     // get the "from" value
public double getFromCF();        // get the conversion factor for the "from" units
public double getToCF();          // get the conversion factor for the "to" units
public void setToValue(double x); // set the "to" value to x

Why? Because this is precisely the functionality required by the Listener object that will be called upon to react to our various events - change in "from" value, change in "from" units, change in "to" units. Following this approach, our Listener object will need to 1) remember the ConversionWindow it belongs to, and 2) call on the above methods to make the conversion happen.

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

public class Ex3 {
  public static void main(String[] args) {
    ConverterWindow w = new ConverterWindow();

    w.setVisible(true);
  }
}
ConverterActionListener.java
import java.awt.event.*;

class ConverterActionListener implements ActionListener {
  private ConverterWindow cw;
  public ConverterActionListener(ConverterWindow cw) {
    this.cw = cw;
  }

  public void actionPerformed(ActionEvent e) {
    // Response to any action is to update toValue based
    // on the values of fromUnits, toUnits and fromValue.
    cw.setToValue(
      cw.getFromValue() * cw.getFromCF() / cw.getToCF()
      );
  }
}
ConverterWindow.java
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;

public class ConverterWindow extends JFrame {
  private JTextField fromValue;
  private JTextField toValue;
  private JComboBox<String> fromUnits;
  private JComboBox<String> toUnits;
  private final String[] units = {
    "feet", "inches", "meters", "centimeters"
  };
  private final double[] cfact = {
    1.0000, 1.0 / 12, 3.28084, 0.0328084
  };

  public ConverterWindow() {
    // Create the four interactive elements of the GUI
    fromValue = new JTextField("1.0", 10);
    toValue   = new JTextField("1.0", 10);
    fromUnits = new JComboBox<String>(units);
    toUnits   = new JComboBox<String>(units);
    toValue.setEditable(false);

    // Create panel with flow layout and add GUI elements
    JPanel dpanel = new JPanel(new FlowLayout());
    dpanel.add(new JLabel("from: "));
    dpanel.add(fromValue);
    dpanel.add(fromUnits);
    dpanel.add(new JLabel(" to: "));
    dpanel.add(toValue);
    dpanel.add(toUnits);

    // Create the ConverterActionListener and set it to listen
    // for any changes to the three editable elements of the GUI
    ActionListener a = new ConverterActionListener(this);
    fromValue.addActionListener(a);
    fromUnits.addActionListener(a);
    toUnits.addActionListener(a);

    // Add panel to the frame, and do some bookkeeping on frame
    this.add(dpanel);
    this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    this.pack();
  }

  public double getFromValue() {
    return Double.parseDouble(fromValue.getText());
  }

  public double getFromCF() {
    return getCF((String)fromUnits.getSelectedItem());
  }

  public double getToCF() {
    return getCF((String)toUnits.getSelectedItem());
  }

  public void setToValue(double x) {
    toValue.setText("" + Math.round(x * 10000) / 10000.0);
  }

  private double getCF(String u) {
    int i = 0;

    while (i < units.length && !units[i].equals(u)) {
      ++i;
    }

    return cfact[i];
  }
}

Or you could use non-static inner classes ...

We had to put a lot of thought and design and work into the previous version of the converter program in order to communicate between the ConverterWindow and the ConverterActionListener. A less principled, but shorter, approach is make the CovnerterActionListener a non-static inner class of ConverterWindow. This way, the CovnerterActionListener has unfettered direct access to all the private fields of the ConverterWindow. Of course this doesn't separate interface and implementation ...

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

public class ConverterWindow extends JFrame {
  private JTextField fromValue;
  private JTextField toValue;
  private JComboBox<String> fromUnits;
  private JComboBox<String> toUnits;
  private final String[] units = {
    "feet", "inches", "meters", "centimeters"
  };
  private final double[] cfact = {
    1.0000, 1.0 / 12, 3.28084, 0.0328084
  };

  class ConverterActionListener implements ActionListener {
    public void actionPerformed(ActionEvent e) {
      // Response to any action is to update toValue based on the values
      // of fromUnits, toUnits and fromValue.,
      double fv = Double.parseDouble(fromValue.getText());
      String fu = (String)fromUnits.getSelectedItem();
      String tu = (String)toUnits.getSelectedItem();
      int    i  = 0;

      while (i < units.length && !units[i].equals(fu)) {
        ++i;
      }

      int j = 0;

      while (j < units.length && !units[j].equals(tu)) {
        ++j;
      }
      double tv = fv * cfact[i] / cfact[j];
      double wp = Math.round(tv * 10000) / 10000.0;
      toValue.setText("" + wp);
    }
  }

  public ConverterWindow() {
    // Create the four interactive elements of the GUI
    fromValue = new JTextField("1.0", 10);
    toValue   = new JTextField("1.0", 10);
    fromUnits = new JComboBox<String>(units);
    toUnits   = new JComboBox<String>(units);
    toValue.setEditable(false);

    // Create panel with flow layout and add GUI elements
    JPanel dpanel = new JPanel(new FlowLayout());
    dpanel.add(new JLabel("from: "));
    dpanel.add(fromValue);
    dpanel.add(fromUnits);
    dpanel.add(new JLabel(" to: "));
    dpanel.add(toValue);
    dpanel.add(toUnits);

    // Create the ConverterActionListener and set it to listen
    // for any changes to the three editable elements of the GUI
    ActionListener a = new ConverterActionListener();
    fromValue.addActionListener(a);
    fromUnits.addActionListener(a);
    toUnits.addActionListener(a);

    // Add panel to the frame, and do some bookkeeping on frame
    this.add(dpanel);
    this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    this.pack();
  }
}
Ex3.java
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;

public class Ex3 {
  public static void main(String[] args) {
    ConverterWindow w = new ConverterWindow();

    w.setVisible(true);
  }
}

Perhaps my favorite version!

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

public class Ex5 {
  public static void main(String[] args) {
    UCFrame f = new UCFrame();

    f.setVisible(true);
  }
}
Responder.java
import java.awt.event.*;

public class Responder implements ActionListener {
  private UCFrame f;
  public Responder(UCFrame f) {
    this.f = f;
  }

  public void actionPerformed(ActionEvent e) {
    f.recalculate();
    f.resetFocus();
  }
}
UCFrame.java
import javax.swing.*;
import java.awt.*;

public class UCFrame extends JFrame {
  private JTextField fromValue;
  private JTextField toValue;
  private JComboBox<String> fromUnits;
  private JComboBox<String> toUnits;
  private final String[] units = {
    "feet", "inches", "meters", "centimeters"
  };
  private final double[] cfact = {
    1.0000, 1.0 / 12, 3.28084, 0.0328084
  };

  // recalculates the toValue from other components' values
  public void recalculate() {
    try {
      double fv = Double.parseDouble(fromValue.getText());
      String fu = (String)fromUnits.getSelectedItem();
      String tu = (String)toUnits.getSelectedItem();
      int    i  = 0;

      while (i < units.length && !units[i].equals(fu)) {
        ++i;
      }
      int j = 0;

      while (j < units.length && !units[j].equals(tu)) {
        ++j;
      }
      double tv = fv * cfact[i] / cfact[j];
      double wp = Math.round(tv * 10000) / 10000.0;
      toValue.setText("" + wp);
    } catch (Exception e)    {
      toValue.setText("error!");
    }
  }

  // Sets focus to the fromValue text field
  public void resetFocus() {
    fromValue.requestFocus();
  }

  // consructor
  public UCFrame() {
    // create components
    JPanel p         = new JPanel(new FlowLayout());
    JLabel fromLabel = new JLabel("From:");
    JLabel toLabel   = new JLabel("To:");

    fromValue = new JTextField(10);
    toValue   = new JTextField(10);
    toValue.setEditable(false); // only for output
    fromUnits = new JComboBox<String>(units);
    toUnits   = new JComboBox<String>(units);
    Responder r = new Responder(this);

    // add action listeners
    fromValue.addActionListener(r);
    fromUnits.addActionListener(r);
    toUnits.addActionListener(r);

    // add components to frame & ready for display
    add(p, BorderLayout.CENTER);
    p.add(fromLabel);
    p.add(fromValue);
    p.add(fromUnits);
    p.add(toLabel);
    p.add(toValue);
    p.add(toUnits);
    setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    resetFocus();
    pack();
  }
}

Going in-depth with Listeners...

For an additional explanation on ActionListeners and an example for how to create you own custom listeners, click the link.
How to make your own custom Listeners.