Java Swing Timer und Animation
Willemers Informatik-Ecke
Swing Ereignisse Swing Layout

Swings eigener Timer

Swing stellt, wie die meisten anderen grafischen Oberflächen auch, einen Timer zur Verfügung. Es wäre auch möglich, mit Threads zu arbeiten. In diesem Fall muss aber darauf geachtet werden, dass Swing wie viele Bibliotheken nicht threadfähig ist. Das bedeutet, dass nicht von mehreren Threads parallel Swing-Aufrufe stattfinden dürfen.

Ereignis

Der Timer löst ein Action-Ereignis aus, der mit einem ActionListener bearbeitet wird. Es ist derselbe Listener, der bei einem Buttonklick eingesetzt wird.

Die Klasse muss also ActionListener implementieren. Das führt dazu, dass die Klasse auch die Methode actionPerformed implementieren muss. Anders als beim Button-Event muss der ActionListener nicht mit addActionListener angemeldet werden, weil dies bereits der Konstruktor von Timer übernimmt.

Anlegen eines Timers

import javax.swing.Timer;

Timer timer = new Timer(zeit, listener);
Der Timer wird erzeugt. Der erste Parameter seines Konstruktors legt den Takt in Millisekunden fest, zu dem der Timer wiederholt ein ActionEvent sendet.

Mit der Methode timer.setRepeats kann umgeschaltet werden, ob der Timer wiederholt oder nur einmal auslösen soll.

timer.setRepeats(true);

Start und Stop

Der Timer beginnt erst zu laufen, wenn er über die Methode start einen Startschuss bekommen hat. Mit der Methode stop kann der Timer wieder angehalten werden.
import javax.swing.Timer;

class TimerPanel extends JPanel implements ActionListener {

   Timer timer = new Timer(1000, this); // alle 1000 msec

   public TimerPanel() {
        timer.start(); // Starte den Timer
   }

   @Override
   public void actionPerformed(ActionEvent e) {
      // Tue irgendwas im Sekundentakt
   }
}
Der Aufruf von addActionListener ist im Gegensatz zum JButton nicht erforderlich.

Die Bombe tickt

Das folgende Beispiel simuliert eine tickende Bombe, die rechtzeitig gestoppt werden muss. Der Button startet und stoppt den Timer. Ein Label zeigt die verbleibenden Sekunden beziehungsweise die Explosion an.

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.SwingConstants;
import javax.swing.Timer;

public class ZeitbombeSwingTimer extends JFrame 
                            implements ActionListener {

    private Timer timer;
    private JButton btStartStopp = new JButton("Start");
    private JLabel ticker = new JLabel("Tick", SwingConstants.CENTER);
    private int sekunden = 10;

    public ZeitbombeSwingTimer() {
        setLayout(new BorderLayout());
        add(BorderLayout.SOUTH, btStartStopp);
        add(BorderLayout.CENTER, ticker);
        timer = new Timer(1000, this);
        timer.setRepeats(true); // immer wieder feuern!
        btStartStopp.addActionListener(this);
        setSize(200, 100);
        setVisible(true);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    }

    @Override
    public void actionPerformed(ActionEvent e) {
        // Wer hat die Action losgetreten?
        if (e.getSource() == btStartStopp) { // Der Button!
            sekunden = 10;
            if (timer.isRunning()) {
                timer.stop();
                btStartStopp.setText("Start");
            } else {
                timer.start();
                btStartStopp.setText("Stopp");
                ticker.setText("" + sekunden);
            }
        } else { // Es muss wohl der Timer gewesen sein.
            sekunden--;
            if (sekunden == 0) {
                ticker.setText("Bumm");
                timer.stop();
                btStartStopp.setText("Start");
            } else {
                ticker.setText("" + sekunden);
            }
        }
    }

    public static void main(String[] args) {
        new ZeitbombeSwingTimer();
    }
}
Das Timer-Ereignis meldet sich wie ein Button beim ActionListener. Darum muss dieser die Event-Quellen unterscheiden und entsprechend unterschiedlich reagieren. Die Eventquelle wird über die Methode getSource ermittelt.

Der Timer meldet sich beim Programm quasi-unterbrechend. Tatsächlich handelt es sich nicht um eine echte Nebenläufigkeit. Er fügt sich allerdings recht gefällig in ein Swing-Programm hinein.

Animation

Es soll ein Ball durch das Fenster gleiten. Das wird erreicht indem wie im Film alle paar Millisekunden die Position des Balls verändert wird.

Der Fensterrahmen hat nur die Aufgabe, das Panel festzuhalten und birgt keine Überraschungen, wenn man mit einem JFrame vertraut ist.

import javax.swing.JFrame;
  
public class Frame extends JFrame {

    public static void main(String[] args) {
        new Frame();
    }

    public Frame() {
        this.add(new Panel());
        this.setSize(400, 300);
        this.setVisible(true);
        this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    }
}
Die Verarbeitung des Timer-Events erfolgt im Panel. Es muss darum das Interface ActionListener implementieren.

Um ActionListener zu implementieren, muss die Methode actionPerformed implementiert werden. Diese fordert das Objekt ziel auf, sich zu bewegen und führt zu einem Neuzeichnen des Bildschirminhalts.

Der Timer wird als Attribut angelegt und auf 50 Millisekunden eingestellt. Dabei wird das Panel auch gleich als behandelnde Instanz für den Timer-Event angemeldet.

