Malen in AWT und Swing
Dieser Beitrag wird derzeit noch bearbeitet. Der Text ist deshalb unvollständig und kann Fehler oder ungeprüfte Aussagen enthalten. |
Guter Malcode ist der Schlüssel zu einer leistungsfähigen Anwendung.
Von Amy Fowler.
http://java.sun.com/products/jfc/tsc/articles/painting/
Copyright 1994-2008 Sun Microsystems, Inc.
(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 graphische Benutzerschnittstelle (GUI) die richtigen Bits zur richtigen Zeit darzustellen.
Sowohl AWT (abstraktes Fensterdarstellungstoolkit) als auch Swing liefern solch einen Rahmen.
Aber die APIs, die es 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 Detail. Sein Zweck ist, Entwicklern zu helfen, korrekten und leistungsfähigen GUI Malcode zu schreiben. Während der Artikel die allgemeine 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.
Inhaltsverzeichnis
- 1 Entwicklung des Swing Malsystems
- 2 Malen in AWT
- 3 Zeichnen in Swing
- 4 Zusammenfassung
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 seines 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 der 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 neues 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, seinen 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 wurde sichtbar).
Anwendungsausgelöstes Malen
Bei einem anwendungsausgelösten Malvorgang entscheidet die Komponente, daß sie ihren Inhalt aktualisieren muß, weil sich sein 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. Dies heißt, daß ein Programm den Darstellungscode der Komponente innerhalb einer bestimmten überschriebenen Methode setzen sollte, und der Toolkit ruft diese Methode auf, wenn es Zeit ist zu malen. Die zu überschreibende Methode ist in java.awt.Component:
<code=java>public void paint(Graphics g)</code=java>
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:
<code=java> 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); }</code=java>
Entwickler, die neu bei AWT sind, könnten einen Blick auf das PaintDemo Beispiel werfen wollen, das ein lauffähiges Programmbeispiel liefert, wie man den paint callback in einem AWT Programm verwendet:
http://java.sun.com/products/jfc/tsc/articles/painting/src/PaintDemo.java
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 eine asynchrone paint Operation anfragen können:
<code=java> 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)</code=java>
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:
<code=java> 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(); } };</code=java>
Komponenten, die komplizierte Darstellungen ausgeben, sollten repaint() mit den Argumenten aufrufen, welche nur den Bereich definieren, der die 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:
- Das AWT stellt fest, daß entweder ein Teil oder die ganze Komponente gemalt werden muß.
- Das AWT veranlasst den Event Dispatch Thread, paint() auf der Komponente aufzurufen.
anwendungsausgelöstes Malen
Ein anwendungsausgelöster Malvorgang findet wie folgt statt:
- Das Programm stellt fest, daß entweder ein Teil oder die ganze Komponente in Erwiderung auf eine interne Zustandsänderung neu gezeichnet werden muß.
- Das Programm ruft repaint() auf der Komponente auf, was einen asynchronen Antrag zum AWT registriert, dass diese Komponente neu gezeichnet werden muß.
- 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.
- 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ätzlichen Zeichnen zu tun.
Zusätzliches Zeichnen ist nützlich, wenn ein Programm die zusätzliche Darstellung über den vorhandenen Teilstücke dieser Komponente überlagern möchte. Das UpdateDemo Beispiel zeigt ein Programm, das Nutzen aus dem Verwenden von update() zieht, um zusätzlichen Zeichnen zu tun:
http://java.sun.com/products/jfc/tsc/articles/painting/src/UpdateDemo.java
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 paint() einfach ü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 Komponenten Rahmen völlig in 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. Dieses 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, das folgende zu tun:
<code=java> public class MyContainer extends Container {
public void paint(Graphics g) {
// paint my contents first... // then, make sure lightweight children paint super.paint(g);
} }</code=java>
Wenn der Aufruf von super.paint() fehlt, dann werden die lightweight Nachkommen des Containers nicht dargestellt (ein sehr verbreitetes Problem als JDK 1.1 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. Dies 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 erklärten. 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, was 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 seine Bits unbemalt läßt, die darunterliegende 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:
http://java.sun.com/products/jfc/tsc/articles/painting/src/LightweightDemo.java
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:
- Den Clipbereich 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 ein 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 Komponente beherbergt, wo die Komponente und/oder sein Layoutmanager Informationen über das Layout hat, dann ist er wert dieses Layoutwissen zu verwenden, um intelligenter zu sein über die Bestimmung, welches der Kinder gemalt werden muß. 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 wirkungslos sein kann. Zu Beispiel, wenn ein Container die Komponentenn 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.