Weitere aktuelle Java-Titel finden Sie bei dpunkt.
 Inhaltsverzeichnis   Auf Ebene Zurück   Seite Zurück   Seite Vor   Auf Ebene Vor   Eine Ebene höher   Index


12.4

Offscreen-Images


Offscreen-Images sind für verschiedene Dinge wichtig: Das Offscreen-Image, ein Exemplar von Image, fungiert als ein Zwischenpuffer, in den zuerst einmal alles gezeichnet werden kann, ohne dass es auf dem Bildschirm zu sehen ist, um es schließlich als Ganzes auf den Bildschirm zu kopieren (double-buffering). Ab[1.2] dem JDK 1.2 werden Offscreen-Images von BufferedImage implementiert.

Ein Offscreen-Image kann man bei JDK 1.0 und 1.1 mit der Methode createImage(int, int) erstellen. Alle Klassen, die von Component abgeleitet sind, besitzen diese Methode. Die Parameter geben die Breite und die Höhe des zu erstellenden Bildes an.
  Image imagebuffer = createImage(100, 80);
erzeugt ein Image, das 100 Pixel breit und 80 Pixel hoch ist.

Es ist allerdings zu beachten, dass ein Aufruf von createImage() nur ein Offscreen-Image erzeugt, wenn die Komponente, von der diese Methode aufgerufen wird, bereits der grafischen Oberfläche hinzugefügt wurde. Andernfalls wird null zurückgegeben:
  Button b = new Button("click");
  Image img = b.createImage(100, 80);
  add(b);
Der Aufruf in diesem Listing würde z. B. nicht funktionieren. Die Variable img hätte den Wert null. In folgendem Listing würde der Aufruf allerdings erfolgreich sein:
  Button b = new Button("click");
  add(b);
  Image img = b.createImage(100, 80);
Der Aufruf von createImage() in der init()-Methode eines Applet funktioniert immer, da nach dem Aufruf von init() das Applet bereits hinzugefügt wurde. Bei Applikationen, die einen Aufruf von createImage() innerhalb des Konstruktors eines Frames verwenden, würde es aber Probleme geben.

Man kann diese Abhängigkeit umgehen, indem man den Aufruf von createImage() einfach in der paint()-Methode vornimmt:
public void paint(Graphics g) {
  if (img == null) {
    img = createImage(100, 80);
  }
  // Zeichenoperationen
}
Bei Aufruf von paint() durch den Interpreter ist die Komponente in jedem Fall bereits zur grafischen Oberfläche hinzugefügt worden. (Ausnahme: Der Programmierer ruft direkt paint() auf, wie es im Abschnitt beschrieben wurde.)

Ab dem JDK 1.2 gibt es eine weitere[1.2] Möglichkeit, Offscreen-Images zu erzeugen. Während createImage() ein Image-Exemplar als Ergebnis liefert, gibt es im JDK 1.2 die Klasse BufferedImage, die direkt ein Offscreen-Image repräsentiert. Bei einem createImage()-Aufruf mit dem JDK 1.2 ist das zurügelieferte Image-Exemplar tatsächlich vom Typ BufferedImage. Das ist deshalb möglich, da BufferedImage von Image abgeleitet ist. Ein BufferedImage kann man auch direkt mit Hilfe des new-Operators angelegen:
  BufferedImage bi;
  bi = new BufferedImage(200, 100,
                         BufferedImage.TYPE_INT_ARGB);
In diesem Codeausschnitt wird ein Exemplar der Klasse BufferedImage erzeugt mit der Breite 200, der Höhe 100 und dem Typ BufferedImage.TYPE_INT_ARGB. Bei einem Bild von diesem Typ werden Rot-, Grün-, Blau- und Alpha-Werte eines Pixels zusammen in einem int gespeichert. In der Klasse BufferedImage sind noch weitere Typen definiert, die der Referenz entnommen werden können. TYPE_INT_ARGB und TYPE_INT_RGB sind wohl die am häufigsten verwendeten Typen (bei letzterem Typ wird kein Alpha-Wert gespeichert).

Um in ein Offscreen-Image zu zeichnen, benötigt man sein Graphics-Objekt. Hierzu verwendet man bis einschließlich JDK 1.1 die Methode getGraphics(). getGraphics() ist nicht nur in der Klasse Component definiert, sondern auch in der Klasse Image. Bei Exemplaren der Klasse Image liefert getGraphics() allerdings nur das richtige Ergebnis, wenn es auf ein Image-Exemplar angewendet wird, das durch createImage() erzeugt wurde. Bei Image-Exemplaren, die mit getImage() erzeugt wurden, wird beim Aufruf von getGraphics() ein IllegalAccessError ausgelöst.

Mit folgender Zeile erhält man das Graphics-Objekt des oben erstellten Bildes:
  Graphics imageGraphics = img.getGraphics();
In diesen Grafikkontext wird auf dieselbe Weise gezeichnet, wie in den Grafikkontext, der paint() übergeben wird.

