Reading

Required: These notes!
Recommended: Java in a Nutshell, TO APPEAR

Overview

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 componenets. By default, JFrame's have LayoutManagers of type BorderLayout, which thinks of the screen being divied 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.CENTER);
if you want it added to the East region. These regions automatically expand or shrink to fit the various componenets 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 GIU 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.

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 a new text value (user presses enter while the box has the focus) is considered "the" action for a button. So both components use a the 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.

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.

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 onthe above methods to make the coversion happen.

Ex3.java ConverterWindow.javax
ConverterActionListener.java

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