Titel | Inhalt | Suchen | Index | DOC | Handbuch der Java-Programmierung, 5. Auflage |
<< | < | > | >> | API | Kapitel 34 - Bitmaps und Animationen |
Das Darstellen einer Animation auf dem Bildschirm ist im Prinzip nichts anderes als die schnell aufeinanderfolgende Anzeige einer Sequenz von Einzelbildern. Die Bildfolge erscheint dem menschlichen Auge aufgrund seiner Trägheit als zusammenhängende Bewegung.
Obwohl die prinzipielle Vorgehensweise damit klar umrissen ist, steckt die Tücke bei der Darstellung von animierten Bildsequenzen im Detail. Zu den Problemen, die in diesem Zusammenhang zu lösen sind, gehören:
All dies sind Standardprobleme, die vom Programmierer bei der Entwicklung von Animationen zu lösen sind. Wir werden feststellen, dass Java dafür durchweg brauchbare Lösungen zu bieten hat und die Programmierung kleiner Animationen recht einfach zu realisieren ist.
Das Grundprinzip einer Animation besteht darin, in einer Schleife die Methode repaint wiederholt aufzurufen. Ein Aufruf von repaint führt dazu, dass die paint-Methode aufgerufen wird, und innerhalb von paint generiert die Anwendung dann die für das aktuelle Einzelbild benötigte Bildschirmausgabe.
paint muss sich also merken (oder mitgeteilt bekommen), welches Bild bei welchem Aufruf erzeugt werden soll. Typischerweise wird dazu ein Schleifenzähler verwendet, der das gerade anzuzeigende Bild bezeichnet. Nach dem Ausführen der Ausgabeanweisungen terminiert paint, und der Aufrufer wartet eine bestimmte Zeitspanne. Dann zählt er den Bildzähler hoch und führt den nächsten Aufruf von repaint durch. Dies setzt sich so lange fort, bis die Animation beendet ist oder das Programm abgebrochen wird.
Das folgende Listing stellt eines der einfachsten Beispiele für eine Grafikanimation dar:
001 /* Listing3406.java */ 002 003 import java.awt.*; 004 import java.awt.event.*; 005 006 public class Listing3406 007 extends Frame 008 { 009 int cnt = 0; 010 011 public static void main(String[] args) 012 { 013 Listing3406 wnd = new Listing3406(); 014 wnd.setSize(250,150); 015 wnd.setVisible(true); 016 wnd.startAnimation(); 017 } 018 019 public Listing3406() 020 { 021 super("Animierter Zähler"); 022 setBackground(Color.lightGray); 023 addWindowListener(new WindowClosingAdapter(true)); 024 } 025 026 public void startAnimation() 027 { 028 while (true) { 029 repaint(); 030 } 031 } 032 033 public void paint(Graphics g) 034 { 035 ++cnt; 036 g.drawString("Counter = "+cnt,10,50); 037 try { 038 Thread.sleep(1000); 039 } catch (InterruptedException e) { 040 } 041 } 042 } |
Listing3406.java |
Das Programm öffnet ein Fenster und zählt in Sekundenabständen einen Zähler um eins hoch:
Abbildung 34.3: Ein animierter Zähler
Leider hat das Programm einen entscheidenden Nachteil. Die Animation selbst funktioniert zwar wunderbar, aber das Programm reagiert nur noch sehr schleppend auf Windows-Nachrichten. Wir wollen zunächst dieses Problem abstellen und uns ansehen, wie man die repaint-Schleife in einem eigenen Thread laufen läßt. |
|
Um die vorherige Version des Programms zu verbessern, sollte die repaint-Schleife in einem eigenen Thread laufen. Zusätzlich ist es erforderlich, die Zeitverzögerung aus paint herauszunehmen und statt dessen in die repaint-Schleife zu verlagern. So bekommt der Haupt-Thread des Animationsprogramms genügend Zeit, die Bildschirmausgabe durchzuführen und kann andere Events bearbeiten. Daß in einem anderen Thread eine Endlosschleife läuft, merkt er nur noch daran, dass in regelmäßigen Abständen repaint-Ereignisse eintreffen.
Um das Programm auf die Verwendung mehrerer Threads umzustellen, sollte die Fensterklasse das Interface Runnable implementieren und eine Instanzvariable vom Typ Thread anlegen. Dann wird die Methode startAnimation so modifiziert, dass sie den neuen Thread instanziert und startet. Die eigentliche repaint-Schleife wird in die Methode run verlagert. Schließlich sollte beim Beenden des Programms auch der laufende Thread beendet werden. Hier ist die modifizierte Fassung:
001 /* Listing3407.java */ 002 003 import java.awt.*; 004 import java.awt.event.*; 005 006 public class Listing3407 007 extends Frame 008 implements Runnable 009 { 010 int cnt = 0; 011 012 public static void main(String[] args) 013 { 014 Listing3407 wnd = new Listing3407(); 015 wnd.setSize(250,150); 016 wnd.setVisible(true); 017 wnd.startAnimation(); 018 } 019 020 public Listing3407() 021 { 022 super("Animations-Threads"); 023 setBackground(Color.lightGray); 024 addWindowListener(new WindowClosingAdapter(true)); 025 } 026 027 public void startAnimation() 028 { 029 Thread th = new Thread(this); 030 th.start(); 031 } 032 033 public void run() 034 { 035 while (true) { 036 repaint(); 037 try { 038 Thread.sleep(1000); 039 } catch (InterruptedException e) { 040 //nichts 041 } 042 } 043 } 044 045 public void paint(Graphics g) 046 { 047 ++cnt; 048 g.drawString("Counter = "+cnt,10,50); 049 } 050 } |
Listing3407.java |
Das so modifizierte Programm erzeugt dieselbe Ausgabe wie das vorige, ist aber in der Lage, in der gewohnten Weise auf Ereignisse zu reagieren. Selbst wenn die Verzögerungsschleife ganz entfernt und der Hauptprozess so pausenlos mit repaint-Anforderungen bombardiert würde, könnte das Programm noch normal beendet werden.
Eine der einfachsten und am häufigsten verwendeten Möglichkeiten, eine Animation zu erzeugen, besteht darin, die zur Darstellung erforderliche Folge von Bitmaps aus einer Reihe von Bilddateien zu laden. Jedem Einzelbild wird dabei ein Image-Objekt zugeordnet, das vor dem Start der Animation geladen wird. Alle Images liegen in einem Array oder einem anderen Container und werden in der repaint-Schleife nacheinander angezeigt.
Das folgende Programm speichert die 30 anzuzeigenden Einzelbilder in einem Array arImg, das nach dem Start des Programms komplett geladen wird. Da dieser Vorgang einige Sekunden dauern kann, zeigt das Programm den Ladefortschritt auf dem Bildschirm an:
Abbildung 34.4: Die Ausgabe während des Ladevorgangs
Erst nach dem vollständigen Abschluss des Ladevorgangs, der mit einem MediaTracker überwacht wird, beginnt die eigentliche Animation. Die ganzzahlige Instanzvariable actimage dient als Zähler für die Bildfolge und wird nacheinander von 0 bis 29 hochgezählt, um dann wieder bei 0 zu beginnen. Nach jedem Einzelbild wartet das Programm 50 Millisekunden und führt dann den nächsten Aufruf von repaint durch:
001 /* Listing3408.java */ 002 003 import java.awt.*; 004 import java.awt.event.*; 005 006 public class Listing3408 007 extends Frame 008 implements Runnable 009 { 010 Thread th; 011 Image[] arImg; 012 int actimage; 013 014 public static void main(String[] args) 015 { 016 Listing3408 wnd = new Listing3408(); 017 wnd.setSize(200,150); 018 wnd.setVisible(true); 019 wnd.startAnimation(); 020 } 021 022 public Listing3408() 023 { 024 super("Bitmap-Folge"); 025 addWindowListener(new WindowClosingAdapter(true)); 026 } 027 028 public void startAnimation() 029 { 030 th = new Thread(this); 031 actimage = -1; 032 th.start(); 033 } 034 035 public void run() 036 { 037 //Bilder laden 038 arImg = new Image[30]; 039 MediaTracker mt = new MediaTracker(this); 040 Toolkit tk = getToolkit(); 041 for (int i = 1; i <= 30; ++i) { 042 arImg[i-1] = tk.getImage("images/jana"+i+".gif"); 043 mt.addImage(arImg[i-1], 0); 044 actimage = -i; 045 repaint(); 046 try { 047 mt.waitForAll(); 048 } catch (InterruptedException e) { 049 //nothing 050 } 051 } 052 //Animation beginnen 053 actimage = 0; 054 while (true) { 055 repaint(); 056 actimage = (actimage + 1) % 30; 057 try { 058 Thread.sleep(50); 059 } catch (InterruptedException e) { 060 //nichts 061 } 062 } 063 } 064 065 public void paint(Graphics g) 066 { 067 if (actimage < 0) { 068 g.drawString("Lade Bitmap "+(-actimage),10,50); 069 } else { 070 g.drawImage(arImg[actimage],10,30,this); 071 } 072 } 073 } |
Listing3408.java |
Das vorliegende Beispiel verwendet die Bilddateien jana1.gif bis jana30.gif. Sie zeigen die verschiedenen Phasen des in Schreibschrift geschriebenen Namens »Jana«. Alternativ kann aber auch jede andere Sequenz von Bilddateien verwendet werden. Die folgenden Abbildungen zeigen einige Schnappschüsse der Programmausgabe: |
|
Abbildung 34.5: Animation eines Schriftzugs, Schnappschuß 1
Abbildung 34.6: Animation eines Schriftzugs, Schnappschuß 2
Abbildung 34.7: Animation eines Schriftzugs, Schnappschuß 3
Alternativ zur Anzeige von Bilddateien kann jedes Einzelbild der Animation natürlich auch mit den Ausgabeprimitiven der Klasse Graphics erzeugt werden. Dies hat den Vorteil, dass der Anwender nicht auf das Laden der Bilder warten muss. Außerdem ist das Verfahren flexibler als der bitmap-basierte Ansatz. Der Nachteil ist natürlich, dass die Grafikoperationen zeitaufwändiger sind und eine zügige Bildfolge bei komplexen Sequenzen schwieriger zu erzielen ist.
Als Beispiel für diese Art von Animation wollen wir uns die Aufgabe stellen, eine aus rechteckigen Kästchen bestehende bunte Schlange über den Bildschirm laufen zu lassen. Sie soll an den Bildschirmrändern automatisch umkehren und auch innerhalb des Ausgabefensters von Zeit zu Zeit ihre Richtung wechseln.
Das folgende Programm stellt die Schlange als Vector von Objekten des Typs ColorRectangle dar. ColorRectangle ist aus Rectangle abgeleitet und besitzt zusätzlich eine Membervariable zur Darstellung der Farbe des Rechtecks.
Dieses Beispiel folgt dem allgemeinen Architekturschema für Animationen, das wir auch in den letzten Beispielen verwendet haben. Der erste Schritt innerhalb von run besteht darin, die Schlange zu konstruieren. Dazu wird eine Folge von Objekten der Klasse ColorRectangle konstruiert, und ab Position (100,100) werden die Objekte horizontal nebeneinander angeordnet. Die Farben werden dabei so vergeben, dass die Schlange in fließenden Übergängen von rot bis blau dargestellt wird. Alle Elemente werden in dem Vector snake gespeichert.
Nachdem die Schlange konstruiert wurde, beginnt die Animation. Dazu wird die aktuelle Schlange angezeigt, eine Weile pausiert und dann durch Aufruf der Methode moveSnake die nächste Position der Schlange berechnet. moveSnake ist relativ aufwändig, denn hier liegt der Löwenanteil der »Intelligenz« der Animation. Die Richtung der Bewegung der Schlange wird durch die Variablen dx und dy getrennt für die x- und y-Richtung bestimmt. Steht hier der Wert -1, bewegt sich die Schlange im nächsten Schritt um die Breite eines Rechtecks in Richtung kleinerer Koordinaten. Bei 1 vergrößert sie die Koordinate entsprechend, und wenn der Wert 0 enthalten ist, verändert sich der zugehörige Koordinatenwert im nächsten Schritt gar nicht.
dx und dy werden entweder dann verändert, wenn die Schlange an einem der vier Bildschirmränder angekommen ist und umkehren muss oder (im Mittel bei jedem zehnten Schritt) auch auf freier Strecke. Nachdem auf diese Weise die neue Richtung bestimmt wurde, wird das erste Element der Schlange auf die neue Position bewegt. Alle anderen Elemente der Schlange bekommen dann die Position zugewiesen, die zuvor ihr Vorgänger hatte.
Eine alternative Art, die Schlange neu zu berechnen, würde darin bestehen, lediglich ein neues erstes Element zu generieren, an vorderster Stelle in den Vector einzufügen und das letzte Element zu löschen. Dies hätte allerdings den Nachteil, dass die Farbinformationen von vorne nach hinten durchgereicht würden und so jedes Element seine Farbe ständig ändern würde. Dieses (sehr viel performantere) Verfahren könnte verwendet werden, wenn alle Elemente der Schlange dieselbe Farbe hätten.
Hier ist der Quellcode zu der Schlangenanimation:
001 /* Listing3409.java */ 002 003 import java.awt.*; 004 import java.awt.event.*; 005 import java.util.*; 006 007 class ColorRectangle 008 extends Rectangle 009 { 010 public Color color; 011 } 012 013 public class Listing3409 014 extends Frame 015 implements Runnable 016 { 017 //Konstanten 018 private static final int SIZERECT = 7; 019 private static final int SLEEP = 40; 020 private static final int NUMELEMENTS = 20; 021 private static final Color BGCOLOR = Color.lightGray; 022 023 //Instanzvariablen 024 private Thread th; 025 private Vector snake; 026 private int dx; 027 private int dy; 028 029 public static void main(String[] args) 030 { 031 Listing3409 frame = new Listing3409(); 032 frame.setSize(200,150); 033 frame.setVisible(true); 034 frame.startAnimation(); 035 } 036 037 public Listing3409() 038 { 039 super("Animierte Schlange"); 040 setBackground(BGCOLOR); 041 addWindowListener(new WindowClosingAdapter(true)); 042 snake = new Vector(); 043 } 044 045 public void startAnimation() 046 { 047 th = new Thread(this); 048 th.start(); 049 } 050 051 public void run() 052 { 053 //Schlange konstruieren 054 ColorRectangle cr; 055 int x = 100; 056 int y = 100; 057 for (int i=0; i < NUMELEMENTS; ++i) { 058 cr = new ColorRectangle(); 059 cr.x = x; 060 cr.y = y; 061 cr.width = SIZERECT; 062 cr.height = SIZERECT; 063 x += SIZERECT; 064 cr.color = new Color( 065 i*(256/NUMELEMENTS), 066 0, 067 240-i*(256/NUMELEMENTS) 068 ); 069 snake.addElement(cr); 070 } 071 072 //Vorzugsrichtung festlegen 073 dx = -1; 074 dy = -1; 075 076 //Schlange laufen lassen 077 while (true) { 078 repaint(); 079 try { 080 Thread.sleep(SLEEP); 081 } catch (InterruptedException e){ 082 //nichts 083 } 084 moveSnake(); 085 } 086 } 087 088 public void moveSnake() 089 { 090 Dimension size = getSize(); 091 int sizex = size.width-getInsets().left-getInsets().right; 092 int sizey = size.height-getInsets().top-getInsets().bottom; 093 ColorRectangle cr = (ColorRectangle)snake.firstElement(); 094 boolean lBorder = false; 095 int xalt, yalt; 096 int xtmp, ytmp; 097 098 //Kopf der Schlange neu berechnen 099 if (cr.x <= 1) { 100 dx = 1; 101 lBorder = true; 102 } 103 if (cr.x + cr.width >= sizex) { 104 dx = -1; 105 lBorder = true; 106 } 107 if (cr.y <= 1) { 108 dy = 1; 109 lBorder = true; 110 } 111 if (cr.y + cr.height >= sizey) { 112 dy = -1; 113 lBorder = true; 114 } 115 if (! lBorder) { 116 if (rand(10) == 0) { 117 if (rand(2) == 0) { 118 switch (rand(5)) { 119 case 0: case 1: 120 dx = -1; 121 break; 122 case 2: 123 dx = 0; 124 break; 125 case 3: case 4: 126 dx = 1; 127 break; 128 } 129 } else { 130 switch (rand(5)) { 131 case 0: case 1: 132 dy = -1; 133 break; 134 case 2: 135 dy = 0; 136 break; 137 case 3: case 4: 138 dy = 1; 139 break; 140 } 141 } 142 } 143 } 144 xalt = cr.x + SIZERECT * dx; 145 yalt = cr.y + SIZERECT * dy; 146 //Rest der Schlange hinterherziehen 147 Enumeration e = snake.elements(); 148 while (e.hasMoreElements()) { 149 cr = (ColorRectangle)e.nextElement(); 150 xtmp = cr.x; 151 ytmp = cr.y; 152 cr.x = xalt; 153 cr.y = yalt; 154 xalt = xtmp; 155 yalt = ytmp; 156 } 157 } 158 159 public void paint(Graphics g) 160 { 161 ColorRectangle cr; 162 Enumeration e = snake.elements(); 163 int inleft = getInsets().left; 164 int intop = getInsets().top; 165 while (e.hasMoreElements()) { 166 cr = (ColorRectangle)e.nextElement(); 167 g.setColor(cr.color); 168 g.fillRect(cr.x+inleft,cr.y+intop,cr.width,cr.height); 169 } 170 } 171 172 private int rand(int limit) 173 { 174 return (int)(Math.random() * limit); 175 } 176 } |
Listing3409.java |
Die Schlange kann in einem beliebig kleinen oder großen Fenster laufen. Hier sind ein paar Beispiele für die Ausgabe des Programms, nachdem das Fenster in der Größe verändert wurde: |
|
Abbildung 34.8: Die animierte Schlange, Schnappschuß 1
Abbildung 34.9: Die animierte Schlange, Schnappschuß 2
Abbildung 34.10: Die animierte Schlange, Schnappschuß 3
Alle bisher entwickelten Animationen zeigen während der Ausführung ein ausgeprägtes Flackern, das um so stärker ist, je später ein Bildanteil innerhalb eines Animationsschrittes angezeigt wird. Der Grund für dieses Flackern liegt darin, dass vor jedem Aufruf von paint zunächst das Fenster gelöscht wird und dadurch unmittelbar vor der Ausgabe des nächsten Bildes ganz kurz ein vollständig leerer Hintergrund erscheint.
Leider besteht die Lösung für dieses Problem nicht einfach darin, das Löschen zu unterdrücken. Bei einer animierten Bewegung beispielsweise ist es erforderlich, all die Bestandteile des vorigen Bildes zu löschen, die im aktuellen Bild nicht mehr oder an einer anderen Stelle angezeigt werden.
Auch wenn paint deshalb aufgerufen wird, weil ein bisher verdeckter Bildausschnitt wieder sichtbar wird, muss natürlich der entsprechende Bildausschnitt zunächst gelöscht werden, um die Bestandteile des anderen Fensters zu entfernen. Im Grunde ist es also eine ganz vernünftige Vorgehensweise, das Fenster vor jedem Aufruf von paint zu löschen.
Das Flackern kann nun auf unterschiedliche Weise unterdrückt werden. Die drei gebräuchlichsten Methoden sind folgende:
Jedes dieser Verfahren hat Vor- und Nachteile und kann in verschiedenen Situationen unterschiedlich gut angewendet werden. Wir werden sie in den folgenden Unterabschnitten kurz vorstellen und ein Beispiel für ihre Anwendung geben. Es gibt noch einige zusätzliche Möglichkeiten, das Flackern zu unterdrücken oder einzuschränken, wie beispielsweise das Clipping der Ausgabe auf den tatsächlich veränderten Bereich, aber darauf wollen wir hier nicht näher eingehen.
Den Bildschirm überhaupt nicht zu löschen, um das Flackern zu unterdrücken, ist nur bei nicht bewegten Animationen möglich. Wir wollen uns als Beispiel für ein Programm, das hierfür geeignet ist, das folgende Lauflicht ansehen:
001 /* Listing3410.java */ 002 003 import java.awt.*; 004 import java.awt.event.*; 005 006 public class Listing3410 007 extends Frame 008 implements Runnable 009 { 010 //Konstanten 011 private static final int NUMLEDS = 20; 012 private static final int SLEEP = 60; 013 private static final int LEDSIZE = 10; 014 private static final Color ONCOLOR = new Color(255,0,0); 015 private static final Color OFFCOLOR = new Color(100,0,0); 016 017 //Instanzvariablen 018 private Thread th; 019 private int switched; 020 private int dx; 021 022 public static void main(String[] args) 023 { 024 Listing3410 frame = new Listing3410(); 025 frame.setSize(270,150); 026 frame.setVisible(true); 027 frame.startAnimation(); 028 } 029 030 public Listing3410() 031 { 032 super("Leuchtdiodenkette"); 033 setBackground(Color.lightGray); 034 addWindowListener(new WindowClosingAdapter(true)); 035 } 036 037 public void startAnimation() 038 { 039 th = new Thread(this); 040 th.start(); 041 } 042 043 public void run() 044 { 045 switched = -1; 046 dx = 1; 047 while (true) { 048 repaint(); 049 try { 050 Thread.sleep(SLEEP); 051 } catch (InterruptedException e){ 052 //nichts 053 } 054 switched += dx; 055 if (switched < 0 || switched > NUMLEDS - 1) { 056 dx = -dx; 057 switched += 2*dx; 058 } 059 } 060 } 061 062 public void paint(Graphics g) 063 { 064 for (int i = 0; i < NUMLEDS; ++i) { 065 g.setColor(i == switched ? ONCOLOR : OFFCOLOR); 066 g.fillOval(10+i*(LEDSIZE+2),80,LEDSIZE,LEDSIZE); 067 } 068 } 069 } |
Listing3410.java |
Das Programm zeigt eine Kette von 20 Leuchtdioden, die nacheinander an- und ausgeschaltet werden und dadurch ein Lauflicht simulieren, das zwischen linkem und rechtem Rand hin- und herläuft: |
|
Abbildung 34.11: Die Lauflicht-Animation
Wie kann nun aber das Löschen verhindert werden? Die Lösung basiert auf der Tatsache, dass bei einem Aufruf von repaint nicht gleich paint, sondern zunächst die Methode update aufgerufen wird. In der Standardversion der Klasse Component könnte update etwa so implementiert sein:
001 public void update(Graphics g) 002 { 003 g.setColor(getBackground()); 004 g.fillRect(0, 0, width, height); 005 g.setColor(getForeground()); 006 paint(g); 007 } |
Zunächst wird die aktuelle Hintergrundfarbe ausgewählt, um in dieser Farbe ein ausgefülltes Rechteck in der Größe des Bildschirms zu zeichnen. Erst nach diesem Löschvorgang wird die Vordergrundfarbe gesetzt und paint aufgerufen.
Da in Java alle Methodenaufrufe dynamisch gebunden werden, kann das Löschen dadurch verhindert werden, dass update durch eine eigene Version überlagert wird, die den Hintergrund unverändert läßt. Durch einfaches Hinzufügen der folgenden drei Zeilen kann das Flackern des Lauflichts vollkommen unterdrückt werden:
001 /* update1.inc */ 002 003 public void update(Graphics g) 004 { 005 paint(g); 006 } |
update1.inc |
Wie schon erwähnt, kann auf das Löschen des Bildschirms nur dann komplett verzichtet werden, wenn die Animation keine Bewegung enthält. Ist sie dagegen bewegt, kann es sinnvoll sein, nur die Teile des Bildes zu löschen, die beim aktuellen Animationsschritt leer sind, im vorigen Schritt aber Grafikelemente enthielten.
Um welche Teile der Grafik es sich dabei handelt, ist natürlich von der Art der Animation abhängig. Zudem muss jeder Animationsschritt Informationen über den vorigen Schritt haben, um die richtigen Stellen löschen zu können. Ein Beispiel, bei dem diese Technik gut angewendet werden kann, ist die bunte Schlange aus dem Abschnitt »Animation mit Grafikprimitiven«.
Da die Schlange bei jedem Schritt einen neuen Kopf bekommt und alle anderen Elemente die Plätze ihres jeweiligen Vorgängers einnehmen, bleibt als einziges wirklich zu löschendes Element das letzte Element der Schlange aus dem vorherigen Animationsschritt übrig. Dessen Position könnte man sich bei jedem Schritt merken und im nächsten Schritt in der Hintergrundfarbe neu zeichnen.
Noch einfacher geht es, indem man an die Schlange einfach ein zusätzliches unsichtbares Element anhängt. Wird nämlich das letzte Element grundsätzlich in der Hintergrundfarbe dargestellt, hinterlässt es keine Spuren auf dem Bildschirm und braucht damit auch nicht explizit gelöscht zu werden! Wir brauchen also nur hinter die for-next-Schleife zur Konstruktion der Schlange ein weiteres, unsichtbares Element an den snake-Vector anzuhängen (in Listing 34.13 in den Zeilen 025 bis 031 eingefügt):
001 /* Schlange2.inc */ 002 003 public void run() 004 { 005 //Schlange konstruieren 006 ColorRectangle cr; 007 int x = 100; 008 int y = 100; 009 for (int i=0; i < NUMELEMENTS; ++i) { 010 cr = new ColorRectangle(); 011 cr.x = x; 012 cr.y = y; 013 cr.width = SIZERECT; 014 cr.height = SIZERECT; 015 x += SIZERECT; 016 cr.color = new Color( 017 i*(256/NUMELEMENTS), 018 0, 019 240-i*(256/NUMELEMENTS) 020 ); 021 snake.addElement(cr); 022 } 023 024 //Löschelement anhängen 025 cr = new ColorRectangle(); 026 cr.x = x; 027 cr.y = y; 028 cr.width = SIZERECT; 029 cr.height = SIZERECT; 030 cr.color = BGCOLOR; 031 snake.addElement(cr); 032 033 //Vorzugsrichtung festlegen 034 dx = -1; 035 dy = -1; 036 037 //Schlange laufen lassen 038 while (true) { 039 repaint(); 040 try { 041 Thread.sleep(SLEEP); 042 } catch (InterruptedException e){ 043 //nichts 044 } 045 moveSnake(); 046 } 047 } |
Schlange2.inc |
Wird nun zusätzlich die Methode update überlagert, wie es auch im vorigen Abschnitt getan wurde, läuft die Schlange vollkommen flackerfrei.
Das Doppelpuffern bietet sich immer dann an, wenn die beiden vorigen Methoden versagen. Das kann beispielsweise dann der Fall sein, wenn es bei einer bewegten Animation zu aufwändig ist, nur den nicht mehr benötigten Teil der Bildschirmausgabe zu löschen, oder wenn der aktuelle Animationsschritt keine Informationen darüber besitzt, welcher Teil zu löschen ist.
Beim Doppelpuffern wird bei jedem Animationsschritt zunächst die gesamte Bildschirmausgabe in ein Offscreen-Image geschrieben. Erst wenn alle Ausgabeoperationen abgeschlossen sind, wird dieses Offscreen-Image auf die Fensteroberfläche kopiert. Im Detail sind dazu folgende Schritte erforderlich:
Durch diese Vorgehensweise wird erreicht, dass das Bild komplett aufgebaut ist, bevor es angezeigt wird. Da beim anschließenden Kopieren die neuen Pixel direkt über die alten kopiert werden, erscheinen dem Betrachter nur die Teile des Bildes verändert, die auch tatsächlich geändert wurden. Ein Flackern, das entsteht, weil Flächen für einen kurzen Zeitraum gelöscht und dann wieder gefüllt werden, kann nicht mehr auftreten.
Die Anwendung des Doppelpufferns ist nicht immer sinnvoll. Sollte eine der anderen Methoden mit vertretbarem Aufwand implementiert werden können, kann es sinnvoller sein, diese zu verwenden. Nachteilig sind vor allem der Speicherbedarf für die Konstruktion des Offscreen-Images und die Verzögerungen durch das doppelte Schreiben der Bilddaten. Hier muss im Einzelfall entschieden werden, welche Variante zum Einsatz kommen soll. In vielen Fällen allerdings können die genannten Nachteile vernachlässigt werden, und die Doppelpufferung ist ein probates Mittel, um das Bildschirmflackern zu verhindern. |
|
Das folgende Programm ist ein Beispiel für die Anwendung des Doppelpufferns bei der Ausgabe einer bewegten Animation. Wir wollen uns dafür die Aufabe stellen, eine große Scheibe über den Bildschirm laufen zu lassen, über deren Rand zwei stilisierte »Ameisen« mit unterschiedlicher Geschwindigkeit in entgegengesetzte Richtungen laufen.
Das folgende Programm löst diese Aufgabe. Dabei folgt die Animation
unserem bekannten Architekturschema für bewegte Grafik und braucht
hier nicht weiter erklärt zu werden. Um das Flackern zu verhindern,
deklarieren wir zwei Instanzvariablen, dbImage
und dbGraphics:
private Image dbImage;
private Graphics dbGraphics;
Glücklicherweise können die zum Doppelpuffern erforderlichen Schritte gekapselt werden, wenn man die Methode update geeignet überlagert:
|
|
Falls nicht schon geschehen, werden hier zunächst die beiden Variablen dbImage und dbGraphics initialisiert. Anschließend wird der Hintergrund gelöscht, wie es auch in der Standardversion von update der Fall ist. Im Gegensatz zu dieser erfolgt das Löschen aber auf dem Offscreen-Image und ist somit für den Anwender nicht zu sehen. Nun wird paint aufgerufen und bekommt anstelle des normalen den Offscreen-Grafikkontext übergeben. Ohne selbst etwas davon zu wissen, sendet paint damit alle seine Grafikbefehle auf das Offscreen-Image. Nachdem paint beendet wurde, wird durch Aufruf von drawImage das Offscreen-Image auf dem Bildschirm angezeigt.
Hier ist der komplette Quellcode des Programms:
001 /* Listing3415.java */ 002 003 import java.awt.*; 004 import java.awt.event.*; 005 006 public class Listing3415 007 extends Frame 008 implements Runnable 009 { 010 private Thread th; 011 private int actx; 012 private int dx; 013 private int actarc1; 014 private int actarc2; 015 private Image dbImage; 016 private Graphics dbGraphics; 017 018 public static void main(String[] args) 019 { 020 Listing3415 frame = new Listing3415(); 021 frame.setSize(210,170); 022 frame.setVisible(true); 023 frame.startAnimation(); 024 } 025 026 public Listing3415() 027 { 028 super("Ameisenanimation"); 029 addWindowListener(new WindowClosingAdapter(true)); 030 } 031 032 public void startAnimation() 033 { 034 Thread th = new Thread(this); 035 th.start(); 036 } 037 038 public void run() 039 { 040 actx = 0; 041 dx = 1; 042 actarc1 = 0; 043 actarc2 = 0; 044 while (true) { 045 repaint(); 046 actx += dx; 047 if (actx < 0 || actx > 100) { 048 dx = -dx; 049 actx += 2*dx; 050 } 051 actarc1 = (actarc1 + 1) % 360; 052 actarc2 = (actarc2 + 2) % 360; 053 try { 054 Thread.sleep(40); 055 } catch (InterruptedException e) { 056 //nichts 057 } 058 } 059 } 060 061 public void update(Graphics g) 062 { 063 //Double-Buffer initialisieren 064 if (dbImage == null) { 065 dbImage = createImage( 066 this.getSize().width, 067 this.getSize().height 068 ); 069 dbGraphics = dbImage.getGraphics(); 070 } 071 //Hintergrund löschen 072 dbGraphics.setColor(getBackground()); 073 dbGraphics.fillRect( 074 0, 075 0, 076 this.getSize().width, 077 this.getSize().height 078 ); 079 //Vordergrund zeichnen 080 dbGraphics.setColor(getForeground()); 081 paint(dbGraphics); 082 //Offscreen anzeigen 083 g.drawImage(dbImage,0,0,this); 084 } 085 086 public void paint(Graphics g) 087 { 088 int xoffs = getInsets().left; 089 int yoffs = getInsets().top; 090 g.setColor(Color.lightGray); 091 g.fillOval(xoffs+actx,yoffs+20,100,100); 092 g.setColor(Color.red); 093 g.drawArc(xoffs+actx,yoffs+20,100,100,actarc1,10); 094 g.drawArc(xoffs+actx-1,yoffs+19,102,102,actarc1,10); 095 g.setColor(Color.blue); 096 g.drawArc(xoffs+actx,yoffs+20,100,100,360-actarc2,10); 097 g.drawArc(xoffs+actx-1,yoffs+19,102,102,360-actarc2,10); 098 } 099 } |
Listing3415.java |
Ein Schnappschuß des laufenden Programms sieht so aus (die beiden »Ameisen« sind in der Abbildung etwas schwer zu erkennen, im laufenden Programm sieht man sie besser):
Abbildung 34.12: Eine Animation mit Doppelpufferung
Durch die Kapselung des Doppelpufferns können Programme sogar nachträglich flackerfrei gemacht werden, ohne dass in den eigentlichen Ausgaberoutinen irgend etwas geändert werden müsste. Man könnte beispielsweise aus Frame eine neue Klasse DoubleBufferFrame ableiten, die die beiden privaten Membervariablen dbImage und dbGraphics besitzt und update in der beschriebenen Weise implementiert. Alle Klassen, die dann von DoubleBufferFrame anstelle von Frame abgeleitet werden, unterstützen das Doppelpuffern ihrer Grafikausgaben automatisch.
Titel | Inhalt | Suchen | Index | DOC | Handbuch der Java-Programmierung, 5. Auflage, Addison Wesley, Version 5.0.2 |
<< | < | > | >> | API | © 1998, 2007 Guido Krüger & Thomas Stark, http://www.javabuch.de |