Malen in AWT und Swing

Aus Byte-Welt Wiki
Wechseln zu: Navigation, Suche

Malen in AWT und Swing

Guter Malcode ist der Schlüssel zu einer leistungsfähigen Anwendung.

Von Amy Fowler.

Copyright 1994-2008 Sun Microsystems, Inc.

Übersetzung: André Uhres (Ich habe bei Sun die Erlaubnis angefragt, diesen Artikel zu übersetzen und hier zu veröffentlichen)

In einem graphischen System ist normalerweise ein Fensterdarstellungstoolkit dafür verantwortlich, einen Rahmen zur Verfügung zu stellen, der es relativ einfach macht, auf einer graphischen Benutzerschnittstelle (GUI) die richtigen Bits zur richtigen Zeit darzustellen. Sowohl AWT (abstraktes Fensterdarstellungstoolkit) als auch Swing liefern solch einen Rahmen. Aber die APIs, die ihn verwirklichen, werden von manchen Entwicklern nicht gut verstanden -- ein Problem, das zu Programmen geführt hat, die nicht so leistungsfähig sind, wie sie sein könnten.

Dieser Artikel beschreibt die AWT und Swing Malmechanismen im Einzelnen. Sein Zweck ist, Entwicklern zu helfen, korrekten und leistungsfähigen GUI Malcode zu schreiben. Während der Artikel die allgemeinen Malmechanismen umfaßt (wo und wann man darstellt), erklärt er nicht, wie man die Swing Graphiken APIs benutzt, um korrekte Graphiken darzustellen. Um zu erlernen wie man nette Graphiken macht, besichtigen Sie bitte die Java 2D Website.

Entwicklung des Swing Malsystems

Als die ursprüngliche AWT API für JDK 1.0 entwickelt wurde, gab es nur heavyweight Komponenten ("heavyweight" bedeutet, daß die Komponente ihr eigenes undurchlässiges natives Fenster hat). Dies erlaubte es dem AWT, sich voll auf das Maluntersystem der nativen Plattform zu stützen. Dieser Entwurf kümmerte sich um Einzelheiten wie Beschädigungserkennung, Clipberechnung und Z-Einrichtung.

Mit der Einführung der lightweight Komponenten in JDK 1.1 (eine "lightweight" Komponente ist eine, die das native Fenster ihres nächsten heavyweight Vorfahrs wiederverwendet), mußte das AWT das Malen für lightweight Komponenten im gemeinsamen Java Code einführen. Somit gibt es feine Unterschiede bezüglich des Malvorgangs für heavyweight und lightweight Komponenten. Nach dem JDK 1.1, als das Swing Toolkit freigegeben wurde, stellte dieses seine eigenen Malmechanismen vor. In den meisten Fällen ähnelt und beruht der Swing Malmechanismus auf dem AWT. Aber er führt auch einige Unterschiede bezüglich der Mechanismen ein, sowie neue APIs, mit denen die Anwendungen den Malvorgang einfacher anpassen können.

Malen in AWT

Zu verstehen, wie die Mal-API von AWT arbeitet, hilft zu erkennen, was einen Malvorgang in einer Fensterdarstellungsumgebung auslöst. In AWT gibt es zwei Arten von Malvorgängen: systemausgelöstes Malen und anwendungsausgelöstes Malen.

Systemausgelöstes Malen

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

Anwendungsausgelöstes Malen

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ß.

Die paint Methode

Ungeachtet dessen, wie ein Malantrag ausgelöst wird, benutzt das AWT einen "callback" ("Wiederholungsbesuch") Mechanismus zum Zeichnen, und dieser Mechanismus ist derselbe für heavyweight und lightweight Komponenten. Das heißt, 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 java.awt.Component:

public void paint(Graphics g)

Wenn AWT diese Methode aufruft, ist der Parameter "Graphics g" mit dem passenden Zustand für das Zeichnen 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. Hier ist ein einfaches Beispiel von einem paint callback, der einen gefüllten Kreis innerhalb einer Komponente darstellt:

    public void paint(Graphics g) {
        // Dynamically calculate size information
        Dimension size = getSize();
        // diameter
        int d = Math.min(size.width, size.height); 
        int x = (size.width - d)/2;
        int y = (size.height - d)/2;
 
        // draw circle (color already set to foreground)
        g.fillOval(x, y, d, d);
        g.setColor(Color.black);
        g.drawOval(x, y, d, d);
    }

Entwickler, die neu bei AWT sind, werden einen Blick auf das PaintDemo Beispiel werfen wollen, das ein lauffähiges Programmbeispiel liefert, wie man den paint callback in einem AWT Programm verwendet:

PaintDemo

Im allgemeinen sollten Programme es vermeiden, darstellenden Code an irgendeinen Punkt zu setzen, wo er von außerhalb des Bereichs der paint callback 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 paint() direkt aufrufen.

Um anwendungsausgelöstes Malen zu ermöglichen, liefert das AWT die folgenden java.awt.Component Methoden, damit Programme einen asynchronen Malvorgang anfragen können:

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

Der folgende Code zeigt ein einfaches Beispiel von einem MouseListener, der repaint() benutzt, um Updates auf einer theoretischen Taste auszulösen, wenn die Maus gedrückt und losgelassen wird:

        MouseListener l = new MouseAdapter() {
            public void mousePressed(MouseEvent e) {
                MyButton b = (MyButton)e.getSource();
                b.setSelected(true);
                b.repaint();            
            }
 
            public void mouseReleased(MouseEvent e) {
                MyButton b = (MyButton)e.getSource();
                b.setSelected(false);
                b.repaint();            
            }
        };

Komponenten, die komplizierte Darstellungen ausgeben, sollten repaint() mit den Argumenten aufrufen, welche nur den Bereich definieren, der eine Aktualisierung erfordert. 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.

