Week 7: GUI I and Action Listeners
Swing, paintComponent, listeners · 01.06.2026
Slides
Download Week 7 Slides (PDF) →
Code Examples
Three reference packages, one per leap we make today. basicui/ is the “five core Swing components” greeting form — JFrame, JPanel, JLabel, JTextField, JButton wired together with a lambda listener. canvas/ drops the pre-built components and shows the paintComponent + repaint() interactive loop. geosnap/ is the advanced one: world ↔ screen AffineTransform plus buffer snapping for clean polygon closure.
Code/Week7/src/at/ac/univie/gis/week7/
├── basicui/
│ └── BasicUIExample.java JFrame + JPanel + JLabel + JTextField + JButton, GridLayout, lambda ActionListener
├── canvas/
│ └── CustomCanvasExample.java extends JPanel, overrides paintComponent, MouseAdapter + repaint() loop
└── geosnap/
└── GeoSnapExample.java AffineTransform world↔screen, buffer snapping to close a polygon
1. basicui/ — the five core Swing components
BasicUIExample.java — A tiny greeting form built from the five components you’ll reach for the most: JFrame (the outer window), JPanel (the invisible canvas that holds everything), JLabel, JTextField, JButton. The panel uses GridLayout(3, 1) so the three components stack and stretch — comment that line out to see the messy default FlowLayout for comparison. The button is wired with a one-line lambda ActionListener: this is the modern shortcut for the verbose anonymous-inner-class form on slide 14.
package at.ac.univie.gis.week7.basicui;
import java.awt.GridLayout;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTextField;
public class BasicUIExample {
public static void main(String[] args) {
// Step 1: create the outer window and tell it to actually quit
// the program when the user closes it.
JFrame frame = new JFrame("Greeting Form");
frame.setSize(400, 200);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
// Step 2: add an invisible panel inside the window. All visible
// components live on the panel, not directly on the frame.
JPanel panel = new JPanel();
panel.setLayout(new GridLayout(3, 1)); // three rows, one column
frame.add(panel);
// Step 3: create the three interactive components.
JLabel label = new JLabel("Enter your name:");
JTextField textField = new JTextField(15);
JButton button = new JButton("Greet me!");
// Step 4: add them to the panel in the order we want them to appear.
panel.add(label);
panel.add(textField);
panel.add(button);
// Step 5: wire the button with a lambda ActionListener.
// The verbose form would be:
// button.addActionListener(new ActionListener() { ... });
button.addActionListener(e -> {
String name = textField.getText();
if (name == null || name.isBlank()) {
label.setText("Please type your name first.");
} else {
label.setText("Hello, " + name + "!");
}
});
// Step 6: finally, make the window visible.
frame.setVisible(true);
}
}
2. canvas/ — paintComponent and the repaint loop
CustomCanvasExample.java — The full interactive cycle in one file: click → listener fires → data updated → repaint() called → Java calls paintComponent() → screen refreshes. The data — a List<int[]> of click points — lives on the panel; paintComponent only reads from it. Try commenting out the repaint() call inside mouseClicked: clicks stop showing up until you resize the window, which is exactly why that one line matters. MouseAdapter is used instead of MouseListener so we only override the one method we care about — a lambda won’t work here because MouseListener has five abstract methods.
package at.ac.univie.gis.week7.canvas;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.RenderingHints;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.List;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JPanel;
public class CustomCanvasExample extends JPanel {
private static final int DOT_RADIUS = 6;
// The "model": our data lives in a simple list of points.
// The view (paintComponent) only reads from this list.
private final List<int[]> points = new ArrayList<>();
public CustomCanvasExample() {
setPreferredSize(new Dimension(600, 500));
setBackground(Color.WHITE);
// Register a MouseListener on this panel so we hear about clicks.
addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
// 1. Update the data: store the click location.
points.add(new int[]{e.getX(), e.getY()});
// 2. Request a redraw - Java will call paintComponent() soon.
repaint();
}
});
}
/**
* IMPORTANT: never call this method yourself. Java calls it whenever the
* panel needs to be (re)drawn. Each call wipes the canvas via
* super.paintComponent() and redraws everything from the current data.
*/
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
// "Upgrade" the brush to Graphics2D so we get anti-aliasing.
Graphics2D g2d = (Graphics2D) g;
g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
RenderingHints.VALUE_ANTIALIAS_ON);
// Connect consecutive points with line segments.
g2d.setColor(Color.BLUE);
for (int i = 1; i < points.size(); i++) {
int[] a = points.get(i - 1);
int[] b = points.get(i);
g2d.drawLine(a[0], a[1], b[0], b[1]);
}
// Then draw a red dot at each clicked location.
g2d.setColor(Color.RED);
for (int[] p : points) {
g2d.fillOval(p[0] - DOT_RADIUS, p[1] - DOT_RADIUS,
DOT_RADIUS * 2, DOT_RADIUS * 2);
}
}
public static void main(String[] args) {
JFrame frame = new JFrame("Custom Canvas: click to draw");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
CustomCanvasExample canvas = new CustomCanvasExample();
// A small control bar with a Clear button (lambda style).
JButton clear = new JButton("Clear");
clear.addActionListener(e -> {
canvas.points.clear();
canvas.repaint();
});
JPanel controls = new JPanel();
controls.add(clear);
// BorderLayout puts the controls at the bottom (SOUTH) and the
// drawing canvas in the middle (CENTER).
frame.setLayout(new BorderLayout());
frame.add(canvas, BorderLayout.CENTER);
frame.add(controls, BorderLayout.SOUTH);
frame.pack();
frame.setVisible(true);
}
}
3. geosnap/ — world ↔ screen coordinates and buffer snapping
GeoSnapExample.java — The advanced one. It combines the three trickier GUI ideas from today’s lecture: (1) the screen/world coordinate mismatch — screen origin is top-left with Y pointing down, but our spatial data uses a Y-up world; (2) AffineTransform as the clean fix — configure it once, then draw using world coordinates directly, and use the inverse to turn mouse clicks back into world coordinates; (3) buffer snapping — a click within a small pixel tolerance of the first vertex snaps onto it exactly, producing a topologically closed polygon instead of a frustrating tiny gap. The snap check is done in screen space because the tolerance is expressed in pixels, which is what the user actually sees. Tighten SNAP_TOLERANCE_PIXELS to 2 and try to close a polygon to feel how frustrating digitising without snapping is.
package at.ac.univie.gis.week7.geosnap;
import java.awt.*;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.geom.AffineTransform;
import java.awt.geom.NoninvertibleTransformException;
import java.awt.geom.Point2D;
import java.util.ArrayList;
import java.util.List;
import javax.swing.*;
public class GeoSnapExample extends JPanel {
private static final double WORLD_SIZE = 100.0;
private static final double SNAP_TOLERANCE_PIXELS = 12.0;
private static final int VERTEX_RADIUS_PIXELS = 5;
/** Polygon vertices in WORLD coordinates (Y points up). */
private final List<Point2D.Double> vertices = new ArrayList<>();
private boolean closed = false;
private final JLabel statusLabel;
public GeoSnapExample(JLabel statusLabel) {
this.statusLabel = statusLabel;
setPreferredSize(new Dimension(600, 600));
setBackground(Color.WHITE);
addMouseListener(new MouseAdapter() {
@Override
public void mouseClicked(MouseEvent e) {
handleClick(e.getX(), e.getY());
}
});
}
/**
* Build the world -> screen transform in three operations:
* 1. translate the origin down to the bottom edge of the panel
* 2. scale so WORLD_SIZE units fit the panel
* 3. flip the Y axis (negative scale on Y) so world-Y goes up on screen
*
* Note: AffineTransform operations are applied in REVERSE order to the
* point - the last call here happens first when the transform is applied.
*/
private AffineTransform worldToScreen() {
double scaleX = getWidth() / WORLD_SIZE;
double scaleY = getHeight() / WORLD_SIZE;
double scale = Math.min(scaleX, scaleY); // uniform scaling, no distortion
AffineTransform t = new AffineTransform();
t.translate(0, getHeight()); // 1) move origin to bottom-left
t.scale(scale, -scale); // 2) & 3) scale + flip Y
return t;
}
private void handleClick(int screenX, int screenY) {
if (closed) {
return; // polygon already closed; ignore further clicks
}
// Buffer-snap check is done in SCREEN space because the tolerance
// is expressed in pixels - that's what the user actually sees.
// We only allow closing once we have at least 3 vertices.
if (vertices.size() >= 3) {
Point2D.Double firstScreen = worldToScreenPoint(vertices.get(0));
double dx = firstScreen.x - screenX;
double dy = firstScreen.y - screenY;
if (Math.hypot(dx, dy) <= SNAP_TOLERANCE_PIXELS) {
closed = true;
statusLabel.setText("Polygon closed with " + vertices.size() + " vertices.");
repaint();
return;
}
}
// Otherwise convert the screen click back into world coordinates and
// add a new vertex. This is the inverse of worldToScreen().
try {
Point2D world = worldToScreen().inverseTransform(
new Point2D.Double(screenX, screenY), null);
vertices.add(new Point2D.Double(world.getX(), world.getY()));
statusLabel.setText(String.format(
"Added vertex %d at world (%.2f, %.2f).",
vertices.size(), world.getX(), world.getY()));
repaint();
} catch (NoninvertibleTransformException ex) {
// Cannot happen for a uniform scale + translate.
}
}
// ... paintComponent draws a faint world grid, the polygon (filled once
// closed, otherwise an open polyline), and a green snap-target ring
// around the first vertex once vertices.size() >= 3. See the full file.
}