Wendet[1.2] man getGraphics() im JDK 1.2 an, wird tatsächlich ein Exemplar von Graphics2D zurückgeliefert, genauso wie es auch bei paint() gemacht wird. Nach der Durchführung einer Typumwandlung kann man auf die neuen Funktionen der Klasse Graphics2D zugreifen. Die Klasse BufferedImage verfügt zusätzlich über die Methode createGraphics(), die direkt ein Exemplar der Klasse Graphics2D zurückliefert:
  BufferedImage bi;
  bi = new BufferedImage(200, 100,
                         BufferedImage.TYPE_INT_ARGB);
  Graphics2D g2 = bi.createGraphics();
  // Hier Zeichenoperationen auf g2 ausführen
Diese hat prinzipiell dieselbe Funktion wie getGraphics() ist jedoch etwas einfacher zu benutzen, da die Typumwandlung in Graphics2D entfällt. getGraphics() ist nur noch aus Gründen der Abwärtskompatibilität verfügbar. Um jedoch createGraphics() benutzen zu können, braucht man zunächst Zugriff auf ein Exemplar der Klasse BufferedImage. Deshalb bietet sich bei der Programmierung von Anwendungen mit Java 2D folgende Vorgehensweise an: Wenn man die Methoden von Java 2D nicht unbedingt benötigt, ist es aus Gründen der Abwärtskompatibilität meist ratsamer, die bisherige Variante zu wählen.

Im folgenden wird die Laufschrift aus dem Abschnitt 12.3 unter Verwendung eines Offscreen-Images optisch ansprechender gestaltet. Hierbei wird zur Demonstration auf die Vorgehensweise bei Verwendung von Java 2D zurückgegriffen.

Startet man die dort implementierte Laufschrift, wird die Schrift zwar angezeigt, aber es fällt auf, dass sie flackert. Dies liegt an der Implementierung von repaint(). Durch repaint() wird nicht sofort paint() aufgerufen, sondern zuerst:
  public void update(Graphics g)
update() bekommt das Graphics-Objekt der entsprechenden Komponente übergeben. Standardmäßig wird die Komponente von update() in der Hintergrundfarbe übermalt und anschließend paint() mit demselben Graphics-Objekt aufgerufen. Würde die Komponente nicht in der Hintergrundfarbe übermalt werden, wäre zwar kein Flackern, aber auch keine Laufschrift zu sehen.

Zum Vermeiden des Flackerns muss man also das Übermalen der Komponente umgehen. Hierzu überschreibt man die Methode update(). Dabei stößt man jedoch auf ein anderes Problem:

Schreibt man denselben Code in update(), der zuvor in paint() stand, wird die Schrift bei der Aktualisierung der Position zwar neu gemalt, ohne dass der Hintergrund gelöscht wird, aber die alte Schrift wird ebenfalls nicht gelöscht. Um mit diesem Problem fertigzuwerden, verwendet man ein Offscreen-Image.

Die Erweiterungen, die man an der Laufschrift vornehmen muss, werden im Folgenden beschrieben.

Zuerst muss man dem Applet zwei weitere Objekte hinzufügen, die dazu dienen sollen, die Schrift zwischenzupuffern:
  BufferedImage imageBuffer; // Offscreen-Image
  // Graphics-Objekt des Offscreen-Images
  Graphics2D graphicsBuffer;

Der Anfang von init() wird um die Initialisierung dieser Objekte erweitert:
  imageBuffer =
    new BufferedImage(getSize().width,
                      getSize().height,
                      BufferedImage.TYPE_INT_ARGB);
  graphicsBuffer = imageBuffer.createGraphics();
  graphicsBuffer.setFont(
       new Font("SansSerif", Font.PLAIN, 30));
  FontMetrics fm =
       getFontMetrics(graphicsBuffer.getFont());
Der Font wird jetzt nicht mehr dem Grafikkontext des Applets, sondern dem des Offscreen-Image zugewiesen. Ein Font gilt nämlich nur innerhalb des Kontexts, dem er zugewiesen wurde.

Die Zeile
  setFont(new Font("SansSerif", Font.PLAIN, 30));
kann deshalb bei der neuen Version des Applets entfallen. Das FontMetrics-Exemplar muss ebenfalls über den Font des Offscreen-Images angefordert werden:
  FontMetrics fm =
       getFontMetrics(graphicsBuffer.getFont());
Jetzt muss man nur noch update() mit dem entsprechenden Code ausstatten (paint() wird hierbei nicht überschrieben):
  public void update(Graphics g) {
    // Zeichnen der Schrift in das Offscreen-Image
    graphicsBuffer.setColor(getBackground());
    graphicsBuffer.fillRect(0, 0,
                     getSize().width, getSize().height);
    graphicsBuffer.setColor(getForeground());
    graphicsBuffer.drawString(TEXT, x, y);
    // Zeichnen des Offscreen-Images in das Applet
    g.drawImage(imageBuffer, 0, 0, this);
  }

In update() wird zuerst das Offscreen-Image mit der aktuellen Hintergrundfarbe übermalt. Ist dies geschehen, wird die Schrift in der aktuellen Vordergrundfarbe an die aktuelle Position innerhalb des Offscreen-Images gezeichnet. Dies geschieht alles unsichtbar für den Betrachter.