paint() gegen update()

Warum unterscheiden wir zwischen systemausgelöstem und anwendungsausgelöstem Malen? Weil AWT jeden dieser Fälle etwas anders für heavyweight Komponenten behandelt (der lightweight Fall wird später besprochen), was leider eine Quelle großer Verwirrung ist.

Für heavyweight Komponenten geschehen diese zwei Arten des Zeichnens in zwei unterschiedlichen Wegen, abhängig davon, ob ein Malvorgang systemausgelöst oder anwendungsausgelöst ist.

systemausgelöstes Malen

dies ist, wie ein systemausgelöster Malvorgang stattfindet:

  1. Das AWT stellt fest, daß entweder ein Teil oder die ganze Komponente gemalt werden muß.
  2. Das AWT veranlasst den Event Dispatch Thread, paint() auf der Komponente aufzurufen.

anwendungsausgelöstes Malen

Ein anwendungsausgelöster Malvorgang findet wie folgt statt:

  1. Das Programm stellt fest, daß entweder ein Teil oder die ganze Komponente in Erwiderung auf eine interne Zustandsänderung neu gezeichnet werden muß.
  2. Das Programm ruft repaint() auf der Komponente auf, was einen asynchronen Antrag zum AWT registriert, daß diese Komponente neu gezeichnet werden muß.
  3. Das AWT veranlasst den Event Dispatch Thread, ein update() auf der Komponente aufzurufen. ANMERKUNG: Erfolgen mehrfache repaint() Aufrufe auf einer Komponente bevor der erste Antrag verarbeitet wird, können die mehrfachen Anträge zu einem einzigen Aufruf von update() zusammengefasst werden. Der Algorithmus für die Bestimmung, wann mehrfache Anträge zusammengefasst werden sollten, ist Implementierungsabhängig. Wenn mehrfache Anträge zusammengefasst werden, ist das resultierende Updaterechteck gleich der Vereinigung der Rechtecke, die in den zusammengefassten Anträgen enthalten sind.
  4. Wenn die Komponente nicht update() überschreibt, löscht die Default-Implementierung von update() den Hintergrund der Komponente (wenn es nicht eine lightweight Komponente ist) und ruft einfach paint() auf.

Da bei der Default-Implementierung das abschließende Resultat dasselbe ist wie bei paint(), verstehen viele Leute den Zweck einer unterschiedlichen update() Methode nicht. Während es zutreffend ist, daß die Default-Implementierung von update() die paint() aufruft, ermöglicht dieser "Updatehaken" einem Programm, den anwendungsausgelösten Malvorgang anders anzufassen, wenn es gewünscht wird. Ein Programm muß annehmen, daß ein Aufruf von paint() andeutet, daß der Bereich, der durch das Cliprechteck vom Graphics definiert wird, "beschädigt" ist und vollständig neu gezeichnet werden muß, gleichwohl ein Aufruf von update() dieses nicht andeutet, was einem Programm ermöglicht, zusätzliches Zeichnen zu tun.

Zusätzliches Zeichnen ("incremental painting") ist nützlich, wenn ein Programm die zusätzliche Darstellung über die vorhandenen Teilstücke dieser Komponente überlagern möchte. Das UpdateDemo Beispiel zeigt ein Programm, das Nutzen aus dem Verwenden von update() zieht, um zusätzliches Zeichnen zu tun:

UpdateDemo

In Wahrheit braucht die Mehrheit der GUI Komponenten kein zusätzliches Zeichnen zu tun. So können die meisten Programme die update() Methode ignorieren und einfach paint() überschreiben, um die Komponente in ihrem gegenwärtigen Zustand darzustellen. Das heißt, daß die systemausgelöste und anwendungsausgelöste Darstellung im Wesentlichen für die meisten Implementierungen gleichbedeutend ist.

Das Malen und die lightweight Komponenten

Von der Perspektive des Anwendungsentwicklers, ist die Mal-API im Allgemeinen dieselbe für lightweight und heavyweight Komponenten (das heißt, er überschreibt paint() und ruft repaint() auf, um Updates auszulösen). Jedoch seit AWTs lightweight Komponentenrahmen völlig im allgemeinen Java Code geschrieben wurde, sind dort einige subtile Unterschiede bezüglich der Weise, wie der Mechanismus für lightweight Komponenten implementiert wird.

Wie lightweight Komponenten gemalt werden

Damit eine lightweight Komponente besteht, benötigt sie einen heavyweight Vorfahr in der Containerhierarchie, um einen Platz zum Malen zu haben. Wenn diesem heavyweight Vorfahr gesagt wird, sein Fenster zu malen, muß er den Malaufruf übersetzen in Malaufrufe an alle seine lightweight Nachkommen. Dies wird durch die paint() Methode von java.awt.Container verarbeitet, welche paint() auf irgendwelchen seiner sichtbaren lightweight Kinder aufruft, die sich mit dem zu malenden Rechteck überschneiden. So ist es für alle Containerunterklassen (lighweight oder heavyweight), welche paint() überschreiben, notwendig, folgendes zu tun:

    public class MyContainer extends Container {
        public void paint(Graphics g) {
	    // paint my contents first...
	    // then, make sure lightweight children paint
	    super.paint(g); 
        }
    }

Wenn der Aufruf von super.paint() fehlt, dann werden die lightweight Nachkommen des Containers nicht dargestellt (ein sehr verbreitetes Problem als JDK 1.1 die lightweight Komponenten einführte).

