Previous Page TOC Index Next Page See Page

10

Animationen, Bilder, Threads und Sound

von
Laura Lemay

Viele haben als erstes Java-Programm eine einfache Animation auf einer Seite im World Wide Web gesehen. Fortlaufende Überschriften, tanzende Schlagzeilen und andere Effekte waren komplett neu, als Java 1995 eingeführt wurde.

Diese Art von Animationseffekten benötigen nur einige Methoden, die in Java implementiert werden müssen. Aber diese Methoden bilden die Grundlage eines jeden Java-Applet, das den Bildschirm dynamisch aktualisiert, z.B. Animationen, sich ändernde Datentabellen und andere Programme.

In Java läßt sich eine Animation erstellen, indem Sie verschiedene Teile des Abstract Windowing Toolkit (AWT) zueinander in bezug setzen. Heute lernen Sie die Grundlagen der Animation in Java kennen: Wie die verschiedenen Teile des Systems zusammenarbeiten, damit sich Figuren bewegen und Applets dynamisch aktualisiert werden.

Das Erstellen von Animationen macht Spaß und läßt sich in Java einfach ausführen, aber Sie können nicht mehr tun, als die vordefinierten Java-Methoden für Linien, Schriftarten und Farben ermöglichen. Wenn Sie wirklich interessante Animationen entwerfen möchten, sollten Sie eigene Bilder für jeden einzelnen Rahmen der Animation verwenden und auch mit selbst ausgewählten Klangdateien arbeiten.

Während des heutigen Tages werden folgende Themen behandelt:

Animationen in Java erstellen

Animationen in Java werden mit zwei grundlegenden Schritten erstellt: Konstruktion eines Einzelbildes (Frame) für die Animation und dann Java zum Zeichnen dieses Einzelbildes veranlassen. Dieser letzte Schritt läßt sich so oft wiederholen, daß die Illusion einer Bewegung entsteht. Aus den einfachen, statischen Applets, die Sie gestern geschrieben haben, haben Sie gelernt, wie der erste Teil bewältigt wird. Heute lernen Sie den zweiten Teil kennen und dazu gehört eigentlich nur, wie man Java anweist, einen Frame zu zeichnen.

Zeichnen und Nachzeichnen

Die paint()-Methode, die Sie gestern gelernt haben, wird von Java immer aufgerufen, wenn ein Applet gezeichnet werden muß – beim erstmaligen Zeichnen des Applet, wenn Applet-Fenster verschoben oder durch ein anderes Fenster überlagert werden. Sie können Java aber auch anweisen, ein Applet zu einem bestimmten Zeitpunkt nachzuzeichnen. Um die Darstellung am Bildschirm zu ändern, erstellen Sie das Bild – oder das Einzelbild –, das Sie zeichnen möchten. Anschließend weisen Sie Java an, dieses Einzelbild zu zeichnen. Wenn Sie diesen Vorgang wiederholen, und zwar schnell genug, erhalten Sie eine Animation innerhalb des Java-Applet.

Wo wird dies alles ausgeführt? Nicht in der paint()-Methode selbst. Die paint()-Methode plaziert lediglich die Bildpunkte am Bildschirm. Mit anderen Worten: Paint() ist zwar verantwortlich für den aktuellen Frame der Animation, aber die eigentlichen Arbeit, d.h. die Änderungen die paint() ausführt, wird in anderen Bereichen des Applet definiert.

In diesen »anderen Bereichen« konstruieren Sie den Frame (definieren Variablen für paint(), richten Farben, Schriftarten und andere Objekte ein, die paint() benötigt) und rufen dann die repaint()-Methode auf. Die Methode repaint() ist der Auslöser mit dem Java paint() aufruft und Ihren Frame zeichnet.


Wenn Sie repaint() und damit auch paint() aufrufen, zeichnen Sie nicht unmittelbar am Bildschirm, wie dies in anderen Fenster- oder Grafikwerkzeugen der Fall ist. Repaint() ist zunächst nur eine Aufforderung an Java, das Applet nachzuzeichnen, sobald dies möglich ist. Wenn zu viele Repaint()-Aufforderungen innerhalb kurzer Zeit erfolgt sind, ruft das System repaint() nur einmal für alle Aufforderungen auf. Die Verzögerung zwischen Aufruf und der tatsächlichen Anzeige ist aber unbedeutend. Doch sollten Sie dies beim Erstellen von Animationen im Gedächtnis behalten.

Starten und stoppen eines Applet

Erinnern Sie sich an die Methoden start() und stop() aus der 8. Lektion? Damit wird die Ausführung eines Applet begonnen und beendet. Sie haben start() und stop() gestern nicht benutzt, weil die gestrigen Applets außer der einmaligen Anzeige nichts bewirkten. Bei Animationen und anderen Java-Applets, die länger verarbeitet und ausgeführt werden, ist start() und stop() erforderlich, um die Ausführung des Applet zu beginnen und beim Verlassen der Seite, die das Applet enthält, zu stoppen. Bei den meisten Applets werden start() und stop() aus genau diesem Grund überschrieben.

Die start()-Methode löst die Ausführung des Applet aus. Sie können entweder die gesamte Arbeit eines Applet in diese Methode einbinden oder andere Objektmethoden dafür verwenden. Im allgemeinen wird start() verwendet, um die Ausführung eines Thread zu beginnen, damit das Applet zum angegebenen Zeitpunkt abläuft.

Die stop()-Methode hingegen bricht die Ausführung eines Applet ab, d.h. wenn die Seite verlassen wird, auf der sich das Applet befindet, läuft das Applet nicht weiter ab und verbraucht keine Systemressourcen mehr. Im allgemeinen wird beim Erstellen einer start()-Methode auch eine zugehörig stop()-Methode definiert.

Die fehlende Verbindung: Threads

Für das Erstellen von Animationen benötigen Sie noch eine weitere wichtige Komponente: Threads. Die Threads werden detailliert am 18. Tag erläutert, doch soll hier schon einmal das Konzept der Threads eingeführt werden: Alle Vorgänge, die in einem Java-Programm kontinuierlich ablaufen und viel Verarbeitungszeit benötigen, sollten in eigenen Threads ausgeführt werden. Eine Animation ist ein solcher Vorgang. Um eine Animation in Java zu erstellen, verwenden Sie deshalb die start()-Methode dazu, einen Thread zu starten, in dessen run()-Methode dann die gesamte Verarbeitung der Animation stattfindet. Damit läßt sich die Animation autonom ausführen und Konflikte mit anderen Teilen des Programms werden vermieden.

Die Threads bilden einen wichtigen Bereich innerhalb von Java und beim Programmieren in Java. Je größer die Java-Programme sind und je mehr Aufgaben diese ausführen, desto wahrscheinlicher werden Sie mit Threads arbeiten. Je nach Ihrer Erfahrung mit Betriebssystemen und den Umgebungen dieser Systeme können Sie das auf das Konzept von Threads zurückgreifen oder auch nicht. Deshalb soll dieses hier von Anfang an erläutert werden.

Lassen Sie uns zunächst mit einem Bild den Sachverhalt veranschaulichen: Stellen Sie sich eine Schulklasse vor, die in einem Bus zu einem Ausflugsziel unterwegs ist. Um die Zeit zu vertreiben, stimmen die Lehrer ein Lied an. Zu Beginn der Fahrt singen die Schüler ein Lied und wenn dieses beendet ist, beginnen sie ein neues. Verschiedene Gruppen innerhalb des Busses könnten zwar auch verschiedene Lieder gleichzeitig singen, dies klänge aber nicht besonders schön. Wenn jeweils nur ein Lied gesungen wird, entsteht eine Art Zeitmonopol für dieses Lied, das nächste kann erst anschließend gesungen werden.

Angenommen, es gibt nun noch eine zweite Schulklasse, die sich in einem zweiten Bus auf demselben Weg zu demselben Ausflugsziel befindet. Beide Busse fahren mit der gleichen Geschwindigkeit und darin befindet sich jeweils eine singende Schulklasse. In dem einen Bus wird jedoch ein anderes Lied gesungen, als im zweiten Bus. In derselben Zeitspanne lassen sich also innerhalb von zwei Bussen zwei verschiedenen Lieder singen, ohne daß dabei ein Konflikt entsteht.

So ähnlich sollten Sie sich Threads vorstellen. In einem gewöhnlichen Thread-Programm, wird das Programm gestartet, der Initialisierungscode ausgeführt, Methoden oder Prozeduren aufgerufen und die Ausführung wird fortgesetzt, bis das Ende erreicht ist oder das Programm beendet wird. Das Programm läuft jedoch innerhalb eine einzelnen Threads ab – d.h. in einem Bus mit all den Schülern.

Multithreading bedeutet in Java, daß sich verschiedene Teile desselben Programms innerhalb derselben Zeitspanne parallel ausführen lassen und keine Querverbindungen bestehen. Die vielen jeweils autonom ausgeführten Threads sind mit den vielen Bussen vergleichbar, in denen sich unterschiedliche Dinge abspielen.

Wenn Sie in Java Threads verwenden, können Sie bestimmte Bereiche eines Applet (oder einer Anwendung) erstellen, die in eigenen Threads ausgeführt werden und jeweils unabhängig voneinander ablaufen. Je nach Anzahl der verwendeten Threads müssen Sie eventuell Rücksicht auf das System nehmen, d.h. in Kauf nehmen, daß alle insgesamt etwas langsamer ablaufen, aber dennoch werden diese gleichzeitig und unabhängig ausgeführt.

Auch wenn Sie nicht viele Threads benötigen, hat sich die Verwendung von Threads in Java als gute Programmierpraxis erwiesen. Als allgemeine Faustregel für ein gut konzipiertes Applet läßt sich festhalten: Wann immer Sie eine Verarbeitung benötigen, die kontinuierlich über einen bestimmten Zeitraum ausgeführt werden soll (z.B. eine Animations-Schleife oder einen Code, der innerhalb einer längeren Zeitspanne ausgeführt wird), sollten Sie diese in einem Thread plazieren.

Applets mit Threads schreiben

Das Erstellen von Applets, die Threads verwenden, ist sehr einfach. In den meisten Fällen müssen Sie für die Verwendung von Threads lediglich Platzhaltercodes eingeben, die sich von einem Applet in ein anderes kopieren lassen. Da diese Technik so einfach ist, gibt es auch keinen Grund auf die Threads in einem Applet zu verzichten, denn die Vorteile sind bei weitem größer.

Um ein Applet zu erstellen, das Threads verwendet, sind die folgenden vier Änderungen notwendig:

