Leepoint Java Reference Notes on the Java language and its standard library.

PaintDemo: a mouse-driven painting program

Event-driven drawing in Swing: mouse listeners, paintComponent, and a BufferedImage that makes the picture persist.

Drawing with the mouse needs two things: somewhere to record what has been drawn, and a paintComponent that redraws it. Getting those the wrong way round is the classic beginner mistake.

The wrong way

public void mouseDragged(MouseEvent e) {
    Graphics g = getGraphics();       // don't
    g.fillOval(e.getX(), e.getY(), 4, 4);
}

This draws, and the drawing vanishes the moment another window passes over it. Swing repaints by calling paintComponent, and anything painted outside that method is not part of the component’s idea of itself. Repainting wipes it.

The right way

Record the points; paint from the record.

public class PaintPanel extends JPanel {
    private final List<Point> points = new ArrayList<>();

    public PaintPanel() {
        addMouseMotionListener(new MouseMotionAdapter() {
            @Override public void mouseDragged(MouseEvent e) {
                points.add(e.getPoint());
                repaint();
            }
        });
    }

    @Override
    protected void paintComponent(Graphics g) {
        super.paintComponent(g);
        for (Point p : points) {
            g.fillOval(p.x - 2, p.y - 2, 4, 4);
        }
    }
}

Three things carry the whole example.

super.paintComponent(g) first. It clears the background. Omit it and previous frames smear.

repaint(), not a direct paint call. It asks Swing to schedule a repaint; Swing then calls paintComponent when it is ready, possibly coalescing several requests. Never call paintComponent yourself.

The list is the drawing. The window is just a view of it. Resize, minimise, cover it — the drawing survives, because it is redrawn from data every time.

Dots or lines

Recording single points and drawing dots leaves gaps when the mouse moves fast, because you get one event per poll, not one per pixel. Storing consecutive pairs and drawing lines between them looks continuous:

g.drawLine(prev.x, prev.y, cur.x, cur.y);

Keeping strokes as separate lists also lets a mouse-release end a stroke, so lifting the mouse does not connect to wherever it next goes down.

Undo, for free

Because the drawing is data, undo is removing the last stroke and calling repaint(). That is the practical payoff of the separation: features that would be very hard against a raw canvas become trivial against a model.

How the program is put together

The example divides into three classes, and the split is the point of it rather than an implementation detail:

  • PaintDemo builds the user interface and holds the main program — the frame, the button panel and the colour choices.
  • PaintPanel is the surface that gets drawn on. It listens for mouse events and it owns the paintComponent override. Everything the mouse does arrives here.
  • Shape is an enum naming the shapes that can be drawn. Enum constants used this way read better than integer constants and the compiler checks them for you.

The listeners and paintComponent never call each other. They communicate through instance variables on the panel: a listener records where the drag started and where it is now, calls repaint(), and paintComponent reads those fields the next time Swing asks it to draw. Trying to draw directly from inside a listener is the mistake this example exists to prevent.

BufferedImage, and why the picture survives

paintComponent is asked to redraw the whole panel whenever the window is uncovered, resized or scrolled. A program that only draws the current stroke loses everything that came before it the first time that happens.

The fix is to accumulate into a BufferedImage — an off-screen image the program owns — and draw that to the panel each time. Every completed stroke is written into the image; paintComponent then blits the image and, if a drag is in progress, the shape being dragged on top of it. The image is the document, the panel is only a view of it.

Graphics2D rather than plain Graphics is what makes this workable: the drawing operations onto the buffered image, stroke widths, and the antialiasing hint below all need the richer class. Casting the Graphics handed to paintComponent is standard:

@Override
protected void paintComponent(Graphics g) {
    super.paintComponent(g);
    Graphics2D g2 = (Graphics2D) g;
    g2.drawImage(canvas, 0, 0, null);
    if (dragging) {
        g2.setColor(currentColor);
        drawCurrentShape(g2);
    }
}

Smoother output

Graphics2D g2 = (Graphics2D) g;
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING,
                    RenderingHints.VALUE_ANTIALIAS_ON);
g2.setStroke(new BasicStroke(3, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND));

Graphics is always a Graphics2D in practice; casting gives you strokes, transforms and antialiasing.