From 33613a85afc4b1481367fbe92a17ee59c240250b Mon Sep 17 00:00:00 2001 From: Sven Eisenhauer Date: Fri, 10 Nov 2023 15:11:48 +0100 Subject: add new repo --- .../hjp5/html/k100226.html | 1504 ++++++++++++++++++++ 1 file changed, 1504 insertions(+) create mode 100644 Master/Reference Architectures and Patterns/hjp5/html/k100226.html (limited to 'Master/Reference Architectures and Patterns/hjp5/html/k100226.html') diff --git a/Master/Reference Architectures and Patterns/hjp5/html/k100226.html b/Master/Reference Architectures and Patterns/hjp5/html/k100226.html new file mode 100644 index 0000000..546f4de --- /dev/null +++ b/Master/Reference Architectures and Patterns/hjp5/html/k100226.html @@ -0,0 +1,1504 @@ + + + +Handbuch der Java-Programmierung, 5. Auflage + + + + + + + + + +
 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

+ +

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

Die repaint-Schleife

+ +

+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
+ +Listing 34.6: Ein animierter Zähler

+ +

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

+ + + + +
 Warnung 
+
+ + + + +

Verwendung von Threads

+ +

+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
+ +Listing 34.7: Verwendung von Threads zur Animation

+ +

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

34.2.2 Abspielen einer Folge von Bitmaps

+ +

+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
+ +Listing 34.8: Abspielen einer Folge von Bitmaps

+

+ + + + + + + + + +
+ +

+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:

+ + + + +
 Hinweis 
+
+

+ + +

+ +

+Abbildung 34.5: Animation eines Schriftzugs, Schnappschuß 1

+

+ + +

+ +

+Abbildung 34.6: Animation eines Schriftzugs, Schnappschuß 2

+

+ + +

+ +

+Abbildung 34.7: Animation eines Schriftzugs, Schnappschuß 3

+ + + + +

34.2.3 Animation mit Grafikprimitiven

+ +

+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
+ +Listing 34.9: Die animierte Schlange

+

+ + + + + + + + + +
+ +

+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:

+ + + + +
 Hinweis 
+
+

+ + +

+ +

+Abbildung 34.8: Die animierte Schlange, Schnappschuß 1

+

+ + +

+ +

+Abbildung 34.9: Die animierte Schlange, Schnappschuß 2

+

+ + +

+ +

+Abbildung 34.10: Die animierte Schlange, Schnappschuß 3

+ + + + +

34.2.4 Reduktion des Bildschirmflackerns +

+ +

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

Bildschirm nicht löschen

+ +

+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
+ +Listing 34.10: Bildschirmflackern reduzieren bei stehenden Animationen

+

+ + + + + + + + + +
+ +

+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:

+ + + + +
 Hinweis 
+
+

+ + +

+ +

+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 }
+
+
+ +Listing 34.11: Standard-Implementierung von update

+ +

+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
+ +Listing 34.12: Modifizierte Version von update

+ + + + +

Nur den wirklich benötigten Teil des Bildschirms löschen +

+ +

+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
+ +Listing 34.13: Modifizierte Schlangenanimation

+ +

+Wird nun zusätzlich die Methode update +überlagert, wie es auch im vorigen Abschnitt getan wurde, läuft +die Schlange vollkommen flackerfrei. + + + + +

Doppelpufferung

+ +

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

+ + + + +
 Tipp 
+
+ +

+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: + + +

+ + + + + +
+ +
+001 /* update2.inc */
+002 
+003 public void update(Graphics g)
+004 {
+005   //Double-Buffer initialisieren
+006   if (dbImage == null) {
+007     dbImage = createImage(
+008      this.getSize().width,
+009      this.getSize().height
+010     );
+011     dbGraphics = dbImage.getGraphics();
+012   }
+013   //Hintergrund löschen
+014   dbGraphics.setColor(getBackground());
+015   dbGraphics.fillRect(
+016     0,
+017     0,
+018     this.getSize().width,
+019     this.getSize().height
+020   );
+021   //Vordergrund zeichnen
+022   dbGraphics.setColor(getForeground());
+023   paint(dbGraphics);
+024   //Offscreen anzeigen
+025   g.drawImage(dbImage,0,0,this);
+026 }
+
+
+update2.inc
+ +Listing 34.14: update-Methode mit Doppelpufferung

+
+ + + + +
 Hinweis 
+
+ +

+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
+ +Listing 34.15: Animation mit Doppelpufferung

+ +

+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 +
+ + + -- cgit v1.2.3