Es ist nützlich anzumerken, daß die Default-Implementierung von Container.update() nicht die Rekursion verwendet, um update() oder paint() auf lightweight Nachkommen aufzurufen. Das heißt, daß irgendeine heavyweight Containerunterklasse, die update() benutzt, um zusätzlichen Zeichnen zu tun, sichergehen muß, daß lightweight Nachkommen wenn nötig rekursiv neu gezeichnet werden. Glücklicherweise benötigen wenige heavyweight Container zusätzliches Zeichnen, also beeinflußt diese Frage die meisten Programme nicht.

lightweight und systemausgelöstes Malen

Der lightweight Rahmencode, der das Fensterdarstellungsverhalten für lightweight Komponenten einführt (sichtbar machen, verstecken, bewegen, die Größe neu bestimmen, usw..), ist völlig in Java geschrieben. Innerhalb der Java Implementierung dieser Funktionen, muß das AWT häufig verschiedenen lightweight Komponenten ausdrücklich erklären, sich zu malen (im Wesentlichen systemausgelöstes Malen, obwohl es nicht mehr vom nativen System ausgeht). Jedoch benutzt der lightweight Rahmen repaint(), um Komponenten zu erklären, sich zu malen, was einen Aufruf von update() anstatt eines Direktaufrufs von paint() ergibt, wie wir oben gesehen haben. Folglich kann systemausgelöstes Zeichnen für lightweight Komponenten zwei Wegen folgen:

  • Der systemausgelöste Malantrag entsteht im nativen System (d.h. der heavyweight Vorfahr der lightweight Komponente wird zum ersten Mal sichtbar), was einen Direktaufruf von paint() ergibt.
  • Der systemausgelöste Malantrag entsteht im lightweight Rahmen (d.h., die Größe der lightweight Komponente wird neu bestimmt), was einen Aufruf von update() ergibt, der in der Default-Implementierung an paint() weitergeleitet wird.

Dies bedeutet, daß es für lightweight Komponenten keine reale Unterscheidung zwischen update() und paint() gibt, was weiter bedeutet, daß die zusätzliche Zeichnentechnik (incremental painting) nicht für lightweight Komponenten verwendet werden sollte.

lightweight und Transparenz

Da lightweight Komponenten den Bildschirmplatz eines heavyweight Vorfahrs "borgen", unterstützen sie die Eigenschaft der Transparenz. Dies funktioniert, weil lightweight Komponenten von hinten nach vorne gemalt werden und folglich, wenn eine lightweight Komponente einige oder all ihre Bits unbemalt läßt, die dahinterliegende Komponente "durchscheint". Dies ist auch der Grund, warum die Default-Implementierung von update() den Hintergrund nicht löscht, wenn die Komponente lightweight ist.

Das LightweightDemo Programmbeispiel zeigt die Transparenzeigenschaft der lightweight Komponenten:

LightweightDemo

Das "intelligente" Malen

Während das AWT versucht, den Prozeß der Darstellung der Komponenten so leistungsfähig wie möglich zu machen, kann die paint() Implementierung einer Komponente selbst eine bedeutende Auswirkung auf die gesamte Leistung haben. Zwei Schlüsselbereiche, die diesen Prozeß beeinflussen können, sind:

  • Die Clipinformationen verwenden, um den Bereich, der dargestellt wird, einzuschränken.
  • Internes Wissen über das Layout verwenden, um den Bereich, welche Kinder gemalt werden, einzuschränken (nur lightweight).

Wenn deine Komponente einfach ist -- zum Beispiel, wenn es eine Drucktaste ist -- dann ist sie die Mühe nicht wert, die Darstellung zu bearbeiten, um nur den Teil zu malen, der das Cliprechteck überschneidet; es ist vorzuziehend, die gesamte Komponente zu malen und die Graphics passend einschränken zu lassen. Wenn du jedoch eine Komponente mit komplizierter Ausgabe erzeugt hast, wie eine Textkomponente, dann ist es notwendig, daß dein Code die Clipinformationen gebraucht, um den Umfang der Darstellung einzuschränken.

Weiter, wenn du einen komplizierten lightweight Container schreibst, der zahlreiche Komponenten beherbergt, wo der Container und/oder sein Layoutmanager Informationen über das Layout hat, dann ist er es wert dieses Layoutwissen zu verwenden, um intelligenter in der Bestimmung zu sein, welche der Kinder gemalt werden müssen. Die Default-Implementierung von Container.paint() schaut einfach der Reihe nach durch die Kinder und prüft die Sichtbarkeit und Überschneidung -- ein Vorgang, der mit bestimmten Layouts unnötigerweise aufwendig sein kann. Zum Beispiel, wenn ein Container die Komponenten in einem Rasterfeld 100x100 auslegt, dann könnte diese Rasterfeldinformation verwendet werden, schneller festzustellen, welche von jenen 10000 Komponenten das Cliptechteck überschneiden und wirklich gemalt werden müssen.

AWT Malrichtlinien

Das AWT stellt eine einfache callback API für das Malen von Komponenten zur Verfügung. Wenn du sie verwendest, treffen die folgenden Richtlinien zu:

  1. Für die meisten Programme sollte aller Malcode innerhalb des Bereichs der paint() Methode der Komponente gesetzt werden.
  2. Programme können einen zukünftigen Aufruf von paint() auslösen, indem sie repaint() aufrufen, aber sie sollten nicht paint() direkt aufrufen.
  3. Bei Komponenten mit komplizierter Ausgabe, sollte repaint() mit Argumenten aufgerufen werden, die nur das Rechteck definieren, das aktualisieren muß, anstatt der keine-Argumente Version, die die gesamte Komponente veranlasst neu gezeichnet zu werden.
  4. Da ein Aufruf von repaint() zuerst einen Aufruf von update() ergibt, der in der Default-Implementierung an paint() weitergeleitet wird, können heavyweight Komponenten update() überschreiben, um zusätzliches Zeichnen (incremental painting) zu tun, wenn es gewünscht wird (lightweight Komponenten unterstützen nicht das zusätzliche Zeichnen).
  5. Erweiterungen von java.awt.Container, die paint() überschreiben, sollten immer super.paint() aufrufen, um sicherzustellen, dass die Kinder gemalt werden.
  6. Komponenten mit komplizierter Ausgabe sollten intelligenten Gebrauch vom Cliprechteck machen, um den Malvorgang auf den Bereich zu beschränken, der sich mit dem Clipbereich überschneidet.