public class MyAppletClass extends java.applet.Applet {

...
}
public class MyAppletClass extends java.applet.Applet implements Runnable {

...
}

Was wird dadurch bewirkt? Der Code enthält nun die Runnable-Schnittstelle für Ihr Applet. Wenn Sie kurz an den 2. Tag zurückdenken, erinnern Sie sich daran, daß Schnittstellen in Java dazu dienen, Methodennamen zu sammeln, die verschiedenen Klassen gemein sind. Dadurch lassen sich diese darin mischen und in jenen Klassen einbinden, für die dieses Verhalten implementiert werden soll. In diesem Fall definiert die Runnable-Schnittstelle jenes Verhalten, das das Applet für die Ausführung eines Thread benötigt; insbesondere wird hiermit eine Standarddefinition für die run()-Methode gegeben. Durch die Einbindung von Runnable informieren Sie andere darüber, daß diese die run()-Methode aufrufen können.

Der zweite Schritt besteht darin eine Instanzvariable einzufügen, die den Thread dieses Applet aufnimmt. Dafür können Sie einen beliebigen Namen wählen. Die Variable gehört dem Typ Thread an (Thread ist eine Klasse in java.lang, sie muß also nicht importiert werden):

Thread runner;

Als dritten Schritt fügen Sie eine start()-Methode ein oder ändern die vorhandene dahingehend ab, daß diese ausschließlich einen neuen Thread erstellt und dessen Ausführung startet:

public void start() {

if (runner == null) {
runner = new Thread(this);
runner.start();
}
}

Wenn Sie start() so eingerichtet haben, daß außer der Ausführung des Thread nichts anderes definiert ist, wo wird dann der Code plaziert, der das Applet ausführt? Dieser wird in eine neue Methode namens run() eingefügt und dies sieht wie folgt aus:

public void run() {

// was Ihr Applet eigentlich tun soll
}

Diese neu definierte run()-Methode überschreibt die Standardversion von run(), die Sie erhalten, wenn Sie die Runnable-Schnittstelle in das Applet einfügen.

Run() kann alle Elemente enthalten, die sich in separaten Threads ausführen möchten: Initialisierungscode, für die aktuelle Schleife für das Applet oder etwas anderes, das in einem eigenen Thread ablaufen soll. Sie können innerhalb von run() auch neue Objekte erstellen und Methoden aufrufen. Diese laufen dann ebenfalls innerhalb dieses Thread ab. Die run()-Methode ist das eigentliche Kernstück Ihres Applet.

Schließlich, wenn die Threads ausgeführt werden und eine start()-Methode dafür definiert ist, sollten Sie auch eine stop()-Methode hinzufügen, um die Ausführung des Thread (und damit alles, was das Applet macht) zu beenden, sobald der Leser die Seite verläßt. stop() sieht ähnlich aus wie start():

public void stop() {

if (runner != null) {
runner.stop();
runner = null;
}
}

Diese stop()-Methode bewirkt zweierlei: Sie hält die Ausführung des Thread an und definiert die Variable des Thread (runner) mit Null. Durch letzteres wird das Thread-Objekt für den Garbage Collector verfügbar, kann also jederzeit nach einer gewissen Zeit aus dem Speicher entfernt werden. Kehrt der Benutzer später auf diese Seite zurück und ruft das Applet wieder auf, erstellt die start()-Methode einen neuen Thread und startet das Applet wieder.

Das ist alles! Vier einfache Änderungen und schon haben Sie ein Applet mit bestem Verhalten erstellt, das in einem eigenen Thread ausgeführt wird.

Zusammenstellung aller Teile

Java-Animationen zu beschreiben ist viel schwieriger als diese anhand von Programmcode zu erläutern, der eine Animation ausführt. Im folgenden Beispiel soll die Beziehung der einzelnen Methoden zueinander deutlich gemacht werden.

Listing 10.1 zeigt ein Beispiel-Applet, welches eine einfache Applet-Animation verwendet, um Datum und Uhrzeit anzuzeigen. Diese Anzeige wird jede Sekunde konstant aktualisiert. Der Code erstellt letztlich eine digitale Uhr (ein Einzelbild von dieser Uhr ist in Abb. 10.1 zu sehen).


Dieses Applet ist für die Verwendung der Klasse GregorianCalendar aus 1.1 JDK geschrieben. Sie können dieselben Funktionen mit 1.02 ausführen, indem Sie die etwas beschränktere (und weniger internationale) Date-Klasse verwenden.

siehe Abbildung

Abbildung 10.1:
Die digitale Uhr

Dieses Applet verwendet die Methoden paint(), repaint(), start() und stop(). Ferner verwendet es Threads.

Listing 10.1: Der komplette Quelltext für DigitalClock.java

 1: import java.awt.Graphics;

2: import java.awt.Font;
3: import java.util.Calendar;
4: import java.util.GregorianCalendar;
5:
6: public class DigitalClock extends java.applet.Applet
7: implements Runnable {
8:
9: Font theFont = new Font("TimesRoman",Font.BOLD,24);
10: GregorianCalendar theDate;
11: Thread runner;
12:
13: public void start() {
14: if (runner == null) {
15: runner = new Thread(this);
16: runner.start();
17: }
18: }
19:
20: public void stop() {
21: if (runner != null) {
22: runner.stop();
23: runner = null;
24: }
25: }
26:
27: public void run() {
28: while (true) {
29: repaint();
30: try { Thread.sleep(1000); }
31: catch (InterruptedException e) { }
32: }
33: }
34:
35: public void paint(Graphics g) {
36: theDate = new GregorianCalendar();
37: g.setFont(theFont);
38: g.drawString("" + theDate.getTime(), 10, 50);
39: }
40: }

Eine Animation ist ein gutes Beispiel dafür, welche Aufgaben in eigenen Threads ausgeführt werden sollten. Beachten Sie die Endlosschleife while() im Applet DigitalClock. Wenn Sie keine Threads verwenden, wird while() im Standard-SystemThread von Java ausgeführt, welcher auch für das Zeichnen am Bildschirm, Benutzereingaben wie Mausklicks und alle internen Aktualisierungen zuständig ist. Wenn Sie jedoch die while()-Schleife im Hauptsystem-Thread ausführen, entsteht dadurch ein Monopol auf alle Java-Ressourcen, das alle anderen Vorgänge, einschließlich des Zeichnens, verhindert. Sie würden nichts auf dem Bildschirm sehen, denn Java wartet darauf, daß die while()-Schleife beendet wird, ehe es andere Aktionen ausführt.

Zunächst sollen die aktuellen Animationsteile im Applet und anschließend die Teile für die Threads erläutert werden.

Die Zeilen 9 und 10 definieren zwei grundlegende Instanzvariablen: theFont und theDate, die jeweils Objekte für die Darstellung der aktuellen Schriftart und des aktuellen Datums enthalten. Darüber erfahren Sie später mehr.

Die Methoden start() und stop() starten und beenden hier einen Thread; die eigentlichen Aktionen des Applet sind in der run()-Methode plaziert (Zeilen 27 und 33).

Innerhalb von run() findet die eigentliche Animation statt. Beachten Sie die while-Schleife in dieser Methode (beginnt mit der Anweisung in Zeile 28). Vorausgesetzt die Bedingung ist immer wahr (Ausgabe von true), endet die Schleife niemals. Innerhalb der while-Schleife ist ein einzelner Frame der Animation definiert.

Als erstes wird in der Schleife die Anweisung repaint() in Zeile 29 aufgerufen, die das Applet nachzeichnet. Die Zeilen 30 und 31 sehen zwar kompliziert aus, fügen aber lediglich eine Pause von 1000 Millisekunden (1 Sekunde) vor der Wiederholung der Schleife ein. Die sleep()-Methode, ein Teil der Thread-Klasse, veranlaßt das Applet zu dieser Pause. Ohne genau definierte sleep()-Methode würde das Applet so schnell wie möglich ausgeführt. Die sleep-Methode kontrolliert exakt, wie schnell die Animation ausgeführt wird. Die try- und catch-Elemente darum herum sorgen dafür, daß Java mit Fehlern umgehen kann, falls diese auftreten. Try und catch behandeln Ausnahmesituationen, die am 17. Tag detailliert beschrieben werden.

Doch nun zu den paint()-Methoden in den Zeilen 34 bis 37. Hier wird eine neue Instanz der Klasse GregorianCalendar erstellt, welche das aktuelle Datum und die Uhrzeit enthalten soll. Beachten Sie, daß diese in Zeile 4 extra importiert wurde. Das neue Objekt GregorianCalendar wird der Instanzvariablen theDate zugewiesen.

In Zeile 37 wird die aktuelle Schriftart mit dem Wert der Variablen theFont definiert und das Datum selbst wird am Bildschirm angezeigt. Beachten Sie, daß die Methode getTime() des GregorianCalendar aufgerufen werden muß, damit Datum und Uhrzeit als Zeichenkette angezeigt werden. Immer wenn paint() aufgerufen wird, wird ein neues theDate-Objekt erstellt, welches das aktuelle Datum und die Uhrzeit enthält.

Werfen wir nun einen Blick auf die Zeilen dieses Applet, das Threads erstellt und verwalten. Sehen Sie sich zunächst die Klassendefinition selbst in den Zeilen 6 und 7 an – beachten Sie, daß die Klassendefinition die Runnable-Schnittstelle enthält. Alle erstellten Klassen, die Threads verwenden, müssen Runnable enthalten.

Zeile 11 definiert eine dritte Instanzvariable für diese Klasse, die runner genannt wird und dem Typ Thread angehört. Diese enthält das Thread-Objekt für dieses Applet.

Die Zeilen 13 und 25 definieren die Platzhaltermethoden start() und stop(), welche lediglich dazu dienen, Threads zu erstellen und zu zerstören. Diese Methodendefinitionen können im wesentlichen von Klasse zu Klasse identisch sein, denn sie dienen nur dazu, die Infrastruktur für den Thread selbst einzurichten.

Und schließlich wird die Hauptarbeit des Applet in der run()-Methode in den Zeilen 27 bis 33 eingerichtet.

Flimmereffekt in Animationen

