Malen in Swing Teil 1: der grundlegende Mechanismus
Inhaltsverzeichnis
Dieser Artikel zeigt den korrekten Gebrauch der Methoden "paintComponent" und "repaint" in einer Swing Anwendung.
Die paintComponent Methode
Zum Malen in Swing wird ein "callback" Mechanismus benutzt ("Wiederholungsbesuch"). Das bedeutet, daß ein Programm den Darstellungscode der Komponente innerhalb einer bestimmten überschriebenen Methode setzen sollte, und das Toolkit ruft diese Methode auf, wenn es Zeit ist zu malen. Die zu überschreibende Methode ist in javax.swing.JComponent:
<code=java>protected void paintComponent(Graphics g)</code=java>
Da in Swing ein Fenster, wie z.B. "JFrame", keine JComponent ist, werden wir nie direkt auf ein Fenster malen, sondern immer auf eine Swing Komponente (z.B. JPanel oder JComponent), die dem Fenster hinzugefügt wurde. Wenn das System die Methode "paintComponent" aufruft, ist der Parameter Graphics g mit dem passenden Zustand für das Malen auf dieser bestimmten Komponente vorkonfiguriert:
- Die Farbe des Graphics Objektes wird auf die foreground-Eigenschaft der Komponente eingestellt.
- Die Schriftart des Graphics Objektes wird auf die font-Eigenschaft der Komponente eingestellt.
- Die "translation" des Graphics Objektes wird so eingestellt, daß die Koordinate (0,0) die obere linke Ecke der Komponente darstellt.
- Das "clip"-Rechteck des Graphics Objektes wird auf den Bereich der Komponente eingestellt, der neu gezeichnet werden muss.
Programme müssen dieses Graphics Object verwenden (oder ein von ihm abgeleitetes) um die Oberfläche darzustellen. Sie sind frei, den Zustand des Graphics Objektes so zu ändern, wie es benötigt wird. Dabei ist es nützlich zu wissen, daß das Graphics Objekt in paintComponent eigentlich ein Graphics2D ist, eine Erweiterung von Graphics mit Verbesserungen bezüglich Steuerung der Geometrie, Umwandlungen von Koordinaten, Farbenverwaltung und Textdarstellung. Graphics2D ist die grundlegende Klasse zur Wiedergabe von 2D Formen, Text und Bildern auf der Java Plattform. Um sie in paintComponent benutzen zu können, müssen wir lediglich das Graphics Objekt nach Graphics2D "casten": <code=java>Graphics2D g2d = (Graphics2D)g;</code=java>
Im allgemeinen sollten Programme es vermeiden, darstellenden Code an irgendeinen Punkt zu setzen, wo er von außerhalb des Bereichs der paintComponent Methode aufgerufen werden könnte. Warum? Weil solcher Code manchmal aufgerufen werden kann, wenn es nicht angebracht ist zu malen -- zum Beispiel bevor die Komponente sichtbar ist oder Zugang zu einem gültigen Graphics Objekt hat. Es wird nicht empfohlen, daß Programme paintComponent() direkt aufrufen.
In einem systemausgelösten Malvorgang, bittet das System eine Komponente, ihren Inhalt darzustellen, normalerweise aus einem der folgenden Gründe:
- Die Komponente wird zum erstenmal sichtbar auf dem Bildschirm abgebildet.
- Die Komponente wird in der Größe verändert.
- Die Komponente wurde beschädigt und muß repariert werden (zum Beispiel wurde etwas verschoben, das vorher die Komponente verdeckte, und ein vorher verdeckter Teil der Komponente wird sichtbar).
In allen drei Fällen ruft das System automatisch die paintComponent Methode der Komponente auf. Es leuchtet ein, daß paintComponent möglichst schnell reagieren sollte, daher werden wir "teure" Sachen dort vermeiden wollen, wie z.B Bilder laden, Datenbank lesen, neue Objekte erzeugen, ...
Das SwingPaintDemo Programmbeispiel zeigt den einfachen Gebrauch der paintComponent() callback Methode von Swing:
<code=java>import java.awt.*; import javax.swing.*;
/*
*************************************************************** * Silly Sample program which demonstrates the basic paint * mechanism for Swing components. *************************************************************** */
public class SwingPaintDemo {
public static void main(final String[] args) { Runnable gui = new Runnable() {
@Override public void run() { JFrame f = new JFrame("Aim For the Center"); f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); Container panel = new BullsEyePanel(); panel.add(new JLabel("BullsEye!", SwingConstants.CENTER), BorderLayout.CENTER); f.getContentPane().add(panel, BorderLayout.CENTER); f.pack(); f.setLocationRelativeTo(null); f.setVisible(true);
} }; //GUI must start on EventDispatchThread: SwingUtilities.invokeLater(gui); }
}
/**
* A Swing container that renders a bullseye background * where the area around the bullseye is transparent. */
class BullsEyePanel extends JPanel {
public BullsEyePanel() { super(); setOpaque(false); // we don't paint all our bits setLayout(new BorderLayout()); setBorder(BorderFactory.createLineBorder(Color.black)); }
@Override public Dimension getPreferredSize() { // Figure out what the layout manager needs and // then add 200 to the largest of the dimensions // in order to enforce a 'round' bullseye Dimension layoutSize = super.getPreferredSize(); int max = Math.max(layoutSize.width, layoutSize.height); return new Dimension(max + 200, max + 200); }
@Override protected void paintComponent(final Graphics g) { super.paintComponent(g); Dimension size = getSize(); int x = 0; int y = 0; int i = 0; while (x < size.width && y < size.height) { g.setColor(i % 2 == 0 ? Color.RED : Color.WHITE); g.fillOval(x, y, size.width - (2 * x), size.height - (2 * y)); x += 10; y += 10; i++; } }
}</code=java>
BEMERKUNGEN:
- Wenn wir paintComponent überschreiben, sollten wir keine dauerhaften Veränderungen an dem übergebenen Graphics Objekt machen. Zum Beispiel sollten wir das "Clip" Rechteck oder die "Transform" nicht verändern. Wenn wir diese Operationen benötigen, können wir einfach mit Graphics#create() ein neues Graphics Objekt ableiten und es manipulieren. Um nicht unnötig Speicherplatz zu verschwenden, sollten wir dieses Graphics Objekt nachher mit dispose() wieder freigeben.
- Wenn wir die Super-Implementation nicht aufrufen, also kein super.paintComponent(g), müssen wir die "opaque" Eigenschaft der Komponente berücksichtigen, das heisst, wenn diese Komponente deckend ist (setOpaque(true)), müssen wir den Hintergrund vollständig in einer deckenden, nicht halbdruchsichtigen, Farbe ausfüllen. Wenn wir das nicht tun, werden wir wahrscheinlich visuelle Verunreinigungen sehen.
Die repaint Methode
Bei einem anwendungsausgelösten Malvorgang entscheidet die Komponente, daß sie ihren Inhalt aktualisieren muß, weil sich ihr interner Zustand geändert hat. Beispiel: ein Button erkennt, daß eine Maustaste betätigt worden ist und stellt fest, daß er einen "niedergedrückten" Button malen muß.
Um anwendungsausgelöstes Malen zu ermöglichen, liefert das Toolkit die folgende java.awt.Component Methode, damit Programme einen asynchronen Malvorgang anfragen können:
<code=java> public void repaint() </code=java>
Der Aufruf der repaint Methode veranlasst indirekt den Aufruf der paintComponent Methode der angesprochenen Komponente, und zwar zu dem für das System am geeignetsten Zeitpunkt. Daraus folgt, daß die repaint Methode natürlich niemals von der paintComponent Methode selbst aufgerufen werden sollte!
Der folgende Code zeigt ein einfaches Beispiel von einem MouseListener, der repaint() benutzt, um Updates auf einem theoretischen Button auszulösen, wenn die Maus gedrückt und losgelassen wird:
<code=java> MouseListener l = new MouseAdapter() {
public void mousePressed(final MouseEvent e) { MyButton b = (MyButton)e.getSource(); b.setSelected(true); b.repaint(); } public void mouseReleased(final MouseEvent e) { MyButton b = (MyButton)e.getSource(); b.setSelected(false); b.repaint(); } };</code=java>
Die Methode MyButton#setSelected setzt die Instanzvariable "selected" welche von paintComponent benutzt wird, um die Ausgabe zu steuern.
Falls paintComponent noch andere Variablen oder Objekte zum Zeichnen benötigt, können wir sie der Komponente über Konstruktorparameter, Settermethoden oder Ähnliches übergeben. Die Referenzen werden dann in Instanzvariablen gespeichert, so daß paintComponent problemlos auf sie zugreifen kann. Die folgende Komponente benutzt z.B. ein Objekt einer (nicht gezeigten) Klasse "Animal". Das Objekt wird im Konstruktor oder mit Hilfe der Methode "setAnimal(..)" an die Komponente übergeben: <code=java>class Cage extends JComponent {
private Animal animal;
public Cage(final Animal animal) { this.animal = animal; }
@Override protected void paintComponent(final Graphics g) { super.paintComponent(g); g.setColor(Color.BLACK); if (animal.getSpecies().equals(Animal.LION)) { g.setColor(Color.RED); } if (animal.getSpecies().equals(Animal.TIGER)) { g.setColor(Color.GREEN); } g.fillOval(animal.getCoord().x, animal.getCoord().y, 10, 10); }
public void setAnimal(final Animal animal) { this.animal = animal; }
}
//Beispiel für die Objektübergabe:
Cage cage = new Cage(new Animal(Animal.LION));// im Konstruktor
//...
cage.setAnimal(new Animal(Animal.TIGER));// mit Hilfe der Methode setAnimal(..)
</code=java>
Der Aufruf von repaint auf der Komponente bewirkt dann den asynchronen Aufruf der paintComponent Methode, welche das aktualisierte "Animal"-Objekt korrekt darstellt:
<code=java>cage.repaint();</code=java>
Die repaint Methode mit Argumenten
Komponenten, die komplizierte Darstellungen ausgeben, sollten repaint() mit den Argumenten aufrufen, welche nur den Bereich definieren, der eine Aktualisierung erfordert:
<code=java> public void repaint(int x, int y, int width, int height) </code=java>
Diese Argumente dienen dazu, das "clip"-Rechteck zu definieren, das im Graphics Objekt der paintComponent Methode den Bereich darstellt, der aktualisieren muss (Graphics#getClipBounds).
Es ist ein allgemeiner Fehler, immer die keine-Argumente Version zu benutzen, die immer die gesamte Komponente neu zeichnet, was häufig zu nicht notwendigen Malverarbeitungen führt. Ein Beispiel für die Verwendung von repaint() mit Argumenten wird im zweiten Teil dieses Tutorials eingeführt: