From 33613a85afc4b1481367fbe92a17ee59c240250b Mon Sep 17 00:00:00 2001
From: Sven Eisenhauer
+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:
+
+
+
+
+
+
+ Titel
+ Inhalt
+ Suchen
+ Index
+ DOC
+ Handbuch der Java-Programmierung, 5. Auflage
+
+ <<
+ <
+ >
+ >>
+ API
+ Kapitel 34 - Bitmaps und Animationen
+
+
+
+
+
+34.2 Animation
+
+
+
+
+
+
+
+
+34.2.1 Prinzipielle Vorgehensweise
+
+
+
+
+Die repaint-Schleife
+
+
+
+
+Listing 34.6: Ein animierter Zähler
+
+
+
+
+
+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.1 + |
| << + | < + | > + | >> + | API + | © 1998, 2007 Guido Krüger & Thomas +Stark, http://www.javabuch.de + |