Wenn Sie dieser Lektion bis hierher gefolgt sind und die Beispiele ausprobiert haben (das Buch also nicht in einem Flugzeug oder der Badewanne lesen), haben Sie vermutlich gemerkt, daß bei der Ausführung des digitalen Uhrprogramms gelegentlich ein Flimmern in der Animation auftritt. (Es spricht nichts dagegen, das Buch in der Badewanne zu lesen, aber dort können Sie den Flimmereffekt nicht nachvollziehen, in diesem Fall müssen Sie mir einfach glauben). Bei diesem Flimmereffekt handelt es sich nicht um einen Fehler im Programm; leider ist das Flimmern ein Nebeneffekt beim Erstellen von Animationen in Java. Da dies relativ lästig ist, lernen Sie im folgenden Teil der heutigen Lektion, wie sich dieser Flimmereffekt reduzieren läßt, um einen besseren und schöneren Animationsablauf zu erzielen.

Wie Flimmern vermieden werden kann

Das Flimmern wird durch die Art verursacht, in der Java die einzelnen Frames eines Applet zeichnet und wiederholt. Am Anfang der heutigen Lektion haben Sie gelernt, daß die repaint()-Methode paint() aufruft. Dies ist nicht ganz richtig. Eine Aufruf von paint() findet als Reaktion auf repaint() statt, jedoch laufen tatsächlich folgende Schritte ab:

1. Der Aufruf von repaint() führt zu einem Aufruf der Methode update().

Die zweite Variante scheint nicht nur kompliziert, sondern ist es auch. Das doppelte Puffern erfordert das Zeichnen auf einer Grafikoberfläche außerhalb des Bildschirms und das anschließende Kopieren dieser gesamten Oberfläche auf den Bildschirm. Da dieses Verfahren nicht ganz so einfach ist, soll es ein wenig später erläutert werden. Zunächst wird die einfachere Lösung untersucht: das Überschreiben von update().

Überschreiben der update-Methode

Die Ursache für das Flimmern liegt in der update()-Methode. Um das Flimmern zu verringern, überschreiben Sie update(). Im folgenden sehen Sie, was die Standardversion von update() bewirkt (befindet sich in der Component-Klasse, die Teil des AWT und eine der Superklassen der Applet-Klasse ist. Weitere Informationen hierzu finden Sie am 12. Tag):

public void update(Graphics g) {

g.setColor(getBackground());
g.fillRect(0, 0, size.width, size.height);
g.setColor(getForeground());
paint(g);
}

Im wesentlichen leert update() den Bildschirm (bzw. um genau zu sein, füllt das Rechteck um das Applet mit der Hintergrundfarbe), richtet die Standardeinstellungen wieder ein und ruft dann paint() auf. Wenn Sie update() überschreiben, sollten Sie zwei Dinge beachten und sicherstellen, daß Ihre update()-Version ähnliche Aufgaben ausführt. In den nächsten beiden Abschnitten werden einige Beispiele durchgearbeitet, in denen update()jeweils auf verschiedene Art überschrieben wird, um das Flimmern zu reduzieren.

Bildschirm nicht leeren

Die erste Lösung zur Verringerung des Flimmereffekts besteht darin, den Bildschirm überhaupt nicht zu leeren. Das funktioniert allerdings nicht bei allen Applets. Im folgenden finden Sie ein Beispiel für ein Applet dieser Art. Das Applet ColorSwirl gibt eine Zeichenkette am Bildschirm aus (»Look to the Cookie!«), aber diese Zeichenkette erscheint in verschiedenen Farben, die dynamisch wechseln. Das Applet flimmert sehr stark. Listing 10.2 zeigt den ursprünglichen Quellcode für dieses Applet und Abb. 10.2 stellt das Ergebnis dar.

siehe Abbildung

Abbildung 10.2:
Die Ausgabe des Applet ColorSwirl im Appletviewer

Listing 10.2: Der komplette Quelltext von ColorSwirl.java

 1: import java.awt.Graphics;

2: import java.awt.Color;
3: import java.awt.Font;
4:
5: public class ColorSwirl extends java.applet.Applet
6: implements Runnable {
7:
8: Font f = new Font("TimesRoman", Font.BOLD, 48);
9: Color colors[] = new Color[50];
10: Thread runner;
11:
12: public void start() {
13: if (runner == null) {
14: runner = new Thread(this);
15: runner.start();
16: }
17: }
18:
19: public void stop() {
20: if (runner != null) {
21: runner.stop();
22: runner = null;
23: }
24: }
25:
26: public void run() {
27:
28: // das Array mit den Farben initialisieren
29: float c = 0;
30: for (int i = 0; i < colors.length; i++) {
31: colors[i] =
32: Color.getHSBColor(c, (float)1.0,(float)1.0);
33: c += .02;
34: }
35:
36: // die Farben durchgehen
37: int i = 0;
38: while (true) {
39: setForeground(colors[i]);
40: repaint();
41:
42: i++;
43: try { Thread.sleep(200); }
44: catch (InterruptedException e) { }
45: if (i == colors.length ) i = 0;
46: }
47: }
48:
49: public void paint(Graphics g) {
50: g.setFont(f);
51: g.drawString("Look to the Cookie!", 15, 50);
52: }
53: }

Drei Aspekte dieses Applet sind interessant und erscheinen Ihnen vielleicht merkwürdig:

Sie wissen nun, welche Aufgabe das Applet hat. Jetzt soll der Flimmereffekt reduziert werden. Das Flimmern entsteht hier weil nach jedem Neuzeichnen des Applet ein Moment eintritt, bei dem der Bildschirm geleert wird. Der Text wechselt nicht sanft von Rot auf Pink und Lila, sondern zunächst von Rot auf Grau, von Pink auf Grau und von Lila auf Grau usw. Dies sieht nicht sehr gelungen aus.

Das Problem entsteht durch das Leeren des Bildschirms, deshalb ist die Lösung einfach: Überschreiben Sie update() und entfernen Sie jenen Teil, in dem der Bildschirm geleert wird. Eigentlich muß der Bildschirm ohnehin nicht geleert werden, da mit Ausnahe der Textfarbe keine Änderungen erfolgen. Sobald das Leeren des Bildschirms aus update() entfernt ist, muß update() lediglich noch paint() aufrufen. Im folgenden finden Sie die solchermaßen angepaßte update()-Methode für dieses Applet (diese Änderung kann nach der paint()-Methode hinter Zeile 51 eingefügt werden):

public void update(Graphics g) {

paint(g);
}

Durch diese drei kleinen Zeilen wird der Flimmereffekt beseitigt.


Wenn Sie die Beispiele auf der CD-ROM durcharbeiten, finden Sie die erste Version von ColorSwirl.java im Ordner FirstColorSwirl und die zweite Fassung im Ordner SecondColorSwirl.

Bilder laden und verwenden

Die grundlegende Arbeit mit Bildern in Java ist eine einfache Angelegenheit. Die Image-Klasse im Paket java.awt sieht abstrakte Methoden für das allgemeine Bildverhalten vor. Zusammen mit den speziellen Methoden, die Applet und Graphics definiert werden, stehen hiermit alle Mittel für das Laden und Anzeigen von Bildern in einem Applet zur Verfügung. Dies ist nicht schwieriger als das Zeichnen eines Rechtecks. In diesem Abschnitt erfahren Sie, wie Bilder in Java-Applets gezeichnet werden.

Bilder einbinden

Um ein Bild in einem Applet anzuzeigen, müssen Sie das Bild zunächst aus dem World Wide Web in das Java-Programm laden. Bilder werden als eigene Dateien, unabhängig von den Java-Klassendateien gespeichert. Aus diesem Grund müssen Sie Java darüber informieren, wo die Bilder zu finden sind.

Die Applet-Klasse enthält eine Methode namens getImage(), mit welcher ein Bild geladen und gleichzeitig automatisch eine Instanz der Image-Klasse für Sie erstellt wird. Um diese zu verwenden, müssen Sie lediglich die java.awt.Image-Klasse in das Java-Programm importieren und den getImage() mit der URL des zu ladenden Bildes versehen. Letzteres läßt sich auf zwei Arten durchführen:

Obwohl die erste Technik die einfachere zu sein scheint (fügen Sie die URL einfach als URL-Objekt ein), ist die zweite jedoch die flexiblere Variante. Da Java-Dateien kompiliert werden müssen, hat eine hart codierte URL zu einem Bild leider den Nachteil, daß Sie nach dem Versetzen der Dateien an eine andere Position, alle Java-Dateien neu kompilieren müssen.

Deshalb wird im allgemeinen die letztere Technik angewendet. Die Applet-Klasse sieht ferner zwei Methoden vor, die für das Basis-URL-Argument von getImage() hilfreich sind:

Welche der beiden oben aufgeführten Methoden Sie verwenden, hängt davon ab, ob sich die Bilder in einer relativen Position zu den HTML-Dateien befinden oder zu den Java-Klassendateien. Wählen Sie jene aus, die sich für Ihre Situation am besten verwenden läßt. Beachten Sie, daß beide Methoden flexibler sind als eine hart codierte URL oder ein Pfadname in der Methode getImage(). Wenn Sie getDocumentBase() oder getImageBase() verwenden, können Sie die HTML-Dateien und Applets an einem anderen Ort verlagern und Java findet die Bilder dennoch (Voraussetzung dafür ist selbstverständlich, daß Sie die Klassendateien und Bilder zusammen verlagern. Wenn Sie ausschließlich die Bilder an eine andere Position bewegen und die Klassendateien an der ursprünglichen Position belassen, müssen Sie die Quelle bearbeiten und neu kompilieren).