Malen in Swing

Swing beginnt mit AWTs grundlegendem Malmodell und erweitert es, um die Leistung zu maximieren und die Erweiterbarkeit zu verbessern. Wie AWT, so unterstützt auch Swing den Malcallback und den Gebrauch von repaint(), um Updates auszulösen. Zusätzlich gibt Swing eingebaute Unterstützung für Doppelpufferung sowie Änderungen, um die zusätzliche Swing Struktur zu unterstützen (wie Ränder und der UI Delegate). Und schließlich stellt Swing die RepaintManager API für jene Programme zur Verfügung, die den Malmechanismus weiter anpassen möchten.

Unterstützung für Doppelpufferung

Eine der bemerkenswertesten Eigenschaften von Swing ist, daß es die Unterstützung für Doppelpufferung direkt ins Toolkit einbaut. Das geschieht durch die Bereitstellung der "doubleBuffered" Eigenschaft in javax.swing.JComponent:

    public boolean isDoubleBuffered()
    public void setDoubleBuffered(boolean o)

Die Doppelpufferung von Swing benutzt einen einzigen offscreen Puffer pro Containerhierarchie (normalerweise einen pro Fenster) wo die Doppelpufferung eingeschaltet worden ist. Obgleich diese Eigenschaft pro Komponente eingeschaltet werden kann, ergibt ihre Einschaltung in einem bestimmten Container den Effekt, dass alle lightweight Komponenten darunter in dem offscreen Puffer dargestellt werden, unabhängig davon wie die individuelle "doubleBuffered" Eigenschaft eingestellt ist.

Der Default-Wert dieser Eigenschaft ist bei allen Swingkomponenten auf "true" gesetzt. Aber die Einstellung, die wirklich zählt, ist in JRootPane, weil diese Einstellung effektiv Doppelpufferung für alles darunter einschaltet. In den meisten Fällen brauchen Swingprogramme nichts zu tun in Bezug auf Doppelpufferung, ausgenommen, zu entscheiden, ob sie ein- oder ausgeschaltet sein sollte (und für eine flackerfreie GUI wirst du wünschen, dass sie eingeschaltet ist!). Swing stellt sicher, daß die passende Art des Graphics Objektes (offscreen Bild Graphics für Doppelpufferung, sonst die original Graphics) der Komponente übergeben wird, so braucht die Komponente lediglich mit ihm zu zeichnen. Dieser Mechanismus wird später in diesem Artikel ausführlicher erklärt (im Abschnitt über die Malverarbeitung).

Zusätzliche Maleigenschaften

Swing stellt einige zusätzliche Eigenschaften von JComponent vor, um die Leistungsfähigkeit der internen Malalgorithmen zu verbessern. Diese Eigenschaften wurden eingeführt, um sich mit den folgenden zwei Fragen zu beschäftigen, die das Zeichnen von lightweight Komponenten zu einem kostspieligen Vorgang machen können:

  • Transparenz: Wenn eine lightweight Komponente gemalt wird, ist es möglich, daß die Komponente nicht alle ihre Bits malt, wenn sie teilweise oder ganz transparent ist; das heißt, daß, wann immer sie neu gezeichnet wird, das was dahinter liegt zuerst neu gezeichnet werden muss. Das erfordert vom System, die Containerhierarchie hinaufzugehen um den ersten zugrundeliegenden heavyweight Vorfahr zu finden, von dem der hinten-nach-vorne Malvorgang ausgehen soll.
  • Überlappende Komponenten: Wenn eine lightweight Komponente gemalt wird, ist es möglich, daß irgendeine andere lightweight Komponente sie teilweise verdeckt; das heißt, daß, wann immer die ursprüngliche lightweight Komponente gemalt wird, alle möglichen Komponenten, die die ursprüngliche Komponente überdecken (wo das Cliprechteck sich mit dem überlappenden Bereich schneidet), auch teilweise neu gezeichnet werden müssen. Dies erfordert vom System, bei jedem Malvorgang viel von der Containerhierarchie zu durchqueren und auf überlappende Komponenten zu überprüfen.

Die Opazität

Um im Fall von undurchlässigen Komponenten die Leistung zu verbessern, fügt Swing eine "opaque" Lese-Schreibeigenschaft zu javax.swing.JComponent hinzu:

    public boolean isOpaque()
    public void setOpaque(boolean o)

Die Einstellungen sind:

  • true: Die Komponente ist damit einverstanden, alle ihre Bits zu malen, die innerhalb ihrer rechteckigen Grenzen enthalten sind.
  • false: Der Komponente gibt keine Garantien über das Malen aller Bits innerhalb ihrer rechteckigen Grenzen.

Die "opaque" Eigenschaft läßt das Malsystem von Swing ermitteln, ob ein repaint Antrag auf einer bestimmten Komponente zusätzliche repaint Anträge der dahinter liegenden Vorfahren erfordert oder nicht. Der Default-Wert der "opaque" Eigenschaft für jede standard Swingkomponente wird durch das gegenwärtige Look and Feel UI Objekt eingestellt. Der Wert ist für die meisten Komponenten "true".

Einer der allgemeinsten Fehler, die Komponentenimplementierungen machen, ist, daß sie die "opaque" Eigenschaft auf "true" belassen, und dennoch den Bereich nicht vollständig darstellen, der durch ihre Grenzen definiert ist, was gelegentliche Unreinheiten in den nicht dargestellten Bereichen ergibt. Wenn eine Komponente entworfen wird, sollte sorgfältig überlegt werden, wie die "opaque" Eigenschaft zu behandeln ist, sowohl um sicherzustellen, daß Transparenz klug benutzt wird, da sie mehr Darstellungszeit kostet, und daß die Übereinkunft mit dem Malsystem eingehalten wird.

Die Bedeutung der "opaque" Eigenschaft wird häufig mißverstanden. Manchmal wird sie verstanden als "den Hintergrund der Komponente transparent machen". Jedoch ist dies nicht die strenge Deutung der Opazität in Swing. Einige Komponenten, wie eine Drucktaste, können die "opaque" Eigenschaft auf false setzen, um der Komponente eine nicht-rechteckige Form zu geben oder Raum um die Komponente herum zu behalten für vorübergehende Sichtbarmachungen, wie eine Fokusanzeige. In diesen Fällen ist die Komponente nicht opaque, aber der Hauptteil seines Hintergrundes wird dennoch ganz ausgefüllt!

Wie vorher definiert, ist die "opaque" Eigenschaft hauptsächlich ein Vertrag mit dem Repaintsystem. Wenn eine Komponente auch die "opaque" Eigenschaft benutzt, um zu definieren, wie Transparenz an den Sichtbarmachungen einer Komponente angewendet wird, dann sollte dieser Gebrauch von der Eigenschaft dokumentiert werden. (es kann für einige Komponenten vorzuziehen sein, zusätzliche Eigenschaften zu definieren, um die Sichtaspekte davon zu steuern, wie Transparenz angewendet wird. Z.B. hat javax.swing.AbstractButton die ContentAreaFilled Eigenschaft zu diesem Zweck.)

Eine andere Frage, die eine Anmerkung wert ist, ist wie sich Opazität auf die Randeigenschaft einer Swingkomponente bezieht. Der Bereich, der durch ein Randobjekt auf einer Komponente dargestellt wird, wird noch als ein Teil der Geometrie dieser Komponente betrachtet. Das heißt, daß, wenn eine Komponente "opaque" ist, sie immer noch für das Füllen des Bereichs verantwortlich ist, der durch den Rand besetzt wird. (der Rand überlagert dann einfach seine Darstellung über die undurchlässige Komponente).

Wenn eine dahinter liegende Komponente durch den Randbereich einer Komponente durchscheinen soll -- das heißt, wenn der Rand Transparenz unterstützt indem isBorderOpaque() "false" zurückgibt (z.B. EmptyBorder oder LineBorder mit roundedCorners) -- dann, muß die Komponente sich selbst auch als nicht opaque definieren und sicherstellen dass sie den Randbereich nicht übermalt.

Siehe auch: http://forum.java.sun.com/thread.jspa?messageID=10329261&start=1

"optimiert" zeichnen

Die Frage des Zeichnens von überlappenden Komponenten ist heikler. Selbst wenn keiner der direkten Geschwister einer Komponente diese überdeckt, ist es immer möglich, daß ein Nichtvorfahr Verwandter (wie ein "Vetter" oder "Tante") sie verdecken könnte. In solch einem Fall könnte das repaint einer einzelnen Komponente innerhalb einer komplizierten Hierarchie eine Menge Treewalking erfordern, um 'korrektes' Zeichnen sicherzustellen. Um unnötiges Durchqueren zu verringern, fügt Swing die read-only Eigenschaft "isOptimizedDrawingEnabled" zu javax.swing.JComponent hinzu:

    public boolean isOptimizedDrawingEnabled()


die Einstellungen sind:

  • true: Die Komponente zeigt an, daß keine seiner direkten Kinder sich überlappen.
  • false: Die Komponente gibt keine Garantie, ob seine direkten Kinder sich überlappen oder nicht.

Indem es die Eigenschaft "isOptimizedDrawingEnabled" überprüft, kann Swing schnell seine Suche nach überlappenden Komponenten zur repaint Zeit eingrenzen.

Da "isOptimizedDrawingEnabled" eine read-only Eigenschaft ist, ist also der einzige Weg, wie Komponenten den Default-Wert ändern können, die Klasse zu erweitern und diese Methode zu überschreiben, um den gewünschten Wert zurückzugeben. Alle standard Swingkomponenten geben "true" für diese Eigenschaft zurück, außer JLayeredPane, JDesktopPane und JViewPort. Wenn wir also z.B. ein JPanel mit einem Layout haben, das überlappende Komponenten zulässt (wie "OverlayLayout"), dann ist es gewöhnlich notwendig, JPanel zu erweitern und isOptimizedDrawingEnabled() zu überschreiben, um "false" zurückzugeben.

Die Malmethoden

Die Richtlinien, die auf AWTs lightweight Komponenten zutreffen, treffen auch auf Swingkomponenten zu -- zum Beispiel wird paint() aufgerufen, wenn es Zeit ist darzustellen -- außer daß Swing den paint() Aufruf in drei verschiedene Methoden unterteilt, die in folgender Reihenfolge aufgerufen werden:

    protected void paintComponent(Graphics g)
    protected void paintBorder(Graphics g)
    protected void paintChildren(Graphics g)

Swingprogramme sollten paintComponent() anstelle von paint() überschreiben. Obgleich die API es erlaubt, gibt es im Allgemeinen keinen Grund paintBorder() oder paintChildren() zu überschreiben (und wenn du es tust, sei dir sicher was du tust!). Dies macht es den Programmen einfacher, nur den Teil des Zeichnens zu überschreiben, den sie erweitern müssen. Z.B. löst dies das AWT Problem, das vorher erwähnt wurde, wo die Unterlassung, super.paint() aufzurufen alle lightweight Kinder am Erscheinen hinderte.