Im Konstruktor wird der Timer gestartet.

Die Method paint übergibt an das Objekt ziel die Panel-Größe, damit dieses prüfen kann, ob es einen Rand erreicht. Anschließend wird ziel als gefüllter Kreis mit fillOval gezeichnet.

import java.awt.Graphics;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JPanel;
import javax.swing.Timer;

public class Panel extends JPanel implements ActionListener {

    private Ziel ziel = new Ziel();
    private Timer timer = new Timer(20, this);

    public Panel() {
        timer.start();
    }

    @Override
    public void paintComponent(Graphics g) {
        super.paintComponent(g);
        ziel.setSize(getWidth(), getHeight());
        g.fillOval(ziel.getX(), ziel.getY(), ziel.groesse(), ziel.groesse());
    }

    @Override
    public void actionPerformed(ActionEvent arg0) {
        ziel.move();
        repaint();
    }
}
Bleibt zu guter Letzt noch das Ziel. Es enhält die eigene Position, liefert diese mit Gettern und führt durch den Aufruf von move zur nächsten Position.
public class Ziel {
  
    int x = 0, y = 0;
    int diffx = 2;
    int diffy = 1;

    public void setSize(int width, int height) {
        if (x>= width || x < 0) {
            diffx *= -1; // Richtung umdrehen
        }
        if (y>=height || y < 0) {
            diffy *= -1; // Richtung umdrehen
        }
    }

    public int getX() {
        return x;
    }

    public int getY() {
        return y;
    }

    public int groesse() {
        return 20;
    }

    public void move() {
        x += diffx;
        y += diffy;
    }
}

Geschmeidig durch Pufferung mit BufferedImage

Wenn eine Animation über den Bildschirm gezogen wird, führt das schnell zu einem unangenehmen Flackern. Das entsteht, weil in der Methode paint viel gerechnet und aufgebaut wird.

Das lässt sich verbessern, indem nicht auf den Bildschirm sondern in ein Image gezeichnet wird, das bei Aufruf von paint mit der Methode drawImage angezeigt wird.

BufferedImage anlegen

import java.awt.image.BufferedImage;
BufferedImage buffer = new BufferedImage(breite, breite, BufferedImage.TYPE_INT_RGB);

Graphics aus dem Image gewinnen

Graphics g = buffer.createGraphics();
// zeichne mit g wie in paint(Graphics g)
g.dispose(); 

paint zeichnet das Image

Die paint-Methode kann entspannen. Die eigentliche Zeichnung kann überall erfolgen. paint muss nur noch dafür sorgen, dass das Image hinein.
@Override
public void paintComponent(Graphics g) {
    super.paintComponent(g);
    g.drawImage(buffer, 0, 0, this);
}

Fenstergrößenänderung

Sobald die Größe des Fensters verändert wird, stimmt die Größe des Images nicht mehr. Die einfachste Lösung besteht darin, ein neues Image in der neuen Größe anzulegen.
@Override
public void paintComponent(Graphics g) {
    super.paintComponent(g);
    if (bufWidth!=getWidth() || bufHeight!=getHeight()) { // Änderung der Fenstergröße
        bufWidth = getWidth();
        bufHeight = getHeight();
        buffer = new BufferedImage(bufWidth, bufHeight, BufferedImage.TYPE_INT_RGB);
        ziel.setSpielfeld(bufWidth, bufHeight);
    } else {
        g.drawImage(buffer, 0, 0, this);
    }
}

ComponentListener übernimmt Größenänderungen

Will man bei Änderungen größere Aktionen durchführen, könnte sich die Verwendung des ComponentListener lohnen. Er muss vom Panel implementiert werden.
public class BufferedPanel extends JPanel implements ComponentListener{
Danach kann man bei Eclipse mit der Tastenkombination [Strg]+[Shift]+[O] den Import erzeugen lassen. Eclipse wird auch bemängeln, dass einige Methoden nicht implementiert sind. Die Reparatur übernimmt Eclipse gern selbst.

Im Konstruktor muss das Fenster als ComponentListener angemeldet werden.

this.addComponentListener(this);
Bei jeder Änderungen der Fenstergröße wird dann die Methode componentResized aufgerufen. Hier kann man nun den eigenen Code ablegen.
@Override
public void componentResized(ComponentEvent arg0) {
    bufWidth = getWidth();
    bufHeight = getHeight();
    buffer = new BufferedImage(bufWidth, bufHeight, BufferedImage.TYPE_INT_RGB);
    ziel.setSpielfeld(bufWidth, bufHeight);
}
Nun können die Vergrößerungsreaktionen des Programms zwar aus der Methode paint entfernt werden. Dennoch wird nach wie vor paint aufgerufen, wenn das Fenster vergrößert oder verkleinert wird.

Unterschiede zwischen paint und Image

Einen Unterschied gibt es allerdings. Während das JPanel im strahlenden Grau erscheint, verwendet das Image als Basisfarbe schwarz. Außerdem wird von paint der Bildschirm gelöscht. Anmerkung: Bei Linux erfolgt das erst bei Aufruf von paint der Basisklasse.

Um jeweils mit einem sauberen Bildschirm zu beginnen, wird das Image mit einem weißen Rechteck gezeichnet und anschließend auf die Zeichenfarbe gesetzt.

g.setColor(Color.white);
g.fillRect(0, 0, bufWidth, bufHeight);
g.setColor(Color.red);

Swing Ereignisse Swing Layout