Im folgenden finden Sie einige Beispiele zu getImage(), damit Sie einen Eindruck davon erhalten, wie diese Methode verwendet wird. Der erste Aufruf von getImage() lädt die Datei von der angegebenen URL (http://www.server.com/files/image.gif). Wenn ein beliebiger Teil der URL sich ändert, müssen Sie das Java-Applet neu kompilieren, um den neuen Pfad einzubeziehen:

Image img = getImage(

new URL("http://www.server.com/files/image.gif"));

In der folgenden Form von getImage(), befindet sich die Datei image.gif im selben Verzeichnis wie die HTML-Dateien, die sich auf dieses Applet beziehen.

Image img = getImage(getDocumentBase(), "image.gif")

In der folgenden Form befindet sich die Datei image.gif im selben Verzeichnis wie das Java-Applet.

Image img = getImage(getCodeBase(), "image.gif")

Wenn Sie über viele Bilddateien verfügen, sollten Sie dafür ein eigenes Unterverzeichnis anlegen. Diese Variante von getImage() sieht für die Datei image.gif im Verzeichnis images wie folgt aus (das Java-Applet befindet sich im selben Verzeichnis):

Image img = getImage(getCodeBase(), "images/image.gif")

Wenn getImage() die angegebene Datei nicht findet, wird null zurückgegeben. DrawImage() auf ein Null-Bild angewendet zeichnet gar nichts. Die Verwendung eines Null-Bildes für andere Zwecke wird vermutlich einen Fehler hervorrufen.


Aktuell unterstützt Java Bilder in den Formaten GIF und JPEG. Andere Bildformate werden vielleicht in Zukunft unterstützt, doch momentan müssen Sie zwischen GIF und JPEG wählen.

Bilder zeichnen

Alle diese Varianten zu getImage() bewirken ausschließlich den Zugriff und Ladevorgang für ein Bild und plazieren dieses in einer Instanz der Image-Klasse. Sie verfügen nun über das Bild und können dieses verwenden.


Das Laden eines Bildes ist als interner Vorgang bei weitem komplexer als hier beschrieben. Wenn Sie ein Bild mit getImage() laden, entwickelt diese Methode zunächst einen Thread, mit dem das Bild geladen wird und kehrt beinahe sofort mit dem Image-Objekt zurück. Dadurch entsteht in Ihrem Programm der Eindruck, das Bild stände quasi sofort zur Verwendung zur Verfügung. Doch es kann einige Zeit in Anspruch nehmen, bis das Bild tatsächlich heruntergeladen und dekomprimiert wird. Aus diesem Grund zeichnen Bild-Applets lediglich teilweise Bilder oder das Bild wird erst allmählich während des Ladevorgangs gezeichnet. Sie können steuern, wie das Applet mit einem teilweise vorhandenen Bild verfahren soll (z.B. wenn Sie wünschen, daß das Applet wartet, bis alle Bildbereiche vorhanden sind, ehe es angezeigt wird). Dazu kann die Schnittstelle ImageObserver eingesetzt werden. Sie erfahren über ImageObserve in dieser Lektion später noch mehr.

Vermutlich möchten Sie ein Bild zunächst einmal wie ein Rechteck oder einen Text anzeigen. Die Graphics-Klasse sieht hierfür zwei Methoden vor, die beide drawImage() genannt werden.

Die erste Version von drawImage() enthält vier Argumente: das Bild für die Anzeige, die X- und Y-Positionen der oberen linken Ecke und folgendes:

public void paint() {

g.drawImage(img, 10, 10, this);
}

Diese erste Version führt die erwarteten Schritte aus: Sie zeichnet das Bild in den Originalabmessungen, wobei sich die obere linke Ecke an der angegebenen X-/Y-Position befindet. Die zweite Version von drawImage() enthält sechs Argumente: Das zu zeichnende Bild, die X- und Y-Koordinaten der oberen linken Ecke, die Breite und Höhe für den umliegenden Bildrahmen und this. Wenn die Argumente für Breite und Höhe des Bildrahmens kleiner oder größer als das tatsächliche Bild sind, wird das Bild automatisch entsprechend angepaßt. Durch die Verwendung dieser zusätzlichen Argumente können Sie die Größe der Bilder entsprechend dem vorhandenen Raum anpassen. Beachten Sie jedoch, daß durch die Skalierung die Bildqualität beeinflußt werden kann.

Um die Größe von Bildern zu ändern, sollten Sie die Originalgröße des jeweiligen Bildes kennen. Ist dies der Fall können Sie ein Bild prozentual vergrößern oder verkleinern, ohne Bildverzerrungen zu erzeugen. In der Image-Klasse stehen zwei Methoden für die Informationen zur Verfügung: getWidth() und getHeight(). Beide verwenden ein einziges Argument, eine Instanz von ImageObserver, die dazu dient, das Laden eines Bildes zu verfolgen (näheres hierzu erfahren Sie später). In den meisten Fällen können Sie this als ein Argument für getWidth() oder getHeight() verwenden.

Wenn Sie z.B. das Ladybug-Bild in einer Variablen namens bugimg gespeichert haben, gibt die folgende Zeile die Breite des Bildes in Pixel zurück:

theWidth = bugimg.getWidth(this);


Es gibt einen weiteren Fall, in dem das Bild auf keine dieser Weisen geladen wird. Dabei können verschiedene Ergebnisse erzielt werden. Wenn Sie getWidth() oder getHeight() aufrufen, ehe das Bild komplett geladen ist, wird jeweils der Wert –1 zurückgegeben. Das Verfolgen des Bilderladens mit Image-Observern kann dabei helfen, die Informationen zum gegebenen Zeitpunkt abzurufen.

Listing 10.3 zeigt eine weitere Verwendung des Bildes ladybug, diesmal wird das Bild mehrmals in verschiedene Größen skaliert (Abb. 10.3 zeigt die Ergebnisse).

siehe Abbildung

Abbildung 10.3:
Ausgabe des Applet Ladybug im Applet-Viewer

Listing 10.3: Der komplette Text von LadyBug.java.

 1: import java.awt.Graphics;

2: import java.awt.Image;
3:
4: public class LadyBug extends java.applet.Applet {
5:
6: Image bugimg;
7:
8: public void init() {
9: bugimg = getImage(getCodeBase(),
10: "images/ladybug.gif");
11: }
12:
13: public void paint(Graphics g) {
14: int iwidth = bugimg.getWidth(this);
15: int iheight = bugimg.getHeight(this);
16: int xpos = 10;
17:
18: // 25 %
19: g.drawImage(bugimg, xpos, 10,
20: iwidth / 4, iheight / 4, this);
21:
22: // 50 %
23: xpos += (iwidth / 4) + 10;
24: g.drawImage(bugimg, xpos , 10,
25: iwidth / 2, iheight / 2, this);
26:
27: // 100%
28: xpos += (iwidth / 2) + 10;
29: g.drawImage(bugimg, xpos, 10, this);
30:
31: // 150% x, 25% y
32: g.drawImage(bugimg, 10, iheight + 30,
33: (int)(iwidth * 1.5), iheight / 4, this);
34: }
35: }

Ein Hinweis zu Image-Observern

Bisher ist das letzte Argument von drawImage() nicht erläutert worden: Dieses mysteriöse this, das auch als Argument für getWidth() und getHeight() verwendet werden kann. Wozu wird dieses Argument verwendet? Es dient dazu, ein Objekt einzuführen, das als ImageObserver fungiert (oder genauer ausgedrückt, ein Objekt, das die Schnittstelle ImageObserver implementiert). Image-Observer beobachten den Ladevorgang eines Bildes und geben Aufschluß darüber, ob en Bild bereits komplett oder nur teilweise geladen ist. Ihr Applet könnte also pausieren bis alle Bilder geladen sind oder eine Lademeldung anzeigen bzw. eine andere Aktion durchführen während es wartet.

Die Applet-Klasse, von der Ihr Applet abgeleitet wurde, enthält ein Standardverhalten für die Bildüberwachung (welches es aus der Superklasse Component erbt), das in den meisten Fällen genügen sollte: das this-Argument für drawImage(), getWidht() und getHeight(). Der einzige Grund, statt dessen ein anderes Argument zu verwenden, besteht, wenn Sie eine größere Kontrolle über die Aktionen des Applet in Fällen wünschen, in denen die Bilder nur teilweise geladen sind oder wenn das asynchrone Laden vieler Bilder verfolgt werden soll.

Bilder bearbeiten

Zusätzlich zu den Grundlagen der Arbeit mit Bildern, die in diesem Abschnitt beschrieben worden sind, enthält das Paket java.awt weitere Klassen und Schnittstellen für eine Änderung der Bilder und deren internen Farben bzw. zum Erstellen von Bitmap-Bildern von Hand.

Animationen mit Bildern erstellen

Das Erstellen von Animationen unter Verwendung von Bildern ist dem Erstellen von Animationen mit Schriftarten, Farben oder Formen sehr ähnlich – Sie benutzen dazu dieselben Methoden und Prozeduren wie beim Zeichnen, Neuzeichnen und Reduzieren von Flimmereffekten, die bereits in diesem Kapitel erläutert wurden. Der einzige Unterschied besteht darin, daß Sie mit einer Reihe von Bildern arbeiten, die nacheinander ablaufen und nicht mit einem Satz von Zeichenmethoden.

Die beste Möglichkeit, die Verwendung von Bildern in einer Animation zu erläutern, besteht darin ein Beispiel durchzuarbeiten. Im folgenden soll dies ausführlich anhand einer Animation einer kleinen Katze namens Neko geschehen.

Ein Beispiel: Neko

Neko ist eine kleine Macintosh-Animation (ein Spiel), das 1989 von Kenji Gotoh geschrieben wurde. »Neko« heißt auf Japanisch »Katze« und die Animation handelt von einer kleinen Katze, die den Mauszeiger über den Bildschirm jagt, schläft, kratzt und sich ansonsten nett verhält. Das Neko-Programm ist seitdem auf fast jede denkbare Plattform übertragen worden und steht auch als Bildschirmschoner zur Verfügung.

Für dieses Beispiel implementieren Sie eine kleine Animation, die auf den Originalgrafiken von Neko basiert. Anders als der Original-Neko, der autonom war (er konnte die Ränder des Fensters »spüren«, sich umdrehen und in eine andere Richtung weiterlaufen), zwingt dieses Applet Neko dazu, von der linken Seite des Fensters aus loszulaufen, in der Mitte zu stoppen, zu gähnen, sich am Ohr zu kratzen, ein bißchen zu schlafen und dann nach rechts weiterzulaufen.


Dies ist bei weitem das umfangreichste Applet, das in diesem Buch bisher erläutert wurde. Wenn ich es hier Zeile für Zeile beschreiben müßte, würde das mehrere Tage in Anspruch nehmen. Deshalb erläutere ich die Teile dieses Applet unabhängig voneinander und lasse die grundsätzlichen Dinge, die Sie bereits gelernt haben (z.B. das Starten und Stoppen von Threads, die run()-Methode usw.) beiseite. Der gesamte Code wird später angegeben, so daß Sie die Einzelteile dann zusammenfügen können.

Schritt 1: Bilder zusammenstellen

Ehe Sie mit dem Schreiben des Java-Codes beginnen, um die Animation zu erstellen, sollten Sie alle Bilder zur Verfügung haben, aus der die Animation selbst besteht. Für diese Fassung von Neko werden 9 Bilder (die Originalversion verwendet 36) benötigt. Diese sind in Abb. 10.4 zu sehen:

siehe Abbildung

Abbildung 10.4:
Die Bilder für das Neko-Applet


Die Bilder für Neko sowie der Quellcode für dieses Applet stehen auf der beiliegenden CD-ROM zur Verfügung.

Für dieses Beispiel wurden die Bilder in einem Verzeichnis mit dem Namen images gespeichert. Es ist nicht so wichtig, wo Sie ihre Bilder abspeichern, Sie müssen nur wissen, wo sie sich befinden, denn diese Information wird später für das Laden der Bilder benötigt.

Schritt 2: Organisieren und Laden der Bilder im Applet

Doch nun zum Applet. Die Grundidee ist, daß Sie über einen Satz von Bildern verfügen und diese schnell hintereinander ablaufen lassen, damit der Eindruck einer Bewegung entsteht. Die einfachste Möglichkeit dies in Java zu definieren, besteht darin, die Bilder in einem Array der Image-Klasse zu speichern und das jeweils aktuelle Bild dann mit Hilfe einer speziellen Variablen anzuzeigen. Während die einzelnen Elemente des Array mit einer for-Schleife durchlaufen werden, können Sie jedes Mal den Wert des aktuellen Bildes ändern.

Für das Applet Neko erstellen Sie Instanzvariablen, um diese beiden Dinge zu implementieren: ein Array für die einzelnen Bilder mit dem Namen Nekopicx und eine Variable des Typs Image namens currentimg, welches das aktuelle Bild für die Anzeige enthält:

Image nekopics[] = new Image[9];

Image currentimg;

Das Bild-Array enthält hier neun Elemente, weil die Neko-Animation über neun Bilder verfügt. Wenn Sie einen größeren oder kleineren Satz von Bildern verwenden, müssen Sie die entsprechende Anzahl der Bilder hier angeben.


Die Klasse java.util enthält eine Klasse (HashTable), die eine Hash-Tabelle implementiert. Wenn Sie mit vielen Bildern arbeiten, ist eine Hash-Tabelle schneller beim Auffinden und Laden der Bilder als das bei einem Array der Fall ist. In diesem Fall soll jedoch ein Array verwendet werden, weil es nur wenige Bilder sind und weil sich Arrays für feste Längen und wiederholende Animationen besser eignen.

Da die Neko-Animation die Katzenbilder an verschiedenen Positionen des Bildschirms zeichnet, möchten Sie vermutlich auch die aktuellen X- und Y-Koordinaten verfolgen, damit die verschiedenen Methoden in diesem Applet erkennen können, wo mit dem Zeichnen begonnen werden soll. Das Y bleibt bei diesem Applet konstant (Neko läuft auf immer der gleichen Y-Koordinate von links nach rechts), aber X variiert. Im folgenden werden für diese beiden Positionen zwei Instanzvariablen eingefügt:

int xpos;

int ypos = 50;

Doch nun zum Hauptteil des Applet. Während der Initialisierung des Applet werden alle Bilder eingelesen und im Array nekopics gespeichert. Diese Art Operation läßt sich besonders gut in einer init()-Methode ausführen.

Angenommen, Sie verwenden neun Bilder mit neuen verschiedenen Dateinamen, so können Sie jedes einzeln mit getImage() aufrufen. Sie können sich diese Arbeit jedoch ein wenig erleichtern, indem Sie ein lokales Array für die Dateinamen (nekosrc ein Array von Zeichenketten) erstellen und dann eine for-Schleife verwenden um diese wechselseitig laden zu lassen. Im folgenden finden Sie die init()-Methode für das Neko-Applet, welche alle Bilder in den Array nekopics lädt:

public void init() {


String nekosrc[] = { "right1.gif", "right2.gif",
"stop.gif", "yawn.gif", "scratch1.gif",
"scratch2.gif","sleep1.gif", "sleep2.gif",
"awake.gif" };
for (int i=0; i < nekopics.length; i++) {
nekopics[i] = getImage(getCodeBase(),
"images/" + nekosrc[i]);
}
}

Beachten Sie, daß in getImage() das Verzeichnis, in dem die Bilder gespeichert sind (images) in den Pfad eingeschlossen ist.

Schritt 3: Bilder animieren

Sobald die Bilder geladen sind, besteht der nächste Schritt darin, die Teile des Applet zu animieren. Dies geschieht innerhalb der run()-Methode des Applet-Thread. In diesem Applet führt Neko fünf wesentliche Aktionen aus:

Sie könnten dieses Applet zwar so animieren, daß das rechte Bild zur gegebenen Zeit am Bildschirm gezeichnet wird, es ist aber sinnvoller, das Applet so zu schreiben, daß viele von Nekos Aktivitäten in einzelnen Methoden enthalten sind. Auf diese Weise lassen sich bestimmte Aktivitäten (insbesondere die Animation von Nekos Laufen) wieder verwenden, wenn Neko dies in unterschiedlicher Reihenfolge ausführen soll.

Zu Beginn wird eine Methode erstellt, die Neko zum Laufen bringt. Da diese zweimal verwendet wird, sollte sie in generischer Form angelegt werden. Erstellen Sie die Methode nekorun(), die zwei Argumente benötigt: die X-Position des Ausgangspunktes und die X-Position des Endpunktes. Neko läuft dann zwischen diesen beiden Positionen (das Y bleibt konstant).

void nekorun(int start, int end) {

...
}

Es gibt zwei Bilder, die das Laufen von Neko darstellen und den Effekt des Laufens erzielen. Sie müssen also zwischen diesen beiden Bildern wechseln (gespeichert an den Positionen 0 und 1 im Bilder-Array) und diese gleichzeitig über den Bildschirm bewegen. Der bewegende Teil läßt sich am einfachsten mit einer for-Schleife zwischen Anfangs- und Endargument definieren. Dafür wird die X-Position als aktueller Schleifenwert verwendet. Das Wechseln der Bilder ist eine Art Test, bei dem sich erkennen läßt, welches Bild bei welcher Schleifenposition aktuell ist; gleichzeitig wird der Wechsel zum nächsten aktuellen Bild definiert. Schließlich rufen Sie für jeden neuen Rahmen repaint() und sleep() auf, um eine kleine Pause für die Animation zu erzeugen.

Angenommen während dieser Animation treten viele Pausen in verschiedenen Intervallen auf, so macht es Sinn, eine Hilfsmethode zu erstellen, die diese Pausen auf einen bestimmten Zeitraum festlegt. Die pause()-Methode enthält deshalb ein Argument, das die Zeit in Millisekunden angibt. Im folgenden finden Sie eine Definition:

void pause(int time) {

try { Thread.sleep(time); }
catch (InterruptedException e) { }
}

Doch zurück zur nekorun()-Methode. Um alles zusammenzufassen: nekorun() bewegt sich von der Ausgangs- zur Endposition. Jeder Wechsel in der Schleife, welcher die aktuelle X-Position definiert, richtet currentimg auf der rechten Seite des Animationsrahmens ein, ruft repaint() auf und legt eine Pause ein. Alles klar? Im folgenden finden Sie die Definition für nekorun():

void nekorun(int start, int end) {

for (int i = start; i < end; i+=10) {
xpos = i;
// Bilder tauschen
if (currentimg == nekopics[0])
currentimg = nekopics[1];
else currentimg = nekopics[0];
repaint();
pause(150);
}
}

Beachten sie, daß die Schleife in der zweiten Zeile um 10 Pixel erhöht wird. Warum 10 Pixel und nicht 5 oder 8? Diese Antwort ist überwiegend von Experimenten bestimmt, dabei entdecken Sie, welche Pixel-Anzahl adäquat ist. Zehn ist für diese Animation angemessen. Wenn Sie eigene Animationen erstellen, müssen Sie mit beiden Abständen experimentieren und die Zeit für den Schlaf herausfinden, bis die Animation ihren Vorstellungen entsprechend abläuft. Nach repaint() wenden wir uns jetzt der paint()-Methode zu, welche die einzelnen Frames zeichnet. In diesem Fall ist die paint()-Methode sehr einfach. paint() ist im wesentlichen verantwortlich für das Zeichnen des aktuellen Bildes an der aktuellen X- und Y-Position. Alle diese Informationen werden in Instanzvariablen gespeichert. Doch ehe mit dem Zeichnen begonnen wird, muß sichergestellt sein, daß die Bilder tatsächlich vorhanden sind (diese können auch gerade noch geladen werden). Um dies festzustellen und sich zu vergewissern, daß kein Bild gezeichnet wird, das nicht vorhanden ist (daraus können die verschiedensten Fehler entstehen) wird ein Text durchgeführt, der sicherstellt, daß currentimg nicht Null ist, ehe drawImage() aufgerufen wird, um das Bild zu zeichnen:

public void paint(Graphics g) {

if (currentimg != null)
g.drawImage(currentimg, xpos, ypos, this);
}

Im folgenden wird die run()-Methode betrachtet, in der sich die wesentlichen Verarbeitungsvorgänge für diese Animation abspielen. Sie haben bereits die nekorun()-Methode erstellt; in run() rufen Sie nun diese Methode mit den entsprechenden Werten auf, um Neko vom linken Bildschirmrand zur Mitte laufen zu lassen:

// von der einen Seite des Bildschirms zur Mitte laufen

nekorun(0, size().width / 2);

Das zweitwichtigste Verhalten von Neko in dieser Animation ist das Anhalten und Gähnen. Für alle diese Momente stehen jeweils eigene Bilder (an den Positionen 2 und 3 des Array) zur Verfügung, d.h. Sie benötigen keine separaten Methoden, um diese zu zeichnen. Sie müssen lediglich das jeweils passende Bild auswählen, repaint() aufrufen und für die Pause die richtige Zeit einstellen. In diesem Beispiel wurde für jede Pause vor dem Anhalten und Gähnen eine Sekunde eingestellt. Die richtige Zeit wurde durch Experimentieren ermittelt. Im folgenden finden Sie den zugehörigen Code:

// stop und pause

currentimg = nekopics[2];
repaint();
pause(1000);

// gähnen
currentimg = nekopics[3];
repaint();
pause(1000);

Und nun zum dritten Teil der Animation: Nekos Kratzen. Für diesen Teil ist keine horizontale Bewegung definiert. Sie wechselt zwischen zwei verschiedenen Kratz-Bildern (an den Positionen 4 und 5 des Bilder-Array). Da das Kratzen jedoch eine eigene Aktion ist, soll hierfür auch eine eigene Methode verwendet werden.

Die nekoscratch()-Methode enthält ein einziges Argument: Die Häufigkeit des Kratzens. Mit diesem Argument können Sie Wiederholungen definieren und innerhalb der Schleife zwischen den beiden verschiedenen Bildern wechseln und diese jeweils neu zeichnen lassen:

void nekoscratch(int numtimes) {

for (int i = numtimes; i > 0; i[md]) {
currentimg = nekopics[4];
repaint();
pause(150);
currentimg = nekopics[5];
repaint();
pause(150);
}
}

Innerhalb der run()-Methode können Sie nekoscratch() mit dem Argument (4) aufrufen:

// viermal kratzen

nekoscratch(4);

Weiter geht’s! Nach dem sich Neko gekratzt hat, schläft er. Auch hierfür benötigen Sie zwei Bilder (an den Positionen 6 und 7 des Array), der Wechsel zwischen den Bildern wiederholt sich jeweils um eine bestimmte Anzahl. Im folgenden finden Sie die nekosleep()-Methode, die ein einziges Argument erwartet. Dieses Argument gibt an, wie oft die Sequenz wiederholt wird:

void nekosleep(int numtimes) {

for (int i = numtimes; i > 0; i[md]) {
currentimg = nekopics[6];
repaint();
pause(250);
currentimg = nekopics[7];
repaint();
pause(250);
}
}

Rufen Sie nekosleep() in der run()-Methode wie folgt auf:

// fünf "Durchgänge" lang schlafen

nekosleep(5);

Am Ende des Applet wacht Neko auf und läuft zur rechten Seite des Bildschirms. Das Bild für das Aufwachen ist das letzte Bild im Array (Position 8) und Sie können hierfür erneut die nekorun-Methode verwenden:

// aufwachen und weiter laufen

currentimg = nekopics[8];
repaint();
pause(500);
nekorun(xpos, size().width + 10);

Schritt 4: Applet fertigstellen

Es gibt noch eine Sache, die zur Fertigstellung des Applet notwendig ist. Die Bilder für die Animation verfügen alle über einen weißen Hintergrund. Wenn Sie diese Bilder auf dem Standardhintergrund von Applets (ein Mittelgrau) zeichnen, entsteht ein nicht sehr attraktives weißes Feld um die einzelnen Bilder. Um dieses Problem zu beheben, definieren Sie einfach die Hintergrundfarbe des Applet am Anfang der run()-Methode als Weiß:

setBackground(Color.white);

Haben Sie nun alles zusammengestellt? Dieses Applet enthält viel Code und viele einzelne Methoden, mit denen eine relativ einfache Animation ausgeführt wird, aber im Grunde ist es nicht kompliziert. Der Kern aller Animationen in Java besteht darin, die Einzelbilder aufzubauen und dann repaint() aufzurufen, um das Zeichnen am Bildschirm zu ermöglichen.

Beachten Sie, daß in diesem Applet keine Schritte unternommen werden, um den Flimmereffekt zu reduzieren. Es hat sich herausgestellt, daß die Bilder dieses Applet und die Zeichenfläche so klein sind, daß das Flimmern hier nicht zum Problem wird. Wenn Sie eine Animation schreiben, sollten Sie die grundlegenden Dinge zuerst erledigen und dann zusätzliche Verhalten einfügen, um den Ablauf zu optimieren.

Um diesen Abschnitt abzuschließen, zeigt Listing 10.4 den kompletten Code für das Neko-Applet.

Listing 10.4: Der komplette Text von Neko.java.

  1: import java.awt.Graphics;

2: import java.awt.Image;
3: import java.awt.Color;
4:
5: public class Neko extends java.applet.Applet
6: implements Runnable {
7:
8: Image nekopics[] = new Image[9];
9: Image currentimg;
10: Thread runner;
11: int xpos;
12: int ypos = 50;
13:
14: public void init() {
15: String nekosrc[] = { "right1.gif", "right2.gif",
16: "stop.gif", "yawn.gif", "scratch1.gif",
17: "scratch2.gif","sleep1.gif", "sleep2.gif",
18: "awake.gif" };
19:
20: for (int i=0; i < nekopics.length; i++) {
21: nekopics[i] = getImage(getCodeBase(),
22: "images/" + nekosrc[i]);
23: }
24: }
25:
26: public void start() {
27: if (runner == null) {
28: runner = new Thread(this);
29: runner.start();
30: }
31: }
32:
33: public void stop() {
34: if (runner != null) {
35: runner.stop();
36: runner = null;
37: }
38: }
39:
40: public void run() {
41: setBackground(Color.white);
42:
43: // von der einen Seite des Bildschirms zur Mitte laufen
44: nekorun(0, size().width / 2);
45:
46: // stop und pause
47: currentimg = nekopics[2];
48: repaint();
49: pause(1000);
50:
51: // gähnen
52: currentimg = nekopics[3];
53: repaint();
54: pause(1000);
55:
56: // viermal kratzen
57: nekoscratch(4);
58:
59: // fünf "Durchgänge" lang schlafen
60: nekosleep(5);
61:
62: // aufwachen und weiter laufen
63: currentimg = nekopics[8];
64: repaint();
65: pause(500);
66: nekorun(xpos, size().width + 10);
67: }
68:
69: void nekorun(int start, int end) {
70: for (int i = start; i < end; i += 10) {
71: xpos = i;
72: // Bilder wechseln
73: if (currentimg == nekopics[0])
74: currentimg = nekopics[1];
75: else currentimg = nekopics[0];
76: repaint();
77: pause(150);
78: }
79: }
80:
81:
82: void nekoscratch(int numtimes) {
83: for (int i = numtimes; i > 0; i--) {
84: currentimg = nekopics[4];
85: repaint();
86: pause(150);
87: currentimg = nekopics[5];
88: repaint();
89: pause(150);
90: }
91: }
92:
93: void nekosleep(int numtimes) {
94: for (int i = numtimes; i > 0; i--) {
95: currentimg = nekopics[6];
96: repaint();
97: pause(250);
98: currentimg = nekopics[7];
99: repaint();
100: pause(250);
101: }
102: }
103:
104: void pause(int time) {
105: try { Thread.sleep(time); }
106: catch (InterruptedException e) { }
107: }
108:
109: public void paint(Graphics g) {
110: if (currentimg != null)
111: g.drawImage(currentimg, xpos, ypos, this);
112: }
113: }

Klänge laden und verwenden

Java bietet eine vordefinierte Unterstützung für das Abspielen von Klängen in Verbindung mit dem Ablauf von Animationen oder zum eigenständigen Abspielen an. Ebenso wie die Unterstützung für Bilder befindet sich auch die Unterstützung für Klänge in den Klassen Applet und AWT. Die Verwendung von Klängen ist also ebenso einfach wie das Laden und Verwenden von Bildern.

Aktuell wird in Java nur ein Klangformat unterstützt: Das Format AU von Sun. AU-Dateien sind kleiner als andere Klangdateien in anderen Formaten, aber die Tonqualität ist nicht besonders gut. Wenn Sie große Ansprüche an die Qualität der Klänge stellen, können Sie die Klangdateien auch als traditionelle HTML-Referenzen (Links zu externen Dateien) einbinden und müssen diese nicht direkt in ein Java-Applet einbinden.

Die einfachste Möglichkeit, einen Klang zu laden und abzuspielen, bietet die play()-Methode. Diese bildet einen Teil der Applet-Klasse und steht deshalb in Applets für Sie zur Verfügung. Die play()-Methode ist der getImage()-Methode sehr ähnlich. Auch sie kann in folgenden beiden Formen verwendet werden:

Die folgende Codezeile lädt beispielsweise die Datei meow.au und spielt den darin enthaltenen Klang ab. Die Datei befindet sich im Verzeichnis audio, welches wiederum im selben Verzeichnis wie das Applet plaziert ist:

play(getCodeBase(), "audio/meow.au");

Die play()-Methode lädt die Klangdatei und spielt den Ton sobald wie möglich ab, nachdem der Aufruf erfolgt ist. Wenn der Klang nicht gefunden wird, erscheint keine Fehlermeldung, der Ton ist dann nur einfach nicht zu hören.

Wenn Sie einen bestimmten Klang wiederholt abspielen möchten, starten und stoppen Sie die Klangdatei oder führen Sie diese als Schleife aus (um sie immer wieder abzuspielen). In diesem Fall verwenden Sie die Applet-Methode getAudioClip(), um die Klangdatei in eine Instanz der AudioClip-Klasse (Teil von java.applet) zu laden. Vergessen Sie nicht, diese zu importieren. Im Anschluß daran, können Sie direkt mit diesem AudioClip-Objekt arbeiten.

Angenommen, Sie haben eine Klangschleife erstellt, die permanent im Hintergrund des Applet ausgeführt werden soll. Im Initialisierungscode können Sie folgende Zeile für eine solche Klangdatei verwenden:

AudioClip clip = getAudioClip(getCodeBase(),

"audio/loop.au");

Um den Clip einmal abzuspielen, verwenden Sie die play()-Methode:

clip.play();

Um einen aktuell ablaufenden Soundclip anzuhalten, verwenden Sie die stop()-Methode:

clip.stop();

Um für den Clip eine Schleife zu definieren (ihn wiederholt abzuspielen), verwenden Sie die loop()-Methode:

clip.loop();

Wenn die getAudioClip()-Methode den angegebenen Klang nicht findet oder diesen aus einem bestimmten Grund nicht laden kann, wird null zurückgegeben. Es ist sinnvoll, den Code für diesen Fall zu testen, ehe Sie die Klangdatei abzuspielen versuchen, da ein versuchter Aufruf der play()-, stop()- und loop()-Methoden für ein Nullobjekt einen Fehler zur Folge hat (eine Ausnahme).

In einem Applet lassen sich beliebig viele Klangdateien abspielen; alle Klänge werden genau so miteinander vermischt, wie sie im Applet abgespielt werden.

Beachten Sie, daß bei der Verwendung von Hintergrundklängen mit Wiederholungsschleifen, die Klangdatei nicht automatisch angehalten wird, wenn der Thread des Applet verlassen wird. Das heißt, wenn ein Leser zu einer anderen Seite wechselt, wird der Klang des ersten Applet weiterhin abgespielt. Sie können dieses Problem lösen, indem Sie den Hintergrundklang des Applet mit der stop()-Methode anhalten:

public void stop() {

if (runner != null) {
if (bgsound != null)
bgsound.stop();
runner.stop();
runner = null;
}
}

Listing 10.5 zeigt eine einfache Grundstruktur für ein Applet, das zwei Klänge abspielt: Der erste, ein Hintergrundklang namens loop.au, wird wiederholt abgespielt. Der zweite, ein Piepsignal (beep.au), wird alle 5 Sekunden abgespielt. (Auf das Bild für dieses Applet habe ich verzichtet, denn es zeigt ausschließlich einen einfachen String am Bildschirm).

Listing 10.5: Der komplette Text des Applet AudioLoop.java.

 1: import java.awt.Graphics;

2: import java.applet.AudioClip;
3:
4: public class AudioLoop extends java.applet.Applet
5: implements Runnable {
6:
7: AudioClip bgsound;
8: AudioClip beep;
9: Thread runner;
10:
11: public void start() {
12: if (runner == null) {
13: runner = new Thread(this);
14: runner.start();
15: }
16: }
17:
18: public void stop() {
19: if (runner != null) {
20: if (bgsound != null) bgsound.stop();
21: runner.stop();
22: runner = null;
23: }
24: }
25:
26: public void init() {
27: bgsound = getAudioClip(getCodeBase(),"audio/loop.au");
28: beep = getAudioClip(getCodeBase(), "audio/beep.au");
29: }
30:
31: public void run() {
32: if (bgsound != null) bgsound.loop();
33: while (runner != null) {
34: try { Thread.sleep(5000); }
35: catch (InterruptedException e) { }
36: if (beep != null) beep.play();
37: }
38: }
39:
40: public void paint(Graphics g) {
41: g.drawString("Playing Sounds....", 10, 10);
42: }
43: }

Zu diesem Applet sind nur einige wenige Dinge anzumerken. Beachten Sie zunächst die init()-Methode in den Zeilen 26 und 29, welche die beiden Klangdateien loop.au und beep.au lädt. Hier wurde kein Versuch unternommen, sicherzustellen, daß Dateien auch tatsächlich wie erwartet geladen werden. Es besteht also die Möglichkeit, daß die Instanzvariablen bgsound und beep als Ergebnis Nullwerte ausgeben, wenn die Datei nicht geladen werden kann. In diesem Fall könnte die start()-Methode oder eine beliebige andere Methode nicht aufgerufen werden. Es sollte daher an andere Position im Applet ein Test dafür ausgeführt werden.

Ein Test für Null wurde deshalb an anderen Stellen eingefügt, insbesondere in der run()-Methode in den Zeilen 32 und 36. Diese Zeilen starten die Klangschleifen und spielen diese ab, aber dies geschieht nur, wenn die Variablen bgsound und beep einen anderen Wert als null enthalten.

Schließlich sollten Sie einen Blick auf Zeile 20 werfen, welche den Hintergrundklang ausdrücklich abschaltet, wenn der Thread angehalten wird. Da das Abspielen von Hintergrundklängen nicht automatisch mit dem Beenden des Thread aufhört, muß dies explizit eingefügt werden.

Mehr zum Flimmereffekt: Doppelpuffer

Sie haben bereits eine einfache Möglichkeit kennengelernt, wie sich der Flimmereffekt in Java-Animationen reduzieren läßt. Eine zweite, etwas komplexere, aber auch meistens sinnvollere Technik zur Kontrolle des Flimmereffekts in Java-Animationen besteht im sogenannten Doppelpuffer.

Bei der Verwendung eines Doppelpuffers erstellen Sie eine zweite Zeichenfläche (sozusagen außerhalb des Bildschirms), nehmen dort alle Zeichnungen vor und zeichnen dann am Ende die gesamte Zeichenfläche in einem Schritt im aktuellen Applet (und damit auf dem Bildschirm). Da sich diese Arbeit hinter den Kulissen vollzieht, wird damit die Möglichkeit ausgeschaltet, daß Zwischenschritte innerhalb des Zeichenvorgangs aus Versehen erscheinen und den Ablauf einer Animation stören.


Das Doppelpuffern ist ein Vorgang, bei dem alle Zeichenaktivitäten in einem Puffer abseits des Bildschirms vorgenommen werden. Anschließend wird der gesamte Inhalt dieses Puffers in einem Schritt am Bildschirm angezeigt. Diese Technik wird Doppelpuffern genannt, weil es zwei Puffer für Grafikausgaben gibt, zwischen denen Sie wechseln.

Die Verwendung des Doppelpuffers ist nicht immer die beste Lösung. Wenn das Applet viele störende Flimmereffekte aufweist, können Sie auch update() überschreiben und zunächst Teile des Bildschirms zeichnen. Dies kann das Problem bereits lösen. Der Doppelpuffer ist weniger effizient als der reguläre Puffer und beansprucht zudem mehr Speicherplatz und Raum, in einigen Fällen kann dies also nicht der beste Lösungsansatz sein. Wenn Sie jedoch rigoros alle Flimmereffekte einer Animation entfernen möchten, funktioniert diese Technik ausgesprochen gut.

Applets mit Doppelpuffern erstellen

Um ein Applet zu erstellen, das einen Doppelpuffer verwendet, benötigen Sie zweierlei: ein sogenanntes Offscreen-Bild und einen Grafikkontext für dieses Bild. Die beiden imitieren den Effekt der Grafikoberfläche eines Applet: Der Grafikkontext (eine Instanz von Graphics) enthält die Zeichenmethoden, wie z.B. drawImage() (und drawString()) und Image enthält die Bildpunkte, die gezeichnet werden sollen.

Um ein Applet mit einem Doppelpuffer zu versehen, sind vier weitere wichtige Schritte erforderlich: Zunächst müssen das Offscreen-Bild und der Grafikkontext Instanzvariablen gespeichert werden, damit diese an die paint()-Methode weitergeleitet werden können. Richten Sie in Ihrer Klassendefinition folgende Instanzvariablen ein:

Image offscreenImage;

Graphics offscreenGraphics;

Als zweiten Schritt erstellen Sie während der Initialisierung des Applet ein Image- und ein Graphics-Objekt und weisen diese jenen Variablen zu (dazu muß die Initialisierung abgewartet werden, damit Sie wissen, wie groß sie werden). Die createImage()-Methode gibt Ihnen eine Instanz von Image, die Sie dann an die getGraphics() Methode senden können, um einen neuen Graphics -Kontext für das Bild zu erhalten:

offscreenImage = createImage(size().width,

size().height);
offscreenGraphics = offscreenImage.getGraphics();

Wann immer Sie jetzt am Bildschirm zeichnen (meist in der paint()-Methode), zeichnen Sie nun Offscreen-Grafiken und nicht auf der Zeichenoberfläche des Applet. Um z.B. ein Bild namens img an Position 10, 10 zu zeichnen, verwenden Sie diese Zeile:

offscreenGraphics.drawImage(img, 10, 10, this);

Am Ende der paint-Methode, wenn alle Zeichnungen im Offscreen-Bild ausgeführt sind, fügen Sie die folgende Zeile ein, um den Offscreen-Puffer auf den tatsächlichen Bildschirm zu übertragen:

g.drawImage(offscreenImage, 0, 0, this);

Vermutlich werden Sie auch update() überschreiben wollen, damit dieses den Bildschirm zwischen den Zeichnungen nicht leert:

public void update(Graphics g) {

paint(g);
}

Im folgenden werden diese vier Schritte noch einmal zusammengefaßt:

1. Fügen Sie Instanzvariablen für den Bild- und Grafikkontext des Offscreen-Puffers ein.

Anmerkung zur Verwendung von Grafik-Kontexten

Wenn Sie ausführlichen Gebrauch von Grafik-Kontexten in Ihren Applets oder Anwendungen machen, sollten Sie sich darüber im klaren sein, daß diese Kontexte häufig bestehen bleiben, nachdem die Arbeit damit abgeschlossen ist, auch wenn keine weiteren Bezüge dazu bestehen. Grafik-Kontexte sind spezielle Objekte im AWT, die im nativen Betriebssystem verwurzelt sind; der Garbage Collector von Java kann diese Grafik-Kontexte nicht selbst entfernen. Wenn Sie mit vielen Grafik-Kontexten arbeiten oder diese wiederholt verwenden, möchten Sie diese vielleicht einmal ausdrücklich entfernen, wenn sie nicht mehr benötigt werden.

Verwenden Sie die dispose()-Methode, um Grafik-Kontexte explizit zu löschen. Ein guter Ort für die Plazierung dieser Methode ist die destroy()-Methode des Applet (diese haben Sie am 8.Tag als eine der Primär-Methoden eines Applet kennengelernt, neben init(), start() und stop()):

public void destroy() {

offscreenGraphics.dispose();
}

Ein Beispiel: Checkers

Im folgenden finden Sie ein weiteres Beispiel für eine einfache Animation: Dieses Applet trägt den Namen Checkers. Ein rotes Oval (ein Damestein) bewegt sich von einem schwarzen auf ein weißes Quadrat wie auf einem Damebrett. Am Ende dieser Bewegung kehrt es zum Ausgangspunkt zurück und bewegt sich erneut. Listing 10.6 zeigt den Code für den ersten Zug in diesem Applet und Abb. 10.5 zeigt das Applet selbst.

Listing 10.6: Das Applet Checker.

 1: import java.awt.Graphics;

2: import java.awt.Color;
3:
4: public class Checkers extends java.applet.Applet implements Runnable {
5:
6: Thread runner;
7: int xpos;
8:
9: public void start() {
10: if (runner == null); {
11: runner = new Thread(this);
12: runner.start();
13: }
14: }
15:
16: public void stop() {
17: if (runner != null) {
18: runner.stop();
19: runner = null;
20: }
21: }
22:
23: public void run() {
24: while (true) {
25: for (xpos = 5; xpos <= 105; xpos+=4) {
26: repaint();
27: try { Thread.sleep(100); }
28: catch (InterruptedException e) { }
29: }
30: xpos=5;
31: }
32: }
33:
34: public void update(Graphics g) {
35:
36: }
37:
38: public void paint(Graphics g) {
39: // Hintergrund zeichnen
40: g.setColor(Color.black);
41: g.fillRect(0,0,100,100);
42: g.setColor(Color.white);
43: g.fillRect(100,0,100,100);
44:
45: // Oval zeichnen
46: g.setColor(Color.red);
47: g.fillOval(xpos,5,90,90);
48: }
49: }

siehe Abbildung

Abbildung 10.5:
Das Applet Checker.

Im folgenden wird erläutert, was dieses Applet ausführt: Die Instanzvariable xpos verfolgt die aktuelle Ausgangsposition des Damesteins (weil er sich horizontal bewegt, bleibt y konstant und muß nicht verfolgt werden, während x sich ändert). In der run()-Methode wird der Wert von x geändert und neu gezeichnet, wobei zwischen jeder Bewegung eine Wartezeit von 100 Millisekunden liegt. Der Damestein bewegt sich von einer Seite des Bildschirms zur anderen und wieder zurück, wobei er wieder seine ursprüngliche Position einnimmt, sobald er auf der rechten Seite des Bildschirms angelangt ist.

In der paint()-Methode werden die Hintergrundquadrate gezeichnet (ein weißes und ein schwarzes) und anschließend wird der Damestein an seine aktuelle Position gesetzt.

Dieses Applet flimmert ebenso wie das ColorSwirl-Applet sehr stark. Das einfache Überschreiben von update() reicht in diesem Fall nicht aus, da Teile des Bildschirms geleert und neu gezeichnet werden, während sich der Damestein quer über den Bildschirm bewegt. Der Flimmereffekt tritt in diesem Applet vor allem deswegen auf, weil zuerst der Hintergrund und dann darauf der Damestein gezeichnet wird.

Sie könnten dieses Applet dahingehend ändern, daß paint() nur jene Elemente, mit clipRect() neu zeichnet, die sich ändern. Auf diese Weise läßt sich das Flimmern reduzieren. Aber bei dieser Strategie müssen die alten und neuen Positionen des Damesteins verfolgt werden und dies ist nicht sehr elegant. Eine bessere Lösung besteht in diesem Fall darin, den Doppelpuffer einzusetzen und damit alle Flimmereffekte auszuschalten. Für dieses Beispiel einen Doppelpuffer einzufügen ist einfach. Fügen Sie zunächst die Instanzvariablen für das Offscreen-Bild und den Grafik-Kontext ein:

Image offscreenImg;

Graphics offscreenG;

Fügen Sie als zweiten Schritt eine init-Methode ein, um den Offscreen-Puffer zu initialisieren:

public void init() {

offscreenImg = createImage(size().width, size().height);
offscreenG = offscreenImg.getGraphics();
}

Drittens ändern Sie die paint()-Methode, um in den Offscreen-Puffer anstatt in den Haupt-Grafikpuffer zu zeichnen:

public void paint(Graphics g) {

// Hintergrund zeichnen
offscreenG.setColor(Color.black);
offscreenG.fillRect(0, 0, 100, 100);
offscreenG.setColor(Color.white);
offscreenG.fillRect(100, 0, 100, 100);

// Oval zeichnen
offscreenG.setColor(Color.red);
offscreenG.fillOval(xpos, 5, 90, 90);

g.drawImage(offscreenImg, 0, 0, this);
}

Und schließlich geben Sie in der destroy()-Methode des Applet explizit an, daß der in offscreenG gespeichert Grafik-Kontext entfernt werden soll:

public void destroy() {

offscreenG.dispose();
}

Listing 10.7 zeigt den gesamten Code für das Applet Checkers (Checkers2.java), der den Doppelpuffer enthält.

Listing 10.7: Kompletter Quelltext von Checkers2.java.

 1: import java.awt.Graphics;

2: import java.awt.Color;
3: import java.awt.Image;
4:
5: public class Checkers2 extends java.applet.Applet implements Runnable {
6:
7: Thread runner;
8: int xpos;
9: Image offscreenImg;
10: Graphics offscreenG;
11:
12: public void init() {
13: offscreenImg = createImage(this.size().width, this.size().height);
14: offscreenG = offscreenImg.getGraphics();
15: }
16:
17: public void start() {
18: if (runner == null); {
19: runner = new Thread(this);
20: runner.start();
21: }
22: }
23:
24: public void stop() {
25: if (runner != null) {
26: runner.stop();
27: runner = null;
28: }
29: }
30:
31: public void run() {
32: while (true) {
33: for (xpos = 5; xpos <= 105; xpos+=4) {
34: repaint();
35: try { Thread.sleep(100); }
36: catch (InterruptedException e) { }
37: }
38: xpos = 5;
39: }
40: }
41:
42: public void update(Graphics g) {
43: paint(g);
44: }
45:
46: public void paint(Graphics g) {
47: // Hintergrund zeichnen
48: offscreenG.setColor(Color.black);
49: offscreenG.fillRect(0,0,100,100);
50: offscreenG.setColor(Color.white);
51: offscreenG.fillRect(100,0,100,100);
52:
53: // Oval zeichnen
54: offscreenG.setColor(Color.red);
55: offscreenG.fillOval(xpos,5,90,90);
56:
57: g.drawImage(offscreenImg,0,0,this);
58: }
59:
60: public void destroy() {
61: offscreenG.dispose();
62: }
63: }

Zusammenfassung

Herzlichen Glückwunsch! Sie haben diese etwas schwierig Lektion, in der viele wichtige Elemente behandelt wurden, bewältigt und eine Menge gelernt. Sie haben die Anwendung und das Überschreiben zahlreicher Methoden gelernt – start(), stop(), paint(), repaint(), run() und update() und einige grundlegende Kenntnisse zum Erstellen von Threads erhalten. Ferner haben Sie erfahren, wie sich Bilder in Applets plazieren, laden und mit der drawImage()-Methode zur Anzeige und Animation verwenden lassen.

Eine Animationstechnik, mit der sich das Flimmern verhindern läßt, ist die Verwendung eines Doppelpuffers, die alle Flimmereffekte in einer Animation ausschaltet und vielleicht ein wenig zu Lasten der Effizienz und Geschwindigkeit einer Animation geht. Wenn Sie Bilder und Grafik-Kontexte verwenden, können Sie einen Offscreen-Puffer erstellen, in den sich zeichnen läßt. Der Inhalt des Puffers wird dann insgesamt in den regulären Bildschirm übertragen.

Sie haben darüber hinaus erfahren, wie sich Klänge in Applets einfügen lassen und wie diese in wiederholter Form oder zu bestimmten Zeitpunkten abgespielt werden bzw. als Hintergrundklänge ablaufen, während das Applet ausgeführt wird. Sie wissen, wie Klänge sowohl mit der play()- als auch mit der getAudio-Clip()-Methode plaziert, geladen und abgespielt werden.

Fragen und Antworten

F Warum muß man so umständlich mit paint, repaint und update und all dem arbeiten? Warum erstellt man nicht einfach eine paint-Methode, welche die Elemente zum gewünschten Zeitpunkt am Bildschirm ausgibt?

A Das AWT-Toolkit von Java ermöglicht es, mehrere Zeichenoberflächen ineinander zu verschachteln. Wenn paint ausgeführt wird, werden alle Teile des Systems nachgezeichnet, beginnend mit der äußersten Oberfläche nach unten zur innersten in der Verschachtelung. Ein Applet wird gleichzeitig mit allem anderen ausgegeben und nicht besonders behandelt. Sie opfern dabei zwar die Unmittelbarkeit des sofortigen Zeichnens, das Applet wird mit dieser Methode aber nahtlos in den Rest des Systems eingebettet.

F Wenn ein Applet Threads verwendet, muß ich also nur festlegen, wo der Thread beginnen und enden soll? Das ist alles? Ich muß also keine Tests in den Schleifen durchführen oder deren Status verfolgen? Es hält dann einfach an?

A Ja, es hält einfach an. Wenn Sie ein Applet in einen Thread einbinden, kann Java die Ausführung des Applet viel besser steuern. Wenn der Thread gestoppt wird, wird auch die Ausführung des Applet beendet und wieder aufgenommen, sobald der Thread wieder gestartet wird. Das geht alles automatisch. Nicht schlecht, was?

F Im Neko-Programm, fügen Sie den Ladevorgang für die Bilder in die init()-Methode ein. Mir scheint, Java benötigt eine sehr lange Zeit für das Laden der Bilder und da init() nicht im Haupt-Thread des Applet liegt, findet hier eine deutliche Pause statt. Warum läßt sich der Ladevorgang nicht am Anfang der run()-Methode einfügen?

A Hinter den Kulissen spielen sich auch noch andere Dinge ab. Die getImage()-Methode lädt das Bild nämlich nicht wirklich, sondern gibt beinahe unmittelbar ein Image-Objekt zurück, damit während der Initialisierung keine langen Verarbeitungszeiten anfallen. Die Bilddaten, auf die GetImage() verweist, werden nicht geladen, solange das Bild nicht benötigt wird. Auf diese Art muß Java keine riesigen Bilder im Arbeitsspeicher aufbewahren, wenn das Programm nur ein kleines Stück davon benötigt. Statt dessen beleibt lediglich die Referenz zu diesen Daten erhalten, während das Laden der notwendigen Bereich später stattfindet.

F Ich habe das Neko-Applet kompiliert und ausgeführt. Dabei geschehen merkwürdige Dinge: Die Animation beginnt in der Mitte und erstellt Frames. Es scheint, also ob nur einige Bilder geladen worden sind, wenn das Applet ausgeführt wird.

A Ja, genau das ist der Fall. Da das Laden der Bilder, das Bild nicht tatsächlich lädt, animiert das Applet sozusagen den leeren Bildschirm, während die Bilder noch geladen werden. Das Applet scheint dann in der Mitte zu starten, Einzelbilder zu erstellen und überhaupt nicht zu funktionieren. Für dieses Problem gibt es drei Lösungen. Die erste besteht darin, eine Animationsschleife zu verwenden (d.h. von vorne zu beginnen, sobald es angehalten wird). Eventuell werden die Bilder dann geladen und die Animation funktioniert korrekt. Als zweite Lösung, die allerdings nicht sehr gut ist, können Sie eine kleine Pause vor der Ausführung definieren, damit die Bilder geladen werden können, ehe die Animation ausgeführt wird. Die dritte und beste Möglichkeit ist es, Image-Observer zu verwenden, mit deren Hilfe sich sicherstellen läßt, daß kein Bereich der Animation abgespielt wird, ehe nicht die notwendigen Bilder geladen sind. Nähere Erläuterungen hierzu erhalten Sie in der Dokumentation zur Schnittstelle ImageObserver.

F Ich habe ein Applet mit einem Hintergrundklang geschrieben, das die Methoden getAudioClip() und loop() verwendet. Der Klang funktioniert einwandfrei, aber er hört nicht mehr auf. Ich habe schon versucht, den aktuellen Thread zu beenden, aber der Klang geht trotzdem weiter.

A In einem kleinen Hinweis zum Abschnitt Klänge habe ich bereits erwähnt, daß Hintergrundklänge nicht im Haupt-Thread des Applet ablaufen, wenn Sie diesen Thread beenden, wird der Klang weiter ausgeführt. Die Lösung ist einfach. Halten Sie den Klang mit derselben Methode an, mit der Sie auch den Thread beendet haben:

runner.stop();  //den Thread stoppen

bgsound.stop(); //und auch den Klang stoppen

(c) 1997 SAMS
Ein Imprint des Markt&Technik Buch- und Software- Verlag GmbH
Elektronische Fassung des Titels: Java in 21 Tagen, ISBN: 3-8272-2009-2

Previous Page Page Top TOC Index Next Page See Page