Nun muss man lediglich das »gefüllte« Offscreen-Image als Ganzes in den Grafikkontext des Applets einbinden. Auf diese Weise wird die Schrift immer an die richtige Position gesetzt und ein Flackern vermieden.

Interessante Effekte können erzielt werden, wenn man mit setXORMode(Color) den Zeichenmodus ändert. Ist die Hintergrundfarbe gleich der Vordergrundfarbe, wird das Zeichenelement in der Farbe gezeichnet, die setXORMode() als Parameter übergeben wurde. Bei den anderen Farben findet ein Farbwechsel statt, die angezeigte Farbe ist allerdings nicht vorhersehbar.

Ein Zeichenelement, das im XOR-Modus gemalt wird, ist also immer zu sehen: Ein »Untertauchen« eines Zeichenobjekts, weil Vorder- und Hintergrundfarbe gleich gewählt wurden, ist nicht möglich. Auf diese Weise könnte man z. B. bei einem Bildbearbeitungsprogramm die Auswahl eines Bildausschnitts realisieren. Das Rechteck, das den ausgewählten Ausschnitt eingrenzt, ist immer unabhängig von den Farben des Bildes zu sehen.

Benutzt man nun ein Bild statt des einheitlichen Hintergrundes und zeichnet die Schrift im XOR-Modus, erhält man eine Laufschrift, die ständig ihre Farbe wechselt. Bevor das Bild gezeichnet wird, sollte der Zeichenmodus aber wieder mit setPaintMode() in seinen Anfangszustand zurückversetzt werden:
  public void paint(Graphics g) {
    g.setPaintMode();
    g.drawImage(bgImage, 0, 0, 
                size().width, size().height, this);
    g.setXORMode(Color.red);
    g.drawString(text, x, y);
  }
Ab JDK 1.2[1.2] gibt es weitere Möglichkeiten, den Zeichenmodus zu ändern. In der Klasse Graphics2D kann man über die Methode setComposite() festlegen, wie bei Zeichenoperationen überlappende Pixel behandelt werden. Diese Methode hat einen Parameter vom Typ Composite. Composite ist ein Interface des Pakets java.awt. Die einzige Klasse, die dieses Interface im JDK 1.2 implementiert ist AlphaComposite. AlphaComposite ermöglicht die Definition des Deckungsgrads, bei Überlappung von zwei Grafikobjekten. Das Beispiel
  public void paint(Graphics g) {
    Graphics2D g2 = (Graphics2D)g2;
    g2.setComposite(AlphaComposite.SrcIn);
    // hier Zeichenoperationen
    // ...
  } 
setzt den Zeichenmodus auf vollständig deckend, d. h. das Objekt, das zuletzt gezeichnet wird, erscheint oben. Für oft benötigte Kompositionsregeln sind in der Klasse AlphaComposite Konstanten definiert, die in der elektronischen Referenz nachgeschlagen werden können.

Um die Unterschiede zwischen paint() und update() zu verdeutlichen, hier nochmals eine kurze Erläuterung: Per Voreinstellung bestehen folgende Beziehungen zwischen einzelnen Zeichenmethoden:

Abbildung 12.4: Die einzelnen Aufrufe der Zeichenmethoden
Abbildung 12.4

Der direkte Aufruf von paint() kann bei Applets, die update() überschreiben, Probleme verursachen: Implementiert man in einem Applet update(), wird der oben beschriebene Ablauf unterbrochen. Die ursprüngliche Implementierung von update() wird nicht mehr ausgeführt.

Wird das Applet aus dem sichtbaren Bereich heraus- und anschliessend wieder in diesen Bereich hineingescrollt, ruft der Browser die paint()-Methode des Applets auf. Stehen nun alle Zeichenroutinen in update(), wird bei einem Aufruf von paint() nichts gezeichnet, da diese Methode ja keinen Code enthält. Wenn das Applet nun aus dem sichtbaren Bereich heraus- und anschließend wieder hereingescrollt wird, ist an der Stelle des Applets nur ein grauer Fleck zu sehen: Das Applet wird erst beim nächsten Aufruf von update() wieder mit der eigentlichen Grafik übermalt. Bei der Laufschrift bereitet dies kein allzu großes Problem, da sowieso alle 100 Millisekunden repaint() ausgeführt wird. Bei Applets, die repaint() nicht so häufig aufrufen, kann es jedoch zu Problemen kommen.

Um dies zu vermeiden, hat man mehrere Möglichkeiten: Was eben für repaint() und paint() gesagt wurde, bezieht sich nicht nur auf einen Browser, sondern auch auf Frames bzw. den Appletviewer. Dort wird jedesmal direkt paint() aufgerufen, wenn die Größe verändert wird. Durch Aufruf von repaint() wird auch hier letztendlich update() ausgeführt.


 Inhaltsverzeichnis   Auf Ebene Zurück   Seite Zurück   Seite Vor   Auf Ebene Vor   Eine Ebene höher   Index

Copyright © 2002 dpunkt.Verlag, Heidelberg. Alle Rechte vorbehalten.