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.

22 min readComponent tree and nested panelsBorderLayout, FlowLayout, GridLayout, BoxLayoutListeners: inner class, lambda, method referenceEvent Dispatch Thread and invokeLaterModel–view: one refresh() from the modelBoard of buttons for the second assignment

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.

SwingWeb (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 / gridGrid, StackPanel, HBox / VBox
addActionListener(…)addEventListener("click", …), onClickClick event, setOnAction(…)
Event Dispatch Thread (EDT)the browser’s event loopthe 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.

JFrame“Calculator” – the windowJMenuBarframe.setJMenuBar(…)content paneJPanel · BorderLayoutJMenu “Calc”Clear · Use result as A · ExitnumPanelBorderLayout, in CENTERbuttonPanelGridLayout(1, 0), in SOUTHlabelPanelGridLayout(0, 1), in WESTfieldPanelGridLayout(0, 1), in CENTER5 × JButtonone per operation3 × JLabelA: · B: · Result:3 × JTextFieldoperand1 · operand2 · resultBlue = containers, each with its own layout manager. Grey = what the user sees and clicks.
Fig. 1 · The calculator's component tree. Containers (blue) only arrange; the leaves are the controls. A component has exactly one parent – adding it somewhere else moves it, it is not copied.
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).

BorderLayoutNORTHWCENTERESOUTHFlowLayoutQWERTYBackrow full → wraps,rows are centredGridLayout(3, 3)123456789BoxLayout (Y_AXIS)JLabelJTextFieldJButtonone column, own sizesthe content pane’s defaulta JPanel’s defaultboards, keypadsforms, listscells filled row by row
Fig. 2 · Four layouts, four behaviours. BorderLayout gives CENTER all the leftover space, FlowLayout wraps, GridLayout makes every cell the same size, BoxLayout keeps each component's own size.
LayoutHow it worksUse it for
BorderLayoutfive regions: NORTH, SOUTH, EAST, WEST, CENTER; one component per regionthe main split of a window (default of the content pane)
FlowLayoutleft to right in a row, wraps to a new row when fulla row of buttons, a toolbar (default of JPanel)
GridLayoutrows × cols equal cells, filled row by row; GridLayout(0, 2) = 2 columns, as many rows as neededa game board, a keypad
BoxLayouta single row or column, components keep their preferred sizeforms, 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).

CalculatorCalcA:B:Result:read-only+-*/^numPanel → CENTER of the content paneBorderLayoutlabelPanel → WEST, fieldPanel → CENTERGridLayout(0, 1): one column eachcontent pane: BorderLayoutbuttonPanel → SOUTHGridLayout(1, 0): one rowDraw the screen first, then give every panel one simple layout and nest the panels.
Fig. 3 · The calculator sketched with its panels. The labels and the fields sit in two separate one-column grids – one two-column grid would make the label column as wide as the field column.

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 width

The 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)
FormPick it when
named inner classthe listener has a parameter (a letter, an operation) or several controls share it
anonymous classyou read older code – before lambdas this was the usual form
lambdathe code is short and used once
method referencea 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).

main threadinvokeLater(build the GUI)… then main endsuser and OSclicks, keys, window eventsevent queuebuild the GUIActionEvent (+)KeyEvent: EscrepaintEvent Dispatch Threadtakes one event at a timenextyour listeneractionPerformed(e)returnsrunsA slow listener (Thread.sleep, a long loop) blocks the queue: no repaint, no clicks – the window freezes.Repeated work on a timer goes through javax.swing.Timer, which puts its events into the same queue.
Fig. 4 · The EDT works through the event queue one event at a time. Building the GUI is just the first job in the queue.

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 it

Keep 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 – CalcModel computes and stores the result, Operation holds 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.

userCalcGUIlistener + refresh()CalcModelno Swingresult fieldJTextFieldclick “*”parse A and B: text → doublecalculate(6, MULTIPLY, 7)stores result = 42refresh()getResult()42result.setText("42")On a NumberFormatException or ArithmeticException: clearResult(), refresh(), then an error dialog.
Fig. 5 · One click in the MV calculator. The arithmetic happens in the model; the result field is written in exactly one place, refresh(), and always from 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).

Qwerty+ main(String[])createsQwertyGUI- model : QwertyModel- frame : JFrame- display : JTextField- statusLabel : JLabel- capsButton : JToggleButton- letterButtons : List<JButton>- createMenuBar() : JMenuBar- bindKey(String, AbstractButton)- refresh()- backspace(ActionEvent)model1QwertyModel- text : StringBuilder- capsLock : boolean+ type(String)+ backspace()+ clear()+ toggleCapsLock()+ isCapsLock() : boolean+ applyCase(String) : String+ getText() : String+ getLength() : intinner classQwertyListener- letter : String+ actionPerformed(ActionEvent)«interface»ActionListener+ actionPerformed(ActionEvent)model.type(letter)Swing types appear only in QwertyGUI; QwertyModel imports nothing from javax.swing.
Fig. 6 · The QWERTY program in MV. QwertyGUI holds the components and the listeners; QwertyModel holds the text and the Caps state. The ⊕ marks QwertyListener as an inner class of QwertyGUI.
  • 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
        }
    });
}
EventListenerWhat it does
Q…Y button or keyQwertyListener(letter) – inner classappends the letter in the current case (model.type)
Back button or Backspacethis::backspace – method referencedeletes the last character, if any (model.backspace)
CLR button or Esclambdaclears the text (model.clear)
Caps togglelambdaswitches case, the button labels follow (model.toggleCapsLock)
QWERTY → Exit (Ctrl+Q)anonymous classexits 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.

Calcmain: creates and wirescalc.viewCalculatorViewbuilds the UI · show…, on…calc.controllerCalculatorControllerevents → model → viewcalc.modelCalculatorModelhistory · memoryCalculation«record»Operation«enum» · the rulesshow…()eventscalculate()changedbutton labels – the view’s only import from the modelcalc.model imports nothing from javax.swing: the JUnit tests call it directly, without a window.
Fig. 7 · Three layers, three packages. The model never calls the view or the controller; it only announces that it changed.
  • 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 through onXxx(…) 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)));
}
TypeThe handed-over code receivesUsed for
Runnablenothingmost buttons and menu items, the model’s change notification
Consumer<Operation>an operationonOperation: which operation button was pressed
IntConsumeran intonHistorySelected: 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)
Classesmodel + viewmodel + view + controller
Who handles events?the viewthe 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 assignmentslarger 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.