Das SwingPaintDemo Programmbeispiel zeigt den einfachen Gebrauch der paintComponent() callback Methode von Swing:

SwingPaintDemo

Das Malen und der UI Delegate

Die meisten standard Swingkomponenten haben ihren Look and Feel implementiert durch getrennte look-and-feel Objekte (genannt "UI Beauftragte"), für die steckbare Look and Feel Eigenschaft von Swing. Das heißt, daß das meiste oder das ganze Zeichnen der standard Komponenten dem UI Delegate übergeben wird und dies geschieht folgendermaßen:

  1. paint() ruft paintComponent() auf.
  2. Wenn die "ui" Eigenschaft nicht "null" ist, ruft paintComponent() ui.update() auf.
  3. Wenn die "opaque" Eigenschaft der Komponente "true" ist, füllt ui.update() den Hintergrund der Komponente mit der Hintergrundfarbe und ruft ui.paint() auf.
  4. ui.paint() stellt den Inhalt der Komponente dar.

Das heißt, daß Unterklassen von Swingkomponenten, die einen UI Delegate haben (im Gegensatz zu direkten Unterklassen von JComponent), super.paintComponent() innerhalb ihrer paintComponent Überschreibung aufrufen sollten:

    public class MyPanel extends JPanel {
        protected void paintComponent(Graphics g) {
	    // Let UI delegate paint first 
	    // (including background filling, if I'm opaque)
	    super.paintComponent(g); 
	    // paint my contents next....
        }
    }

Wenn aus irgendeinem Grund die Erweiterung der Komponente nicht zulassen möchte, daß der UI Delegate malt (wenn sie zum Beispiel die sichtbaren Elemente der Komponente vollständig ersetzt), kann sie den Aufruf von super.paintComponent() weglassen, aber sie ist dann für das Ausfüllen ihres eigenen Hintergrundes verantwortlich, wenn die "opaque" Eigenschaft "true" ist, wie im Abschnitt über die "opaque" Eigenschaft besprochen wurde.

Malverarbeitung

Swing verarbeitet "repaint" Anträge in einer etwas anderen Weise als AWT, obgleich das Resultat für den Anwendungsprogrammierer im Wesentlichen dasselbe ist -- paint() wird aufgerufen. Swing tut dies, um seine RepaintManager API zu unterstützen (wird später besprochen), sowie zur Verbesserung der Malleistung. In Swing kann das Malen zwei Wegen folgen, wie unten beschrieben:

(a) Der Malantrag entsteht im ersten heavyweight Vorfahr (normalerweise JFrame, JDialog, JWindow oder JApplet):

  1. der "event dispatching thread" ruft paint() für diesen Vorfahr auf,
  2. die Default-Implementierung von Container.paint() ruft rekursiv paint() auf allen lightweight Nachkommen auf,
  3. wenn die erste Swingkomponente erreicht wird, macht die Default-Implementierung von JComponent.paint() folgendes:
    1. wenn die "doubleBuffered" Eigenschaft der Komponente "true" ist und Doppelpufferung ist im RepaintManager der Komponente eingeschaltet, wandelt sie das Graphics Objekt um zu einem passenden offscreen Graphics.
    2. ruft paintComponent() auf (und übergibt offscreen Graphics, wenn "doubleBuffered")
    3. ruft paintBorder() auf (und übergibt offscreen Graphics, wenn "doubleBuffered")
    4. ruft paintChildren() auf (und übergibt offscreen Graphics, wenn "doubleBuffered") welche die Eigenschaften "clip", "opaque" und "optimizedDrawingEnabled" verwendet, um genau festzustellen, für welche Nachkommen, "paint()" rekursiv aufzurufen ist.
    5. wenn die "doubleBuffered" Eigenschaft der Komponente "true" ist und Doppelpufferung ist im RepaintManager der Komponente eingeschaltet, kopiert sie das offscreen Bild auf die Komponente unter Benutzung des ursprünglichen onscreen Graphics Objektes.
ANMERKUNG: die JComponent.paint() Schritte #1 und #5 werden in den rekursiven Aufrufen von paint() übersprungen (in paintChildren(), beschrieben in Schritt #4) weil alle lightweight Komponenten innerhalb einer Swingfensterhierarchie dasselbe offscreen Bild für Doppelpufferung teilen.

