GUI Apps

Swing Windows

This represents sections 12.1 and 12.2 in the textbook which present the basics of a Swing GUI application. At the basis is the application's main window called a JFrame, represented by the class:
javax.swing.JFrame
Here's a demo which creates and displays a JFrame:
ShowWindow.java  
Let's take a look at the statements after:
JFrame window = new JFrame();
The first one simply sets a title:
window.setTitle("A Simple Window");
The next sets the size:
window.setSize(WINDOW_WIDTH, WINDOW_HEIGHT);
The next sets the behavior on closing the window from the Operating System's window manager close button:
window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
If you remove this, on close, the default behavior is to make the window "goes away" but the application does not exit. The last statement makes the window appear:
window.setVisible(true);
Although this statement is often the last one in a GUI setup, the code execution does not "stop" here, which you can verify by adding a last statement:
System.out.println("The End");

Textbook Frame Setup

The SimpleWindow setup is way too simplistic for any kind of Java Swing GUI app. We need to add features and change behaviors of aspects of the JFrame. The idea is to create a GUI class which is an extension of the JFrame and modify it. Here is the class and a main class driver:
SimpleWindow.java  
SimpleWindowDemo.java  
When you run it, the outcome is the same as in ShowWindow, but the creation of the Window is different. In particular, you'll see the new code:
public class SimpleWindow extends JFrame
The usage of extends is explained in the followup course. Just take it without question now. The significance is the SimpleWindow class gets all the behaviors of JFrame, but we can add/modify other behaviors.

The main driver program is almost nothing. Its entire job is to invoke the constructor which does all the work. You can compare the differences between SimpleWindow constructor code and ShowWindow main function:
SimpleWindow
setTitle("A Simple Window");
...
setVisible(true);
ShowWindow
window.setTitle("A Simple Window");
...
window.setVisible(true);

The final step is to merge these two into one program, effectively adding a main function into SimpleWindow:
EmbeddedMain.java  
The main function has one line:
EmbeddedMain em = new EmbeddedMain();
The em object is never used, and so we could just write:
new EmbeddedMain();

GUI Starter Program

The Embedded main is a single-file GUI application. Making a single file is a simplification to accommodate beginning users of Swing or and other GUI applications. In general, there are two key aspects of a GUI application: In reality, these aspects should be kept in separate classes, but we'll play along with the textbook's approach.

Here is a GUI application highlighting other key features which are part of a GUI application:

GUIStarter
import java.awt.BorderLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
 
public class GUIStarter extends JFrame {
 
  private class ButtonHandler implements ActionListener {
    @Override
    public void actionPerformed(ActionEvent evt) {
      System.out.println("handler called");
    }
  }
 
  public GUIStarter() {
    setTitle("GUI Starter");
    setLocationRelativeTo(null);
 
    add(centerPanel(), BorderLayout.CENTER);
    // or simply:
    // add( centerPanel() );    
  }
 
  private JPanel centerPanel() {
    JPanel panel = new JPanel();
 
    JLabel label = new JLabel("Hello");
    panel.add(label);
 
    JButton button = new JButton("Press");
    ButtonHandler handler = new ButtonHandler();
    button.addActionListener(handler);
    panel.add(button);
 
    return panel;
  }
 
  public static void main(String[] args) {
    GUIStarter window = new GUIStarter();
    window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
 
    // if you want to set the size to the enclosed components:
    //window.pack();
 
    // otherwise, set a specific size:
    window.setSize(350, 250);
 
    window.setVisible(true);
 
    // if you want to restrict resizing:
    //window.setResizable(false);
  }
}
select
Run and click the button. You'll see output displayed in the Output window. Let's go through the features being suggested. We will expand on them later.

JFrame, JPanel and Layouts

In the Swing GUI terms, JFrame and JPanel are referred to as containers in that they hold other components (including other containers). You can normally just add a component into a container, but where it goes is up to a so-called layout specification.

Our first new component is the "center JPanel" created from this code:
  private JPanel centerPanel() {
    JPanel panel = new JPanel();
 
    JLabel label = new JLabel("Hello");
    panel.add(label);
 
    JButton button = new JButton("Press");
    ButtonHandler handler = new ButtonHandler();
    button.addActionListener(handler);
    panel.add(button);
 
    return panel;
  }
