Malen in Swing Teil 1: der grundlegende Mechanismus

Aus Byte-Welt Wiki
Version vom 23. März 2022, 19:49 Uhr von Stefanhuglfing (Diskussion | Beiträge)
(Unterschied) ← Nächstältere Version | Aktuelle Version (Unterschied) | Nächstjüngere Version → (Unterschied)
Zur Navigation springenZur Suche springen

Dieser Artikel zeigt den korrekten Gebrauch der Methoden "paintComponent" und "repaint" in einer Swing Anwendung. Über den Unterschied zwischen AWT und Swing, siehe den Artikel:

Malen in AWT und Swing. Dort wird auch gezeigt, warum wir in Swing "paintComponent" anstatt "paint" benutzen.

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:

protected void paintComponent(Graphics g)

Da in Swing ein Fenster, wie z.B. "JFrame" oder "JApplet", keine javax.swing.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 Objekt verwenden (oder ein mit Graphics#create() von ihm abgeleitetes, siehe Bemerkung weiter unten) 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":

Graphics2D g2d = (Graphics2D)g;

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, dass 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:

BullsEye.jpg

import java.awt.*;
import javax.swing.*;

/*
 ***************************************************************
 * Silly Sample program which demonstrates the basic paint
 * mechanism for Swing components.
 ***************************************************************
 */
public class SwingPaintDemo {

    public SwingPaintDemo() {
    }

    public static void createAndShowGui() {
        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);
    }

    public static void main(final String[] args) {
        Runnable gui = new Runnable() {

            @Override
            public void run() {
                createAndShowGui();
            }
        };
        //GUI must start on EventDispatchThread:
        SwingUtilities.invokeLater(gui);
    }

    /**
     * A Swing container that renders a bullseye background
     * where the area around the bullseye is transparent.
     */
    private static class BullsEyePanel extends JPanel {

        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++;
            }
        }
    }
}

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 von dem vorgegebenen Graphics g ableiten und es manipulieren. Um nicht unnötig Speicherplatz zu verschwenden, sollten wir dieses Graphics Objekt nachher mit dispose() wieder freigeben. Alternativ können wir auch die alten Werte der Eigenschaften sichern ehe wir sie verändern, und dann wieder zurücksetzen nachdem wir alles gemalt haben. Diese Alternative ist leistungsfähiger, weil kein zweites Graphics Objekt angelegt und wieder freigegeben werden muss.
  • Wenn wir die Super-Implementation nicht aufrufen, also kein super.paintComponent(g), müssen wir die "opaque" Eigenschaft der Komponente berücksichtigen, das heißt, wenn diese Komponente deckend ist (d.h. isOpaque() gibt "true" zurück), müssen wir den Hintergrund vollständig in einer deckenden, nicht halb durchsichtigen, Farbe ausfüllen. Wenn wir das unterlassen, werden wir wahrscheinlich visuelle Verunreinigungen sehen. Es ist also einfacher, immer "super.paintComponent(g)" aufzurufen, dann kümmert sich Swing um den Hintergrund, so dass wir keine visuellen Verunreinigungen sehen werden.
  • Es ist generell keine gute Idee, setPreferredSize(..) innerhalb von "paintComponent" aufzurufen. Statt dessen ist es oft besser, getPreferredSize() zu überschreiben um dort die gewünschte Größe zurückzugeben.
  • Zu Demonstrationszwecken ist es sinnvoll, die Mallogik vollständig innerhalb der JComponent Klasse zu halten. Aber wenn unsere Anwendung mehrere Instanzen einer Grafik erfassen muss, können wir ihren Code in eine separate Klasse auslagern, so dass jede Grafik als ein einzelnes Objekt behandelt werden kann. Diese Technik ist in der 2D Spiele-Programmierung üblich und wird manchmal als Sprite-Animation bezeichnet. Siehe auch: Performing Custom Painting, wo im Teil "Refining the Design" die Klasse RedSquare den Malcode in eine Methode auslagert, die ein Graphics-Objekt akzeptiert, und aus der paintComponent-Methode des Panels aufgerufen wird.
  • Es ist auch oft vorteilhaft, das Malen von ausgelagerten Grafiken unabhängig von ihrer konkreten Umsetzung zu gestalten. Dazu erstellen wir eine Schnittstelle Paintable (oder wie auch immer wir sie nennen wollen), die die paint-Methode der Grafik definiert, zum Beispiel public void paint( Graphics g ). Wenn alle Grafikklassen diese Schnittstelle implementieren, können wir in der paintComponent-Methode diese "Paintable"-Objekte malen, ohne dass wir uns um ihren konkreten Typ kümmern müssen. Wir können sie sogar in einer Liste zusammenfassen, die wir dann zum Malen in einer Schleife durchlaufen können. Siehe auch: Einsteigertutorial von Beni und Roar, wo das "interface Figure" die Mal-Schnittstelle darstellt.
  • Jede Swing Komponente hat eine getGraphics-Methode. Diese Methode gibt einen Grafik Kontext zurück, mit dem wir außerhalb der paintComponent-Methode auf die Komponente malen können. Die offizielle Richtlinie ist, dass wir das nicht tun sollten, auch wenn es in einigen Fällen bequemer erscheinen mag. Es steht im Widerspruch zum Swing callback-Mechanismus, der das mit getGraphics Gemalte wieder zerstören kann, z.B. wenn die Komponente in der Größe verändert wird.

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:

    public void repaint()

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:

        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();
            }
        };

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 "Cage" 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:

CageLion.jpg

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(..)

Der Aufruf von repaint auf der "Cage" Komponente bewirkt dann den asynchronen Aufruf der paintComponent Methode, welche das aktualisierte "Animal"-Objekt korrekt darstellt:

cage.repaint();

CageTiger.jpg

BEMERKUNG:

Um ein zu animierendes Objekt zu zeichnen, gibt es grob gesagt zwei Möglichkeiten:

  • Wir zeichnen das Objekt auf eine JComponent, die wir zur Basiskomponente hinzufügen.
  • Wir zeichnen das Objekt direkt auf die Basiskomponente.

Um das Objekt zu verschieben, benutzen wir im ersten Fall die Methode "Component#setLocation" und im anderen Fall verändern wir die Koordinaten bevor wir "repaint" aufrufen. Dieser Animationsvorgang muss in einem eigenen Thread geschehen. Eine einfache Möglichkeit, diesen Thread zu verwirklichen, besteht darin, einen javax.swing.Timer zu benutzen.

Um bei dem obigen Beispiel den Löwen (lion) im Käfig (cage) zum Laufen zu bringen, könnten wir z.B. folgendes programmieren:

Timer timer = new javax.swing.Timer(500, new ActionListener() {

    @Override
    public void actionPerformed(final ActionEvent e) {
        lion.setCoord(new Point(lion.getCoord().x + 2, lion.getCoord().y));
        cage.repaint();
    }
});
timer.start();

Durch diesen Timer bewegt sich der Löwe in Schritten von jeweils 2 Pixel jede halbe Sekunde (=500 Millisekunden) nach rechts.

Das Tutorial von Quaxli zeigt eine Möglichkeit auf, wie man kleinere Spiele in Java selbst programmiert: download

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:

    public void repaint(int x, int y, int width, int height)

Diese Argumente dienen dazu, das "clip"-Rechteck zu definieren, das im Graphics Objekt der paintComponent Methode den Bereich darstellt, der aktualisieren muss (Graphics#getClipBounds). Die paintComponent Methode muss nur das neuzeichnen, was sich mit dem "clip"-Rechteck überschneidet.

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:

Malen in Swing Teil 2: ein einfaches Malprogramm

Fragen

Das Thema wurde nicht ausreichend behandelt? Du hast Fragen dazu und brauchst weitere Informationen? Lass Dir von uns helfen!

Wir helfen dir gerne!


Dir hat dieser Artikel gefallen? Oder Du hast Fehler entdeckt und möchtest zur Berichtigung beitragen? Prima! Schreibe einen Kommentar!

Du musst angemeldet sein, um einen Kommentar abzugeben.