Session 5 · Programming Technology
GUI programming with Swing
Swing from the ground up: a window as a tree of panels, layout managers, listeners in four forms and the Event Dispatch Thread – then the calculator and an on-screen keyboard split into a Swing-free model and a view, and the same calculator in MVC.
From this session on, everything we write has a window. The rest of the semester – the second and the third assignment included – uses Swing, the GUI toolkit that ships with the JDK. This session covers the three ideas every Swing program is made of: a tree of components, a layout manager per container that decides where things go, and listeners that react when the user does something. Then we look at how to keep the program's rules out of the window, so the GUI stays small and the logic stays testable.
The whole session in one breath: build a tree of panels, give each panel one simple layout, attach a listener to every control, and let each listener do three things only – read the input, call the model, redraw the screen from the model.
Why Swing
Swing is part of the JDK, so there is nothing to install and every IDE runs it out of the box. The interface is built in code, step by step, with no generated files or hidden magic – which makes it a good tool for learning how GUIs work. It is also not a toy: IntelliJ IDEA, Android Studio and NetBeans itself are Swing applications, and plenty of banking and enterprise desktop software is too.
The more important reason: the concepts carry over. The web, other desktop toolkits and mobile frameworks all use the same three ideas with different names.
| Swing | Web (HTML / JS / React) | Other desktop toolkits |
|---|---|---|
JFrame, JPanel (containers) | <body>, <div> | Window, Stage, Panel |
JButton, JTextField, JLabel | <button>, <input>, <span> | Button, TextBox / TextField, Label |
layout manager (BorderLayout …) | CSS flexbox / grid | Grid, StackPanel, HBox / VBox |
addActionListener(…) | addEventListener("click", …), onClick | Click event, setOnAction(…) |
| Event Dispatch Thread (EDT) | the browser’s event loop | the UI thread |
A window is a tree of components
A Swing interface is a tree. The root is the window, a JFrame. Its content pane holds panels (JPanel), and the panels hold the controls: buttons, text fields, labels. The smallest useful program already shows the pattern – a frame, a panel, two components and a listener:
Hello – the body of main
SwingUtilities.invokeLater(() -> {
JFrame frame = new JFrame("Hello");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
JLabel label = new JLabel("Hello!");
JButton button = new JButton("Click me");
button.addActionListener(e -> label.setText("Hello, Swing!"));
JPanel panel = new JPanel(); // FlowLayout by default
panel.add(label);
panel.add(button);
frame.add(panel); // goes into the content pane
frame.pack(); // size from the components
frame.setLocationRelativeTo(null); // centre of the screen
frame.setVisible(true); // always last
});The calculator from Session 1 comes back this week, rewritten. Its tree has three levels of panels (Fig. 1). The leaves are what the user sees; everything above them exists only to arrange the leaves.
- component
- Anything visible: JButton, JLabel, JTextField, JToggleButton, JList …
- container
- A component that holds others: JFrame, JPanel. Every container has a layout manager.
- content pane
- The part of a JFrame below the menu bar. frame.add(x) actually adds to it.
- layout manager
- The object that computes the position and size of a container’s children.
- listener
- An object whose method runs when an event happens, e.g. an ActionListener on a button.
Layout managers
In Swing you don't give components pixel positions. Each container has a layout manager that computes where its children go and how big they are – and computes it again when the window is resized, when the font is larger on another machine, or when a label's text gets longer. The course uses four of them (Fig. 2).
| Layout | How it works | Use it for |
|---|---|---|
BorderLayout | five regions: NORTH, SOUTH, EAST, WEST, CENTER; one component per region | the main split of a window (default of the content pane) |
FlowLayout | left to right in a row, wraps to a new row when full | a row of buttons, a toolbar (default of JPanel) |
GridLayout | rows × cols equal cells, filled row by row; GridLayout(0, 2) = 2 columns, as many rows as needed | a game board, a keypad |
BoxLayout | a single row or column, components keep their preferred size | forms, vertical lists |
A real screen is never one layout. You get it by nesting panels, each with the simplest layout that fits its part. Draw the window first and mark the panels on the drawing, then build the tree from the drawing (Fig. 3).
CalcGUI – labels and fields in two columns
JPanel labelPanel = new JPanel(new GridLayout(0, 1, GAP, GAP)); // 0 rows = as many as needed
labelPanel.add(new JLabel("A:"));
labelPanel.add(new JLabel("B:"));
labelPanel.add(new JLabel("Result:"));
JPanel fieldPanel = new JPanel(new GridLayout(0, 1, GAP, GAP));
fieldPanel.add(operand1);
fieldPanel.add(operand2);
fieldPanel.add(result);
JPanel numPanel = new JPanel(new BorderLayout(GAP, GAP));
numPanel.add(labelPanel, BorderLayout.WEST); // only as wide as the labels
numPanel.add(fieldPanel, BorderLayout.CENTER); // the rest of the widthThe five operation buttons are not written one by one. The program loops over the Operation enum and creates a button per value, each with the same listener class and a different parameter. Adding a new operation is one new line in the enum; the button appears by itself.
CalcGUI – one button per operation
JPanel buttonPanel = new JPanel(new GridLayout(1, 0, GAP, GAP)); // one row
for (Operation operation : Operation.values()) {
JButton button = new JButton(operation.getSymbol());
button.addActionListener(new CalcActionListener(operation));
buttonPanel.add(button);
}Operation.java – symbol + function per constant (outline)
public enum Operation {
ADD("+", (a, b) -> a + b),
SUBTRACT("-", (a, b) -> a - b),
// … MULTIPLY, DIVIDE (throws on b == 0), POWER("^", Math::pow)
private final String symbol;
private final DoubleBinaryOperator function;
// constructor, getSymbol(), apply(a, b) that rejects non-finite results
}Events and listeners
A GUI program is event-driven. main builds the window and ends; from then on the program only does something when an event arrives – a click, a key press, a menu choice. You attach a listener to each event. For buttons and menu items that is an ActionListener, an interface with a single method, actionPerformed(ActionEvent e).
The same listener can be written in four ways, and the QWERTY example uses all four on purpose:
QwertyGUI – the four forms of a listener
// 1. Named inner class – it has a parameter and several buttons share it
class QwertyListener implements ActionListener {
private final String letter;
QwertyListener(String letter) { this.letter = letter; }
@Override
public void actionPerformed(ActionEvent e) {
model.type(letter);
refresh();
}
}
button.addActionListener(new QwertyListener(letter));
// 2. Anonymous class – the form you will meet in older code
exitItem.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
System.exit(0);
}
});
// 3. Lambda – short code used once
clearButton.addActionListener(e -> { model.clear(); refresh(); });
// 4. Method reference – an existing method with the same parameter
backButton.addActionListener(this::backspace); // void backspace(ActionEvent e)| Form | Pick it when |
|---|---|
| named inner class | the listener has a parameter (a letter, an operation) or several controls share it |
| anonymous class | you read older code – before lambdas this was the usual form |
| lambda | the code is short and used once |
| method reference | a method with a matching parameter list already exists |
What do -> and :: mean? addActionListener wants an object with an actionPerformed method. Because ActionListener has only that one method (it is a functional interface), you can give just the method body: e -> display.setText(""), parameter on the left, code on the right. A method reference names an existing method instead of writing a body. this::backspace does not call backspace – it hands the method over, to be called later, when the button is pressed. Before the :: stands whose method it is: this, or any other object, e.g. model::clearHistory.
An inner class and a lambda can both use the fields of the enclosing object (model, display), so you don't have to pass everything in as a parameter. A lambda can only use local variables that are effectively final – this matters in loops, as the board example below shows.
One thread for the whole GUI
Swing is not thread-safe. Every component must be created and changed on one thread, the Event Dispatch Thread (EDT). Listeners run on it automatically. The only place you have to think about it is main, which runs on a different thread – so it hands the construction of the GUI over with SwingUtilities.invokeLater (Fig. 4).
The consequence for you: a listener must return quickly. While it runs, the window does not repaint and does not react – a Thread.sleep or a long loop in a listener freezes the whole window. For something that has to happen repeatedly (a clock, a countdown, an animation later in the semester) use javax.swing.Timer, which also runs its code on the EDT:
A Timer instead of sleep
Timer timer = new Timer(1000, e -> secondsLabel.setText(String.valueOf(++seconds)));
timer.start(); // runs every second, on the EDT; timer.stop() ends itKeep the rules out of the window
A GUI program does three things: it holds the state and applies the rules, it shows the state, and it reacts to the user. If all three live in one class, the class grows fast, every change touches everything, and nothing can be tested without clicking. So we split it. In the model–view (MV) structure:
- the model is a plain Java class with no Swing in it –
CalcModelcomputes and stores the result,Operationholds the rules of each operation; - the view builds the window, and its listeners call the model and then redraw the screen from the model with one method,
refresh(). In MV the view is also the controller.
Follow one click through the calculator (Fig. 5). The listener only forwards: it turns text into numbers, lets the model compute, and asks refresh() to show the model.
CalcGUI.CalcActionListener – read, call the model, refresh
@Override
public void actionPerformed(ActionEvent e) {
try {
double a = parseOperand(operand1, "A"); // text → number, may throw
double b = parseOperand(operand2, "B");
model.calculate(a, operation, b); // the model computes
refresh(); // the view shows the model
} catch (NumberFormatException | ArithmeticException ex) {
model.clearResult(); // no stale result on screen
refresh();
JOptionPane.showMessageDialog(frame, ex.getMessage(), "Error",
JOptionPane.ERROR_MESSAGE);
}
}Converting between text and numbers stays in the view: the model works with numbers, the screen works with strings. The parse also guards against input Double.parseDouble happily accepts but a calculator should not – "NaN" and "Infinity" – and accepts a decimal comma as well as a point. Division by zero doesn't throw for double (it gives infinity), so Operation checks it explicitly and throws an ArithmeticException – an exception from the model, reported by the view.
The QWERTY keyboard
The exercise of the session is a small on-screen keyboard: a display, the letters Q W E R T Y, a Back and a CLR button. Try it yourself first (Task 1 below); this section explains how the reference solution is structured (Fig. 6).
- The letter buttons are created in a loop from an array and kept in a list, so
refresh()can relabel them – exactly how a game board is built later. - The display is read-only (
setEditable(false)) and not focusable, so key presses reach the buttons. - The text and the Caps state are not read back from the components (
display.getText(),capsButton.isSelected()). The model owns them; the components only show them.
QwertyGUI.refresh() – one method redraws everything
/** The whole screen, always redrawn from the model. */
private void refresh() {
display.setText(model.getText());
statusLabel.setText("Characters: " + model.getLength());
capsButton.setSelected(model.isCapsLock());
for (int i = 0; i < LETTERS.length; i++) {
letterButtons.get(i).setText(model.applyCase(LETTERS[i]));
}
}Because every listener ends with refresh(), the display, the counter, the Caps button and the button labels can never disagree. Keeping four components in sync "by hand" in four listeners is where GUI bugs come from.
The solution also listens to the physical keyboard. The recommended way in Swing is a key binding: the window's InputMap maps a key to a name, the ActionMap maps the name to an action, and the action simply clicks the matching button. Unlike a KeyListener, this works whichever component has the focus.
QwertyGUI.bindKey – a key presses a button
private void bindKey(String keyStroke, AbstractButton button) {
JRootPane root = frame.getRootPane();
root.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
.put(KeyStroke.getKeyStroke(keyStroke), keyStroke); // key → name
root.getActionMap().put(keyStroke, new AbstractAction() { // name → action
@Override
public void actionPerformed(ActionEvent e) {
button.doClick(); // as if the button had been clicked
}
});
}| Event | Listener | What it does |
|---|---|---|
Q…Y button or key | QwertyListener(letter) – inner class | appends the letter in the current case (model.type) |
Back button or Backspace | this::backspace – method reference | deletes the last character, if any (model.backspace) |
CLR button or Esc | lambda | clears the text (model.clear) |
Caps toggle | lambda | switches case, the button labels follow (model.toggleCapsLock) |
| QWERTY → Exit (Ctrl+Q) | anonymous class | exits the program |
The same calculator in MVC
The calcMVC example is the calculator rebuilt the way you would structure a larger application, with a history list, a memory (MC, MR, M+, M−), errors shown in a status bar instead of a dialog, and JUnit tests for the model. It is a Maven project with three packages – one per layer (Fig. 7). The next session goes deeper into this separation; here it is worth seeing what changes compared to MV.
- Model (
calc.model): the rules and the state – history, memory. When it changes it notifies its subscribers, without knowing who they are. - View (
calc.view): builds the window and shows only strings it is given. It doesn't hand out its buttons; the controller subscribes throughonXxx(…)methods, so whether a function is a button, a menu item or a shortcut is the view's own business. - Controller (
calc.controller): reads text from the view, parses it, calls the model, reports errors, and redraws the view when the model changes.
CalculatorModel – compute, store, notify
private final List<Runnable> listeners = new ArrayList<>();
public void addChangeListener(Runnable listener) {
listeners.add(listener);
}
public Calculation calculate(double left, Operation operation, double right) {
double result = operation.apply(left, right); // the rules live here
Calculation calculation = new Calculation(left, operation, right, result);
history.add(0, calculation);
fireChanged(); // "something changed"
return calculation;
}CalculatorController – wiring both directions
public CalculatorController(CalculatorModel model, CalculatorView view) {
this.model = model;
this.view = view;
view.onOperation(this::calculate); // user event → controller
view.onEnterInOperand(() -> calculate(lastOperation));
view.onClearHistory(model::clearHistory); // straight to the model
model.addChangeListener(this::render); // model changed → view
render();
}The view's side of onOperation shows why Consumer<Operation> is the parameter type: every operation button gets the same handler, and the handler is told which operation was chosen.
CalculatorView.onOperation
public void onOperation(Consumer<Operation> handler) {
operationButtons.forEach((operation, button)
-> button.addActionListener(e -> handler.accept(operation)));
}| Type | The handed-over code receives | Used for |
|---|---|---|
Runnable | nothing | most buttons and menu items, the model’s change notification |
Consumer<Operation> | an operation | onOperation: which operation button was pressed |
IntConsumer | an int | onHistorySelected: which history entry was chosen |
Where does the arithmetic go – the controller or the model? The model. The controller only mediates; the rules (what mod means, what division by zero does) are the core of the program. If they were in the controller, only clicking could test them, and a command-line version would have to rewrite them.
CalculatorController.calculate – mediates, doesn't compute
double left = parse(view.getOperandText(Operand.A)); // text → number
double right = parse(view.getOperandText(Operand.B));
Calculation calculation = model.calculate(left, operation, right); // the model computes
view.showResult(format(calculation.result())); // number → text| MV (calc, qwerty) | MVC (calcMVC) | |
|---|---|---|
| Classes | model + view | model + view + controller |
| Who handles events? | the view | the controller |
| Who updates the screen? | the view, after every event (refresh()) | the controller, when the model reports a change |
| When is it the right size? | small programs – plenty for the assignments | larger programs, several views of one model |
Towards the second assignment
The second assignment is a two-player board game (or a logic game) in Swing. The shared requirements:
- a user-friendly interface and an object-oriented solution – a layered architecture is not mandatory, but separating the model from the view is strongly recommended;
- the board drawn with controls (a grid of buttons) or with graphics;
- the window always fits the board, for every board size;
- a New game menu with the board sizes the task specifies; the program detects the end of the game, announces the winner (or a draw) and starts a new game automatically;
- documentation: task analysis, a UML class diagram of the program, and the event – listener pairs with a short description of each.
The fragments below are building blocks, not solutions. Board is your game's model – the rules – and you write it for your own task.
The structure to aim for: a model (Board, Field, Player …) that holds the board and the rules and imports nothing from Swing, so you can test it with JUnit as in Session 4; and a view (…GUI, BoardGUI) that builds the window, forwards events to the model and redraws the board from it.
Common mistakes
Practice
Task 1
An on-screen QWERTY keyboard
Build a window with a display (a text field) and the buttons Q, W, E, R, T, Y, Back (deletes the last character) and CLR (clears the display). Only the buttons may change the display. Add a menu with an Exit item, and use MV: the text lives in a model class.
Approach
Content pane with BorderLayout: the display in NORTH, a FlowLayout panel of keys in CENTER. Create the letter buttons in a loop over an array; each needs a listener that knows its letter.
private static final String[] LETTERS = {"Q", "W", "E", "R", "T", "Y"};
JPanel keyPanel = new JPanel(); // FlowLayout
for (String letter : LETTERS) {
JButton button = new JButton(letter);
button.addActionListener(/* a listener that knows its letter */);
keyPanel.add(button);
}The model
A StringBuilder is the natural field – it appends and deletes at the end cheaply. Every listener changes the model, then calls one refresh() that copies getText() into the display.
public class QwertyModel {
private final StringBuilder text = new StringBuilder();
public void type(String letter) { text.append(letter); }
public void backspace() { /* delete the last char if there is one */ }
public String getText() { return text.toString(); }
}Task 2
Caps and a character counter
Add a Caps toggle button that switches between upper and lower case – the labels of the letter buttons follow – and a status bar in SOUTH showing Characters: n.
Approach
A JToggleButton stays pressed, but don't ask it for its state: store capsLock in the model and let refresh() set the toggle, the labels and the counter.
capsButton = new JToggleButton("Caps");
capsButton.addActionListener(e -> {
model.toggleCapsLock();
refresh(); // refresh() calls capsButton.setSelected(model.isCapsLock())
});Task 3
Type with the real keyboard
Make the physical Q…Y, Backspace and Esc keys work as if the matching button had been clicked.
Approach
Use key bindings on the root pane with WHEN_IN_FOCUSED_WINDOW, and let the action call the button's doClick() – that way the key and the button share one listener. Key names for KeyStroke.getKeyStroke are "Q", "BACK_SPACE", "ESCAPE". Make the display non-focusable so it doesn't swallow the keys.
Task 4
A full keyboard with a length limit
Extend the keyboard to three rows (QWERTYUIOP, ASDFGHJKL, ZXCVBNM), add Space and an Enter that shows the typed text in a JOptionPane. At most 30 characters fit on the display: when it is full, the letter buttons are disabled.
The rows
One panel per row, and the row panels stacked in a one-column grid.
String[] rows = {"QWERTYUIOP", "ASDFGHJKL", "ZXCVBNM"};
JPanel keyboard = new JPanel(new GridLayout(0, 1)); // one row per panel
for (String row : rows) {
JPanel rowPanel = new JPanel();
// for each char of row: a button, added to rowPanel
keyboard.add(rowPanel);
}The limit
The limit is a rule, so the model enforces it (type does nothing when full). The view only reflects it in refresh().
// in refresh(): the model decides, the view only reflects it
boolean full = model.getLength() >= QwertyModel.MAX_LENGTH;
for (JButton button : letterButtons) {
button.setEnabled(!full);
}Task 5
A new operation for the calculator
Add a remainder operation (mod) to the MV calculator. You should not have to touch CalcGUI at all – if you do, ask why.
Approach
The buttons are generated from Operation.values(), so one new enum constant is the whole change. Think about which input must be rejected, as DIVIDE does.
MODULO("mod", (a, b) -> {
// which check does DIVIDE already do that this one needs too?
return a % b;
}),Task 6
A counter, twice
Write a window with a label showing a number, a +1 and a Reset button. First in MV (the view calls the model, then refresh()), then in MVC (the model notifies change listeners, a controller wires the view's buttons to the model). Compare which class changed.
The MVC version
The model keeps a List<Runnable> of listeners and calls them after increment and reset. The view doesn't know the model; the controller connects both directions.
// the view only exposes what the controller needs
public void showValue(int value) { valueLabel.setText("Value: " + value); }
public JButton getIncButton() { return incButton; }
// the controller connects both directions
view.getIncButton().addActionListener(e -> model.increment());
model.addChangeListener(() -> view.showValue(model.getValue()));For more detail on layouts and listeners, the Oracle tutorial Creating a GUI With Swing is still the best reference – in particular Laying Out Components Within a Container and Writing Event Listeners.
Summary
- A Swing window is a tree: JFrame → content pane → panels → controls. Every component has exactly one parent.
- Each container has one layout manager. Complex screens come from nesting panels with simple layouts – sketch the screen first.
- BorderLayout for the main split, FlowLayout for rows of buttons, GridLayout for boards and keypads, BoxLayout for forms.
- Listeners can be named inner classes, anonymous classes, lambdas or method references; this::method hands a method over, it doesn’t call it.
- Build the GUI with SwingUtilities.invokeLater and keep listeners fast – they run on the one Event Dispatch Thread.
- Keep the rules in a Swing-free model. A listener reads input, calls the model and calls refresh(); refresh() redraws everything from the model.
- MV is plenty for the assignments; MVC adds a controller and a model that notifies its listeners.