Two new subcomponents are present: One of each is created and added into a JPanel object and the JPanel object is placed within the JFrame by the code:
add(centerPanel(), BorderLayout.CENTER);
What is being suggested here is that there are different "regions" in the JFrame in which to add another component.

Moving control statements out of the constructor

To simplify the constructor presentation, we can move some of the statements into main. The statements there present alternative possibilities of setting the GUI and resizability, including: You should try both of these features, commenting out the window.setSize statement.

Handling a button click

Yet another new feature to be explained in the follow-up course. GUI applications are event-driven, meaning that they do all their actions when the user generates some event. The easiest event to manage is the button click.

If we take a look where the button is created we see:
JButton button = new JButton("Press");
ButtonHandler handler = new ButtonHandler();
button.addActionListener(handler);
The handler is an object of the ButtonHandler class defined as:
private class ButtonHandler implements ActionListener {
  @Override
  public void actionPerformed(ActionEvent evt) {
    System.out.println("handler called");
  }
}
This is new. We are defining a class within the GUIStarter class at the data member level. It is called an inner class. Once again, we don't explain or try to amplify, but just use it.

The second novelty is this:
class ButtonHandler implements ActionListener
The ActionListener is called an interface. Again, don't concern yourself with what this means, just use it. The implication is that the ButtonHandler class must define the function:
public void actionPerformed(ActionEvent ae) {
  // what to do when activated
}
Every event-handler class must implement some "listener" interface and define some handler function. The button in our application assigns a handler object by
button.addActionListener(handler);
Once you've accepted the ButtonHandler class, it hopefully seems reasonable to accept that when you press the button, the actionPerformed function is called.

Making the window center

The following (optional) statement places the window in the center of the screen:
setLocationRelativeTo(null);

TextField Usage

In order to get a GUI app to do something, there is often some form of user input. Think of what you see in web interactions. A web form asks you to fill in a textfield, press a button and then something happens. We have the equivalent thing in Java Swing applications, the component:
javax.swing.JTextField
We want to look at a simple GUI app motivated by the textbook's KiloConverter example, which takes a numeric input (miles) and converts it into kilometers.

MyKiloConverter

This is my version. Enter miles into the left textfield and press the button.

MyKiloConverter
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JTextField;
 
public class MyKiloConverter extends JFrame {
  private JTextField kilosField = new JTextField(8);
  private JTextField milesField = new JTextField(8);
 
  private class ButtonHandler implements ActionListener {
    @Override
    public void actionPerformed(ActionEvent ae) {
      System.out.println("handler called");
 
      String kilosText = kilosField.getText().trim();
 
      if (kilosText.isEmpty()) {
        JOptionPane.showMessageDialog(null, "kilos field cannot be empty");
        return;
      }
 
      double kilos = Double.parseDouble(kilosText);
      double miles = kilos * 0.6214;
      milesField.setText( String.format("%.2f", miles) );
    }
  }
 
  public MyKiloConverter() {
    setTitle("MyKiloConver");
    setLocationRelativeTo(null);
 
    add(centerPanel(), BorderLayout.CENTER);
    add(northPanel(), BorderLayout.NORTH);
 
    milesField.setEditable(false);
    milesField.setBackground(Color.WHITE);
  }
 
  private JPanel northPanel() {
    JPanel panel = new JPanel();
    JLabel label = new JLabel("Convert Kilos to Miles");
    panel.add(label);
    return panel;
  }
 
  private JPanel centerPanel() {
    JPanel panel = new JPanel();
    JButton button = new JButton("Convert");
    panel.add(kilosField);
    panel.add(button);
    panel.add(milesField);
    ButtonHandler handler = new ButtonHandler();
    button.addActionListener(handler);
    return panel;
  }
 
