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.
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!
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:
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);
}
}
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.
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);
}
}
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.
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);
}
}
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("");
}
}
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
}
}
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 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);
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.
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.
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);
}
}
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()
);
}
}
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];
}
}
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 ...
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();
}
}
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);
}
}
import javax.swing.*;
import java.awt.*;
public class Ex5 {
public static void main(String[] args) {
UCFrame f = new UCFrame();
f.setVisible(true);
}
}
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();
}
}
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();
}
}
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.