(b) Der Malantrag entsteht durch einen Aufruf von repaint() auf einer Erweiterung von javax.swing.JComponent:

  1. JComponent.repaint() registriert einen asynchronen repaint Antrag beim RepaintManager der Komponente, der invokeLater() benutzt, um einen Runnable entstehen zu lassen, der den Antrag später auf dem "event dispatching thread" verarbeitet.
  2. der Runnable wird auf dem "event dispatching thread" ausgeführt und er veranlasst den RepaintManager der Komponente, paintImmediately() auf der Komponente aufzurufen, die folgendes tut:
    1. verwendet das Cliprechteck und die Eigenschaften "opaque" und "optimizedDrawingEnabled", um die 'root' Komponente festzustellen, von der aus der Malvorgang anfangen muß (um sich um Transparenz und möglicherweise überlappende Komponenten kümmern zu können).
    2. wenn die "doubleBuffered" Eigenschaft der 'root' Komponente "true" ist und Doppelpufferung ist im RepaintManager vom 'root' eingeschaltet, wandelt sie das Graphics Objekt um zu einem passenden offscreen Graphics.
    3. ruft paint() auf der 'root' Komponente auf (was die JComponent.paint() Schritte #2-4 oben unter (a) durchführt), und verursacht so, daß alles unter dem 'root' gemalt wird, was sich mit dem Cliprechteck überschneidet.
    4. wenn die "doubleBuffered" Eigenschaft der 'root' Komponente "true" ist und Doppelpufferung ist im RepaintManager der 'root' Komponente eingeschaltet, kopiert sie das offscreen Bild auf die Komponente unter Benutzung des ursprünglichen onscreen Graphics Objektes.
ANMERKUNG: wenn mehrfache Aufrufe von repaint() auf einer Komponente oder irgendwelchen seiner Swingvorfahren auftreten, bevor der repaint Antrag verarbeitet wird, können jene mehrfachen Anträge in einen einzigen Callback von paintImmediately() auf der obersten Swingkomponente zusammengefasst werden, auf der repaint() aufgerufen wurde. Z.B. wenn ein JTabbedPane eine JTable enthält und beide Aufrufe von repaint() absenden, bevor irgendwelche anstehenden Anträge dieser Hierarchie verarbeitet werden, ist das Resultat ein einziger Aufruf von paintImmediately() auf dem JTabbedPane, was veranlasst, daß paint() auf beiden Komponenten durchgeführt wird.

Das heißt, daß update() für SwingKomponenten nie aufgerufen wird.

Obgleich repaint() einen Aufruf von paintImmediately() ergibt, gilt es nicht als "Malcallback" , und Malcode sollte nicht innerhalb einer paintImmediately() gesetzt werden. Tatsächlich gibt es im Allgemeinen überhaupt keinen Grund, paintImmediately() zu überschreiben.

Synchrones Zeichnen

Wie im vorhergehenden Abschnitt beschrieben, dient paintImmediately() als Ausgangspunkt um einer einzelnen Swingkomponente zu sagen, sich zu malen und zu überprüfen, ob alles erforderliche Zeichnen passend geschieht. Diese Methode kann auch für synchrone Malanträge verwendet werden, wie sein Name andeutet, was manchmal bei Komponenten erfordert ist, die sicherstellen müssen, daß ihr Aussehen in Echtzeit mit ihrem internen Zustand 'Schritt hält' (z.B. gilt dies für das JScrollPane während eines Rollvorgangs).

Programme sollten diese Methode nicht direkt aufrufen, es sei denn, es gibt eine tatsächliche Notwendigkeit zum Zeichnen in Echtzeit. Das ist so, weil durch das asynchrone repaint() mehrfache überlappende Anträge leistungssteigernd zusammengefasst werden, während das bei Direktaufrufen von paintImmediately() nicht geschieht. Zusätzlich ist die Richtlinie für das Aufrufen dieser Methode, daß sie auf dem "event dispatching thread" aufgerufen werden muß; es ist keine API, die für den "multi-threading" Ihres Malcodes bestimmt ist! Für mehr Einzelheiten über das "single-threaded" Modell von Swing, siehe den archivierten Artikel "Threads and Swing" :

Threads and Swing

Der RepaintManager

Der Zweck der Swingklasse "RepaintManager" ist, die Leistungsfähigkeit der repaint Verarbeitung auf einer Swing Containerhierarchie zu maximieren, und auch zur Implementierung des "revalidation" Mechanismus von Swing (letzteres wird das Thema für einen weiteren Artikel sein, siehe auch: Validierung mit Swing). Sie implementiert den repaint Mechanismus, indem sie alle repaint Anträge von Swing Komponenten abfängt (also werden sie nicht mehr durch das AWT verarbeitet) und ihren eigenen Zustand beibehält in Bezug auf das, was aktualisiert werden muss (bekannt als "dirty regions", also "verschmutzte Bereiche"). Schließlich benutzt sie invokeLater(), um die anstehenden Anträge auf dem "event dispatching thread" zu verarbeiten, wie im Abschnitt "Malverarbeitung" beschrieben (Option b). [Der RepaintManager wird eine Serie von Malanträgen möglichst zusammenfassen, wenn (und nur wenn) der "Anführer" der Serie mit invokeLater verschickt wurde, aber noch nicht auf dem EDT verarbeitet wurde.]

Für die meisten Programme kann der RepaintManager als Teil des internen Systems von Swing angesehen werden und praktisch ignoriert werden. Jedoch liefert seine API den Programmen die Möglichkeit einer feineren Steuerung von bestimmten Aspekten des Zeichnens.

Der "gegenwärtige" RepaintManager

Der RepaintManager ist entworfen, um dynamisch ausgewechselt zu werden, obgleich es durch die Default-Implementierung eine einzige Instanz gibt. Die folgenden statischen Methoden lassen Programme auf den "gegenwärtigen" RepaintManager zugreifen und ihn ersetzen:

    public static RepaintManager currentManager(Component c)
    public static RepaintManager currentManager(JComponent c)
    public static void setCurrentManager(RepaintManager aRepaintManager)

Den "gegenwärtigen" RepaintManager ersetzen

Ein Programm würde den RepaintManager global erweitern und ersetzen, indem es folgendes tut:

    RepaintManager.setCurrentManager(new MyRepaintManager());

Du kannst dir RepaintManagerDemo ansehen als ein einfaches lauffähiges Beispiel für die Installation von einem RepaintManager, der Informationen ausdruckt über das, was neu gezeichnet wird:

RepaintManagerDemo

Ein interessanterer Grund für das Erweitern und das Ersetzen des RepaintManagers wäre die Art und Weise zu verändern, wie repaint Anträge verarbeitet werden. Zur Zeit ist der interne Zustand, der durch die Default-Implementierung benutzt wird, um schmutzige Regionen aufzuspüren, "package privat" und folglich nicht durch Unterklassen zugänglich. Jedoch können Programme ihre eigenen Mechanismen für das Aufspüren von schmutzigen Regionen und für das Zusammenfassen von repaint Anträgen einführen, indem sie die folgenden Methoden überschreiben:

    public synchronized void addDirtyRegion(JComponent c, int x, int y, int w, int h) 
    public Rectangle getDirtyRegion(JComponent aComponent)
    public void markCompletelyDirty(JComponent aComponent) 
    public void markCompletelyClean(JComponent aComponent) {

Die addDirtyRegion() Methode ist die, die aufgerufen wird, wenn repaint() auf einer Swingkomponente aufgerufen wird, und kann folglich angespannt werden, um alle repaint Anträge abzufangen. Wenn ein Programm diese Methode überschreibt (und nicht super.addDirtyRegion() aufruft), dann wird es dafür verantwortlich, invokeLater() zu benutzen, um einen Runnable auf die "EventQueue" zu setzen, der paintImmediately() auf einer passenden Komponente aufruft (Übersetzung: nicht für Leute mit schwachem Herzen).

Globale Steuerung der Doppelpufferung

Der RepaintManager stellt eine API zur Verfügung, um die Doppelpufferung global zu ermöglichenden und zu sperren:

    public void setDoubleBufferingEnabled(boolean aFlag)
    public boolean isDoubleBufferingEnabled()

Während der Verarbeitung eines Malvorgangs wird diese Eigenschaft innerhalb JComponent überprüft, um festzustellen, ob man einen offscreen Puffer für die Darstellung verwendet. Diese Eigenschaft hat den Default-Wert "true", aber die Programme, die Doppelpufferung für alle Swingkomponenten global sperren möchten, können folgendes tun:

    RepaintManager.currentManager(mycomponent).setDoubleBufferingEnabled(false);

Anmerkung: da die Default-Implementierung von Swing einen einzigen RepaintManager instanziert, ist das "mycomponent" Argument irrelevant.

Swing Malrichtlinien

Swingprogramme sollten diese Richtlinien verstehen, wenn Malcode geschrieben wird:

  1. Für Swingkomponenten wird immer paint() aufgerufen als Ergebnis sowohl von systemausgelösten als auch von anwendungsausgelösten Malanträgen; update() wird nie auf Swingkomponenten aufgerufen.
  2. Programme können einen zukünftigen Aufruf von paint() auslösen, indem sie repaint() aufrufen, aber sie sollten paint() nicht direkt aufrufen.
  3. Auf Komponenten mit komplizierter Ausgabe, sollte repaint() mit Argumenten aufgerufen werden, die nur das Rechteck, das aktualisieren muß, definieren, und nicht die keine-Argumente-Version, die die gesamte Komponente veranlasst, neu gezeichnet zu werden.
  4. Die Swing Implementierung von paint() unterteilt den Aufruf in 3 verschiedene callback Methoden:
    • paintComponent(): Erweiterungen von Swingkomponenten, die ihren eigenen Malcode einführen möchten, sollten diesen Code innerhalb des Bereichs der Methode paintComponent() setzen (nicht innerhalb von paint()).
    • paintBorder()
    • paintChildren()
  5. Swing stellt zwei Eigenschaften vor, um die Malleistung zu maximieren:
    • opaque: malt die Komponente alle ihre Bits oder nicht?
    • optimizedDrawingEnabled: können irgendwelche der Kinder dieser Komponente sich überschneiden?
  6. Wenn die opaque Eigenschaft einer Swingkomponente auf true gesetzt wird, dann erklärt sie sich damit einverstanden, alle Bits zu malen, die innerhalb ihrer Grenzen enthalten sind (dies schließt das Löschen ihres eigenen Hintergrundes innerhalb von paintComponent() ein), andernfalls können Bilschirmunreinheiten auftreten.
  7. Das Setzen entweder von opaque oder optimizedDrawingEnabled auf false für eine Komponente verursacht mehr Verarbeitung bei jedem Malvorgang, folglich empfehlen wir vernünftigen Gebrauch von Transparenz und überlappenden Komponenten zu machen.
  8. Erweiterungen von Swingkomponenten, die UI Delegates haben (einschließlich JPanel), sollten gewöhnlich super.paintComponent() innerhalb ihrer eigenen paintComponent() Implementierung aufrufen. Da der UI Delegate die Verantwortung für das Löschen des Hintergrundes auf undurchlässigen Komponenten hat, kümmert er sich um #6.
  9. Swing unterstützt eingebaute Doppelpufferung über die doubleBuffered Eigenschaft von JComponent, und ihr Default-Wert ist true für alle Swingkomponenten, gleichwohl das Setzen auf true auf einem Swingcontainer den allgemeinen Effekt hat, sie für alle lightweight Nachkommen dieses Containers einzuschalten, unabhängig von ihren einzelnen Einstellungen.
  10. Es wird ausdrücklich empfohlen, daß Doppelpufferung für alle Swingkomponenten ermöglicht wird.
  11. Komponenten, die komplizierte Ausgaben darstellen, sollten intelligenten Gebrauch vom Cliprechteck machen, um die Malvorgänge auf diejenigen zu beschränken, die sich mit dem Clipbereich schneiden.

Zusammenfassung

Beide, AWT und Swing, liefern APIs, um es den Programmen einfach zu machen, ihren Inhalt korrekt am Bildschirm darzustellen. Obgleich wir empfehlen, daß Entwickler für die meisten GUI Bedürfnisse Gebrauch von Swing machen, ist es nützlich, AWTs Malmechanismus zu verstehen, weil derjenige von Swing darauf aufbaut.

Um die beste Leistung von diesen APIs zu erhalten, müssen Anwendungsprogramme die Verantwortung dafür übernehmen, die in diesem Dokument umrissenen Richtlinien zu befolgen.