  public static void main(String[] args) {
    MyKiloConverter app = new MyKiloConverter();
    app.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
 
    // if you want to set the size to the enclosed components:
    app.pack();
 
    // otherwise, set a specific size:
    //app.setSize(350, 250);
 
    app.setVisible(true);
 
    // if you want to restrict resizing:
    //app.setResizable(false);
  }
}
select
Here we are using a new second region in the layout in addition to the Center region, the North region (we'll explain fully later). Doing so allows us to ensure that the descriptive label "Convert Kilos to Miles", always appears above the other components. The points to make about the textfield usage is this:
  1. We want to "expose" the textfield objects at the data member level so that the ButtonHandler class can get access to their contents.
  2. The contents of a textfield is referred to as it "text" property and accessed through getter and setter member functions:
    field.getText()
    field.setText("something")
    
  3. We don't want the user to be able to key text into the miles field since this is supposed to be the result of conversion. Thus we add this line in the constructor:
    milesField.setEditable(false);
    

Textbook KiloConverter program

This is the second version the author presents in section 12.2 where the button does something:
KiloConverter.java  
Same idea, but (in my opinion), it is a much weaker version when you have to generate a message dialog to present the conversion; you might just as well abandon the GUI aspect all together!

Containers and Layouts

What I'm doing here is roughly what is done in 12.3 in the textbook. In the starter demo above, you see the usage of the components JFrame, JPanel and JLabel. The first two of these are containers in that they hold other components. The begging question is how do they hold other components, i.e., where are they placed in the container. The placement of components is determined by the container's layout. The next example is intended to visually illustrate the default layouts of the JFrame and JPanel: Saying that these are the default layouts, means that the layout can be changed to something other than the default. Try running this example:

FramePanelDemo
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.FlowLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
 
public class FramePanelDemo extends JFrame {
 
  private class ButtonHandler implements ActionListener {
    @Override
    public void actionPerformed(ActionEvent ae) {
      System.out.println("handler called");
    }
  }
 
  public FramePanelDemo() {
    add(centerPanel(), BorderLayout.CENTER);
    add(northPanel(), BorderLayout.NORTH);
    add(southPanel(), BorderLayout.SOUTH);
    add(eastPanel(), BorderLayout.EAST);
    add(westPanel(), BorderLayout.WEST);
  }
 
  private JPanel centerPanel() {
    JPanel panel = new JPanel();
    panel.add(new JLabel("Center-1"));
    panel.add(new JLabel("Center-2"));
    panel.add(new JLabel("Center-3"));
    panel.add(new JLabel("Center-4"));
    panel.setBackground(Color.red);
    return panel;
  }
 
  private JPanel northPanel() {
    JPanel panel = new JPanel();
    panel.add(new JLabel("North-1"));
    panel.add(new JLabel("North-2"));
    panel.setBackground(Color.yellow);
    return panel;
  }
 
  private JPanel southPanel() {
    JPanel panel = new JPanel(new FlowLayout(FlowLayout.LEFT));
    panel.add(new JLabel("South-1"));
    panel.add(new JLabel("South-2"));
    panel.setBackground(Color.MAGENTA);
    return panel;
  }
 
  private JPanel eastPanel() {
    JPanel panel = new JPanel();
    panel.add(new JLabel("East-1"));
    panel.add(new JLabel("East-2"));
    panel.setBackground(Color.CYAN);
    return panel;
  }
 
  private JPanel westPanel() {
    JPanel panel = new JPanel();
    panel.add(new JLabel("West-1"));
    panel.add(new JLabel("West-2"));
    panel.setBackground(Color.PINK);
    return panel;
  }
 
  public static void main(String[] args) {
    FramePanelDemo app = new FramePanelDemo();
    app.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
 
    // if you want to set the size to the enclosed components:
    app.pack();
 
    // otherwise, set a specific size:
    //app.setSize(350, 250);
 
    app.setVisible(true);
 
    // if you want to restrict resizing:
    //app.setResizable(false);
  }
}
select
The frame's BorderLayout is made pretty clear by the background colorizations. In particular, a BorderLayout maintains 5 different regions: North, East, South, West, Center with these expanding properties: Adding to a region explicitly is done by referencing the region with a constant, e.g.,
add( centerPanel(), JFrame.CENTER );
add( northPanel(), JFrame.NORTH );
You can omit the region identifier:
add( centerPanel() );
in which case it defaults to the CENTER region.

Within each region is a JPanel, each holding multiple JLabels. The JPanel's FlowLayout can be inferred from the behavior: The centered vs. left-aligned features are differentiated by the panel in the North, using the default FlowLayout, vs. the panel in the south, where the FlowLayout used to create the panel is altered:
JPanel panel = new JPanel(new FlowLayout(FlowLayout.LEFT));

BoxLayout

One thing that missing is the ability to force the alignment of components vertically. The FlowLayout does so only "under duress," so to speak. We need something else. Here is an enhanced version of the KiloConverter program.

KilosMilesBothWays
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JTextField;
 
public class KilosMilesBothWays extends JFrame {
 
  private JTextField kilosField = new JTextField(10);
  private JTextField milesField = new JTextField(10);
 
  private class K2MHandler implements ActionListener {
    @Override
    public void actionPerformed(ActionEvent ae) {
      System.out.println("K2M handler called");
 
      String kilosText = kilosField.getText().trim();
 
      if (kilosText.isEmpty()) {
        JOptionPane.showMessageDialog(null, "kilos field cannot be empty");
        return;
      }
 
      double kilos = Double.parseDouble(kilosText);
      double miles = kilos * 0.6214;
      milesField.setText( String.format("%.2f", miles) );
    }
  }
 
  private class M2KHandler implements ActionListener {
    @Override
    public void actionPerformed(ActionEvent ae) {
      System.out.println("M2K handler called");
 
      String milesText = milesField.getText().trim();
 
      if (milesText.isEmpty()) {
        JOptionPane.showMessageDialog(null, "miles field cannot be empty");
        return;
      }
 
      double miles = Double.parseDouble(milesText);
      double kilos = miles / 0.6214;
      kilosField.setText( String.format("%.2f", kilos) );
    }
  }
 
  public KilosMilesBothWays() {
    this.setTitle("KilosMilesBothWays");
    setLocationRelativeTo(null);
 
    add(centerPanel(), BorderLayout.CENTER);
    add(westPanel(), BorderLayout.WEST);
    add(eastPanel(), BorderLayout.EAST);
  }
 
  private JPanel centerPanel() {
    JPanel panel = new JPanel();
    panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
 
    JButton miles2kilos = new JButton("<=");
    JButton kilos2miles = new JButton("=>");
    panel.add(miles2kilos);
    panel.add(kilos2miles);
 
    M2KHandler m2khandler = new M2KHandler();
    K2MHandler k2mhandler = new K2MHandler();
    miles2kilos.addActionListener(m2khandler);
    kilos2miles.addActionListener(k2mhandler);
 
    // do center alignments
    miles2kilos.setAlignmentX(Component.CENTER_ALIGNMENT); // optional
    kilos2miles.setAlignmentX(Component.CENTER_ALIGNMENT); // optional
 
    panel.setBackground(Color.YELLOW);
    return panel;
  }
 
  private JPanel westPanel() {
    JPanel panel = new JPanel();
    panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
 
    JLabel label = new JLabel("Kilos");
 
    panel.add(label);
    panel.add(kilosField);
 
    // prevent field from expanding vertically
    kilosField.setMaximumSize(kilosField.getPreferredSize());
 
    label.setAlignmentX(Component.CENTER_ALIGNMENT); // optional
 
    panel.setBackground(Color.PINK);
    return panel;
  }
 
  private JPanel eastPanel() {
    JPanel panel = new JPanel();
    panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
 
    JLabel label = new JLabel("Miles");
 
    panel.add(label);
    panel.add(milesField);
 
    // prevent field from expanding vertically
    milesField.setMaximumSize(milesField.getPreferredSize());
 
    label.setAlignmentX(Component.CENTER_ALIGNMENT); // optional
 
    panel.setBackground(Color.CYAN);
    return panel;
  }
 
  public static void main(String[] args) {
    KilosMilesBothWays app = new KilosMilesBothWays();
    app.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
 
    app.pack();
    app.setVisible(true);
    //app.setResizable(false);
  }
}
select
The new thing is applying a BoxLayout to a JPanel. This is somewhat strange:
JPanel panel = new JPanel();
panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
Oddly, you need the panel in both places in this statement. Of course, we're telling it to add vertically.

Other gimmicks are unfortunately necessary. By default, a textfield put into a BoxLayout will expand the way the layout goes, meaning it will expand vertically. You don't want this. The easiest fix is to do what we've done:
kilosField.setMaximumSize(kilosField.getPreferredSize());
If you comment out this line you'll soon see the problem.

The other gimmick is making the alignment so that the appearance is better. These are all the statements marked "optional" in comments. Try taking them out to seem what happens. The appearance, although OK, is definitely not as good as with them.

This application has two buttons, and their behaviors are different enough that we want to make two different handlers.

GridLayout

This is the layout that the textbook champions for positional placement. A GridLayout makes the container into a rectangular grid of the specified number of rows and columns:
setLayout(new GridLayout(ROWS, COLS));
Here is the first example. The call within the constructor is:
setLayout(new GridLayout(2, 3));
which actually changes the default BoxLayout of the Frame to a GridLayout.
GridWindow.java  
Note the way that buttons are simply added one by one without reference to the position. They are added, as in a FlowLayout, from left-to-right, top-to-bottom.

You cannot help to observe that the buttons are automatically resized both horizontally and vertically. The textbook's fix for this resizing is to, instead, add auxiliary JPanels to the grid and then add buttons into these panels, getting the following:
GridPanelWindow.java  
You can sort-of achieve a vertical layout with a GridLayout if you use:
setLayout(new GridLayout(0,1));
This indicates 0 rows and 1 column; however, the 0 is ignored as you add components, giving arbitrarily many rows with one column. Here is a demo program:

VerticalGrid
import java.awt.BorderLayout;
import java.awt.FlowLayout;
import java.awt.GridLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTextField;
 
public class VerticalGrid extends JFrame {
 
  private class ButtonHandler implements ActionListener {
    @Override
    public void actionPerformed(ActionEvent ae) {
      System.out.println("handler called");
    }
  }
 
  public VerticalGrid() {
    setTitle("Vertical Grid");
    setLocationRelativeTo(null);
    add(centerPanel(), BorderLayout.CENTER);
  }
 
  private JPanel centerPanel() {
    JPanel panel = new JPanel();
    panel.setLayout(new GridLayout(0,1));
 
    JLabel helloLabel = new JLabel("Hello Label");
    JTextField textfield = new JTextField(8);
    JButton button = new JButton("Hello Button");
 
    panel.add(helloLabel);
 
    JPanel buttonHolder = new JPanel(new FlowLayout(FlowLayout.LEFT));
    JPanel textfieldHolder = new JPanel(new FlowLayout(FlowLayout.LEFT));
    buttonHolder.add(button);
    textfieldHolder.add(textfield);
 
//    panel.add(button);
//    panel.add(textfield);
 
    panel.add(buttonHolder);
    panel.add(textfieldHolder);
 
    return panel;
  }
 
  public static void main(String[] args) {
    VerticalGrid app = new VerticalGrid();
    app.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
 
    // if you want to set the size to the enclosed components:
    //app.pack();
 
    // otherwise, set a specific size:
    app.setSize(350, 250);
 
    app.setVisible(true);
 
    // if you want to restrict resizing:
    //app.setResizable(false);
  }
}
select

GridBagLayout

If you really want to "do it right" in terms of getting the exact layout you want, the GridBagLayout is the way to go. It is the most general layout which can achieve the effects of all the other layouts. It is, however, beyond the scope of the textbook and the course.

Multiple buttons with one handler

When you have multiple buttons which all do the same thing, more or less, it is a good idea to create one action listener only shared among multiple buttons.

You can distinguish which button was actually pressed in one of two ways: These are the textbook's versions available from source. The first version uses the getActionCommand and the second uses the getSource. The file name displayed in the textbook is the first one listed below, but is incorrectly named EventObjectWindow.java.
EventObject.java  
By default, the actionCommand is the button's text. However, it is often better to separate the two by explicitly setting the actionCommand with:
button.setActionCommand("-- something --");

Shopping List App

Here is a usage of this concept. This application presents a shopping list of items, each with and add and remove button. Clicking on add button adds the item to the list, and clicking on the remove button removes it. The list is presented in a new component called a JTextArea, which is just a multi-line text entry component, compared to the single-line JTextField.

ShoppingList
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.FlowLayout;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.ArrayList;
import java.util.Collections;
import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTextArea;
 
public class ShoppingList extends JFrame {
 
  private String listing(ArrayList<String> choices) {
    String output = "";
    for (String choice : choices) {
      output += choice + "\n";
    }
    return output;
  }
 
  private class AddHandler implements ActionListener {
    @Override
    public void actionPerformed(ActionEvent evt) {
      String item = evt.getActionCommand();
      if (choices.contains(item)) {
        return;
      }
      choices.add(item);
      Collections.sort(choices);
      content.setText(listing(choices));
    }
  }
 
  private class RemoveHandler implements ActionListener {
    @Override
    public void actionPerformed(ActionEvent evt) {
      String item = evt.getActionCommand();
      choices.remove(item);
      content.setText(listing(choices));
    }
  }
 
  private String[] items = {
    "cheese", "chicken", "fish", "broccoli", "eggs", "salad", "pasta", "bread",
    "tortillas", "hot sauce"
  };
 
  private ArrayList<String> choices = new ArrayList<>();
 
  // we want the # columns = 10, the rows can be set
  // to 0, because the textarea will expand vertically regardless
  private JTextArea content = new JTextArea(0, 10);
 
  public ShoppingList() {
    setTitle("Shopping List");
    setLocationRelativeTo(null);
 
    add(centerPanel(), BorderLayout.CENTER);
    add(eastPanel(), BorderLayout.EAST);
 
    content.setEditable(false);
    content.setMargin(new Insets(5, 5, 5, 5)); // textarea padding
  }
 
  private JPanel centerPanel() {
    JPanel panel = new JPanel();
    panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
 
    AddHandler handler = new AddHandler();
    RemoveHandler remove_handler = new RemoveHandler();
 
    for (String item : items) {
      JPanel holder = new JPanel(new FlowLayout(FlowLayout.LEFT));
 
      JLabel label = new JLabel(item);
      label.setForeground(Color.decode("#cc0000"));
 
      JButton addButton = new JButton("add");
      JButton removeButton = new JButton("remove");
 
      holder.add(addButton);
      holder.add(removeButton);
      holder.add(label);
 
      addButton.setActionCommand(item);
      addButton.addActionListener(handler);
 
      removeButton.setActionCommand(item);
      removeButton.addActionListener(remove_handler);
 
      panel.add(holder);
    }
    return panel;
  }
 
  private JPanel eastPanel() {
    JPanel panel = new JPanel();
    panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
 
    JLabel label = new JLabel("SHOPPING LIST");
 
    panel.add(label);
    panel.add(content);
 
    // force left-alignment of things in the box
    label.setAlignmentX(Component.LEFT_ALIGNMENT);
    content.setAlignmentX(Component.LEFT_ALIGNMENT);
 
    return panel;
  }
 
  public static void main(String[] args) {
    ShoppingList app = new ShoppingList();
    app.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    app.pack();
    app.setVisible(true);
    //app.setResizable(false);
  }
}
select

Stateful Buttons

We now want to add the Swing equivalents of other features commonly found in web applications: radio buttons and checkboxes. The Swing classes are JRadioButton and JCheckBox. We call them "stateful buttons" because they can act like buttons when you check them, and they also maintain a state of being checked or not.

These two types of buttons have different appearances as you would expect; however they have potentially identical behaviors, which is different from the web. Try running this demo program and checking the components to see what happens:

RadioCheckDemo
import java.awt.BorderLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.BoxLayout;
import javax.swing.ButtonGroup;
import javax.swing.JCheckBox;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JRadioButton;
 
public class RadioCheckDemo extends JFrame {
 
  private class ButtonHandler implements ActionListener {
    @Override
    public void actionPerformed(ActionEvent ae) {
      System.out.println("radio1 checked: " + radio1.isSelected());
      System.out.println("radio2 checked: " + radio2.isSelected());
      System.out.println("check1 checked: " + check1.isSelected());
      System.out.println("check2 checked: " + check2.isSelected());
      System.out.println();
    }
  }
 
  private JRadioButton radio1 = new JRadioButton("radio1");
  private JRadioButton radio2 = new JRadioButton("radio2");
 
  private JCheckBox check1 = new JCheckBox("check1");
  private JCheckBox check2 = new JCheckBox("check2");
 
  public RadioCheckDemo() {
    setTitle("RadioCheckDemo");
    setLocationRelativeTo(null);
    add(centerPanel(), BorderLayout.CENTER);
  }
 
  private JPanel centerPanel() {
    JPanel panel = new JPanel();
    panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
 
    JPanel radios = new JPanel();
    radios.add(radio1);
    radios.add(radio2);
 
    JPanel checks = new JPanel();
    checks.add(check1);
    checks.add(check2);
 
    panel.add(radios);
    panel.add(checks);
 
    ButtonGroup btnGroup = new ButtonGroup();
 
//    btnGroup.add(radio1);
//    btnGroup.add(radio2);
//    btnGroup.add(check1);
//    btnGroup.add(check2);
 
    ButtonHandler handler = new ButtonHandler();
    radio1.addActionListener(handler);
    radio2.addActionListener(handler);
    check1.addActionListener(handler);
    check2.addActionListener(handler);
 
    return panel;
  }
 
  public static void main(String[] args) {
    RadioCheckDemo app = new RadioCheckDemo();
    app.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    app.pack();
    app.setVisible(true);
    //app.setResizable(false);
  }
}
select
You should observe that, without any changes, the buttons all act the same. What you want, however, is to create groupings of the buttons, particularly for the radio buttons so that the checking is exclusive, i.e., either one is checked or another.

On web applications, radio buttons always form mutually exclusive groups (only one checked at a time), whereas checkboxes form inclusive groups allowing multiple choices to be checked at the same time.

In Java swing, you can make a group of either type of button. The objects of a new class ButtonGroup is used to perform the groupings. Now go back into the source code into the centerPanel and make some of the changes suggested in the comments.
    btnGroup.add(radio1);
    btnGroup.add(radio2);
With this in place the buttons behave as you would expect. If you uncomment all 4 lines, then everything is part of an exclusive group, probably not what you want.

Testing checked status

The other important thing to do with these buttons is to see whether they are checked or not. This is done with the getter member call:
boolean is_checked = stateful_button.isSelected();
In our example, when you check a button, the status of each of the 4 is printed out for confirmation.

Joe's Automotive Example

This example is based on an idea from the textbook in Chapter 12: Programming Challenges #6 (Joe's AutoMotive). We add a twist to the thing by saying you can get a 7% discount if you pay cash.

You need the following helper class. Its objects represent a service that Joe's is able to complete, and its price.

JoesAutoService
public class JoesAutoService {
  private final String name;
  private final double price;
 
  public JoesAutoService(String name, double price) {
    this.name = name;
    this.price = price;
  }
 
  public String getName() {
    return name;
  }
 
  public double getPrice() {
    return price;
  }
}
select
Here is the GUI app. It uses both checkboxes to choice one or more service to perform, and then a radio group to decide the final price based on cash or credit choice.

JoesAuto
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.FlowLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.ArrayList;
import javax.swing.BoxLayout;
import javax.swing.ButtonGroup;
import javax.swing.JCheckBox;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JRadioButton;
import javax.swing.JTextField;
 
public class JoesAuto extends JFrame {
 
  private JoesAutoService[] services = {
    new JoesAutoService("Oil Change", 26.00),
    new JoesAutoService("Lube Job", 26.00),
    new JoesAutoService("Radiator Flush", 30.00),
    new JoesAutoService("Transmission Flush", 80.00),
    new JoesAutoService("Inspection", 15.00),
    new JoesAutoService("Muffler Replacement", 100.00),
    new JoesAutoService("Tire Rotation", 20.00),
  };
 
  private ArrayList<JCheckBox> serviceChoices = new ArrayList<>();
 
  private JRadioButton cash = new JRadioButton("cash");
  private JRadioButton credit = new JRadioButton("credit", true);
 
  private JTextField priceField = new JTextField(10);
 
  private class Handler implements ActionListener {
    @Override
    public void actionPerformed(ActionEvent evt) {
      System.out.println("handler called");
      double price = 0;
      for (int i = 0; i < services.length; i++) {
        if (serviceChoices.get(i).isSelected()) {
          price += services[i].getPrice();
        }
      }
      if (cash.isSelected()) {
        price -= .07 * price;
      }
      priceField.setText(String.format("%.2f", price));
    }
  }
 
  public JoesAuto() {
    setTitle("Joe's Auto");
    setLocationRelativeTo(null);
 
    for (JoesAutoService service : services) {
      serviceChoices.add(new JCheckBox(service.getName()));
    }
 
    add(centerPanel(), BorderLayout.CENTER);
 
    priceField.setEditable(false);
    priceField.setBackground(Color.WHITE);
    priceField.setText("0.00");
 
    ButtonGroup btnGrp = new ButtonGroup();
    btnGrp.add(cash);
    btnGrp.add(credit);
  }
 
  private JPanel centerPanel() {
    JPanel panel = new JPanel();
 
    Handler handler = new Handler();
 
    panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
    for (JCheckBox checkbox : serviceChoices) {
      checkbox.addActionListener(handler);
      panel.add(checkbox);
      checkbox.setAlignmentX(Component.LEFT_ALIGNMENT);
    }
 
    cash.addActionListener(handler);
    credit.addActionListener(handler);
 
    // cash/credit panel: a radiobutton group
    JPanel cashCreditPanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
    cashCreditPanel.add(cash);
    cashCreditPanel.add(credit);
 
    panel.add(cashCreditPanel);
    cashCreditPanel.setAlignmentX(Component.LEFT_ALIGNMENT);
 
    // price panel: a label and textfield
    JPanel pricePanel = new JPanel(new FlowLayout(FlowLayout.LEFT));
    pricePanel.add(new JLabel("price: $"));
    pricePanel.add(priceField);
 
    panel.add(pricePanel);
    pricePanel.setAlignmentX(Component.LEFT_ALIGNMENT);
 
    return panel;
  }
 
  public static void main(String[] args) {
    JoesAuto app = new JoesAuto();
    app.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    app.pack();
    app.setVisible(true);
  }
}
select

ComboBoxes

If you think about web applications requiring user input, the most common features are textfields and buttons. Following those another very common feature is a drop-down list which allow the user to make a selection of a restricted set of choices. The equivalent of a drop-down list in Java Swing is the GUI component called the JComboBox. In our application below, the object created as a data member is:
private JComboBox itemSelections = new JComboBox();
A singular feature of the JComboBox is that its actual contents are controlled by a separate so-called "model object" of the class DefaultComboBoxModel, also created as a data member:
private DefaultComboBoxModel itemsModel = new DefaultComboBoxModel();
This model object is then associated to the JComboBox object from the frame via this call in the Controller constructor:
itemSelections.setModel(itemsModel);
Maintaining the contents is done using a variety of member functions, the most common one being the addElement function, which we use in the constructor as follows:
for (String item : items) {
  itemsModel.addElement(item);
}
The elements in a combobox can be any Java objects, what is displayed is the result of the toString function.

In order to access a selected item, you then need to work with the list object itself. The most common operation is what we have used in this application:
String item = (String) itemSelections.getSelectedItem();

StoreList


StoreList
import java.awt.BorderLayout;
import java.awt.FlowLayout;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import javax.swing.DefaultComboBoxModel;
import javax.swing.JButton;
import javax.swing.JComboBox;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JTextArea;
 
public class StoreList extends JFrame {
 
  private String listing(ArrayList<String> choices) {
    String output = "";
    for (String choice : choices) {
      output += choice + "\n";
    }
    return output;
  }
 
  private JTextArea content = new JTextArea();
 
  private String[] items = {
    "cheese", "chicken", "fish", "broccoli", "eggs", "salad",
    "pasta", "bread", "tortillas", "hot sauce"
  };
  private JComboBox itemSelections = new JComboBox();
  private DefaultComboBoxModel itemsModel = new DefaultComboBoxModel();
 
  private ArrayList<String> choices = new ArrayList<>();
 
  private class AddHandler implements ActionListener {
 
    @Override
    public void actionPerformed(ActionEvent evt) {
      String item = (String) itemSelections.getSelectedItem();
      System.out.println(item);
      if (choices.contains(item)) {
        return;
      }
      choices.add(item);
      Collections.sort(choices);
      content.setText(listing(choices));
    }
  }
 
  public StoreList() {
    setTitle("Store List");
    setLocationRelativeTo(null);
 
    itemSelections.setModel(itemsModel);
 
    Arrays.sort(items);
    for (String item : items) {
      itemsModel.addElement(item);
    }
 
    add(northPanel(), BorderLayout.NORTH);
    add(content, BorderLayout.CENTER);
 
    content.setEditable(false);
    content.setMargin(new Insets(5, 5, 5, 5));
  }
 
  private JPanel northPanel() {
    JPanel panel = new JPanel(new FlowLayout(FlowLayout.LEFT));
 
    JButton addButton = new JButton("add");
    AddHandler handler = new AddHandler();
    addButton.addActionListener(handler);
 
    panel.add(itemSelections);
    panel.add(addButton);
 
    return panel;
  }
 
  public static void main(String[] args) {
    StoreList app = new StoreList();
    app.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    app.setSize(350, 250);
    app.setVisible(true);
  }
}
select


© Robert M. Kline