9 Grafik und Animation

Eine der für viele Anwender wichtigsten Fähigkeiten von Java ist Grafikfähigkeit. Dies betrifft sowohl Java-Applets als auch normale Programme.

Die meisten der schon von Anfang an vorhandenen Grafikfähigkeiten von Java sind in der Graphics-Klasse untergebracht, die ein Bestandteil des Paketes java.awt ist. Andere finden Sie in der Image-Klasse, die ebenso zu diesem Paket gehört. Diese Klassen oder am besten das ganze Paket sollte auf jeden Fall am Anfang jeder Sourcedatei importiert werden, bevor man die Grafikfähigkeiten von Java vollständig nutzen kann. Seit Java 1.2 gibt es noch zahlreiche weitere Pakete, die insbesondere für 2D-Java und Swing wichtig sind. Auch diese müssen Sie natürlich importieren, wenn Sie damit arbeiten wollen oder entsprechend qualifizierte Angaben machen.

Wir werden am Beginn des Kapitels die Grafikmöglichkeiten von Java ansprechen, die schon in der ersten Variante vorhanden waren. Dies sind grundlegende Grafikvorgänge, die auch die Basis für die neuen Grafiktechniken wie 2D-Java (diese folgen im zweiten Abschnitt) sind. Es gibt durchaus technische Gründe, zuerst die älteren Grafiktechniken durchzusprechen. Zum einen muss berücksichtigt werden, dass die neuen Grafikmöglichkeiten noch einige Zeit nicht in allen Darstellungplattformen - insbesondere den Browsern - wiedergegeben werden können. Java 1.0x ist unter gewissen Umständen immer noch relevant für die Programmierung von Applets. Auch die prinzipiell in die Java-2-Plattform integrierte Lösung mit dem standardmäßig in die Plattform integrierten Java-Plug-In kann nicht immer eingesetzt werden. Die Version 1.0x ist deshalb vielfach der kleinste gemeinsame Nenner, der von allen Java-fähigen Browsern verstanden werden sollte. Folgetechniken werden dagegen im Wesentlichen für die Programmierung von neuen Applikationen oder Applets, die auf neueren Plattformen laufen sollen, verwendet. Aber sogar bei Applikationen wird noch nach dem 1.0x-Standard programmiert. Entweder, um dem Integrationskrieg um die Java-Versionen nach 1.0x zu entgehen, weil die alten Möglichkeiten ausreichen oder wenn bereits zu viele Bestandteile in diesem Standard erstellt wurden.

Es gibt aber auch didaktische Gründe für einen Start mit den alten Grafiktechniken. Es werden die grundlegenden Techniken zur Verwendung von den Grafikmöglichkeiten von Java anhand dieser einfachen Methoden leichter deutlich. Festhalten sollte man aber schon jetzt, dass es unter den neuen Techniken für sämtliche der Methoden, die das java.awt.Graphics nutzen, einen Ersatz gibt oder dessen Anwendung sich direkt ableiten lässt.

Wir werden aus der Vielzahl von Grafikmethoden nur eine kleine Auswahl präsentieren können, die aber die wichtigsten Grundtechniken beschreiben sollten. Insbesondere sollten Sie beachten, dass es für viele der vorgestellten Methoden mehrere Varianten des gleichen Namens gibt, von denen wir dann nur eine ausgewählte Version präsentieren.

Wir kennen bereits einige Fähigkeiten zur Darstellung von Bildern und Text. Dazu kommen noch reichlich weitere Möglichkeiten, die das Zeichnen von Zeilen, Gebilden, Bildern und Text in verschiedenen Schriften, Stilen und Farben ermöglichen.

9.1 Zeichnen, Update und neu zeichnen

Allgemeine Zeichnenvorgänge in Java erfolgen, indem eine beliebige Grafikmethode innerhalb einer Methode aufgerufen wird. Dies muss nicht zwingend die uns bereits bekannte paint()-Methode sein. Diese ist »nur« eine ganz besondere Methode, um bestimmte Grafikvorgänge auszulösen. Großer Vorteil dieser Methode ist, dass sie in jedem Applet automatisch zur Verfügung steht und mit zahlreichen Mechanismen gekoppelt ist, die eine sinnvolle Verwaltung des Bildschirms gewährleisten. Aber nicht nur Applets können diese Methode automatisch verwenden. Auch eigenständige Applikationen, die mit einer grafischen Oberfläche ausgestattet sind, können diese Methode vollkommen analog einsetzen.

Diese Methode spart Ihnen auch, dass Sie sich selbst um die Beschaffung eines Grafikkontexts kümmern müssen. Zwar kann man das im Allgemeinen Fall über die Syntax

Graphics m_Graphics;  
m_Graphics = getGraphics();

machen und sich eine Instanz von Graphics erstellen, die zur Ausgabe verwendet wird. Sie können anschließend beispielsweise mit

m_Graphics.drawString("Melde irgendwas", 10, 40);

auf sämtliche Grafikmethoden zugreifen. Die in der Klasse Component als public Graphics getGraphics() definierte Methode gibt den Graphics-Kontext von der Komponente zurück bzw. null, wenn die Komponente keinen aktuellen Grafikbezug hat.

Das ist jedoch recht umständlich und wird wie gesagt alles in der paint()-Methode elegant im Hintergrund erledigt. Deshalb werden die meisten Zeichnenoperationen in der paint()-Methode durchgeführt werden.

Wir wissen bereits, dass Java-Applets sich selbst neu zeichnen, indem Sie die paint()-Methode überschreiben. Wie jedoch wird die paint()-Methode aufgerufen? Es gibt dazu drei verschiedene Methoden, die verwendet werden, um das Applet oder die grafische Oberfläche einer Applikation neu zu zeichnen.

1. Die Methode public void paint(Graphics g) selbst. Sie wird immer dann aufgerufen, wenn eine oder die grafische Oberfläche einer Applikation neu gezeichnet werden muss. Dies ist immer beim ersten Aufruf des Applets bzw. Programms der Fall, aber auch jedes Mal dann, wenn das Fenster verschoben oder zwischenzeitlich von einem anderen Fenster überlagert wurde. Der paint()-Methode wird eine Instanz der Graphics-Klasse weitergegeben, die sie für das Zeichnen verschiedener Formen und Bilder verwenden kann. Sie können übrigens auch Java jederzeit dazu veranlassen, ein Applet unabhängig von solchen oben beschriebenen Standardereignissen nachzuzeichnen. Dies erfolgt jedoch nicht mit einem direkten Aufruf von paint(), wie man sich vielleicht zuerst denkt. Es muss die repaint()-Methode verwendet werden.
2. Die Methode public void repaint() kann jederzeit aufgerufen werden, wann auch immer das Applet oder die grafische Oberfläche einer Applikation neu gezeichnet werden muss, unabhängig von einem Standardereignis. Sie ist der Auslöser, der Java veranlasst, die paint()-Methode sobald wie möglich aufzurufen und das Applet neu zu zeichnen. Die Aussage »so bald wie möglich« weist bereits darauf hin, dass die paint()-Methode unter Umständen mit einer gewissen Verzögerung ausgeführt werden kann, wenn andere Ereignisse zuerst zu Ende geführt müssen. Die Verzögerung ist jedoch meist sehr gering.
3. Die Methode public void update(Graphics g) wird von repaint() aufgerufen. Die Standard-update()-Methode löscht den vollständigen Zeichenbereich des Applets bzw. der grafischen Oberfläche einer Applikation (oft ruft das den unangenehmen Flimmereffekt bei schnellen Bildsequenzen hervor) und ruft anschließend die paint()-Methode auf, die dann das Applet vollständig neu zeichnet. Es ist also genau genommen so, dass nicht die repaint()-Methode direkt die paint()-Methode aufruft, sondern nur indirekt über die update()-Methode.

9.2 Punkte, Linien, Kreise und Bögen

Die Graphics-Klasse verfügt über verschiedene Arten von Methoden zum Zeichnen von Grafiken, unter anderem über die folgenden Arten:

9.2.1 Das Koordinatensystem

Bevor wir nun tatsächlich unsere künstlerische Ader testen wollen, müssen wir uns noch ein bißchen mit Mathematik (ebenfalls eine Kunst) beschäftigen. Es geht um den Begriff Koordinatensystem. Intuitiv kann sich wahrscheinlich jeder unter dem Begriff etwas vorstellen, aber wie würden Sie ihn exakt definieren? Ich möchte dazu das umfangreiche Bronstein-Taschenbuch der Mathematik (angeblich die Bibel der Mathematiker, in der Tat aber nur die Bibel der mathematischen Randwissenschaft - Mathematiker rechnen nicht ;-)) - geringfügig verständlicher formuliert zitieren:

Als Koordinatensystem eines n-dimensionalen affinen Raums wird eine Menge bezeichnet, die aus einem Punkt 0 des affinen Raums (Ursprung des Koordinatensystems) und n linear unabhängigen Verktoren des zum affinen Raum gehörigen Vektorraums besteht. Jeder Punkt des affinen Raums lässt sich, ausgehend vom Punkt 0, in eineindeutiger (wirklich einein...) Weise als Linearkombination der n Vektoren darstellen.

Alles klar? Wenn nicht, machen Sie sich keine Sorgen: So abstrakt werden wir die Definition eines so genannten kartesischen Koordinatensystems nicht benötigen. Ich werde es deshalb einfacher formulieren.

In einem Koordinatensystem lässt sich durch die Angabe von (in unserem 2-dimensionalen Fall) zwei Werten (so genannte Vektoren, die vom Ursprung des Koordinatensystems ausgehen) ein beliebiger Punkt in dem Koordinatensystem eindeutig festlegen. Schiffe-Versenken arbeitet mit einem solchen Koordinatensystem oder Excel, Schach, geografische Karten. Dabei werden allerdings unterschiedliche Einheiten zum Festlegen eines Punktes in dem Koordinatensystem verwendet.

Um in dem bei Java verwendeten einfachen, zweidimensionalen, kartesischen Koordinatensystem einen Punkt festlegen zu können, sind zwei Angaben notwendig - ein x-Wert und ein y-Wert. Sie werden dies aus dem Mathematikunterricht in der Schule kennen. Die Angaben von x und y legen einen Punkt eineindeutig fest.

Wir müssen allerdings bei Java aufpassen, denn das dort verwendete Koordinatensystem unterscheidet sich ein wenig von dem sonst oft verwendeten System. Die obere linke Ecke des Bildschirms (genauer: des Bildschirmbereichs eines Applets oder dem Fenster einer Applikation) wird mit (0, 0) - also dem Koordinatensystem-Ursprung - abgebildet (sonst ist es meist die untere linke Ecke). Dabei ist der x-Wert die Anzahl der Bildschirm-Pixel von links ausgehend, also in der Waagrechten, und y ist die Zahl der Pixel, von oben angefangen, also in der Senkrechten. Das Koordinaten-Tupel (42, 10) beschreibt z.B. einen Punkt, der 42 Pixel vom linken Rand des Bildschirmbereichs und 10 Pixel vom oberen Rand des Bildschirmbereichs entfernt ist.

Jede Grafikmethode von Java verwendet irgendwelche Koordinatenangaben, denn es muss ja festgelegt werden, wo eine Ausgabe beginnt und oft noch bis wohin die Ausgabe erfolgen soll.

Die Grafikfähigkeit von Java umfasst so viele spezielle Zeichen-Techniken, Fonts, Farben und Animationstechniken, die wir nicht alle individuell diskutieren können. Wir wollen hier die Grundlagen der verfügbaren Merkmale und Methoden und die (hoffentlich für Sie ebenfalls) wichtigsten Aspekte als Einführung erörtern. Weitergehende Informationen finden Sie bei der Beschreibung der Java-Klassenbibliotheken.

Schauen wir uns nun die wichtigsten Methoden an.

9.2.2 Eine Linie zeichnen - die drawLine()-Methode

Zu den einfachsten Techniken der Graphics-Klasse gehört das Zeichnen von Linien. Dies geschieht mit der Methode

public abstract void drawLine(int x1, int y1, int x2, int y2).

Sie hat zwei Koordinatenpaare:

Zwischen den beiden Tupeln wird eine Linie gezogen.

Beispiel:

import java.awt.Graphics;
public class ZieheLinie extends java.applet.Applet {
public void paint(Graphics g) {
g.drawLine(42,42,100,100);
}  }

Abbildung 9.1:  Eine Linie

9.2.3 Ein Rechteck zeichnen - die drawRect()-Methode

Kommen wir nun zum Zeichnen von Rechtecken. Um ein Rechteck zu zeichnen, könnten Sie die drawLine()-Methode verwenden, indem Sie jeweils am Endpunkt einer Linie im rechten Winkel eine neue Linie ansetzten. Sofern die beiden gegenüber liegenden Linien gleich lang sind, haben Sie ein Rechteck. Diese Methode ist sehr flexibel und erlaubt Rechtecke, die in jede denkbare (zweidimensionale) Richtung gedreht sind. Sie setzt jedoch viel Sorgfalt und ein gewisses Gehirnschmalz voraus. Für Rechtecke, die nicht in der vertikalen/horizontalen Ausrichtung gedreht werden sollen, gibt es eine eigene Graphics-Methode - die Methode

public void drawRect(int x, int y, int width, int height).

Sie verwendet wieder zwei Koordinaten-Tupel, die jedoch anders als die drawLine()-Tupel zu verstehen sind.

Um beispielsweise ein Rechteck bei dem Punkt (150, 100) zu beginnen, das 200 Pixel breit und 120 Pixel hoch ist, würde der Methodenaufruf folgendermaßen aussehen.

Beispiel:

import java.awt.Graphics;
public class MaleRec extends java.applet.Applet {
public void paint(Graphics g) {
g.drawRect(150, 100, 200, 120);
}  }

Abbildung 9.2:  Ein Rechteck

9.2.4 Ein gefülltes Rechteck zeichnen - fillRect()

Die drawRect()-Methode zeichnet nur die Umrandung einer Box. Wenn Sie einen gefüllten Kasten zeichnen wollen, können Sie die Methode

public abstract void fillRect(int x, int y, int width, int height)

verwenden, die die gleichen Parameter wie drawRect() verwendet (siehe Abbilfung 9.3).

import java.awt.Graphics;
public class MaleRecFil extends java.applet.Applet {
public void paint(Graphics g) {
g.fillRect(10, 100, 200, 42);
}  }

Abbildung 9.3:  Ein gefülltes Rechteck

9.2.5 Löschen eines Bereichs - clearRect()

Ganz wichtig ist die Möglichkeit, einen rechteckigen Bereich wieder löschen zu können. Sie können dazu mit der Methode

public abstract void clearRect(int x, int y, int width, int height)

arbeiten. Sie verwendet die gleichen Parameter wie drawRect(), lässt sich indes natürlich zum Löschen sämtlicher Kunstwerke (unabhängig von der zum Erzeugen verwendeten Methode) in dem spezifizierten Bereich verwenden.

Beispiel: g.clearRect(10, 100, 200, 42);

Um einen vollständigen Bereich eines Applets zu löschen, macht die folgende Syntax Sinn, denn sie beinhaltet die exakten Größenangaben eines beliebigen Applets als Eckpunkte des zu löschenden Bereichs.

g.clearRect(0, 0, this.size.width(), this.size.height());

9.2.6 Kopieren eines Bereichs - copyArea()

Ebenfalls wichtig ist die Möglichkeit, einen rechteckigen Bereich in einen anderen Bildschirmbereich kopieren zu können. Sie können dazu mit der Methode

public abstract void copyArea(int x, int y, int width, int height, int dx, int dy)

arbeiten. Sie verwendet weitgehend die gleichen Parameter wie clearRect(). Jedoch müssen Sie außerdem den Zielbereich über ein zusätzliches (x, y)-Tupel spezifizieren. Dieser wird über die Angaben dx - die horizontale Distanz zum Kopieren der Pixel - und dy - die vertikale Distanz zum Kopieren der Pixel - festgelegt.

Beispiel: g.copyArea(10, 100, 200, 42, 200, 200);

9.2.7 Ein 3D-Rechteck zeichnen - die draw3dRect()-Methode

Die Graphics-Klasse verfügt ebenso über eine Methode zum Zeichnen von dreidimensionalen Rechtecken. Leider zeichnet die Graphics-Klasse diese Elemente mit einer sehr geringen Höhe oder Tiefe, wodurch der 3D-Effekt kaum sichtbar wird. Die Syntax für

public void draw3DRect(int x, int y, int width, int height, boolean raised) 

ist ähnlich der von drawRect(), nur dass diese einen booleschen Extraparameter am Ende hat, der angibt, ob das Rechteck erhöht angezeigt werden soll oder nicht. Der Erhöhungs-/Vertiefungseffekt wird erzeugt, indem helle und dunkle Linien um die Grenzen des Rechtecks gezogen werden.

import java.awt.Graphics;
public class MaleRec3D extends java.applet.Applet {
public void paint(Graphics g) {
g.draw3DRect(10, 100, 200, 42,true);
}  }

Abbildung 9.4:  Man sieht leider kaum einen 3D-Effekt.

9.2.8 Ein gefülltes 3D-Rechteck zeichnen - fill3dRect()

Die fill3dRect()-Methode ist das dreidimensionale Analogon zu der fillRect()-Methode. Die Syntax für

public void fill3DRect(int x, int y, int width, int height, boolean raised)

ist identisch mit der von draw3DRect() und zeigt wie diese kaum einen 3D-Effekt an (vor allem bei unglücklicher Farbwahl).

import java.awt.Graphics;
public class MaleRec3DFil extends java.applet.Applet {
public void paint(Graphics g) {
g.fill3DRect(10, 100, 200, 42,true);
}  }

9.2.9 Abgerundete Rechtecke - drawRoundRect()

Neben normalen und 3D-Rechtecken können Sie ebenso Rechtecke mit abgerundeten Ecken zeichnen. Die Syntax der Methode

public abstract void drawRoundRect(int x, int y, int width, int height, int arcWidth, int 
arcHeight)

ist drawRect() ähnlich, nur dass sie noch über zwei weitere Parameter verfügt:

arcWidth 
arcHeight

Diese Parameter zeigen an, wie stark die Ecken abgerundet werden sollen.

Das erste Argument arcWidth bestimmt den Winkel der Abrundung auf der horizontalen und der zweite Parameter arcHeight den Winkel auf der vertikalen Ebene. Das bedeutet, je größer die Winkelwerte sind, desto stärker gerundet erscheint das Rechteck.

import java.awt.Graphics;
public class MaleRecRound extends java.applet.Applet {
public void paint(Graphics g) {
g.drawRoundRect(10, 100, 50, 42,5,5);
g.drawRoundRect(110, 200, 100, 42,5,50);
g.drawRoundRect(150, 250, 25, 42,50,25);
}  }

Abbildung 9.5:  Abgerundete Ecken

9.2.10 Abgerundete gefüllte Rechtecke - fillRoundRect()

Das Analogon für abgerundete gefüllte Rechtecke ist die Methode

public abstract void fillRoundRect(int x, int y, int width, int height, int arcWidth, int arcHeight).

Die Syntax ist identisch zu der drawRoundRect()-Methode.

import java.awt.Graphics;
public class MaleRecRoundFil extends java.applet.Applet {
public void paint(Graphics g) {
g.fillRoundRect(10, 100, 200, 42,42,42);
g.fillRoundRect(10, 200, 200, 42,15,20);
g.fillRoundRect(100, 250, 200, 42,20,5);
}  }

Abbildung 9.6:  Gefüllte Rechtecke mit abgerundeten Ecken

9.2.11 Zeichnen von Polygonen - drawPolygon()

Unter einem Polygon oder Vieleck versteht man eine grafische Form mit einer nicht zwingend vorgebenden Anzahl von Ecken und Kanten und beliebigen Winkeln zwischen den Kanten. Die Summe der Winkel muss bei einem geschlossenen Vieleck immer 360 Grad sein und die letzte Linie muss in diesem Fall den Anfangspunkt der ersten Linie berühren.

Ein Polygonzug kann aber auch offen sein. Dies bedeutet, dass der Endpunkt der letzten Linie nicht mit dem Anfangspunkt der ersten Linie übereinstimmt. In dem Fall kann (und wird meist) die Summe der Winkel von 360 Grad abweichen.

Ein Rechteck ist der Spezialfall eines geschlossenen Vierecks (sicher keine Überraschung), daneben fallen darunter Dreiecke, Fünfecke usw. Besonderheit ist bei Vielecken, dass im Regelfall keine Symmetrie zwischen den Kanten (wie etwa beim Rechteck oder Quadrat) herrscht.

Um ein Vieleck zu zeichnen, können Sie wieder die drawLine()-Methode bemühen, indem Sie jeweils am Endpunkt einer Linie im beliebigen Winkel eine neue Linie ansetzten. Sofern sich die letzte Linie am Endpunkt und die erste Linie am Anfangspunkt berühren, haben Sie ein geschlossenes Vieleck. Diese Methode ist sehr flexibel und erlaubt beliebige Vielecke. Sie setzt jedoch viel Sorgfalt voraus und ist eigentlich unsinnig, denn es gibt eine eigene und genauso flexible Methode:

public abstract void drawPolygon(int[] xPoints, int[] yPoints, int nPoints)

Die fehlende Symmetrie macht jedoch mehr Angaben über den Verlauf eines Vielecks notwendig, als es bei dem Spezialfall eines Rechtecks notwendig war.

Sie haben zwei Optionen, wenn Sie Polygone zeichnen. Sie können entweder die zwei Datenbereiche (Arrays), die die x- und die y-Koordinaten der Punkte enthalten, weitergeben, oder Sie können eine Instanz einer Polygon-Klasse weitergeben. Machen wir uns an einem Beispiel das erste Verfahren deutlich.

import java.awt.Graphics;
public class ZeichnePolygon extends java.applet.Applet {
// Definiere ein array mit X-Koordinaten 
 int xCoords[] = { 10, 40, 60, 30, 10 };
// Definiere ein array mit Y-Koordinaten
 int yCoords[] = { 20, 0, 10, 60, 40 };
 public void paint(Graphics g)  {
// Zeichne ein 5-Eck
   g.drawPolygon(xCoords, yCoords, 5); 
 }  }

Abbildung 9.7:  Ein Polygonzug

Es wird in dem Beispiel eine Linie von dem Tupel (xCoords[0], yCoords[0]) zu dem Tupel (xCoords[1], yCoords[1]), dann zu dem Tupel (xCoords[2], yCoords[2]) usw. gezogen.

Das zweite Verfahren verwendet ein Polygon-Objekt. Dieses Verfahren ist beispielsweise dann nützlich, wenn Sie nachträglich Punkte in das Vieleck einfügen wollen.

Die Klasse Polygon gehört zum Paket java.awt.

import java.awt.Graphics;
import java.awt.Polygon;
public class ZeichnePolygon2 extends java.applet.Applet {
// Definiere ein Array mit X-Koordinaten 
int xCoords[] = { 10, 40, 60, 300, 10, 30, 88 };
// Definiere ein Array mit Y-Koordinaten
int yCoords[] = { 20, 0, 10, 60, 40, 121, 42 };
// Bestimme Anzahl Ecken über Methode length
// des Array-Objekts mit x-Koordinaten
int anzahlEcken = xCoords.length;
public void paint(Graphics g) {
Polygon poly = new Polygon(xCoords, yCoords, anzahlEcken);
// Zeichne ein 7-Eck
g.drawPolygon(poly); 
}  }

Die beiden Arrays müssen für die x- und y-Koordinaten gleich groß sein. Wenn dies nicht der Fall ist, kann nur ein n-Eck gezeichnet werden, dessen maximale Anzahl der Ecken dem Index des kleineren Arrays entspricht.

Wenn Sie eine Instanz einer Polygon-Klasse erstellt haben, können Sie die Methode

public Rectangle getBoundingBox()

verwenden, um den Bereich zu ermitteln, der von diesem Polygon abgedeckt wird (die Minimum- und Maximum-x- und y-Koordinaten):

Rectangle boundingBox = myPolygon.getBoundingBox();

Die Rectangle-Klasse, die von getBoundingBox() zurückgegeben wird, enthält Variablen, die die x- und y-Koordinaten, die Höhe und die Breite des Rechtecks anzeigen.

Ab der JDK-Version 1.1 ist die Methode getBoundingBox() durch getBounds() ersetzt.

Sie können auch bestimmen, ob ein Punkt in einem Polygon liegt oder sich außerhalb befindet, indem Sie

public boolean inside(int x, int y) 

mit den x- und y-Koordinaten des Punktes aufrufen:

if (myPolygon.inside(5, 10)) {
// der Punkt (5, 10) ist innerhalb diese Polygons
}

Die Methode inside() ist als deprecated bezeichnet und ab der JDK-Version 1.1 durch die Methode public boolean contains(int x, int y) ersetzt, die die identische Funktionalität bietet.

9.2.12 Zeichnen von gefüllten Polygonen - fillPolygon()

Sie können natürlich gefüllte Polygone zeichnen. Dazu dient die Methode

public abstract void fillPolygon(int[] xPoints, int[] yPoints, int nPoints),

die in der Syntax wieder identisch zu der drawPolygon()-Methode ist. Beachten Sie jedoch, dass bei der fillPolygon()-Methode nur geschlossene Polygone erzeugt werden, d.h., Anfangs- und Endpunkt sind identisch.

import java.awt.Graphics;
public class ZeichnePolygonFil extends java.applet.Applet {
// Definiere ein array mit X-Koordinaten 
int xCoords[] = { 10, 30, 10, 30, 10, 15, 42 };
// Definiere ein array mit Y-Koordinaten
int yCoords[] = { 10, 20, 10, 60, 70, 42, 42 };
int anzahlEcken = xCoords.length;
public void paint(Graphics g) {
// Zeichne ein 7-Eck
g.fillPolygon(xCoords, yCoords, anzahlEcken);  
}  }

Abbildung 9.8:  Ein gefülltes Polygon, das sich in zwei Teile teilt

9.2.13 Zeichnen von Kreisen und Ellipsen - drawOval()

Ich möchte noch mal die Mathematik zu Hilfe rufen. Was ist eigentlich der Unterschied zwischen einem Kreis und einer Ellipse? Es gibt keinen, außer dass der Kreis eine ganz spezielle Ellipse ist. Wenn die beiden Brennpunkte der Ellipse in einen Punkt fallen, hat man eben einen Kreis. Folgerichtig hat die Graphics-Klasse keine extra Methode für einen Kreis, sondern nur eine Methode für Ellipsen - die drawOval()-Methode. Diese besitzt die gleichen Koordinaten-Angaben wie die drawRect()-Methode. Wie das? Nun, Sie wissen vielleicht aus dem Mathematikunterricht (ich weiß, es langt bald, aber Grafik hat eine Menge mit Mathematik zu tun und für irgendwas muss ein Mathematikstudium taugen :-> ), dass man in ein Rechteck in bestimmten Fällen eindeutig eine Ellipse oder in jedem Fall in ein Quadrat eindeutig einen Kreis einschreiben kann. In der Mitte jeder Seite des Rechtecks bzw. Quadrats stößt das eingeschriebene runde Objekt in genau einem Punkt an die Linie des umgebenden rechteckigen Objekts.

Sie geben also die Koordinaten der oberen linken Ecke dieses umschreibenden Rechtecks im ersten (x,y)-Tupel an die Methode

public abstract void drawOval(int x, int y, int width, int height)

weiter. Das zweite (x,y)-Tupel legt die Breite und die Höhe des umschreibenden Rechtecks und damit auch des Ovalkörpers fest. Wenn Breite und Höhe gleich sind, haben Sie einen Kreis.

import java.awt.Graphics;
public class ZeichneOval extends java.applet.Applet {
public void paint(Graphics g) {
// Zeichne eine Ellipse
g.drawOval(50, 100, 200, 120);
// Zeichne einen Kreis
g.drawOval(175, 175, 200, 200);
}  }

Das eindeutige Umschreiben eines rechteckigen Körpers mit einer ovalen Form ginge ebenso, aber in Java wird das Einschreiben genutzt.

Sie können auch die bereits beschriebene drawRoundRect()-Methode verwenden, um einen Kreis zu zeichnen, wenn Sie die Abrundung so definieren, dass die Winkel identisch sind und der Beginn der Abrundung in der Mitte des Rechtecks beginnt. Eine Ellipse funktioniert ähnlich, nur sind dann immer nur die beiden symmetrischen Winkelangaben identisch, nicht alle vier (sonst haben wir den Spezialfall Kreis).

Abbildung 9.9:  Eine Ellipse und  ein Kreis

9.2.14 Zeichnen von gefüllten Kreisen und Ellipsen - fillOval()

Das Analogon für gefüllte Kreisen und Ellipsen ist die Methode

public abstract void fillOval(int x, int y, int width, int height).

Die Syntax ist identisch mit der drawRect()-Methode.

import java.awt.Graphics;
public class ZeichneOvalFil extends java.applet.Applet {
public void paint(Graphics g) {
// Zeichne eine Ellipse
g.fillOval(50, 15, 250, 50);
// Zeichne einen Kreis
g.fillOval(250, 150, 50, 50);
}  }

Abbildung 9.10:  Gefüllte Ellipsen

9.2.15 Zeichnen von Bögen - drawArc()

Ein Bogen ist Teil einer Ellipse oder eines Kreis. Der Unterschied ist eigentlich nur, dass die Figur nicht geschlossen wird. Im Prinzip ist die Geschichte jedoch auch nicht viel schwieriger als das Zeichnen der vollständigen Figuren. Java stellt die folgende Methode dafür zur Verfügung:

public abstract void drawArc(int x, int y, int width, int height, int startAngle, int 
arcAngle).

Die ersten vier Parameter sind identisch mit der drawOval()-Methode. Man muss der Methode nur zusätzlich mitteilen, ab wo und bis wo man die Figur gezeichnet haben möchte. Dazu muss man nur wissen, dass ein Kreis (oder auch Oval im allgemeinen Sinn) in 360 Grad unterteilt werden kann.

Der fünfte Parameter der drawArc()-Methode bestimmt den Anfangswinkel von einer gedachten vertikalen Mittellinie aus gesehen, ab dem der Bogen gezeichnet werden soll.

Der sechste Parameter legt den Winkel fest, wie weit der Bogen gehen soll. Dabei bedeutet der Gradwert, wie weit der Bogen ab dem Startpunkt gezeichnet wird und in welche Richtung er geht. Die positive Richtung in Java ist entgegen dem Uhrzeigersinn. Mit Angabe eines negativen Parameters wird in Richtung des Uhrzeigersinns gezeichnet. Es handelt sich also nicht um den Winkel von der gedachten vertikalen Mittellinie, wie bei Parameter 5. Ein Halbkreis wird also als Parameter 6 die Angabe 180 haben.

import java.awt.Graphics;
public class ZeichneBogen extends java.applet.Applet {
public void paint(Graphics g) {
g.drawArc(50, 15, 50, 50, 90, 180);
g.drawArc(50, 200, 150, 50, 120, -90);
}  }

Abbildung 9.11:  Bögen

9.2.16 Zeichnen von gefüllten Bögen - fillArc()

Das Analogon zu der drawArc()-Methode für gefüllte Bögen ist die Methode

public abstract void fillArc(int x, int y, int width, int height, int startAngle, int 
arcAngle).

Die Syntax ist identisch. Zwischen Anfangs- und Endpunkt wird eine gerade Linie gezogen und die Figur ausgefüllt.

import java.awt.Graphics;
public class ZeichneBogenFil extends java.applet.Applet {
public void paint(Graphics g) {
g.fillArc(50, 15, 50, 50, 90, 90);
g.fillArc(150, 150, 50, 50, 120, 180);
}  }

Abbildung 9.12:  Gefüllte Bögen

9.3 Farbangaben

Java setzt in seinem ursprünglichen Konzept (wir werden noch das neue Konzept kennen lernen) sämtliche Farben aus so genannten Primärfarben des Lichts zusammen. Dies sind die Farben Rot, Grün und Blau. Man nennt dieses Farbmodell das RGB-Modell (Red-Green-Blue). Das bedeutet, dass Farben aus der Summe von rotem, grünem und blauem Licht zusammengesetzt werden.

Vielleicht werden Sie jetzt stutzen, weil Ihnen als Primärfarben Rot, Gelb und Blau einfallen. Dies ist nicht falsch, es handelt sich nur um ein anderes Farbmodell, das statt Grün Gelb verwendet. Man findet es bei pigmentbasierenden Farben (etwa Buntstiften). Das Lichtfarbmodell basiert jedoch auf der grünen Grundfarbe und dies wird von Java verwendet.

Einige der üblichen Kombinationen sind:

  • rot + grün = braun (oder gelb, je nach Helligkeit)
  • grün + blau = cyan (hellblau)
  • rot + blau = magenta (lila)

Schwarz entsteht, wenn es kein Licht gibt, weiß wird mit der Kombination aller Primärfarben gebildet: rot + grün + blau (in gleicher Menge) = weiß. Dies ist genau umgekehrt zu dem Pigment-basierendem Modell.

Sie definieren eine Farbe im RGB-Modell, indem Sie angeben, wie viel rotes, grünes und blaues Licht in der Farbe enthalten ist. Sie können dies beispielsweise mit einer Zahl zwischen 0 und 255 oder mit Gleitkommazahlen zwischen 0,0 und 1,0. Mehr Informationen zu diesem Thema finden Sie im Anhang.

Kommen wir nun zu den Objekten und Methoden, mit denen unter Java die Farben verändert und für die diversen Grafik-Methoden gesetzt werden können.

9.3.1 Die Color-Klasse

Um ein Objekt in einer bestimmten Farbe zeichnen und darstellen zu können, können Sie eine Instanz der Color-Klasse erzeugen.

Wir schauen uns drei Arten an, mit denen Sie eine Farbe können erzeugen können:

1. public Color(int red, int green, int blue). Damit wird eine Farbe mit Rot-, Grün- und Blau-Werten zwischen 0 und 255 erzeugt.
2. public Color(int rgb). Damit wird eine Farbe mit Rot-, Grün- und Blau-Werten zwischen 0 und 255 erzeugt, bei der jedoch alle Werte zu einem einzigen Integerwert kombiniert werden. Die Bits 16-23 enthalten den Rot-Wert, die Bits 8-15 enthalten den Grün-Wert und 0-7 enthalten den Blau-Wert. Diese Werte werden normalerweise in hexadezimaler Schreibweise notiert, damit Sie die Farbwerte ganz einfach sehen können. 0x123456 würde beispielsweise einen Rot-Wert von 0x12 (18 dezimal) ergeben, einen Grün-Wert von 34 (52 dezimal)) und einen Blau-Wert von 56 (96 dezimal). Denken Sie daran, dass jede Farbe in hexadezimaler Schreibweise genau 2 Zeichen einnimmt.
3. public Color(float red, float green, float blue). Damit wird eine Farbe mit Rot-, Grün- und Blau-Werten von 0.0 und 1.0 erzeugt.

Eine Instanz einer Farbe erstellen Sie also beispielsweise so:

Color pinkColor = new Color(255, 192, 192);
Color MeineFarbe = new Color(0.0, 1.0, 0.25);

Eine gängige Farbe können Sie noch schneller zur Verfügung haben, denn die Color-Klasse definiert verschiedene in Klassenvariablen gespeicherte Standardfarbobjekte, auf die Sie per Punktnotation zugreifen können.

Beispiele:

Color.white
Color.pink

Auch zu den Standardfarbobjekten finden Sie mehr Informationen im Anhang im Abschnitt über Farben.

9.3.2 Farben setzen - setColor()

Wenn Sie einmal eine Farbe erstellt haben oder ein Standardfarbobjekt verwenden wollen, können Sie die Zeichenfarbe mit der Methode

public abstract void setColor(Color c)

verändern. Als Parameter geben Sie das gewünschte Farbobjekt an.

import java.awt.*;
public class Farbe extends java.applet.Applet {
public void paint(Graphics g) {
// Erzeugen von Farbobjekten
Color pinkColor = new Color(255, 192, 192);
Color irgendeineFarbe1 = new Color(255, 100, 92);
Color irgendeineFarbe2 = new Color(155, 192, 192);
Color irgendeineFarbe3 = new Color(55, 192, 192);
Color irgendeineFarbe4 = new Color(5, 12, 120);
// Zeichne eine Ellipse
g.setColor(irgendeineFarbe2);
g.drawOval(5, 5, 150, 250);
// Zeichne eine Ellipse
g.setColor(irgendeineFarbe1);
g.fillOval(50, 15, 250, 150);
// Zeichne einen Kreis
g.setColor(pinkColor);
g.fillOval(250, 150, 150, 150);
// Zeichne einen gefüllten Bogen
g.setColor(irgendeineFarbe3);
g.fillArc(50, 15, 100, 100, 90, 90);
// Zeichne einen weiteren gefüllten Bogen
// in einer anderen Farbe
g.setColor(irgendeineFarbe4);
g.fillArc(150, 150, 50, 50, 120, 180);
// direkte Verwendung einer Farbkonstanten
g.setColor(Color.green);
g.drawRect(40,40,100,200);
}  }

Abbildung 9.13:  Verschiedene Farben

9.3.3 Hintergrundfarben und Vordergrundfarben pauschal setzen - setBackground() und setForeground()

Normalerweise ist die Hintergrundfarbe eines Applets weiß oder dunkelgrau (je nach Einstellungen im Container). Wenn Sie einmal eine Farbe erstellt haben oder ein Standardfarbobjekt verwenden wollen, können Sie damit ebenso die Hintergrundfarbe eines Applets individuell mittels der Methode

public void setBackground(Color c)

setzen.

public void paint(Graphics g) {  
/* Hier wird auf einem rosa Hintergrund ein grünes Rechteck gezeichnet*/ 
Color pinkColor = new Color(255, 192, 192);
setBackground(pinkColor);
g.setColor(Color.green);
g.drawRect(40,40,100,20);
}

Wenn Sie die Farbe für alle Zeichenobjekte innerhalb eines Applets oder einer Applikation mit grafischer Oberfläche pauschal vorbelegen wollen, können Sie die Methode

public void setForeground(Color c)

verwenden. Als Parameter geben Sie wieder das gewünschte Farbobjekt an.

import java.awt.*;
public class BackFarbe extends java.applet.Applet {
public void paint(Graphics g) {
Color irgendeineFarbe1 = new Color(200, 100, 100);
setBackground(irgendeineFarbe1);
setForeground(Color.blue);
g.fillRect(40,120,300,120);
g.fillOval(5, 5,300, 150);
}  }

Abbildung 9.14:  Global gesetzte Farben

Die Angaben für Hinter- und Vordergrundfarben sollten in einer Klasse nur einmal gesetzt werden. Es sind globale Angaben, die bei Bedarf mit den individuellen Farbangaben verändert werden sollten, nicht durch erneutes Verändern der globalen Angaben.

Vielleicht sind Sie von den bisherigen einfachen Beispielen mit den ziemlich einfachen Formen etwas enttäuscht. Dem kann man abhelfen. Wir werden jetzt ein berühmtes und optisch immer wieder eindrucksvolles Beispiel erstellen.

Dabei verwenden wir nur die bereits besprochenen Grafikmethoden, die zu dem grundlegenden Grafikkonzept von Java gehören und die weitergehenden Techniken nicht bemühen (damit bleibt das Beispiel für mehr Plattformen lauffähig). Ergänzend wird deutlich, wie schön Mathematik sein kann :*). Es geht um das berühmte Apfelmännchen.

9.3.4 Ein Apfelmännchen als Java-Applet

Fast jeder, der sich intensiver mit Programmierung auseinandersetzt, wird irgendwann einmal auf Fraktale stoßen. Diese selbstähnlichen Gesellen bieten sich mit ihrer rekursiven Struktur geradezu zum grafischen Austesten von Rekursionen an. Unter einer Rekursion versteht man einen Programmieralgorithmus, der das Ergebnis eines Rechenschritts als Grundlage für den nächsten Schritt benötigt. Nur der erste Schritt (oder abzählbar viele) wird (werden) mittels Startwerten gesetzt.

Es gibt Fraktal-Programme in fast allen Programmiersprachen und selbstverständlich kann man zur Programmierung von Fraktalen die innovative Sprache Java heranziehen. Das folgende Beispiel ist ein kleines Java-Programm zur Generierung des wohl berühmtesten Fraktals - dem Apfelmännchen.

Die Entdeckung von Fraktalen geht wesentlich auf Arbeiten des amerikanischen Mathematikers und Physikers B.B. Mandelbrot zurück. Dieser beschäftigte sich u.a. in den 70er- und 80er-Jahren mit rekursiv definierten Formeln (besonders wichtig ist das 1982 erschienene Buch »The Fractal Geometry of Nature«). Die immer leistungsfähiger werdenden Computer ermöglichten bald ein grafisches Umsetzen von diesen rekursiv definierten Mustern. Die unzweifelhaften ästhetischen Resultate sorgten schnell für Aufmerksamkeit, aber vor allem die verblüffende Ähnlichkeit zu in der Natur vorkommenden Prozessen und Formen (das Prinzip der Selbstähnlichkeit in jeder Vergrößerungstiefe - denken Sie an einen Farn oder einen Blumenkohl) führte zu einer immer stärkeren Bedeutung der Fraktale (oder oft nach ihrem Entdecker Mandelbrotmenge genannt). Das aus den Fraktalen entstandene Forschungsgebiet ist übrigens heute als Chaostheorie bekannt.

Fraktale bieten sich bei der Geschwindigkeit heutiger Computer für interessante grafische Experimente an, da die grafischen Effekte auf relativ einfach zu programmierenden Grundstrukturen basieren. Erst der rekursive Aufruf - etwa in Form von ebenfalls einfachen Schleifen - erzeugt die eigentlichen interessanten Effekte. Allerdings sollte für die Grundstrukturen neben Funktionen von reellen Zahlen auf jeden Fall die komplexe Ebene eingeschlossen werden (sonst wird es optisch ziemlich langweilig).

Das Apfelmännchen ist wegen seiner relativ einfachen Grundstruktur das verbreitetste (und bekannteste) Fraktal und wird im folgenden Beispiel in Java umgesetzt. Trotz einer äußert komplexen grafischen Struktur bleibt der Source ziemlich klein und kompakt. Beachten sollten Sie bei einem Test, dass die Umsetzung rekursiver Formeln sehr stark von der Rechenleistung des Computers abhängt. Wenn Sie mit einem älteren Rechner arbeiten, kann die Berechnung und Darstellung des Apfelmännchens etwas dauern. Im Source sind die Stellen mit Kommentaren gekennzeichnet, wo Sie die Rechentiefe verändern können. Damit kann die Geschwindigkeit des Aufbaus (zeilenweise von oben nach unten) auf Kosten der Genauigkeit verbessert werden (und umgekehrt).

Für die Bildschirmauflösung sollten mindestens 256 Farben gewählt sein, da die Berechnung der Farbe eines Bildpunktes sich aus dem Zusammensetzen von drei RGB-Werten- jeweils zwischen 0 und 255 - ergibt. Die Farbe wird im Beispiel nur durch Manipulation des Rot- und Grünwertes verändert, was aber willkürlich ist und natürlich ebenso für die anderen RGB-Werte funktioniert (für alle drei gleichzeitig oder nur einen). Bei komplexeren Versionen des Apfelmännchens kann man die drei RGB-Werte auch unabhängig voneinander in getrennten Rekursionen für jeden Parameter ermitteln. In dem Beispiel liegt die Grundfarbwahl im Rotbereich. Auch sie kann aber jederzeit in einen anderen Farbbereich verlegt werden (siehe Kommentare im Source und in den Screens). Durch die nicht-linearen komplexen Funktionen können Sie übrigens sicher sein, dass ein geringfügiges »Wackeln« an den Startwerten das Ergebnis extrem beeinflussen kann. Das wird in unserem Beispiel noch nicht so deutlich zu spüren. Aber wenn man einen zufälligen Ausschnitt unseres Ergebnisses vergrößern würde, würde die Wahl des Ausschnittes nach spätestens 2 bis 3 Wiederholungen das Ergebnis massiv beeinflussen. Die Wahrscheinlichkeit, dass Sie eine exakte Kopie eines anderen Vergrößerungsvorgangs erstellt haben, ist kleiner, als 7(!) Richtige im Lotto zu haben (will heißen, kaum von 0 verschieden). Dies ist ebenfalls wesentlicher Bestandteil der Chaostheorie. Vielleicht kennen Sie das Beispiel von dem Schmetterling, der am Äquator mit den Flügeln schlägt und damit in Europa das Wetter zum Kippen bringt. Das ist kein Witz, man kann es beweisen. Das Problem ist nur, dass es mehr als einen flügelwackelnden Schmetterling gibt (von größeren Ereignissen wie startenden Flugzeugen auf Rheinmain oder Atombombentest in der Karibik ganz zu schweigen). Man nennt diese verschiedenen Effekte, die das Ergebnis beeinflussen können, Randbedingungen. Fraktal-Programme, die solche Ausschnittsbildungen mit sich ständig verändernden Randbedingungen zulassen, erzeugen immer wieder neue Ergebnisse.

Nehmen wir uns jetzt endlich den Source vor. Wir werden ein paar Features nutzen, die erst bei der Diskussion des Java-AWT vorkommen. Nicht viele, aber es wird einfacher dadurch. Nehmen Sie diese erst einmal hin, die zentralen Methoden des Apfel-Applets haben wir schon durchgesprochen.

// Einbinden der Packages mit dem import-Befehl
import java.awt.*;
import java.applet.*;
import java.util.*;
//Die Klasse Apfel wird als Applet definiert 
public class Apfel extends Applet {
  public void init()  { 
// Einstiegspunkt für das Applet mit Layoutangaben
   setLayout(new BorderLayout());
   helloPanel hp=new helloPanel();
   add("Center",hp);
 }
/* Unser Event-Handler in Java reagiert auf die entsprechenden Fensterereignisse, d.h. in 
unserem Fall bewirkt ein Schließen des Fensters ein Programmende. Mehr Funktionalität ist in 
dem Beispiel nicht integriert. */
 public boolean handleEvent(Event e) { 
  switch (e.id)  {
   case Event.WINDOW_DESTROY:
   System.exit(0);
   return true;
   default:
   return false;
  }
 }  }
class helloPanel extends Panel {
  public helloPanel()  {
// Setzen der Hintergrundfarbe
   setBackground(Color.blue); 
  }
 public int iteration(double realteil,double imaginaerteil) {   
/* Diese Methode enthält die eigentliche Iteration zur Berechnung der Farbe eines Bildpunktes 
des Apfelmännchens. Genau genommen wird hier zum einen die Entscheidung getroffen, welcher 
Zweig in der Zeichenmethode paint() zu verwenden ist. Zum anderen wird ein Modulo-Faktor für 
die tatsächliche Farbwahl berechnet. Dabei werden zwei Parameter benötigt - der Realteil und 
der Imaginärteil. */
   double x,y,x2,y2,z;
   int k=0;
   x=0;
   y=0;
   x2=y;
   y2=0;
   z=0;
   for (k=0;k<1000;k++) {
// Der komplexe Apfelmännchen-Grundalgorithmus
     y=2*x*y+imaginaerteil;
     x=x2-y2+realteil;
     x2=x*x;
     y2=y*y;
     z=x2+y2;
     if (z>4) return k; 
   }
   return k ;
 }
 public void paint(Graphics g) {
 /* In der Zeichenmethode paint() wird der eigentliche Bildaufbau vorgenommen. */
 /* Startwerte für das Apfelmännchen. Ein Verändern der Startwerte führt zu einer Veränderung 
der Größe und/oder Position des Apfelmännchens. Die Grundfläche für den Aufbau des 
Apfelmännchens bleibt allerdings unberührt. */
  double restart=-2;
  double reend=1;
  double imstart=-1;
  double imend=1;
//Alternative Startwerte zum Experimentieren   
 /*   double restart=-3; 
   double reend=2;
   double imstart=-2;
   double imend=2;
 */
  double restep,imstep,imquad,repart,impart;
  int x,y,farbe;
 /* Veränderung der Schrittweiten bei der Berechnung beeinflusst ebenfalls Größe und Position 
des Fraktals */
// Schrittweite für den Realteil 
  restep=(reend-restart)/200; 
// Schrittweite für den Imaginärteil 
  imstep=(imend-imstart)/200; 
  y=0; // Zählvariable
// Zuweisung eines Startwertes für den 
// Imaginärteil in der Rekursion
  impart=imstart; 
 /* Beginn der Rekursion. Zwei ineinander verschachtelte for-Schleifen. Die äußere Schleife 
berechnet den Realteil, die innere Schleife den Imaginärteil. */
// Jeder y-Wert entspricht einer Bildschirmzeile
  for (y=0;y<200;y++)  {
// Zuweisung eines Startwertes für den Realteil 
// in der Rekursion
   repart=restart; 
// Jeder x-Wert entspricht einer Spalte
   for (x=0;x<200;x++) {
/* Berechnung der Entscheidungsvariable für die Farbe eines Bildpunktes  */
    farbe=iteration(repart,impart);
    if(farbe==1000) {
// Zeichne an der Position x,y einen schwarzen 
// Punkt
        g.setColor(Color.black);
   g.drawLine(x,y,x+1,y);
    } 
    else { 
/* Hier wird die Farbe eines Bildpunktes vom eigentlichen Apfelmännchen explizit berechnet. 
Die 3 Angaben in der Color-Angabe sind RGB-Werte (Rot-Grün-Blau) und legen die jeweilige 
Intensität der Farbanteile fest. Nur der erste Parameter wird jeweils neu berechnet. Dabei 
ist bei Manipulationen des Rotbereichs darauf zu achten, dass das Resultat zwischen 0 und 255 
bleibt. Hier im Beispiel liegt die Grundfarbwahl im Rotbereich. Sie kann aber jederzeit durch 
Veränderung der Parameter in einen anderen Farbbereich verlegt werden. */
  Color jr = new Color(255-(farbe%52*5),255-(farbe%52*5),125);
/* Alternative Grundfarbe. Dabei wird sowohl der Rotanteil, als auch der Grünanteil 
manipuliert. */
/*Color jr = new Color(255-(farbe%26*10), 120, 125); */
   g.setColor(jr);  
// Zeichne an Position x,y einen Punkt mit 
// dem Farbwert jr
   g.drawLine(x,y,x+1,y);} 
/* Neue Werte für die Iteration*/
      repart=repart+restep;}  
   impart=impart+imstep;}
     }
  }

Mit den nicht auskommentierten Grundeinstellungen bekommen wir ein gelbes Apfelmännchen mit schwarzem Innenbereich und blauer Hintergrundfarbe (am rechten und unteren Rand zu sehen).

Abbildung 9.15:  Das berühmte Apfelmännchen

Wenn wir ein bisschen mit den Anfangswerten und dem Hintergrund experimentieren, bekommen wir ein gänzlich verschiedenes Farbspektrum.

Abbildung 9.16:  Sämtliche Farben bewegen sich im helleren Farbspektrum.

9.3.5 Abrufen von Farbinformationen

Wenn Sie eine Farbe haben, können Sie sowohl die Rot-, Grün- und Blau-Werte einzeln ermitteln, als auch die Farbangabe als Ganzes oder die Vordergrund- und Hintergrundfarben. Dazu dienen die folgenden Methoden:

  • Die Methode public int getRed() liefert den Rotanteil einer Farbe als int-Wert zurück.
  • Die Methode public int getGreen() liefert den Grünanteil einer Farbe als int-Wert zurück.
  • Die public int getBlue()-Methode liefert den Blauanteil einer Farbe als int-Wert zurück.
  • Die Methode public static Color getColor(String nm) ermittelt die aktuelle Grafikfarbe und gibt ein entsprechendes Farbobjekt zurück.
  • Die Methode public Color getBackground() ermittelt die aktuelle Hintergrundfarbe und gibt ein entsprechendes Farbobjekt zurück.
  • Die Methode public Color getForeground() ermittelt die aktuelle Vordergrundfarbe und gibt ein entsprechendes Farbobjekt zurück.

9.3.6 Textausgabe über den Zeichnen-Modus

Die Graphics-Klasse enthält diverse Methoden, um Textzeichen und Zeichenketten sowie allgemeine Zeichen zu zeichnen. Wir kennen ja bereits die drawString()-Methode. Um nun mit der Graphics-Klasse Text auszugeben, rufen Sie in der Regel

public abstract void drawString(String str, int x, int y) 

auf und übergeben die Zeichenkette, die Sie zeichnen wollen, samt der x- und y-Koordinaten für den Anfang der darzustellenden Zeichen. Die Koordinaten definieren genau genommen den Beginn einer gedachten Grundlinie, auf der der darzustellende Text aufsitzt.

Diese Methode ist allerdings nur zur Ausgabe von Strings zu verwenden. Wenn primitive Datentypen ausgeben werden sollen, kann man damit nicht (direkt) arbeiten. Entweder, diese Datentypen werden in Strings konvertiert (beispielsweise durch Verknüpfung mit einem String über + oder Anwendung von Wrappern) oder man muss auf eine der Schwester-Methoden zurückgreifen, die dafür bereitgestellt werden:

drawBytes(byte[], int, int, int, int) 
drawChars(char[], int, int, int, int)

  • Der erste Parameter bezeichnet dabei eine Reihe von Zeichen/Byte.
  • Der zweite Parameter - der offset-Parameter - bezieht sich auf die Position des ersten Zeichens oder Bytes in dem zu zeichnenden Datenfeld. Diese wird oft Null sein, da Sie gewöhnlich vom Anfang des Datenbereiches an zeichnen wollen.
  • Der dritte Parameter ist eine Ganzzahl für die Position des letzten zu zeichnenden Zeichens.
  • Parameter 4 und 5 sind die bekannten Anfangskoordinaten.

Zusätzlich spielen die Font-Klasse und die FontMetrics-Klasse beim Textzeichnen eine wichtige Rolle. Die Font-Klasse stellt bestimmte Fonts dar (Name, Stil, Punktgröße), während FontMetrics Informationen über den Font enthält wie die tatsächliche Höhe und Breite eines bestimmten Zeichens.

9.3.7 Erstellen von Fontobjekten

Um Text auf dem Bildschirm ausgeben zu können, ist ein sinnvoller Weg, dass Sie zuerst eine Instanz der Klasse Font erstellen. Fontobjekte stellen einen einzelnen Font dar. Fontnamen sind Zeichenketten, die die Familie der Schrift bezeichnen (Arial, TimesRoman usw.). Fontstile sind Konstanten, die in der Font-Klasse definiert sind und die Sie über die übliche Punktnotation ansprechen können. Die Punktgröße ist die Größe der betreffenden Schriftart entsprechend der im Schrifttyp enthaltenen Definition. Wenn Sie eine Schrift auswählen, müssen Sie die Punktgröße der Schrift angeben. Die Punktgröße ist ein Begriff aus dem Druckwesen, der sich auf die Größe der Schrift bezieht. Es gibt 100 Punkte pro Inch (dots per inch = dpi) bei einem Drucker, diese Größe gilt aber nicht zwangsläufig auch für den Bildschirm (Stichwort: verschiedene Bildschirmauflösungen). Ein typischer Punktgrößenwert für Drucktext ist 12 oder 14. Die Punktgröße zeigt nicht die Anzahl der Pixel in der Höhe oder der Breite an. Es handelt sich vielmehr um einen vergleichenden Begriff. Eine Punktgröße von 24 ist doppelt so groß wie die von 12.

Um nun ein Fontobjekt zu erstellen, verwenden Sie die drei beschriebenen Fonteigenschaften als Argumente:

  • den Schriftnamen
  • die Schriftformatierung
  • die Punktgröße

Es gilt folgende Syntax:

Font f = new Font(<Fontname>,<Fontstil>,<Punktgröße>)

Sie können in Java aus einer Vielzahl verschiedener Schriften auswählen. Es handelt sich um die auf den meisten Plattformen unterstützten Zeichensätzen.

Sie können, wie wir schon gesehen haben, nicht nur zwischen verschiedenen Schriften auswählen, Sie können ebenso mehrere Schriftformatierungen auswählen. Beispielsweise Font.PLAIN (normale Schrift), Font.BOLD (fette Schrift) und Font.ITALIC (kursive Schrift). Diese Formatierungen können auch kombiniert werden, sodass Sie eine Schrift fett und kursiv formatieren können: Font.BOLD + Font.ITALIC.

Die Konstanten repräsentieren so angeordnete int-Werte so, dass eine Addition einen neuen int-Wert ergibt, der eindeutig ist und sich nur aus dieser Kombination gegeben kann.

Die folgende Deklaration erstellt eine Times-Roman-Schrift, die fett und kursiv ist und eine Punktgröße von 12 hat:

Font myFont = new Font("TimesRoman", Font.BOLD + Font.ITALIC, 12);

Das explizite Setzen eines Schrifttyps erfolgt mit der folgenden Methode:

public abstract void setFont(Font font)

Hier ist ein etwas umfangreicheres Beispiel.

import java.awt.Font;
import java.awt.Graphics;
public class FontTest extends java.applet.Applet {
 public void paint(Graphics g) {
 Font f = new Font("TimesRoman", Font.PLAIN, 18);
 Font fb = new Font("TimesRoman", Font.BOLD, 18);
 Font fi = 
  new Font("TimesRoman", Font.ITALIC, 18);
 Font fbi = new Font("TimesRoman", 
  Font.BOLD + Font.ITALIC, 18);
 Font f2 = new Font("Arial", Font.PLAIN, 10);
 Font f2b = new Font("Courier", Font.BOLD, 12);
 Font f2i = new Font("Arial", Font.ITALIC, 34);
 Font f22i = 
  new Font("TimesRoman", Font.ITALIC, 20);
 g.setFont(f);
 g.drawString("Hier haben wir einen normalen (plain) Font - TimesRoman", 10, 25);
 g.setFont(fb);
 g.drawString("Hier haben wir einen fetten (bold) Font - TimesRoman", 10, 50);
 g.setFont(fi);
 g.drawString("Dies ist ein kursiver (italic) Font - TimesRoman", 10, 75);
 g.setFont(fbi);
 g.drawString("Hier haben wir einen fetten und kursiven (bold italic) Font - TimesRoman", 10, 
100);
 g.setFont(f2);
 g.drawString("Hier haben wir einen wieder normalen (plain) Font in Schriftgröße 10 - Arial", 
10, 125);
 g.setFont(f2b);
 g.drawString("Hier haben wir einen bold Font in Schriftgröße 12 - Courier", 10, 150);
 g.setFont(f2i);
 g.drawString("Groß und kursiv - Arial", 10, 200);
 g.setFont(f22i);
 g.drawString("Hier haben wir einen italic Font in Schriftgröße 20 - TimesRoman", 10, 225);
 }  }

Abbildung 9.17:  Verschiedene Fonts

Bei Bedarf kann man mit der folgenden Methode das Font-Objekt abfragen:

public abstract Font getFont()

9.3.8 Abfragen der zur Verfügung stehenden Fonts

Mit einer speziellen Methode der Toolkit-Klasse können Sie die auf einem Computersystem zur Verfügung stehenden Fonts abfragen. Damit stellen Sie beispielsweise sicher, dass Sie keine Fonts verwenden, die auf der Zielplattform nicht vorhanden sind. Für einen solchen Fall können Sie einen Default-Font setzen, der auf jeder Plattform zur Verfügung steht (etwa Courier). Die Methode

public abstract String[] getFontList()

gibt ein Datenfeld zurück, das die Namen der verfügbaren Schriften enthält.

Beispiel:

fontList = getToolkit().getFontList();

Es kann allerdings sein, dass diese Methode nicht ganz zuverlässig arbeitet. Außerdem sollten Sie beachten, dass die folgenden Fontnamen mittlerweile als deprecated gelten und durch die zweitgenannten ersetzt werden sollten:

  • TimesRoman (neu Serif)
  • Helvetica (neu SansSerif)
  • Courier (neu Monospaced)

9.3.9 Informationen über einen speziellen Font abfragen

Sie können bei Bedarf in Java über Fonts und Fontobjekte anhand einfacher Methoden Informationen abfragen, die auf alle Graphics- und Fontobjekte anzuwenden sind.

Einige Methoden sollen hier dem Namen nach aufgelistet werden. Sie sind mit einer Ausnahme alle aus der Font-Klasse. Nur die bereits erwähnte Methode getFont() gehört zur Graphics-Klasse.

Methode Beschreibung
getFont() Ausgabe des gesetzten Fontobjekts.
getName() Ausgabe des Namens von dem Font als Zeichenkette.
getSize() Ausgabe der Größe des aktuellen Fonts als Ganzzahl.
getStyle() Der Stil des aktuellen Fonts als Ganzzahl. 0 = normal 1 = fett 2 = kursiv 3 = fett + kursiv
isPlain() Rückgabe true oder false, wenn der Fontstil normal ist oder nicht.
isBold() Rückgabe true oder false, wenn der Fontstil fett ist oder nicht.
isItalic() Rückgabe true oder false, wenn der Fontstil kursiv ist oder nicht.

Tabelle 9.1:   Abfrage von Font-Details

Wenn Sie mehr Informationen über den aktuellen Font wünschen, können Sie diverse Methoden in der Klasse FontMetrics nutzen. Dazu erstellen Sie am besten ein FontMetrics-Objekt und werten dieses dann mittels dieser FontMetrics-Methoden aus. Ihnen stehen dazu beispielsweise die folgenden Methoden zur Verfügung, die wir hier nur mit Namen angeben:

Methode Beschreibung
getAscent() Ausgabe der Entfernung zwischen der Grundlinie und der oberen Grenze eines Buchstabens (des so genannten Aufstrichs).
getDescent() Ausgabe der Entfernung zwischen der Grundlinie und der unteren Grenze eines Buchstabens (des so genannten Abstrichs). Etwa beim q oder y.
getLeading() Ausgabe der Zeilenhöhe, d.h. des Abstands zwischen dem Abstrich der oberen und dem Aufstrich der -darunter befindlichen Zeile.
getHeight() Ausgabe der Gesamthöhe der Schrift, also der Summe von Aufstrich, Zeilenabstand und Abstrich.
stringWidth() Die volle Breite der Zeichenkette in Pixel.
charWidth() Die Breite eines bestimmten Zeichens.

Tabelle 9.2:   Weitergehende Informationen zu Fonts

9.4 Die Java-Zeichenmodi

Die Graphics-Klasse verfügt über zwei verschiedene Modi, um Figuren zu zeichnen:

  • den paint-Modus
  • den XOR-Modus

9.4.1 Der Paint-Modus

Das Arbeiten im Paint-Modus bedeutet, dass eine neue Zeichnung alle Punkte einer bereits dort vorhandenen Abbildung überschreiben. Dies muss aber nicht zwingend so sein. Es gibt noch einen weiteren Zeichenmodus namens XOR, was für »eXclusive OR« steht.

9.4.2 Der XOR-Zeichenmodus

Den XOR-Zeichenmodus kombiniert das Pixel, das Sie zeichnen möchten, und das Pixel, das sich an der Stelle auf dem Bildschirm befindet, an der Sie zeichnen möchten. Wenn Sie ein weißes Pixel an einer Stelle zeichnen möchten, wo sich gegenwärtig ein schwarzes Pixel befindet, werden Sie dort ein weißes Pixel zeichnen. Wenn Sie ein weißes Pixel an einer Stelle zeichnen möchten, wo bereits ein weißes Pixel ist, werden Sie ein schwarzes Pixel zeichnen.

Dies wird Sie vielleicht überraschen, aber denken Sie bitte einmal an die bitweisen Operatoren zurück. Auch dort hatten wir das Umkehren von Bits schon vorgefunden. Hier ist nun eine praktische Anwendung. In frühen Animationen (etwa auf Schwarz-Weiß-Basis) hat man diese Technik viel genutzt.

Um den Zeichenmodus in den XOR-Modus zu verändern, rufen Sie den setXOR-Modus auf und geben Sie an ihn die Farbe weiter, die Sie als XOR-Farbe verwenden wollen. Dazu dient die Methode

public abstract void setXORMode(Color c1).

Beispiel:

public void paint(Graphics g) {
g.setXORMode(getBackground());
g.fillRect(40, 10, 40, 40);
g.fillOval(0, 0, 30, 30);
}

9.5 Zeichnen von Bildern

Wir sind nun bei einem ganz spannenden Abschnitt der Java-Grafikfähigkeiten angelangt. Dabei besteht der Vorgang aus zwei getrennten Schritten:

  • Laden von Bildern
  • Ausgeben von Bildern

9.5.1 Laden von Bildern - getImage()

Um ein Bild anzeigen zu können, müssen Sie sich zuerst das Bild von irgendwo holen. Irgendwo kann ein lokaler Rechner, aber ebenso das Netz sein. Dies wird nicht von der Graphics-Klasse erledigt, sondern beispielsweise mit der Methode

public Image getImage(URL url),

die zur Applet-Klasse gehört. Wir haben diese Methode auch schon im Rahmen eines Beispiels im Kapitel über die Applet-Erstellung verwendet. Die Methode lädt ein Bild und erstellt automatisch eine Instanz der Image-Klasse. Um sie zu verwenden, brauchen Sie nur der Methode die Adresse (URL) des Bildes übergeben. Von dieser Methode gibt es diverse Varianten - sogar in der Applet-Klasse. Einmal mit einem Argument (ein Objekt vom Typ URL) und einmal mit zwei Argumenten (ein Objekt vom Typ URL und eine Zeichenkette, die den Pfad oder Dateinamen des Bildes in Bezug zu einer Basis enthält).

Die einfachere Variante mit einem Argument entspricht einer hartcodierten Pfadangabe auf das Bild und ist deshalb ziemlich unflexibel. Wenn beispielsweise das Projekt in ein anderes Verzeichnis kopiert wird oder sich das Projektverzeichnis sonst ändert, muss das Projekt neu kompiliert werden.

Die zweite Variante mit den zwei Argumenten lässt sich in Verbindung mit zwei weiteren Methoden der Applet-Klasse äußerst anpassungsfähig einsetzen, die wir bereits kennen:

  • Die Methode getDocumentBase() zur Rückgabe eines Objekts vom Typ URL, das das Verzeichnis der HTML-Datei enthält, die das Applet eingebunden hat.
  • Die Methode getCodeBase() zur Rückgabe der URL des Applets.

Wir können nun getImage() zum Holen von Bildern sehr flexibel einsetzen.

Beispiele:

Image bild = getImage(getDocumentBase(), "Bild1.gif")
Image bild = getImage(getCodeBase(), "Bild2.gif")

Das erste Beispiel lädt das Bild aus dem Verzeichnis, wo sich die HTML-Datei befindet, das zweite Beispiel sucht das Bild in dem Verzeichnis, wo das Applet platziert ist.

Kann Java eine angegebene Datei nicht finden, gibt getImage() den Wert null aus. Dies ist im Prinzip kein Problem, das Programm läuft weiter, es wird nur kein Bild angezeigt.

9.5.2 Anzeigen von Bildern - drawImage()

Die Graphics-Klasse ermöglicht es, Bilder mit der drawImage()-Methode darzustellen. Wir haben diese Methode schon verwendet. Auch drawImage() gibt es in mehreren Varianten:

boolean drawImage(Image img, int x, int y, ImageObserver observer)
boolean drawImage(Image img, int x, int y, int width, int height, ImageObserver observer)

  • Der erste Parameter in beiden Varianten ist das Bild selbst.
  • Parameter 2 und 3 sind die Koordinatenangaben für die linke obere Ecke des Anzeigebereichs.
  • Der observer-Parameter hat die Aufgabe, zu beobachten, wann das Bild gezeichnet werden kann. Wenn Sie drawImage() innerhalb eines Applets aufrufen, können Sie dieses selbst als Observer (mit dem this-Operator) übergeben, da die Applet-Klasse die ImageObserver-Schnittstelle eingebaut hat. Dazu gleich noch etwas mehr.
  • Die Parameter width und height in der zweiten Variante dienen dazu, die Breite und die Höhe des Kastens festzulegen, in dem das Bild gezeichnet wird. Sind die Argumente für diese Größenangaben der Bildbox kleiner oder größer als das Bild selbst, wird das Bild automatisch skaliert, d.h. es wird an die Größe der Bildbox angepasst. Man kann diese Parameter zu einer bewussten Verzerrung von Bildern nutzen, um damit Effekte zu erzielen.

Beispiel:

g.drawImage(Bild1 ,10 , 10, this);

Erstellen wir ein vollständiges Programmbeispiel.

import java.awt.Image;
import java.awt.Graphics;
public class DrawImage2 extends java.applet.Applet {
 Image samImage; 
 Image bild;
 public void init() {
// Bild laden - ein jpg-File
  samImage = getImage(getDocumentBase(), "images/Safety.jpg"); 
 }
 public void paint(Graphics g) {
  g.drawImage(samImage, 0, 0, this); 
 }  }

Abbildung 9.18:  Für die Darstellung eines Fotos ist das JPG/JPEG-Format besser als das GIF-Format.

Wenn man Verzerrungen bei der Verwendung der zweiten Variante vermeiden möchte, stellt die Image-Klasse zwei Methoden zur Verfügung, die die tatsächliche Größe des zu ladenden Bildes herausfinden können. Damit kann man beispielsweise über einen prozentualen Wert der Breite und Höhe das Bild verkleinern oder vergrößern. Es handelt sich um die Methoden

public abstract int getWidth(ImageObserver observer)

und

public abstract int getHeight(ImageObserver observer),

die in dieser Variante als Argument eine Instanz von dem Imageobserver (meist this) besitzen.

Bei animierten GIFs werden diese Methoden unter Umständen nicht zuverlässig sein, denn dort können sich einzelne Sequenzen in der Größe unterscheiden.
/* Laden und Zeichnen eines Bildes, das in mehrfacher Hinsicht verkleinert und vergößert 
sowie verzerrt und überlagert wird */
import java.awt.Image;
import java.awt.Graphics;
public class DrawImage3 extends java.applet.Applet {
 Image bild;
 public void init() {
// Bild laden
  bild = getImage(getCodeBase(), "images/kuerb.gif");
 }
 public void paint(Graphics g) {
  int bildbreite = bild.getWidth(this);
  int bildhoehe = bild.getHeight(this);
  int xpos = 10;
  int ypos = 10;
  g.drawImage(bild, xpos, ypos, bildbreite , bildhoehe, this);
   xpos += bildbreite + 10;
   ypos += bildhoehe + 10;
   g.drawImage(bild, xpos , ypos, bildbreite / 2, bildhoehe / 2, this);
   xpos += bildbreite + 10;
   ypos += bildhoehe + 10;
   g.drawImage(bild, xpos , ypos, bildbreite * 3, bildhoehe * 3, this);
   xpos += (bildbreite * 3) + 10;
   g.drawImage(bild, xpos , ypos, bildbreite * 3, bildhoehe , this);
   xpos += bildbreite + 10;
   ypos += bildhoehe + 10;
   g.drawImage(bild, xpos , ypos, bildbreite, bildhoehe, this);
   g.drawImage(bild, xpos + 20 , ypos + 10, bildbreite, bildhoehe, this);
   g.drawImage(bild, xpos + 50 , ypos + 20, bildbreite, bildhoehe, this);
  }  }

Abbildung 9.19:  Verschiedene Darstellungen desselben Bild s

9.5.3 Der Imageobserver und der MediaTacker

Wollen wir uns noch ein bisschen näher mit dem Imageobserver beschäftigen. Dieser hat die Aufgabe, zu beobachten, wann das Bild gezeichnet werden kann. Aber was bedeutet das eigentlich?

Es kann (und wird in der Internet-Praxis) häufig vorkommen, dass zu ladende Bilder über das Netzwerk sehr langsam transportiert werden. Da wir Multithreading zur Verfügung haben, kann es passieren, dass Sie anfangen, das Bild zu zeichnen, obwohl es möglicherweise noch nicht vollständig angekommen ist. Um zu sehen, ob ein Bild schon bereit ist, um angezeigt zu werden, steht Ihnen die Hilfsklasse MediaTracker zur Verfügung. Um einen solchen MediaTracker zu verwenden, müssen Sie zuerst einen für Ihr Applet erzeugen. Das ist mit der folgenden Syntax möglich:

MediaTracker myTracker = new MediaTracker(this);

Wenn Sie im nächsten Schritt das Bild laden, verwenden Sie den getImage()-Befehl. Beispielsweise:

Image myImage = getImage("Image1.gif");

Um den MediaTracker nun anzuweisen, das Bild zu beobachten, übergeben Sie dem MediaTracker das Bild über eine numerische ID. Diese ID kann für mehrere Bilder verwendet werden. Sie können so mit einer einzigen ID sehen, ob eine ganze Gruppe von Bildern zur Anzeige bereit ist. Im einfachsten Fall können Sie einem Bild die ID Null geben:

myTracker.addImage(myImage, 0);

Damit ist das Bild dem MediaTracker zum Beobachten zugewiesen. Wenn Sie einmal begonnen haben, ein Bild zu suchen, können sie es laden und mit der Methode

public void waitForID(int id) throws InterruptedException

warten, bis es fertig geladen ist:

myTracker.waitForID(0);

Sie können auch mit der Methode

public void waitForAll() throws InterruptedException

auf alle Bilder warten:

myTracker.waitForAll();

Sie wollen vielleicht nicht die ganze Zeit warten, bis ein Bild geladen ist, bevor Ihr Applet startet. Sie können dann mit der Methode

public int statusID(int id, boolean load)

bereits anfangen zu laden. Wenn Sie statusID() aufrufen, übergeben Sie die ID, für die Sie einen Status haben wollen, und einen booleschen Operator, der angibt, ob der Ladevorgang für das Bild starten soll oder nicht. Wenn Sie true übergeben, wird das Bild geladen:

myTracker.statusID(0, true);

In Verbindung mit statusID() steht

public int statusAll(boolean load),

das den Status aller Bilder im MediaTracker überprüft:

myTracker.statusAll(true);

Die statusID()- und die statusAll()-Methoden geben einen int-Wert zurück, der aus den folgenden Flags erstellt wird:

  • MediaTracker.ABORTED, wenn das Laden von Bildern abgebrochen wurde.
  • MediaTracker.COMPLETE, wenn Bilder komplett geladen wurden.
  • MediaTracker.LOADING, wenn sich Bilder noch im Prozess des Ladens befinden.
  • MediaTracker.ERRORED, wenn ein Fehler beim Laden der Bilder aufgetreten ist.

Sie können auch mittels checkID() und checkAll() überprüfen, ob ein Bild vollends geladen worden ist. Alle Variationen von checkAll() und checkID() geben einen booleschen Wert zurück, der true ist, wenn alle überprüften Bilder geladen worden sind. Folgende Varianten sind von Bedeutung:

  • Die Version boolean checkID(int id) gibt den Wert true zurück, wenn das Bild mit einer spezifischen ID geladen worden ist. Sie startet den Ladevorgang nicht, wenn das Bild nicht schon geladen wird.
  • Die Version boolean checkID(int id, boolean startLoading) wird true zurückgegeben, wenn das Bild mit einer spezifischen ID geladen worden ist. Wenn startLoading true ist, wird es den Ladevorgang für das Bild starten, das noch nicht geladen worden ist.
  • Die Methode boolean checkAll() wird true zurückgeben, wenn alle Bilder, die der MediaTracker überprüft, geladen worden sind. Der Ladevorgang wird nicht gestartet, wenn ein Bild nicht schon geladen wird.
  • In der Version boolean checkAll(boolean startLoading) wird true zurückgegeben, wenn alle Bilder, die der MediaTracker überprüft, geladen worden sind. Wenn startLoading den Wert true hat, wird es alle Bilder laden, deren Ladevorgang noch nicht gestartet worden ist.

Wir werden in dem am Ende des Kapitels folgenden Applet einen MediaTracker verwenden, der einen Ladevorgang bei einer Animation überwacht. Vorher wollen wir noch einige Animationstechniken durchsprechen.

9.6 Animationen

Unter einer Animation versteht man erst einmal nur ein Aneinanderreihen von Bildern, die meist so schnell angezeigt werden, dass für das menschliche Auge der Eindruck von Bewegung entsteht.

Eine Animation umfasst in Java zwei wesentliche Schritte:

  • Aufbau eines Animationsrahmens
  • Abspielen der Animation

9.6.1 Aufbau eines Animationsrahmens

Hierunter ist alles zu verstehen, was die Animation vorbereitet:

  • Ermitteln der Größe des Ausgabebereichs
  • Farbeinstellungen
  • Positionieren der Animation
  • Erstellen oder Laden der einzelnen Animationsbilder
  • Aufbau von einzelnen Animationssequenzen

Außer dem letzten Punkt ist nichts dabei, was neu für uns ist. Und auch der letzte Punkt der Aufzählung lässt sich als das Zusammenfassen von einzelnen Bildern in Gruppen leicht erklären.

Das Abspielen einer Animation ist jedoch wirklich neu und damit werden wir uns jetzt beschäftigen.

9.6.2 Abspielen einer Animation

Eine Animation kann ganz einfach aufgebaut sein. Wenn wir beispielsweise die Position eines Bildes in kleinen Schritten über den Ausgabebereich verschieben, haben wir bereits eine - zugegeben nicht sonderlich innovative - Animation. Wenn wir dabei zusätzlich die Größe noch (geringfügig, sonst wird es zu viel ruckeln) verändern (größer, wenn sich das Objekt auf den Betrachter hin bewegen soll und kleiner, wenn sich das Objekt von dem Betrachter weg bewegen soll), wird die Sache schon etwas spannender. Bauen wir ein Applet entsprechend um.

Die Animationen verwenden Bilder, die sich innerhalb des Unterverzeichnisses images befinden.
import java.awt.Image;
import java.awt.Graphics;
public class Animation1 extends java.applet.Applet {
 Image bild;
 public void init() {
// Bild laden
  bild = getImage(getCodeBase(), "images/kuerb.gif");
  resize(600, 200);
 }
 public void paint(Graphics g) {
  for (int i=0; i < 1000; i++) {
    int bildbreite = bild.getWidth(this);
    int bildhoehe = bild.getHeight(this);
    int xpos = 10;  // Startposition X
    int ypos = 10;  // Startposition Y
    g.drawImage(bild, (int)(xpos + (i/2)) , (int)(ypos + (i/10)), (int)(bildbreite * (1 + (i/
1000))),(int) (bildhoehe * (1 + (1/1000))), this);
   }
 }  }

Wenn Sie das Applet laufen lassen, werden Sie sehen, dass sich der Kürbis von links nach rechts über den Bildschirm bewegt. Allerdings werden Sie gleich bemerken, dass sich dabei ein paar Effekte einstellen, die wahrscheinlich so nicht gewünscht sind. Das Flimmern werden wir später betrachten, zuerst kümmern wir uns darum, die jetzt noch gezogene Spur zu beseitigen. Wie das geht, wissen Sie bereits. Wir benötigen die repaint()-Methode.

Abbildung 9.20:  Immerhin bewegt sich was.

Allerdings werden Sie beim Umbau des Applets schnell bemerken, dass der Aufruf der repaint()-Methode nirgends so richtig reinpasst. Wir müssen offensichtlich noch mehr tun.

Der erste Schritt könnte sein, die for-Schleife in die Startmethode zu verlegen. Zusätzlich müssen dann die Variablen bildbreite, bildhoehe, xpos und ypos aus der paint()-Methode an zentrale Stellen verlagert werden.

Der zweite Denkansatz ist okay, der erste nicht. Zwar kann man das Applet tatsächlich so umkonstruieren. Der Aufruf der repaint()-Methode innerhalb der for-Schleife wird jedoch enttäuschend sein.

Die Lösung liegt auf der Hand. Wir müssen zum einen die for-Schleife abarbeiten lassen, zum anderen während jedes Schleifendurchgangs über die repaint()-Methode eine Ausgabe aufrufen. Das sind zwei Vorgänge, die quasi gleichzeitig ablaufen. Und wie nennt man sowas? Multithreading. Wir haben hier eine klassische Anwendung von Multithreading. Im Prinzip ist jede Animation nur als Multithreading-Anwendung vernünftig zu konstruieren.

Machen wir also unser Applet Multithreading-fähig und verlagern die for-Schleife in die dann notwendige run()-Methode.

import java.awt.Image;
import java.awt.Graphics;
public class Animation2 extends java.applet.Applet implements Runnable {
 Image bild;
 int bildbreite;
 int bildhoehe;
 int xpos = 10;  // Startposition X
 int ypos = 10;  // Startposition Y
 Thread MeinThread;
 public void init() {
// Bild laden
   bild = getImage(getCodeBase(), "images/kuerb.gif");
   bildbreite = bild.getWidth(this);
   bildhoehe = bild.getHeight(this);
  }
  public void paint(Graphics g) {
    g.drawImage(bild, xpos, ypos ,bildbreite,bildhoehe, this);
   }
  public void run() {
   for (int i=0; i < 1000; i++) {
     xpos = (int)(xpos + (i/4));
     ypos=(int)(ypos + (i/10));
     bildbreite = (int)(bildbreite * (1 + (i/500)));
     bildhoehe= (int) (bildhoehe * (1 + (1/500)));
     repaint();
     pause(100);
 }  }
  public void start() {
   if (MeinThread == null) {
     MeinThread = new Thread(this);
     MeinThread.start();
 }  }
  public void stop() {
   if (MeinThread != null) {
     MeinThread.stop();
     MeinThread = null;
 }  }
 void pause(int time) {
  try { 
   Thread.sleep(time); 
  }
  catch (InterruptedException e) { 
  }
 }  }

Abbildung 9.21:  Der Kürbis läuft von links oben nach rechts unten.

Sie werden sehen, dass hier schon eine recht vernünftige Bewegung zu erkennen ist. Das Bild wird übrigens zeilenweise verschoben, wie in der nächsten Abbildung einer Animation mit einer anderen Grafik recht gut zu erkennen ist.

Abbildung 9.22:  Die Momentaufnahme macht den Aufbau der Bild verlagerung gut deutlich.

Der Appletviewer des JDK ab der Version 1.2 macht bei der Darstellung der und den folgenden Animationen unter Umständen Schwierigkeiten. Sie müssen unter Umständen den Reload-Befehl auslösen, bevor Sie etwas sehen. Ansonsten läuft die Animation aber in allen anderen Referenzdarstellungsmedien. Sowohl in verschiedenen Browsern, als auch Appletviewern der Vorgängerversionen des JDK gibt es keine Probleme. Zwar verwenden wir mit der Methode stop() für die Threads eine als deprecated gekennzeichnete Methode, aber dies sollte keine Rolle spielen.

Erweitern wir das Applet jetzt noch ein wenig und wechseln gleichzeitig die verwendete Grafik (eine Erdkugel). Wir lassen die Erdkugel vom rechten Rand wieder zurückkommen und platzieren sie dann zurück an die Orginalposition. Die ganze Aktion packen wir in eine Endlos-Schleife und haben damit eine permanent laufende Animation. Durch die Multithreading-Fähigkeit des Applets brauchen wir uns keine Sorgen zu machen, die Endlosschleifen nicht unterbrechen zu können (eigentlich bietet die Appletklasse schon genug Abbruchmöglichkeiten, aber wir wollen sauber arbeiten).

import java.awt.*;
import java.util.*;
public class Animation3 extends java.applet.Applet implements Runnable 
 Image bild;
 int bildbreite;
 int bildhoehe;
 int bildbreite_anfang;
 int bildhoehe_anfang;
 int xpos = 10;  // Startposition X
 int ypos = 10;  // Startposition Y
 Thread MeinThread;
 public void init() {
// Bild laden
   bild = getImage(getCodeBase(), "images/img0001.gif");
   bildbreite = bild.getWidth(this);
   bildhoehe = bild.getHeight(this);
   bildbreite_anfang=bildbreite;
   bildhoehe_anfang=bildhoehe;
   setBackground(Color.cyan); // Hintergrundfarbe
   resize(850, 450);
  }
 public void paint(Graphics g) {
/* Wir verfolgen die Koordinaten der Erdkugel am Bildschirm über die Methode drawString mit. 
Dazu müssen wir diese vom Typ Interger in String konvertieren. Dies geschieht mit der 
toString-Methode.  */
   String stng = Integer.toString(xpos);
// Die Ausgabe der X-Koordinate
   g.drawString("X-Koordinate: " + stng, 350,20);  
   stng = Integer.toString(ypos);
// Die Ausgabe der Y-Koordinate
   g.drawString("Y-Koordinate: " + stng, 500,20);
// Bild-Ausgabe
   g.drawImage(bild, xpos, ypos ,bildbreite,bildhoehe, this);  
  }
 public void run() {
/* 4 lokale Variablen (eine davon als Konstante definiert), um den Rückweg der Erdkugel zu 
steuern. */
   double schrittX=0;
   final double schrittY =  1.172413793103;
   int umkehrX=0;
   int umkehrY=0;
   while (true)  // Beginn Endlosschleife
   {
   for (int i=0; i < 80; i++) {
// Laufe von links oben nach rechts unten
    xpos = (int)(xpos + (i/4));
    ypos=(int)(ypos + (i/10));
    bildbreite = (int)(bildbreite * (1 + (i/500)));
    bildhoehe= (int) (bildhoehe * (1 + (i/500)));
    repaint();
    pause(100);
   }  // Ende erste For-Schleife
// Nun kehre die vertikale Richtung um und laufe 
// zurück nach links, aber immer noch nach unten. 
// Die Animationsgeschwindigkeit wird
// beschleunigt durch kürzere Pausen.
   for (int i=0; i < 40; i++) {
   xpos = (int)(xpos - (i/4));
   ypos=(int)(ypos + (i/10));
   bildbreite = (int)(bildbreite * (1 - (i/500)));
   bildhoehe= (int) (bildhoehe * (1 - (i/500)));
   repaint();
   pause(25);
   }  // Ende zweite for-Schleife
/* Nun kehre die horizontale Richtung auch um und laufe zurück nach links oben. Dazu nehmen 
wir für die X-Koordinate die aktuelle Position und die Anfangskoordinaten und berechnen 
daraus die Schrittgrößen für einen Rückweg in 290 Einzelsequenzen. Die Y-Schritte sind vorher 
berechnet und als Konstante gesetzt (Verwendung von final). Die Pausen werden nochmal 
verkürzt, aber durch die höhere Anzahl der Einzelsequenzen verlangsamt sich die Bewegung der 
Erdkugel.*/
  umkehrX=xpos;
  umkehrY=ypos;
  schrittX = (xpos - 10) / 290 ;
  for (int i=0; i < 290; i++) {
   xpos = (int)(umkehrX - (i * schrittX));
   ypos=(int)(umkehrY- (i * schrittY));
   bildbreite = (int)(bildbreite * (1 - (i/500)));
   bildhoehe= (int) (bildhoehe * (1 - (i/500)));
   repaint();
   pause(10);
  }  // Ende dritte for-Schleife
/* Kleinere Nachjustierungen, um Rechenungenauigkeiten auszugleichen. Sehen tun Sie nichts 
davon, das geht hamonisch über. */
  xpos = 10;
  ypos = 10;
  bildbreite=bildbreite_anfang;
  bildhoehe=bildhoehe_anfang;
  repaint();
  }//Ende der while-Schleife  
  }
  public void start() {
    if (MeinThread == null) {
      MeinThread = new Thread(this);
      MeinThread.start();
 }  }
  public void stop() {
    if (MeinThread != null) {
      MeinThread.stop();
      MeinThread = null;
 }  }
  void pause(int time) {
   try { 
   Thread.sleep(time); 
   }
   catch (InterruptedException e) { 
 }  }
// Hier folgt die Mausaktion, mit der das Applet 
// gestoppt werden kann.
public boolean mouseDown(Event evt, int x, int y) {
  stop();
  return true;
 }  }

Abbildung 9.23:  Die Animation läuft solange, bis Sie die Maustaste klicken oder das Fenster schließen.

Die Erdkugel läuft zuerst von der linken oberen Ecke des Fensters an den rechten Rand (ungefähr die Mitte der Fensterhöhe), dann zurück nach links, jedoch immer noch nach unten, um dann wieder noch oben abzudrehen.

Die Geschwindigkeit der Bewegung verändert sich, da wir nicht mit gleich großen Verschiebungen in der einzelnen Bewegungssequenzen arbeiten.

Wenden wir uns noch einer anderen Art der Animation zu. Wir werden dabei nicht nur ein Bild verwenden, das über den Bildschirm bewegt wird, sondern eine Sequenz von Bildern, die durch nacheinander folgende Einblendungen den Eindruck von Bewegung vermitteln. Dabei soll es auf Mausaktionen (stoppen) reagieren, Multithreading-fähig sein, einen MediaTracker verwenden und als Hauptfunktion eine Reihe von Bildern laden und diese dann in Form einer Endlosschleife als bewegte Animation ausgeben. Der Source wird vollständig mit Kommentaren dokumentiert, sodass er sicher verständlich bleibt.

import java.awt.*;
import java.util.*;
public class Animation4 extends java.applet.Applet implements Runnable {
Image bild;
int bildbreite;
int bildhoehe;
int xpos;
int ypos;
// Nummer des aktuellen Bildes
private int AktuellesBild; 
// Das Array wird die zu ladenden Bilder aufnehmen
private Image m_Images[]; 
Thread MeinThread;
// Anzahl der zu ladenden Bilder
private final int ANZAHL_BILDER = 18; 
// Kontrollvariable, ob alle Bilder geladen wurden.
private boolean m_fAllLoaded = false; 
// Eine Instanz von Graphics, die für diverse 
// Ausgabeaktionen genutzt wird.
private Graphics ausgabe;
public void init() {
  setBackground(Color.green); // Hintergrundfarbe
  resize(250, 250);
 }
public void paint(Graphics g) {
// Solange die Bilder noch geladen werden 
// (m_fAllLoaded ist in der Zeit false)
// wird eine Meldung ausgegeben. Danach 
// (m_fAllLoaded ist dann true) 
// gibt paint das aktuelle Bild aus.
//----------------------------------
  if (!m_fAllLoaded) {
// Wenn die Testvariable, ob alle Bilder geladen 
// sind, false ist, wird abgebrochen
    g.drawString("Loading images...", 10, 20);
    return;
   }
// Bild-Ausgabe
   g.drawImage(bild, xpos, ypos ,bildbreite,bildhoehe, this); 
 }
 public void run() {
// Initialisierung der Anzahl der Bilder
   AktuellesBild = 0; 
/* Wenn die Webseite nach Verlassen (kein destroy, nur stop) wieder besucht wird, dann sind 
die Bilder immer noch geladen. Die Testvariable m_fAllLoaded ist also noch true. Andernfalls 
werden die Bilder geladen */
//--------------------------------------
   if (!m_fAllLoaded) {
     repaint();
     ausgabe = getGraphics();
// Das Array wird 18 Elemente haben
     m_Images = new Image[ANZAHL_BILDER]; 
/* Wir verwenden hier einen MediaTracker, um den Ladevorgang der Bilder zu verfolgen. */
     MediaTracker tracker = new MediaTracker(this);
     String strImage;
/* Dieses Mal laden wir die Bilder nicht in der init()-Methode, sondern in der run()-Methode. 
Für jedes Bild in der Animation wird zuerst ein String mit dem Pfad zu dem Bild konstruiert; 
danach werden die Bilder in das m_Images-Array geladen und damit dieses Array initialisiert. 
*/
//------------------------------------------
     for (int i = 1; i <= ANZAHL_BILDER; i++) {
       strImage = "images/img00" + ((i < 10) ? "0" : "") + i + ".gif";
       m_Images[i-1] = getImage(getDocumentBase(), strImage);
/* Das gerade geladene Bild wird dem MediaTracker als geladen gemeldet und steht für die 
Ausgabe damit bereit */
       tracker.addImage(m_Images[i-1], 0); 
      }
// Exception-Behandlung bei Ladefehlern.
     try  {
       tracker.waitForAll();
       m_fAllLoaded = !tracker.isErrorAny();
      }
     catch (InterruptedException e){
     }
// Fehlermeldung bei Ladefehler
  if (!m_fAllLoaded) {
   stop();
   ausgabe.drawString("Error loading images!", 10, 40);
    return;
    }
// Breite des aktuellen Bildes
   bildbreite = m_Images[0].getWidth(this); 
// Höhe des aktuellen Bildes
   bildhoehe = m_Images[0].getHeight(this);
   xpos=(size().width - bildbreite) / 2;
   ypos=(size().height - bildhoehe) / 2;
   }
  repaint();
  while (true) // Beginn Endlosschleife
   {
    try // Exceptionhandling
     {
      bild=m_Images[AktuellesBild];
      repaint(); // Zeige aktuelles Bild an
      AktuellesBild++; // Zähle Bildindex hoch
// Wenn Bildindex das letzte Bild erreicht hat
// setze den Zähler wieder auf 0
  if (AktuellesBild == 18) AktuellesBild = 0; 
/* Unterbrechung von 50 Millisekunden. Ganz wichtig, denn in dieser Zeit können andere 
Aktionen laufen*/
      Thread.sleep(50); 
   }
   catch (InterruptedException e) {
      stop();
   }
  }//Ende der while-Schleife
 }
 public void start() {
   if (MeinThread == null) {
     MeinThread = new Thread(this);
     MeinThread.start();
 }  }
 public void stop() {
   if (MeinThread != null) {
     MeinThread.stop();
     MeinThread = null;
 }  }
 void pause(int time) {
 try { 
   Thread.sleep(time); 
  }
 catch (InterruptedException e) { 
 }  }
// Hier folgt die Mausaktion, mit der das Applet 
// gestoppt werden kann.
 public boolean mouseDown(Event evt, int x, int y) {
   stop();
   return true;
  }  }

Abbildung 9.24:  Ein komplexeres Applet mit der Animation einer drehenden Erdkugel

Die Animation zeigt eine sich drehende Erdkugel, die auf einer Position verbleibt.

Ihnen werden sicher die Flimmereffekte aufgefallen sein, die die meisten der Animationen beeinflusst haben. Wir wollen deshalb unsere Animationstechnik ein wenig optimieren.

9.6.3 Flimmereffekte in Animationen reduzieren

Der Hauptgrund für das Flimmern bei Animationen liegt oft in der repaint()-Methode. Diese ruft vor der Ausführung der paint()-Methode die update()-Methode auf (die genau genommen erst die paint()-Methode anschließend aufruft) und da ist das Flimmerproblem häufig angesiedelt. Die update()-Methode leert in der Orginalform den ganzen Bildschirm (genauer - den Anzeigebereich des Applets). Die Teile des Bildschirms, die sich nicht ändern, werden danach schneller aufgebaut, als diejenigen, die neu gezeichnet werden müssen. Daraus resultiert der Flimmereffekt.

Um die update()-Methode daran zu hindern, den Bildschirm jedes Mal zu leeren, können wir sie überschreiben und erreichen damit in vielen Fällen eine Reduzierung des Flimmereffekts.

Überschreiben der update()-Methode

Bei Animationen, wo es nicht notwendig ist, den Bildschirm vor einer neuen Ausgabe zu leeren (dies ist dann der Fall, wenn sich keine grundlegenden Angaben wie die Farbe verändern oder Bereiche geleert werden müssen), führt die folgende kleine Aktion zu einer erheblichen Reduzierung des Flimmerns.

public void update(Graphics g) {
paint(g);
}

Mit dieser kleinen Veränderung wird der Bildschirm vor dem Aufruf der paint()-Methode überhaupt nicht mehr geleert.

Wir werden nun eine Animation damit so optimieren, dass das - ohne die überschriebene update()-Methode vorhandene - Flimmern fast vollständig verschwindet.

Die Animation mit der sich drehenden Erdkugel ist ein perfekter Kandidat. Bauen Sie dort die überschriebene update()-Methode ein und das Flimmern verschwindet fast vollständig. Die Lademeldung (»Error loading images!«) müssen Sie jedoch - falls sie stört - von Hand beseitigen, da sie in dem Beispiel außerhalb des neu aufgebauten Bildschirmbereichs steht.

Das Überschreiben der update()-Methode hat jedoch Grenzen, wie wir gleich sehen werden.

Setzen wir nun zwei unserer letzten Animationen zusammen und ergänzen sie etwas. Unter anderem wird auch die nachfolgend noch beschriebene Clipping-Technik verwendet.

import java.awt.*;
import java.util.*;
public class Animation5 extends java.applet.Applet implements Runnable {
Image bild;
int bildbreite;
int bildhoehe;
int bildbreite_anfang;
int bildhoehe_anfang;
int xpos = 10; // Startposition X
int ypos = 10; // Startposition Y
Thread MeinThread;
// Nummer des aktuellen Bildes
private int AktuellesBild; 
// Das Array wird die zu ladenden Bilder aufnehmen
private Image m_Images[]; 
// Anzahl der zu ladenden Bilder
private final int ANZAHL_BILDER = 18; 
// Kontrollvariable, ob alle Bilder geladen 
// wurden.
private boolean m_fAllLoaded = false; 
// Eine Instanz von Graphics, die für diverse 
// Ausgabeaktionen genutzt wird.
private Graphics ausgabe; 
public void init() {
setBackground(Color.cyan); // Hintergrundfarbe
resize(850, 450);
}
public void paint(Graphics g) {
/* Solange die Bilder noch geladen werden (m_fAllLoaded ist in der Zeit false), wird eine 
Meldung ausgegeben. Danach (m_fAllLoaded ist dann true) gibt paint das aktuelle Bild aus. */
//-------------------------------------------
/* Wenn die Testvariable, ob alle Bilder geladen sind, false ist, wird abgebrochen */
if (!m_fAllLoaded) {
g.drawString("Loading images...", 10, 20);
return;
}
g.clipRect(xpos ,ypos,bildbreite,bildhoehe);
// Bild-Ausgabe
g.drawImage(bild, xpos, ypos ,bildbreite,bildhoehe, this); 
/* Wir verfolgen die Koordinaten der Erdkugel am Bildschirm über die Methode drawString mit. 
Dazu müssen wir diese vom Typ Interger in String konvertieren. Dies geschieht mit der 
toString-Methode. */
g.clipRect(250 ,0,700,50);
String stng = Integer.toString(xpos);
// Die Ausgabe der X-Koordinate
g.drawString("X-Koordinate: " + stng, 350,20); 
stng = Integer.toString(ypos);
// Die Ausgabe der Y-Koordinate
g.drawString("Y-Koordinate: " + stng, 500,20);
stng = Integer.toString(AktuellesBild);
// Die Ausgabe der Bild-Nr.
g.drawString("Bild-Nr.: " + stng, 250,20); 
}
public void run() {
/* 4 lokale Variablen (eine davon als Konstante definiert), um den Rückweg der Erdkugel zu 
steuern. */
double schrittX=0;
final double schrittY = 1.172413793103;
int umkehrX=0;
int umkehrY=0;
// Initialisierung der Anzahl der Bilder
AktuellesBild = 0; 
/* Wenn die Webseite nach Verlassen (kein destroy, nur stop) wieder besucht wird, dann sind 
die Bilder immer noch geladen. Die Testvariable m_fAllLoaded ist also noch true. Andernfalls 
werden die Bilder geladen */
//----------------------------------
 if (!m_fAllLoaded) {
 repaint();
 ausgabe = getGraphics();
// Das Array wird 18 Elemente haben
 m_Images = new Image[ANZAHL_BILDER]; 
/* Wir verwenden hier einen MediaTracker, um den Ladevorgang der Bilder zu verfolgen. */
 MediaTracker tracker = new MediaTracker(this);
 String strImage;
/* Dieses Mal laden wir die Bilder nicht in der init()-Methode, sondern in der run()-Methode. 
Für jedes Bild in der Animation wird zuerst ein String mit dem Pfad zu dem Bild konstruiert; 
danach werden die Bilder in das m_Images-Array geladen und damit dieses Array initialisiert. 
*/
 //----------------------------------
 for (int i = 1; i <= ANZAHL_BILDER; i++) {
 strImage = "images/img00" + ((i < 10) ? "0" : "") + i + ".gif";
 m_Images[i-1] = getImage(getDocumentBase(), strImage);
/* Das gerade geladene Bild wird dem MediaTracker als geladen gemeldet und steht für die 
Ausgabe damit bereit */
 tracker.addImage(m_Images[i-1], 0); 
 }
// Exception-Behandlung bei Ladefehlern.
try {
tracker.waitForAll();
m_fAllLoaded = !tracker.isErrorAny();
}
catch (InterruptedException e) {
}
if (!m_fAllLoaded) // Fehlermeldung bei Ladefehler
{
 stop();
 ausgabe.drawString("Error loading images!", 10, 40);
 return;
}
// Breite des aktuellen Bildes
bildbreite = m_Images[0].getWidth(this); 
// Höhe des aktuellen Bildes
bildhoehe = m_Images[0].getHeight(this);
bildbreite_anfang=bildbreite;
bildhoehe_anfang=bildhoehe;
}
repaint();
while (true) // Beginn Endlosschleife
{
 for (int i=0; i < 80; i++) {
// Zuweisung des jeweiligen Bildes
  AktuellesBild = AktuellesBild + 1;
// Wenn Bildindex das letzte Bild erreicht hat
// setze Zähler wieder auf 0
  if (AktuellesBild == 18) AktuellesBild = 0; 
  bild=m_Images[AktuellesBild];
// Laufe von links oben nach rechts unten
  xpos = (int)(xpos + (i/4));
  ypos=(int)(ypos + (i/10));
  bildbreite = (int)(bildbreite * (1 + (i/500)));
  bildhoehe= (int) (bildhoehe * (1 + (i/500)));
  repaint();
  pause(100);
 } // Ende erste For-Schleife
/* Nun kehre die vertikale Richtung um und laufe zurück nach links, aber immer noch nach 
unten. Die Animationsgeschwindigkeit wird beschleunigt durch kürzere Pausen. */
 for (int i=0; i < 40; i++) {
/* Um in der Folge der Bilder keinen Sprung auftreten zu lassen, wird die Zählvariable beim 
letzten Stand weitergezählt. */
  AktuellesBild = AktuellesBild + 1;
// Wenn Bildindex das letzte Bild erreicht hat
  if (AktuellesBild == 18) 
  AktuellesBild = 0; // setze Zähler wieder auf 0
  bild=m_Images[AktuellesBild];
  xpos = (int)(xpos - (i/4));
  ypos=(int)(ypos + (i/10));
  bildbreite = (int)(bildbreite * (1 - (i/500)));
  bildhoehe= (int) (bildhoehe * (1 - (i/500)));
  repaint();
  pause(25);
 } // Ende zweite For-Schleife
/* Nun kehre die horizontale Richtung auch um und laufe zurück nach links oben. Dazu nehmen 
wir für die X-Koordinate die aktuelle Position und die Anfangskoordinaten, und berechnen die 
daraus die Schrittgrößen für einen Rückweg in 290 Einzelsequenzen. Die Y-Schritte sind vorher 
berechnet und als Konstante gesetzt (Verwendung von final). Die Pausen werden nochmal 
verkürzt, aber durch die höhere Anzahl der Einzelsequenzen verlangsamt sich die Bewegung der 
Erdkugel.*/
 umkehrX=xpos;
 umkehrY=ypos;
 schrittX = (xpos - 10) / 290 ;
 for (int i=0; i < 290; i++) {
/* Um in der Folge der Bilder keinen Sprung auftreten zu lassen, wird  die Zählvariable beim 
letzten Stand weitergezählt. */
  AktuellesBild = AktuellesBild + 1;
// Wenn Bildindex das letzte Bild erreicht hat
  if (AktuellesBild == 18) 
  AktuellesBild = 0; // setze Zähler wieder auf 0
  bild=m_Images[AktuellesBild];
  xpos = (int)(umkehrX - (i * schrittX));
  ypos=(int)(umkehrY- (i * schrittY));
  bildbreite = (int)(bildbreite * (1 - (i/500)));
  bildhoehe= (int) (bildhoehe * (1 - (i/500)));
  repaint();
  pause(10);
 } // Ende dritte For-Schleife
/* Kleinere Nachjustierungen, um Rechenungenauigkeiten auszugleichen. Sehen tun Sie nichts 
davon, das geht harmonisch über. */
 xpos = 10;
 ypos = 10;
 bildbreite=bildbreite_anfang;
 bildhoehe=bildhoehe_anfang;
 repaint();
}//Ende der while-Schleife
}
 public void start() {
 if (MeinThread == null); {
 MeinThread = new Thread(this);
 MeinThread.start();
 }   }
 public void stop() {
 if (MeinThread != null) {
 MeinThread.stop();
 MeinThread = null;
 }   }
 void pause(int time) {
 try { Thread.sleep(time); }
 catch (InterruptedException e) { }
 }
// Hier folgt die Mausaktion, mit der das Applet 
// gestoppt werden kann.
public boolean mouseDown(Event evt, int x, int y){
stop();
return true;
}  }

Diese Animation wird sozusagen das Highlight unseres Animations-Abschnitts. Zum einen wird die Erdkugel über den Bildschirm verschoben, zum anderen dreht sie sich dabei. Wir verfolgen die wichtigsten Positionsangaben und das jeweils angezeigte Bild am Bildschirm mit. Versuchen Sie jetzt einmal, die update()-Methode hier wie im letzten Beispiel zu überschreiben. Das Resultat wird nicht befriedigend sein (es sei denn, es soll ein spezieller Effekt erzielt werden).

Abbildung 9.25:  Ohne ein Neuzeichnen lassen sich manche Animationen nicht vernünftig darstellen.

Hier offenbart sich die Grenze des Überschreibens von der update()-Methode. Ohne Überschreiben der update()-Methode flimmert der Bildschirm zwar, jedoch verschwinden zumindest die ungewollten Spuren der verschobenen Erdkugel.

Es gibt jedoch noch eine andere Optimierungstechnik, um das Flimmerproblem anzugehen. Diese Lösung heißt Clipping.

9.6.4 Clipping

Clipping bezeichnet eine Technik bei grafischen Systemen, die beim Zeichnen von grafischen Objekten verhindern soll, dass ein Bereich den anderen überschreibt. Normalerweise wird der ganze Bildschirmbereich geleert oder der ganze Bereich ohne zu Leeren überschrieben.

Sie können über die Clipping-Technik den Bereich, der geleert werden soll, begrenzen, um festzulegen, wo Sie innerhalb des Ausgabefensters etwas zeichnen wollen. Obwohl der ganze Bildschirm die Anweisung zum Nachzeichnen bekommt, wird nur der Teil innerhalb des Clipping-Bereichs neu gezeichnet.

Bei einer sich in der Position nicht verändernden Animation - etwa der sich auf der Stelle drehenden Erdkugel - umrahmen Sie einfach die Erdkugel so knapp wie möglich. Aber was ist, wenn sich das Animationsobjekt über den Bildschirm bewegt?

Sie müssen dabei den sich verändernden Bereich während der Animation verfolgen und den Clipping-Rahmen immer um das sich bewegende Objekt halten. Die Koordinaten des sich während der Animation verändernden Bereichs verwenden Sie dann in einer Methode, die der drawRect()-Methode zum Zeichen eines Rechtecks sehr ähnlich ist. Um die Grenzen für Ihren Clipping-Bereich zu setzen, verwenden Sie die Methode

public abstract void clipRect(int x, int y, int width, int height).

Sie verwendet dazu wieder zwei Koordinaten-Tupel, die wie bei den drawRect()-Tupeln zu verstehen sind.

  • Das Tupel (x, y), das die linke obere Ecke eines Rechtecks bestimmt.
  • Das Tupel (width, height), das die Breite und die Höhe des Rechtecks festlegt.

Um beispielsweise einen Clipping-Bereich bei dem Punkt (150, 100) zu beginnen, der 200 Pixel breit und 120 Pixel hoch ist, würde der Methodenaufruf folgendermaßen aussehen:

g.clipRect(150, 100, 200, 120);

Sie können die Clipping-Technik ebenso dazu verwenden, ein zu zeichnendes Objekt abzuschneiden, d.h., obwohl der eigentliche Zeichenbereich des Objekts größer ist, wird auf dem Bildschirm nur innerhalb des Clipping-Rechtecks gezeichnet. Der restliche Bereich des Bildschirms bleibt unberührt.

Bei einer Animation müssen Sie also immer genau wissen, wo im Bildbereich ein Neuzeichnen Sinn macht. Wenn Sie dies im Griff haben, ergänzen Sie die paint()-Methode vor den eigentlichen Ausgaben. Sie sieht dann so aus:

public void paint(Graphics g) {
/* Solange die Bilder noch geladen werden (m_fAllLoaded ist in der Zeit false), wird eine 
Meldung ausgegeben. Danach (m_fAllLoaded ist dann true) gibt paint das aktuelle Bild aus. */
//----------------------------------------------
if (!m_fAllLoaded) {
/* Wenn die Testvariable, ob alle Bilder geladen sind, false ist, wird eine Meldung 
ausgegeben */
g.drawString("Loading images...", 10, 20);
return;
}
g.clipRect(xpos ,ypos,bildbreite,bildhoehe);
// Bild-Ausgabe
g.drawImage(bild, xpos, ypos ,bildbreite,bildhoehe, this); 
/* Wir verfolgen die Koordinaten der Erdkugel am Bildschirm über die Methode drawString mit. 
Dazu müssen wir diese vom Typ Interger in String konvertieren. Dies geschieht mit der 
toString-Methode.  */
g.clipRect(250 ,0,700,50);
String stng = Integer.toString(xpos); 
// Die Ausgabe der X-Koordinate
g.drawString("X-Koordinate: " + stng, 350,20);  
stng = Integer.toString(ypos); 
// Die Ausgabe der Y-Koordinate
g.drawString("Y-Koordinate: " + stng, 500,20);
stng = Integer.toString(AktuellesBild); 
// Die Ausgabe der Bild-Nr.
g.drawString("Bild-Nr.: " + stng, 250,20);  
}

Das Flimmern wird sich merklich verringern. Die Erdkugel ist von einem clipRect()-Kasten umgeben, der sie verfolgt und immer umgibt. Und nur der wird bei einem repaint()-Aufruf neu gezeichnet.

Wenn Sie jetzt ein bisschen aufpassen, wird Ihnen auffallen, dass die Koordinatenangaben nicht mehr angezeigt werden. Warum das? Wir haben sie mit einem eigenen clipRect()-Kasten umgeben und dennoch sind sie weg. Dummerweise funktioniert die mehrfache Verwendung der clipRect()-Methode nicht. Die Koordinatenausgaben liegen in dem nicht neu gezeichneten Bereich. Wir könnten zwar den clipRect()-Kasten erweitern, aber dann müssten wir fast den ganzen Bildschirm neu zeichnen lassen und die Reduzierung des Flimmerns - unser eigentliches Ziel - wäre verfehlt.

Eine halbwegs befriedigende Lösung wäre eine exakte Berechnung des minimal notwendigen Bereichs zum Neuzeichnen vor jedem Aufruf der repaint()-Methode, um damit wenigstens zeitweise das Flimmern zu verringern. Eine andere Alternative wäre, mittels zwei - eventuell synchronisierter - Threads zu arbeiten, wobei jeder seinen eigenen Clipping-Bereich verfolgt. Dies ist sicher recht umständlich und dafür ist der Nutzen oft zu gering. Sie sehen, auch Clipping ist kein Allheilmittel, wenn Ausgaben in weit voneinander entfernten Bereichen des Bildschirms notwendig werden. Da muss man unter Umständen Flimmern in Kauf nehmen.

Es gibt jedoch noch eine alternative Optimierungstechnik für Animationen, die wir uns noch anschauen wollen.

9.6.5 Double-Buffering

Double-Buffering (doppeltes Puffern) heißt, dass Sie bereits vor dem Zeichnen ein Bild erstellen oder es laden und in einem Off-Screen-Bereich (also einem nicht angezeigten Bildschirmbereich) zwischenspeichern (puffern), um es dann nur noch mit einem schnellen Kopierbefehl in den angezeigten Zeichenbereich zu verschieben.

Sie erzeugen sozusagen eine zweite, nicht-sichtbare Oberfläche, in der bereits alles vorgezeichnet ist und dann auf einmal im sichtbaren Bereich angezeigt wird.

Um Double-Buffering ausführen zu können, müssen wir fünf Schritte durchführen.

1. Erstellen einer Offline-Oberfläche und eines Graphics-Kontexts.

Es müssen für eine Offline-Oberfläche und ein Graphics-Kontext Instanzvariablen erstellt werden, damit sie an die paint()-Methode übergeben werden können. Dies funktioniert so:

Image OfflineOberflaeche;
Graphics OffScreenBereich;

2. In der init()-Methode ein Image- und ein Graphics-Objekt erstellen.

Nun müssen in der init()-Methode des Applets ein Image- und ein Graphics-Objekt erstellt werden, die diesen Instanzvariablen zugewiesen werden. Allerdings können Sie diese Objekte erst dann erstellen, wenn Sie ihre Größe kennen. Das gesamte Verfahren funktioniert wie folgt mit der Methode

public abstract Image createImage(byte[] imagedata, int imageoffset, int imagelength),

die eine Instanz von Image liefert:

OfflineOberflaeche = createImage(this.size().width,this.size().height);

Die so entstandene Instanz übergeben Sie dann der getGraphics()-Methode, um einen neuen Grafikkontext für das Bild zu erhalten:

OffScreenBereich = OfflineOberflaeche.getGraphics();

Die als

public abstract Graphics getGraphics()

definierte Methode der Klasse Image kann nur in diesem Zusammenhang aufgerufen werden, um einen neuen Grafikkontext für einen Off-Screen-Bereich zu generieren. Sie ist nicht mit der in Component als public Graphics getGraphics() definierten Methode zu verwechseln, die den Graphics-Kontext von dieser Komponente zurückgibt bzw. Null, wenn die Komponente keinen aktuellen Grafikbezug hat.

3. Ausgaben in der Off-Screen-Bereich umleiten.

Alle Ausgaben, die bisher über die paint()-Methode erfolgt sind, werden jetzt mittels der Instanz OffScreenBereich in die Off-Screen-Oberfläche geschrieben. Beispielsweise mit

OffScreenBereich.drawLine(0,0,100,100);

4. Überschreiben der update()-Methode.

Als Nächstes überschreiben Sie die update()-Methode wie folgt:

public void update(Graphics g) {
paint(g);
}

5. Am Ende der paint()-Methode einen Kopierbefehl einfügen.

Im letzten Schritt fügen Sie am Ende der paint()-Methode einen Kopierbefehl ein, der die Off-Screen-Oberfläche auf dem realen Bildschirm ausgibt:

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

Diese Technik macht vor allem dann Sinn, wenn die Erstellung einer Sequenz zeitaufwändig ist. Wenn also die Zeit zum eigentlichen Zeichen länger ist, als auf Grund der Zeitintervalle zwischen den einzelnen Bildsequenzen zur Verfügung steht. Durch Multithreading kann eine Folgesequenz schon im Hintergrund erstellt werden, während die Vorgängersequenz noch auf dem Bildschirm ist.

Beispiele sind aufwändige Zeichnenoperationen oder ein relativ langsames Laden von Bildern zur Laufzeit der Animation (etwa aus dem Netz).

Wenn Sie diese Technik auf unsere letzte Animation mit der sich drehenden Erdkugel, die sich über den Bildschirm bewegt, anwenden, wird das Resultat ein erheblich verstärktes Flimmern sein. Außerdem wird die Spur der Erdkugel nicht gelöscht. Sie können es gerne ausprobieren (der Source liegt auf der CD als Animation7.java bei) und sich vom Resultat Augenschmerzen holen.

Oft macht also auch diese Technik keinen Sinn und es kann sogar zu einer Verschlechterung des Ergebnisses kommen. Auch die anderen Techniken sind keine Wundermittel. Try and Error ist vielfach angesagt, um eine Animation zu optimieren. Die drei beschriebenen Techniken können Ihnen gleichwohl oftmals dabei helfen.

9.7 Das 2D-API

Wir wollen uns nun mit den Grafik-Erweiterungen von Java beschäftigen. 2D-Java ist Bestandteil der Java Foundation Classes (JFC) und ein Teil dessen, was Sun Swing nennt und sämtliche Techniken umfasst, die die GUI-Fähigkeit von Java ausmachen (insbesondere die AWT-Erweiterungen). Das Java-2D-API ist ein Satz von Klassen für erweiterte 2D-Grafik und die Bildbearbeitung. Es beinhaltet zahlreiche neue Stricharten (unterschiedliche Dicke, Endformen, Typen), Texte und Bilder. Das API unterstützt die allgemeine Bilderstellung und so genannte Alphachannel-Bilder, einen Satz von Klassen zur Unterstützung von exakten, einheitlichen Farbdefinitionen und -konvertierungen, sowie zahlreiche anzeigeorientierten Bildoperatoren. Mit dem Java-2D-API sind Sie in der Lage, das gleiche Bildmodell sowohl für die Bildschirmausgabe als auch für den Ausdruck zu verwenden. Dies ermöglicht nun auch unter Java WYSIWYG (What You See Is What You Get), d.h., die exakte Bildschirmanzeige dessen, was im Ausdruck zu sehen sein wird (Druckvorscha>).

Die zu dem Modell gehörenden Klassen werden als Ergänzung zu den bisherigen Klassen in den Paketen java.awt und java.awt.image gesehen.

Beachten Sie, dass das Java-2D-API beim Wechsel von der JDK-1.2-Betaversion auf die Finalversion 1.2 stark verändert wurde. Leider oft ohne Hinweis darauf, was aus den bisher gültigen Elementen geworden ist und was als Ersatz fungiert.

Im Fall von Applets werden die Java-2D-Techniken in Ihrem Browser wahrscheinlich nur unter Verwendung des Java-Plug-Ins funktionieren. Sie können aber auch auf den aktuellen Appletviewer zurückgreifen.

9.7.1 Java-2D-Grundlagen

Das Java-2D-API behandelt Formen, Text, und Bilder und unterstützt einen einheitlichen Mechanismus für Bildveränderungen (etwa Rotation und Skalierung). Neben der Font- und Farbunterstützung kann man über das Java-2D-API so genannte Grafikprimitive (grafische Objekte wie ein Linienobjekt oder ein Kreisobjekt) kontrollieren, d.h., wie sie sich in einem 2D-Grafik-Kontext verhalten. Man kann spezifische Charakteristiken wie Höhe und Breite, Farbe, Füllmuster usw. angegeben, aber auch Überblendungen und ähnliche Effekte.

Der Koordinatenraum

Das Java-2D-API definiert zwei Koordinatensysteme:

  • den Anwender-Koordinatenraum
  • den Ausgabe-Koordinatenraum.

Der Ursprung des Ausgabe-Koordinatenraums liegt in der oberen linken Ecke, wobei die x-Koordinate nach rechts zeigt und in diese Richtung gezählt wird. Die y-Koordinate wächst nach unten, hat also eine Orientierung von oben nach unten. Das ist analog zu dem bisherigen Koordinatensystem.

Der Vorteil einer solchen von oben nach unten definierten Koordinatenangabe ist gerade bei Druckausgaben offensichtlich, denn das Blatt wird von oben nach unten bedruckt. Alle grafischen Objekte werden jedoch solange in einem Ausgabe-unabhängigen Anwender-Koordinatenraum beschrieben, bis sie auf einem Ausgabemedium wie dem Bildschirm oder dem Drucker ausgegeben werden. Das Übersetzungskonzept von einem Graphics2D-Objekt beinhaltet Transformationsmöglichkeiten, um das Objekt dann in den Ausgabe-Koordinatenraum zu konvertieren. Normalerweise wird die Standardtransformation von einem Anwender-Koordinatenraum in einen Ausgabe-Koordinatenraum ohne Veränderung der Orientierung erfolgen.

9.7.2 Zeichnen unter dem Java-2D-API

Das Java-2D-API verwendet ein Zeichnenmodell, das von dem neuen java.awt-Package zum Zeichnen auf dem Bildschirm (AWT Drawing Model) definiert wird. In diesem Modell implementiert jedes Komponentenobjet eine paint()-Methode, die automatisch herangezogen wird, wenn irgendetwas zu Zeichnen ist. Wenn dies geschieht, wird ein Graphics-Objekt weitergegeben, das genau weiß, wie in der Komponente zu zeichnen ist.

Zeichnen unter dem AWT Drawing Model

Die Technik, die wir hier noch einmal rekapitulieren wollen, haben wir bisher in dem Kapitel bereits verwendet. Wir ziehen zur Veranschaulichung des grundsätzlichen Zeichnens unter dem AWT Drawing Model nochmals das nachfolgende Beispiel heran. Dabei werden wir - um auch dieses Vorgehen zu zeigen - kein Applet, sondern ein eigenständiges Java-Programm mit grafischer Oberfläche verwenden. Dessen Grundstruktur (inklusive Schließbefehl) haben wir in Kapitel 6 besprochen.

Wir wollen ein rotes Rechteck zeichnen. Um dies nun unter der Verwendung von java.awt zu tun, implementieren Sie Component.paint() und gehen wie folgt vor:

import java.awt.*;
import java.awt.event.*;
class MaleRechtFill extends Frame {
public MaleRechtFill() {
  addWindowListener(new WindowAdapter() {
  public void windowClosing(WindowEvent e){
    dispose();
    System.exit(0);
  }  });
}
public static void main(String args[]) {
MaleRechtFill mainFrame = new MaleRechtFill();
mainFrame.setSize(400, 400);
mainFrame.setTitle("Test");
mainFrame.setVisible(true);
}
public void paint(Graphics g) {
g.setColor(Color.red);
g.fillRect(50, 70, 200, 100);
}  }

Abbildung 9.26:  Ein rotes Rechteck in einer eigenständigen Applikation

Dieses Beispiel illustriert die grundsätzliche Vorgehensweise für jeden Zeichenvorgang:

1. Spezifizieren Sie die notwendigen Attribute für die Form, die Sie zeichnen wollen, indem Sie eines (oder auch mehrere) der Grafikattribute über die passende Methode verwenden, etwa setColor().
2. Definieren Sie die Form, die Sie zeichnen wollen - in unserem kleinen Beispiel ein Rechteck.
3. Legen Sie das genaue Aussehen der Form fest, indem Sie eine passende Grafikmethode verwenden - etwa zum Füllen.

Schritt 2 und 3 werden in unserem Beispiel mit einer einzigen Grafikmethode erledigt, die Form und Aussehen festlegt - in unserem Fall fillRect().

Zeichnen mit dem Java-2D-API

Schauen wir uns nun an, wie mit dem Java-2D-API gezeichnet wird. Sie werden erstaunt sein (oder auch nicht, wenn Sie aus den bisherigen Bemerkungen zu Java 2D, Swing & Co richtig geschlossen haben, dass die Erweiterungen oft nur ergänzen und nicht grundsätzlich verändern), aber die grundsätzlichen Zeichenprozesse sind identisch, wenn Sie die Java-2D-API-Features verwenden. Das Java-2D-API fügt einfach zusätzliche unterstützende Features zur Spezifizierung von Zeichenstilen, komplexen Formen und diverser Zeichnenprozesse hinzu.

Um diese weitergehende Möglichkeiten zu nutzen, implementieren Sie die paint()-Methode wie gehabt, müssen aber zusätzlich den Graphics-Parameter in ein Graphics2D-Objekt casten. Dies geht beispielsweise so:

Graphics2D g2d = (Graphics2D) g;

Wir wollen unser rotes Rechteck von eben nun mittels Java-2D-API zeichnen. Dazu müssen Sie nur die paint()-Methode entsprechend modifizieren. Beispiel:

import java.awt.*;
import java.applet.*;
import java.awt.image.*;
import java.awt.geom.*;
import java.awt.event.*;
class MaleRechtFill2 extends Frame {
public MaleRechtFill2() {
  addWindowListener(new WindowAdapter() {
  public void windowClosing(WindowEvent e) {
    dispose();
    System.exit(0);  }  });
}
public static void main(String args[]) {
MaleRechtFill2 mainFrame = new MaleRechtFill2();
mainFrame.setSize(400, 400);
mainFrame.setTitle("Test");
mainFrame.setVisible(true);
}
public void paint(Graphics g) {
// Graphics-Parameter in ein Graphics2D-Objekt 
// casten, damit die Graphics2D-Funktionalitäten
// genutzt werden können.
  Graphics2D g2d = (Graphics2D) g;
  // 1. Spezifizieren der Attribute
  g2d.setColor(Color.red);
  // 2. Definieren der Form. 
  // (Verwende Even-Odd-Regel)
  GeneralPath path = new GeneralPath(GeneralPath.WIND_EVEN_ODD); 
// untere linke Ecke
  path.moveTo(100.0f, 200.0f); 
  // untere rechte Ecke
  path.lineTo(200.0f, 200.0f); 
// obere rechte Ecke
  path.lineTo(200.0f, 300.0f); 
  // obere linke Ecke
  path.lineTo(100.0f, 300.0f);
  path.closePath(); // Schließen des Rechtecks
  // 3. Füllen der Form
  g2d.fill(path);
}  }

Abbildung 9.27:  Ein rotes Rechteck in der 2D-Technik

Der Basisprozess zum Zeichnen des Rechtecks mit den Java-2D-API-Klassen ist identisch mit dem java.awt, außer dass Sie die neue Java-2D-API-Klasse GeneralPath zum Definieren des Rechtecks verwenden. Sie ist ein Bestandteil des java.awt.geom-Pakets.

Das Objekt, mit dem wir agieren, ist dann aber nicht an die normale Rechteck-Form gebunden. Die Anzahl der Eckpunkte legt die Anzahl der Ecken der geometrischen Form fest, die auch in keiner Weise an rechte Winkel gebunden ist. Die Methode lineTo() ist ähnlich wie die bekannte drawLine()-Methode, nur können Sie über das GeneralPath-Objekt noch eine ganze Menge mehr machen. Und wenn es nur das Verschieben und Füllen ist, wie in unserem Beispiel. Erweitern wir das Beispiel ein wenig, damit das deutlich wird.

import java.awt.*;
import java.applet.*;
import java.awt.image.*;
import java.awt.geom.*;
import java.awt.event.*;
class MaleRechtFill3 extends Frame {
public MaleRechtFill3() {
  addWindowListener(new WindowAdapter() {
  public void windowClosing(WindowEvent e) {
    dispose();
    System.exit(0);
  }  });
}
public static void main(String args[]) {
MaleRechtFill3 mainFrame = new MaleRechtFill3();
mainFrame.setSize(450, 520);
mainFrame.setTitle("Test");
mainFrame.setVisible(true);
}
public void paint(Graphics g) {
// Graphics-Parameter in ein Graphics2D-Objekt 
// casten, damit die Graphics2D-Funktionalitäten
// genutzt werden können.
  Graphics2D g2d = (Graphics2D) g;
  // 1. Spezifizieren der Attribute
  g2d.setColor(Color.red);
  // 2. Definieren der Form. (Even-Odd-Regel)
  GeneralPath path1 = new GeneralPath(GeneralPath.WIND_EVEN_ODD); 
  GeneralPath path2 = new GeneralPath(GeneralPath.WIND_EVEN_ODD); 
  path1.moveTo(10.0f, 50.0f); // untere linke Ecke
  path1.lineTo(200.0f, 80.0f);//untere rechte Ecke
  path1.lineTo(200.0f, 350.0f);//obere rechte Ecke
  path1.lineTo(100.0f, 400.0f);//obere linke Ecke
  path1.closePath(); // Schließen des Rechtecks
  // 3. Füllen der Form
  g2d.fill(path1);
  g2d.setColor(Color.green);
  path2.moveTo(50.0f, 150.0f);//untere linke Ecke
  path2.lineTo(80.0f, 120.0f);//untere rechte Ecke
  path2.lineTo(300.0f, 380.0f);//obere rechte Ecke
  path2.lineTo(100.0f, 480.0f);// obere linke Ecke
  path2.lineTo(120.0f, 200.0f);// obere linke Ecke
  path2.closePath(); // Schließen des Rechtecks
  // 3. Füllen der Form
  g2d.fill(path2);
}  }

Abbildung 9.28:  Überlagerte gefüllte Formen in der 2D-Technik

Das Java-2D-API versetzt Sie in die Lage, die gleichen Mechanismen wie beim Zeichnen einer einfachen Form für viel komplexere Zeichnenoperationen zu verwenden. Wie auch immer, um die Erstellung von Standardformen wie Rechtecken, Ellipsen, Bögen und Kurven zu vereinfachen, stellt das Java-2D-API diverse Subklassen der Klasse Shape in Verbindung mit GeneralPath zur Verfügung. So können Sie beispielsweise statt GeneralPath Rectangle2D.Double zum Definieren des Rechtecks in unserem Beispiel verwenden:

Rectangle2D.Double rect = new Rectangle2D.Double(300, 300, 200, 100);

Dieses Rechteck ist nicht identisch mit dem Rechteck, das über GeneralPath definiert wird. Der Ursprung des GeneralPath-Objekts, das wir in dem ersten Beispiel konstruiert haben, hat als Startpunkt die untere linke Ecke des Rechtecks. Der Ursprung von dem über das Rectangle2D.Double-Objekt konstruierte Rechteck ist (300, 300), die obere linke Ecke des Rechtecks. Wenn Sie ein Rectangle2D.Double-Objekt konstruieren, das mit einer negativen Dimension arbeitet, wird ein leeres Rechteck mit der Größe (0,0) generiert.

Im Allgemeinen läuft das Zeichnen mit den Java-2D-API-Klassen folgendermaßen ab:

1. Spezifizieren der notwendigen beschreibenden Attribute.

Zusätzlich zu den Ihnen schon aus dem »klassischen« Fall bekannten Attributen wie feste Farbfüllungen stellen die Java-2D-Klassen Möglichkeiten für komplexere Füllungen (etwa Verläufe und Muster) zur Verfügung. Für diese komplexere Füllungen können Sie die setPaint()-Methode verwenden (wir kommen in Kürze noch einmal genauer darauf zurück).

2. Definieren einer Form, eines Textstrings oder eines Bildes.

Das Java-2D-API behandelt Pfade bzw. Positionsangaben, Texte, und Bilder gleichartig; sie können rotiert, skaliert, verzerrt, und mit diversen Methoden zusammengesetzt werden. Das Shape-Interface definiert einen ganzen Satz von-Methoden zur Beschreibung von geometrischen Path-Objekten. Das Java-2D-API unterstützt eine Vielzahl von Implementationen von Shape, die gemeinsame Formen wie Rechtecke, Bögen und Ellipsen definieren. GeneralPath ist eine Implementation von des Shape-Interfaces, das Sie zur Definition von beliebigen, komplexen Formen verwenden können, indem Sie eine Kombination von Linien und quadratischen sowie kubischen Beziér-Kurven nutzen.

Der GeneralPath-Konstruktor bekommt einen Parameter, der die so genannte Winding Rule für das jeweilige Objekt spezifiziert. Dabei handelt es sich um Regeln zum Bestimmen, ob ein Zeichenobjekt innerhalb einer Form liegt oder nicht, wenn ein Path-Segment die Form kreuzt. Zwei verschiedene winding rules können für ein GeneralPath-Objekt spezifiziert werden:

  • even-odd winding rule
  • nonzero winding rule

Die erste Regel spezifiziert, wie das Innere eines Path-Segments behandelt wird, wenn ein solches Gebilde zwischen Innen und äusseren Gebieten alterniert, d.h. er durchquert den Rand. Die 2. Regel basiert auf der gedachten Zeichnung von Strahlen von einem gegebenen Punkt aus bis Unendlich in jede Richtung und dann durch Prüfen aller Orte, wo die Strahlen und das Path-Segment sich schneiden.

Die even-odd-Regel wurde in unseren Beispielen verwendet.
Die Felder aus der Klasse java.awt.geom.GeneralPath für die Winding Rules haben sich verändert. Java 2D war bereits in den Betaversionen des JDK 1.2 vorhanden. Dort hießen die Felder static byte EVEN_ODD und static byte NON_ZERO. In der Finalversion des JDK 1.2 wurden diese Feldnamen entfernt! Sie heißen dort static int WIND_EVEN_ODD und static int WIND_NON_ZERO (beachten Sie auch die Veränderung des Datentyps). Leider wurde diese Veränderung kaum dokumentiert. Es kommt bei Verwendung der alten Felder in einem aktuellen JDK nicht einmal eine Meldung beim Komplilieren, dass diese Felder als deprecated gelten. Es wird nur die Fehlermeldung ausgegeben, dass diese Felder nicht in der Klasse vorhanden sind. Umgekehrt können Sie im JDK einer Vorgängerversion natürlich die neuen Feldnamen nicht verwenden. Positiver Effekt des offensichtlich stark veränderten Konzeptes mit den neuen Felder ist eine erheblich bessere Unterstützung der Grafikausgabe auf einigen Plattformen. Eine bessere Dokumentation bezüglich der Felder wäre jedoch wirklich wünschenswert gewesen.

3. Festlegen des Aussehens der Form.

Dritter Schritt ist das Festlegen des Aussehens der Form, des Textstrings oder des Bildes. Dazu können Sie eine der passenden Graphics2D-Methoden verwenden.

Bevor ein Java-2D-API-Objekt gezeichnet wird, wird es meist über eine zu dem Graphics2D-Objekt gehörende Transformation manipuliert. Eine solche Transformation nimmt einen Punkt oder Pfad und transformiert ihn in einen neuen Punkt oder Pfad. Das Default-Transformationsobjekt kreiert eine einfache Skala von Ausgabekoordinaten, wenn das Graphics2D-Objekt konstruiert wird. Um dann solche Effekte wie Rotation, Translation oder benutzerdefinierte Skalierung zu erhalten, kreieren Sie ein Transform-Objekt und verwenden es mit dem Graphics2D-Objekt.

Die Standardtransformation, die in das Java-2D-API implementiert ist, ist die so genannte affine Transformation, die lineare Transformationen wie Translation, Rotation, Skalierung, und Verzerrungen unterstützt (auch darauf werden wir noch eingehen).

9.7.3 Komplexere Zeichnenoperationen im 2D-API

Die Technik für komplexere Zeichnenoperationen im 2D-API ist nahezu identisch mit dem bisherigen einfachen Beispiel. Dies ist eine der großen Stärken des 2D-API. Nehmen wir eine kleine Veränderung unserer bisherigen Beispiele vor und zeichnen zweite Rechtecke, wobei das zweite einen Teil des ersten Rechtecks überdeckt, sowie um 45° gedreht ist. Wir füllen das zweite Rechteck mit einer Farbe (nehmen wir blau) und verwenden zusätzlich eine der neuen 2D-Techniken - die Transparenz. Das neue Rechteck soll zusätzlich zu 50 % transparent sein, sodass das erste Rechteck im überdeckten Bereich immer noch sichtbar ist. Was bisher unter Java ein erhebliches Problem darstellte, kann mit dem Java-2D-API leicht realisiert werden.

Um auch bei der Verwendung des 2D-API nicht auf Applets zu verzichten, wird das Beispiel in ein Applet umgebaut (was es sogar kürzer macht - die paint()-Methode funktioniert aber identisch in der eigenständigen Applikation). Dies soll aber noch einen weiteren Zweck verfolgen. Wenn Sie das Applet ohne das Java-Plug-In im Browser laden, wird es nicht funktionieren.

Das Beispiel sieht dann aus wie folgt:

import java.awt.*;
import java.applet.*;
import java.awt.image.*;
import java.awt.geom.*;
public class MaleRechtFill4 extends Applet {
public void paint(Graphics g) {
  Graphics2D g2d = (Graphics2D) g;
  g2d.setColor(Color.red);
  GeneralPath path = new GeneralPath(GeneralPath.WIND_EVEN_ODD);
  path.moveTo(0.0f, 0.0f); //  untere linke Ecke
  path.lineTo(200.0f, 0.0f); // untere rechte Ecke
  // obere rechte Ecke
  path.lineTo(200.0f, -100.0f);
  path.lineTo(0.0f, -100.0f); // obere linke Ecke
  path.closePath(); // Schließen des Rechtecks 
  AffineTransform at = new AffineTransform();
  at.setToTranslation(300.0, 400.0);
  g2d.transform(at);
  g2d.fill(path);
  // Hinzufügen eines 2. Rechtecks
  // Definieren der Farbe
  g2d.setColor(Color.blue); 
  AlphaComposite comp =AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.5f);
  g2d.setComposite(comp); //setzen composite-Modus
  // Rotatation um 45 Grad gegen den Uhrzeigersinn
  at.setToRotation(-Math.PI/4.0);   
  g2d.transform(at);
  g2d.fill(path);
}  }

Wenn Sie das Applet mit einer normalen <APPLET>-Referenz in einen Browser laden, werden Sie Probleme bekommen. Ohne das Java-Plug-In wird es nicht laufen.

Abbildung 9.29:  Der Navigator macht ohne Plug-In Probleme.

Mit dem Java-Plug-In und der Referenz über <OBJECT> bzw. <EMBED> geht es dann.

Sollten Sie die mittels des im ersten Kapitel beschriebenen HTML-Konverter erstellte HTML-Datei mit dem <OBJECT> und dem <EMBED>-Tag als Basis nehmen, wird der Appletviewer das Applet zweimal öffnen.

Abbildung 9.30:  Zwei sich teilweise überdeckende Rechtecke

Analysieren wir das Beispiel:

  • Der Zeichenprozess ist für beide Rechtecke identisch.
  • Das Rechteck wird über die Verwendung eines GeneralPath-Objekts definiert.
  • Die Attribute zur Festlegung des Aussehens werden gesetzt, indem die Methode setColor() für die Farbe und die Methode setComposite() zur Spezifizierung der 50 %-Transparenz für das blaue Rechteck angewendet wird.
  • Die Transformationen werden angewandt, bevor die Rechtecke erstellt werden (Graphics2D.transform() wird verwendet, um beide Rechtecke am Punkt (300, 400) zu positionieren und das blaue Rechteck um 45º gegen den Uhrzeigersinn zu rotieren.)
  • Die Rechtecke werden endgültig durch den jeweiligen Aufruf von fill() dargestellt.

Wir haben in dem Beispiel auf einige Java-Techniken vorgegriffen, die gleich noch näher beleuchtet werden. Wichtig ist, dass das eben durchgesprochene Beispiel die grundsätzliche Vorgehensweise zur Definition von neuen Farbattributen, Transformationsprozessen und der Darstellung des Objekts zeigt.

Definition der beschreibenden Attribute:

  • Definition der Farbe. In unserem Beispiel muss zur Zeichnung des zweiten Rechtecks mit einer zu 50 % transparenten, blauen Farbe zuerst die Farbe gesetzt werden:
  • g2d.setColor(Color.blue);
  • Definition der Gestaltungsoperation. Dabei müssen Sie festlegen, wie die neuen Farben bei Überlagerungen mit bereits existierenden Farben zusammenspielen sollen. Dazu kreieren Sie ein so genanntes AlphaComposite-Objekt. Ein solches AlphaComposite-Objekt definiert eine Kompositionsoperation, die spezifiziert, wie Farben ineinander übergehen, d.h. verschmelzen. In unserem Fall haben wir ein AlphaComposite-Objekt kreiert, das die Transparenz auf 50 % setzt und die neue Farbe über die alte Farbe legt. Hierfür spezifizieren Sie die SRC_OVER-Operation und einen Alpha-Wert von 0.5 bei der Erstellung eines AlphaComposite-Objekts. Um das neue Composite-Objekt aufzurufen, verwenden Sie folgenden Source:
  • Graphics2D.setComposite.AlphaComposite comp =
    AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.5f);
    g2d.setComposite(comp);
  • Definition der Rotation.
    Die Rotation wird über die Erstellung einer neuen Instanz der Klasse AffineTransform und Aufruf der Methode setToRotation() zum Spezifizieren der Rotation um 45 Grad entgegen des Uhrzeigersinns bewerkstelligt. Die Transformation wird dann mit der vorangehenden Transformation zusammengefügt, indem die Transformation des Graphics2D-Objekts (Translation auf (300,400)) wie folgt über die Methode transform() aufgerufen wird:
  • at.setToRotation(-Math.PI/4.0);
    g2d.transform(at);

Die Effekte von aufeinander folgenden Transformationsaufrufen sind kumulativ - (an)häufend, anwachsend. Dies bedeutet, dass durch jeden des aufeinander folgenden Transformationsaufrufe zu der aktuellen Position eine weitere Drehung entgegen des Uhrzeigersinns fortgeführt wird.

9.7.4 Text unter Java 2D

Auch im Bereich der Textbehandlung hat das neue Java-2D-API einige Erweiterungen gebracht. Diese reichen von der einfachen Verwendung von Schriftarten bis zum professionellen Management von Zeichenlayouts und Schriftarten-Features.

Im Wesentlichen wird über die neue Fontklasse des Java-2D-API gegenüber der existierenden Klasse zur Verwendung von verschiedenen Schriftarten eine bessere Kontrolle über Schriftarten gewährleistet. Es ist nun möglich, mehr Informationen über eine Schriftart, etwa den Beziér-Pfad von individuellen Zeichenformen, mit den Schriftarten zu verwalten. Die Java-2D-API-Fontklasse löst die alte Klasse vollständig ab, was bedeutet, dass sie natürlich all das kann, was diese auch konnte.

Konkretes Zeichnen von Text

Die Technik zum Zeichnen von Text ist unter dem 2D-API identisch mit der Verwendung eines GeneralPath-Objekts zum Definieren einer Form. Sie können unsere bisherigen Ausführungen zum Zeichnen eines Textes nahezu unverändert übernehmen, nur kreieren Sie ein Font-Objekt und verwenden es durch den Aufruf von Graphics2D.drawString().

Wir werden ein Beispiel durchsprechen, das auch auf Text zusätzlich noch eine der neuen 2D-Erweiterungen verwendet - die Rotation eines Texts, was nur als Rotation eines beliebigen Graphics2D-Objekts verstanden wird. Ein String soll gegen den Uhrzeigersinn gedreht (bis er fast auf dem Kopf steht) und außerdem in der Größe von 60 Punkten dargestellt werden. Danach wird er über zwei Rechtecke platziert, die sich um einen Winkel gedreht überdecken.

import java.awt.*;
import java.applet.*;
import java.awt.image.*;
import java.awt.geom.*;import java.awt.font.*;
public class MaleRechtFill5 extends Applet {
public void paint(Graphics g) {
  Graphics2D g2d = (Graphics2D) g;
  g2d.setColor(Color.red);
  GeneralPath path = new GeneralPath(GeneralPath.WIND_EVEN_ODD);
  path.moveTo(0.0f, 0.0f); //  untere linke Ecke
  path.lineTo(200.0f, 0.0f); // untere rechte Ecke
  path.lineTo(200.0f, -100.0f);// oben rechts
  path.lineTo(0.0f, -100.0f); // oben links
  path.closePath(); // Schließen des Rechtecks 
  AffineTransform at = new AffineTransform();
  at.setToTranslation(300.0, 400.0);
  g2d.transform(at);
  g2d.fill(path);
  // Hinzufügen eines 2. Rechtecks
  g2d.setColor(Color.blue); // Definieren Farbe
  AlphaComposite comp = AlphaComposite.getInstance(AlphaComposite.SRC_OVER, 0.5f);
//setzen des composite-Modus
  g2d.setComposite(comp); 
  // Rotatation um 45 Grad gegen den Uhrzeigersinn
  at.setToRotation(-Math.PI/4.0);   
  g2d.transform(at);
  g2d.fill(path);
// eine 80 point-Version von Helvetica-BoldOblique
  Font myFont = new Font("Helvetica-BoldOblique", Font.PLAIN, 60);
 // Anzeige von String in gelb 
  g2d.setColor(Color.yellow);
  g2d.setFont(myFont);  // Setzen der Schriftart
// String ist undurchsichtig gezeichnet, daher 
// wird es hier leicht transparent gesetzt
  g2d.setComposite(AlphaComposite. getInstance(AlphaComposite.SRC_OVER, 0.7f));
  at.setToRotation(-Math.PI/2.0);   
  g2d.transform(at);
//  Text zeichnen
  g2d.drawString("Das ist der Hammer", -100f, 50f);
}  }

Abbildung 9.31:  Ein gedrehter Text  in mitten der Rechtecke

9.8 Bilder unter Java 2D

Zur Bildverarbeitung stehen Ihnen unter dem Java-2D-API alle Möglichkeiten der bisherigen Klassen in java.awt und java.awt.image zur Verfügung. Zusätzlich gibt es eine Vielzahl von neuen Klassen, inklusive BufferedImage, ComponentColorModel und ColorSpace.

Damit erhalten Sie eine erweiterte Kontrolle über Bilder. So können Sie in gleicher Weise Bilder kreieren, die über das RGB-Modell hinausgehen und absolut genaue Farbdefinitionen zur Reproduktion beinhalten. Darüber hinaus lassen sich nun Pixel direkt in Speicher verwalten.

Wie alle anderen grafischen Elemente werden Bilder mit einer Transformation bearbeitet, die dem Graphics2D-Objekt zugeordnet ist. Dies bedeutet, auch Bilder können skaliert, rotiert, verzerrt oder transformiert werden. Darüber hinaus beinhalten Bilder ihre eigenen Farbinformationen.

9.8.1 Anzeige von Bildern unter Java 2D

Die Anzeige eines Bildes unter Java 2D ist unkompliziert und wie fast immer eine einfache Übertragung der bisherigen Technik. Wenn Sie ein Bild haben, rufen Sie einfach Graphics2D.drawImage() mit einer gewünschten Transformation auf. Dabei können Sie - je nach verwendetem Konstruktor - das Bild über das Netz mit einer URL laden (Image meinBild = applet.getImage(url);) oder auch lokal mit einer Pfadangabe als String wie in dem folgenden Beispiel:

public void paint(Graphics g) {
Graphics2D g2d = (Graphics2D) g;
Image image = getImage(getDocumentBase(),"images/Img1.jpg");
AffineTransform at = new AffineTransform();
at.setToTranslation(100.0, 100.0);
g2d.drawImage(image, at, this);
}

Abbildung 9.32:  Ein Bild wird wie jedes andere Grafikobjekt behandelt.

Sie können ein Bild ganz einfach rotieren. Dazu müssen Sie vor der Ausgabe mit g2d.drawImage(image, at, this); nur Folgendes einfügen:

at.rotate(Math.PI/4.0);

9.8.2 Transparenz und Bilder

Wir haben eben angedeutet, dass Bilder unter Java 2D weitergehende Informationen zu jedem Pixel verwalten können. Dazu zählen auch Informationen über die Transparenz für jedes Pixel in einem Bild.

Diese Informationen werden Alphachannel genannt und werden in Verbindung mit der aktuellen Komposition mit einem anderen Objekt verwendet, um Überblendeffekte mit der aktuellen Zeichnung zu realisieren. Daraus resultieren viele interessante Effekte, wie man sie mittlerweile oft in Webseiten sieht.

Wir kennen bereits die notwendige Technik, um diese Transparenz bzw. Übergangeffekte zu realisieren - das AlphaComposite-Objekt, das SRC_OVER für die Kompositionsoperation verwendet.

Erstellen wir zuerst ein Beispiel, das aus unseren bisherigen Beispielen eine kleine Animation zusammenfügt. Dabei arbeiten wir mit Transparenzmodi, Positionierungen, Rotationen und Verschiebungen. Das Beispiel in der ersten Version mischt Java 2D mit alten Verfahren.

import java.awt.*;
import java.applet.*;
import java.awt.image.*;
import java.awt.geom.*;
public class Animation2D extends java.applet.Applet implements Runnable {
 Image bild, image;
 int bildbreite;
 int bildhoehe;
 int xpos = 10;  // Startposition X
 int ypos = 10;  // Startposition Y
 Thread MeinThread;
 public void init() {
// Bild laden
   bild = getImage(getCodeBase(), "images/kuerb.gif");
   bildbreite = bild.getWidth(this);
   bildhoehe = bild.getHeight(this);
   image = getImage(getDocumentBase(),"images/Img1.jpg");
   resize(600, 600);
  }
  public void paint(Graphics g) {
  Graphics2D g2d = (Graphics2D) g;
  Graphics2D g2d2 = (Graphics2D) g;
  AffineTransform at1 = new AffineTransform();
  at1.setToTranslation(250.0, 30.0);
  at1.rotate(Math.PI/4.0);
  g2d.setComposite(AlphaComposite. getInstance(AlphaComposite.SRC_OVER, 0.7f));
  g2d2.setComposite(AlphaComposite. getInstance(AlphaComposite.SRC_OVER, 0.7f)); 
 /* g2d.setComposite(AlphaComposite. getInstance(AlphaComposite.SRC_OVER, 0.5f));
  g2d2.setComposite(AlphaComposite. getInstance(AlphaComposite.SRC_OVER, 0.3f)); */
  g2d.drawImage(image, at1, this);
  g2d2.drawImage(bild, xpos, ypos ,bildbreite,bildhoehe, this);
    }
  public void run() {
 for (int i=0; i < 32; i++) {
     xpos = (int)(xpos + (i/4));
     ypos=(int)(ypos + (i/5));
     bildbreite = (int)(bildbreite * (1 + (i/30)));
     bildhoehe= (int) (bildhoehe * (1 + (i/30)));
     repaint();
     pause(500);
 }  }
  public void start() {
   if (MeinThread == null) {
     MeinThread = new Thread(this);
     MeinThread.start();
 }  }
  public void stop() 
  {
   if (MeinThread != null) {
     MeinThread.stop();
     MeinThread = null;
 }  }
 void pause(int time) {
  try { Thread.sleep(time); }
  catch (InterruptedException e) { }
 }  }

Das Beispiel nutzt eine Animation von vorher, in der ein Bild (ohne Einschränkung ein GIF) verschoben und vergrößert wird. Dabei überstreicht es den Bereich eines Bilds, das per Java 2D positioniert, gedreht und transparent gesetzt wurde. Das verschobene GIF wird zwar mit einer drawImage()-Methode aus der alten Technik angezeigt und positioniert (also auch verschoben), aber diese wird auf ein Graphics2D-Objekts angewandt (die Methode wurde aus Graphics vererbt). Dementsprechend kann auch dieses GIF transparent gesetzt werden. Sie sehen die Effekte, wenn sich die Bilder überlagern. Wenn Sie die Transparenzmodi ändern (dazu gibt es bereits eine auskommentierte Variante im Source), wird man die Effekte gut erkennen können.

Abbildung 9.33:  Ein Bild und ein GIF transparent überlagert

Abbildung 9.34:  Die Überlagerung in einem geänderten Transparenzmodus - das Bild scheint stärker durch

Im Appletviewer müssen Sie unter Umständen den Reload-Befehl auslösen, bevor Sie den Kürbis sehen.

Diese Animation wird gewaltig flimmern. Natürlich könnten wir jetzt die oben beschriebenen Techniken zur Optimierung anwenden, aber darum geht es hier nicht. Wenn wir vollständig auf Java 2D umsteigen, werden auch diverse Optimierungen automatisiert. Das wollen wir nicht komplett machen, sondern nun auch das GIF vollständig als Graphics2D-Objekt behandeln und entsprechend rotieren, positionieren und ausgeben.

Die nachfolgende Animation läuft auch im Appletviewer ohne den Reload-Befehl.
import java.awt.*;
import java.applet.*;
import java.awt.image.*;
import java.awt.geom.*;
public class Animation2D2 extends java.applet.Applet implements Runnable {
 Image bild, image;
 Thread MeinThread;
 double xpos = 100.0;
 double ypos = 100.0;
 AffineTransform at2;
 public void init() {
// Bild laden
   bild = getImage(getCodeBase(), "images/kuerb.gif");
   image = getImage(getDocumentBase(),"images/Img1.jpg");
   resize(600, 600);
   at2 = new AffineTransform();
  }
  public void paint(Graphics g) {
  Graphics2D g2d = (Graphics2D) g;
  Graphics2D g2d2 = (Graphics2D) g;
  AffineTransform at1 = new AffineTransform();
  at1.setToTranslation(250.0, 30.0);
  at1.rotate(Math.PI/4.0);
  at2.setToTranslation(xpos, ypos);
  at2.rotate(Math.PI/1.8);
  g2d.setComposite(AlphaComposite. getInstance(AlphaComposite.SRC_OVER, 0.5f));
  g2d2.setComposite(AlphaComposite. getInstance(AlphaComposite.SRC_OVER, 0.3f)); 
  g2d.drawImage(image, at1, this);
  g2d2.drawImage(bild, at2, this);
   }
  public void run()  {
   for (int i=0; i < 40; i++) {
     xpos = (double)(xpos + (i/4));
     ypos=(double)(ypos + (i/5));
     repaint();
     pause(500);
 }  }
  public void start() {
   if (MeinThread == null) {
     MeinThread = new Thread(this);
     MeinThread.start();
 }  }
  public void stop() {
   if (MeinThread != null) {
     MeinThread.stop();
     MeinThread = null;
 }  }
 void pause(int time) {
  try { 
   Thread.sleep(time); 
   }
  catch (InterruptedException e) { 
  }
 }  }

Abbildung 9.35:  Der Kürbis bewegt sich auf sein Opfer zu.

9.8.3 Rendering als Bestandteil von Java 2D

Eine neue Technik in Java 2D nennt sich Rendering, was Übersetzung oder Übertragung bedeutet. Darunter versteht man einen Prozess, um ein grafisches Objekt auf einem (beliebigen) Ausgabegerät wiederzugeben. Dazu ist eine Serie von Schritten notwendig, die in manchen Literaturquellen als Rendering Pipeline benannt wird.

Wenn sie das 2D-API zur Ausgabe verwenden, wird der Rendering-Prozess über ein Graphics2D-Objekt und seine Statusattribute kontrolliert. Mit diesen Statusattributen können Sie einen Clippingpfad zur Limitierung des Bereichs, das ausgegeben werden soll, setzen, oder Farben und Füllungen definieren. Diese Attribute werden bei der Steuerung des Ausgabeproszesses dann angewandt.

Der konkrete Rendering-Prozess

Der konkrete Rendering-Prozess kann in vier Grundschritte aufgeteilt werden.

Das grafische Objekt wird in grafische Primitive konvertiert und in den Ausgaberaum transformiert - unter Verwendung der Transformation von dem Graphics2D-Objekt. Dies legt fest, wo das grafische Objekt ausgegeben werden sollte. Wie dies dann aber exakt geschieht, hängt von dem Typ des auszugebenen grafischen Objekts ab:

  • Wenn eine Form auszugeben ist, wird der PathIterator zum Holen der Elemente in dem Pfad der Form verwendet. Wenn der Pfad gezeichnet wird, wird das Stroke-Objekt in dem Graphics2D-Objekt verwendet, um den Pfad in einen gezeichneten Pfad zu konvertieren. Die Geometrie der Form wird in die Ausgabekoordinaten transformiert, die mit Graphics2D assoziiert sind.
  • Wenn ein Text auszugeben ist, wird das Layout von den Figuren (Glyphs genannt), die dem auszugegebenden Textstring enthalten, über die Information in dem String-Font festgelegt. Die Glyphs werden in Umrisse konvertiert, die von dem Shape-Objekt festgelegt werden. Diese Shape-Objekte, die die Glyphs repräsentieren, werden wie normale Formen ausgegeben.
  • Wenn ein Bild auszugeben ist, wird die begrenzende Box (in Anwender-Koordinaten) in Ausgabe-Koordinaten transformiert, indem die mit Graphics2D assoziierte Transformation verwendet wird.

Bei der Ausgabe wird der aktuelle Clipping-Pfad zur Begrenzung der Ausgabeoperation verwendet. Die auszugebende Farbe wird nach folgenden Kriterien festgelegt:

  • Für Bilder wird die Farbe von den Imagedaten selbst genommen.
  • Für Text und Pfade wird das aktuelle Paint- oder Color-Objekt in Graphics2D bezüglich der Farbe abgefragt.
  • Wenn das Objekt konkret auf dem Ziel ausgegeben werden soll, wird das aktuelle Composite-Objekt verwendet.

9.8.4 Kontrolle der Ausgabequalität

Java 2D gibt Ihnen vielfältige Möglichkeiten zur Kontrolle der Ausgabequalität eines beliebigen grafischen Objekts auf einem gerasterten grafischen Ausgabegerät. Dabei muss jedoch immer bedacht werden, dass einige Kurvenformen und diagonale Linien immer approximiert (berechnet) werden müssen und dabei Stufeneffekte entstehen. Dies werden Sie vor allem dann bemerken, wenn die Auflösung des Ausgabegeräts nur sehr niedrig ist. Um diese Effekte zu reduzieren, stellt Java nun eine Technik zur Verfügung, die Antialiasing genannt wird und zum Glätten von Ecken führt (auf Kosten der Geschwindigkeit der Ausgabe). Sie können in der Java-2D-API festlegen, ob Objekte bei der Ausgabe so schnell wie möglich oder möglichst glatt ausgegeben werden sollen (soweit die Plattform dies unterstützt).

Sie können die Ausgabepräferenz für ein Graphics2D-Objekt spezifizieren, indem Sie die Methode setRenderingHints() aufrufen. Es gibt zwei Typen von Ausprägungen:

1. Die erste indiziert, ob ein Objekt bei der Ausgabe antialiasiert werden soll oder nicht.
2. Der zweite dient zum Festlegen einer Präferenz zwischen Geschwindigkeit und Qualität.

9.8.5 Transformation von 2D-Objekten

Das Java-2D-API unterstützt eine spezielle Transformationsklasse - AffineTransform. Wir haben sie bereits verwendet. Diese unterstützt sämtliche klassischen Grafikoperationen wie Skalierung, Rotation, und Zusammensetzen.

Affine Transformationen

Der Begrif »Affine Transformation« stammt aus der Mathematik und bedeutet eine lineare Transformation auf einem Satz von grafischen Primitiven. Es werden immer gerade Linien in gerade Linien transformiert und parallele Linien gehen wieder in parallele Linien über. Verändert werden jedoch die Abstände zwischen den einzelnen Punkten und die Winkel zwischen nicht-parallelen Linien. Dies mag vielleicht sehr abstrakt klingen, ist aber genau das, was wir auch bisher schon in unseren Beispielen getan haben. Außerdem ist es nur die abstrakte Beschreibung dessen, was Sie bei einer solchen Aktion auch sicher erwarten. Beispielsweise erwarten Sie sicher, dass bei einer reinen Rotation (einer typischen affinen Transformation) das grafische Objekt nur gedreht und nicht deformiert wird. Falls dies zusätzlich gewünscht wird, muss man mehrere affine Transformationen hintereinander ausführen. Eine einzelne affine Transformation wird immer nur eine lineare Aktion durchführen.

Eine affine Transformation basiert immer auf einer 2-dimensionalen Matrix, die in der AffineTransform-Klasse enthalten ist und die Sie niemals direkt aufrufen müssen (eine erhebliche Erleichterung gegenüber vielen anderen Programmiersprachen und auch dem bisherigen Java-Konzept). Sie müssen bloß die gewünscht Sequenz von Rotationen, Translationen oder anderen Transformation auswählen und anwenden.

Die mit dem Graphics2D-Objekt assoziierten Transformationen stehen in allen Formen, Texten und Bildern zur Verfügung, die Sie von dem Anwenderraum in den Ausgaberaum zeichnen (die meist verwendete Aktion). Graphics2D implementiert eine Version von drawImage(), die eine Instanz von AffineTransform als Parameter verwendet (wir haben die Methode in den letzten Beispielen verwendet). Wenn Sie diese Version von drawImage() verwenden, wird das Bild genauso gezeichnet, wie Sie es mit der Transformation von dem Graphics2D-Objekt verkettet haben.

9.8.6 Neue Formen kreieren

Mittels der Shape-Schnittstelle können Sie in Java 2D eine Klasse kreieren, die einen neuen Typ von Form definiert (beispielsweise ein spezielles Polygon). Dazu müssen Sie bloß die folgenden Methoden der Shape-Schnittstelle implementieren:

contains() 
getBounds() 
getBounds2D() 
getPathIterator() 
intersects()

9.8.7 Stroking Paths

Zeichnen (Stroking) einer Form wie ein GeneralPath-Objekt ist äquivalent dazu, als würde ein logischer Stift über die Segmente des GeneralPath geführt werden. Das Stroke-Objekt schließt die Eigenschaften des verwendeten Strichs durch diesen Stift mit ein. Das Java-2D-API unterstützt diverse Basis-Stroke-Klassen für die verschiedensten Basisstrichformen und -stile.

9.8.8 Füllen von Formen

Das Java-2D-API beinhaltet eine Vielzahl von einfachen Füllmechanismen, aber auch komplexe Mechanismen zum Ausfüllen von Formen (etwa Farbverläufe). Zur Vereinfachung dieser komplexen Vorgänge definiert das 2D-API eine neue Graphics2D-Methode, die setPaint() heißt, sowie ein Interface mit Namen Paint. Zwei Implementationen von dem Paint-Interface werden unterstützt:

GradientPaint
TexturePaint

Sämtliche Zeichenvorgänge erfolgen mit einem Paint-Objekt. Ein Color-Objekt ist beispielsweise ein sehr einfacher Typ von eine Paint-Objekts, und die setColor()-Methode ist ein Spezialfall von setPaint(). Daraus folgt, dass setColor() ein Paint-Objekt mit nur einer Farbe installiert.

9.8.9 Komposition von Bildern

Für die komplexere Komposition verwendet Java 2D die AlphaComposite-Klasse, eine Implementation vom dem Composite-Interface. Diese Klasse unterstützt eine ganze Anzahl von verschiedenen Stilen zur Zusammenfügung von Bildern. Instanzen von dieser Klasse beinhalten Kompositionsregeln, die exakt beschreiben, wie eine neue Farbe in eine bereits bestehende Farbe geblendet werden soll. Der alpha-Wert wird von einem Color-, Paint- oder Image-Objekt genommen und mit Pixelinformationen kombiniert.

Benutzerdefinierte Kompositionsregeln

Indem Sie die Composite- und CompositeContext-Interfaces verwenden, können Sie einen neuen Typ von Kompositionsregeln erstellen. Ein Composite-Objekt unterstützt dazu ein CompositeContext-Objekt.

9.8.10 Transparenz

Eine der wichtigsten und am meisten verwendeten Regeln in der AlphaComposite-Klasse ist SRC_OVER - die Angabe, wie Objekte durchscheinend angezeigt werden können. Das Verfahren haben wir in den letzten Beispielen mehrfach angewandt. Wenn AlphaComposite.SRC_OVER verwendet wird, wird indiziert, wie die neue Farbe (source color) in die existierende Farbe (destination color) eingeblendet wird.

AlphaComposite-Objekte können mittels extra alpha-Werten angegeben werden, die die Tranparenz von Objekten beim Zeichnen festlegen. Diese alpha-Werte werden mit den alpha-Werten des Grafikobjekts kombiniert. Diese kombinierten alpha-Werte legen fest, wie viel von der existierenden Farbe durch die neue Farbe hindurch angezeigt werden soll. Man kann das Durchscheinen vollständig unterbinden (alpha=1.0) oder auch die neue Farbe vollständig durchscheinen lassen (alpha=0.0). Werte dazwischen entsprechen den verschiedenen Abstufungen.

9.8.11 Text und Fonts unter Java 2D

Jegliche Transformations- und Zeichnentechnik von der Java-2D-API kann auf Textstrings angewendet werden. Dazu erhalten Sie zahlreiche Klassen für das Textlayout und feinabgestimmte Schriftkontrolle.

Text-Management

Mit dem Java-2D-API erhalten Sie eine erweiterte Font-Klasse, die erheblich weitergehende Möglichkeiten zur Kontrolle von Schrifarten bereitstellt als die originale Klasse (der Vorgängerversionen) zum Verwalten der Schriftart. Dazu zählen die Spezifikation von detailierten Fontinformationen sowie deren Zugriff.

Ein String wird durch die enthaltenen Zeichen festgelegt, aber es gibt natürlich noch andere Informationen über ihn - etwa die ausgewählte Schriftart. Diese Vereinigung des Zeichens selbst und seinem exakten Aussehen wird Glyph genannt. Wie auch immer, ein String lässt sich auf Ebene der einzelnen Zeichen auf vielfältige Art und Weise manipulieren:

  • verschiedene Formatierungen
  • Ausrichtung
  • Position
  • Schriftart
  • Metrik
  • Größe
  • Umrisse

Jedes Font-Objekt beinhaltet diese Attribute, die man direkt über die Methoden ansprechen kann, die von der Font-Klasse zur Verfügung gestellt werden. Die Font-Klasse erlaubt zusätzlich den Zugriff auf Metrik- und Umrissinformationen über die getGlyphMetrics() und getGlyphOutline()-Methoden. Die Form, die durch getGlyphOutline() zurückgegeben wird, ist skaliert, indem die Fontgröße und -transformation verwendet wird, aber reflektiert nicht die Transformation, die einem Graphics2D-Objekt zugeordnet ist.

Zugriff auf Textpfade

Über die Font.getGlyphOutline()-Methode können Sie beliebig auf die Form von jedem Glyph in einem Font zugreifen. Die StyledString-Klasse stellt Ihnen darüber hinaus die getStringOutline()-Methode zur Verfügung, die Konversion von einem Block von Text in einen Umriss vereinfacht.

Text-Transformation

Über die Font.deriveFont()-Methode können Sie ein neues Font-Objekt mit veränderten Attributen erstellen. Wir werden als Beispiel einen Text um einen zentralen Punkt rotieren lassen.

import java.awt.*;
import java.applet.*;
import java.awt.image.*;
import java.awt.geom.*;
import java.awt.font.*;
public class DrehSchrift extends Applet {
public void paint(Graphics g) {
Graphics2D g2d = (Graphics2D) g;
// Transformation für den font.
AffineTransform fontAT = new AffineTransform();
fontAT.setToScale(72.0, 72.0);
// Beschreibung des font und instanziere ihn
Font theFont = new Font("Helvetica", Font.PLAIN, 50);
g2d.setColor(Color.blue);  // Setzen der Farbe
g2d.setFont(theFont);  // Setzen der Schriftart
Font theDerivedFont = theFont.deriveFont(fontAT);
// Definieren der Renderingtransformation
AffineTransform at = new AffineTransform();
at.setToTranslation(300.0, 300.0);
g2d.transform(at);
at.setToRotation(Math.PI / 4.0);
// Kreiere ein String-Objekt zur Spezifizierung 
// von Text und transformiertem font.
String ss = new String("Ei gude wie");
// Zeichnen von 8 Kopien des strings
for (int i = 0; i < 8; i++) {
g2d.drawString(ss, 0.0f, 0.0f);
g2d.transform(at);
}  }  }

Abbildung 9.36:  Drehschrift

9.8.12 Farbmanagement unter Java 2D

Zur Verwaltung von Farben finden Sie in dem Java-2D-API viele Erweiterungen und vor allem Erleichterungen für die Anwendung.

Spezifizieren von Farben

Zur Anzeige einer Form in einer bestimmten Farbe benötigen Sie einen Weg, um diese Farbe beschreiben zu können. Dazu gibt es eine Vielzahl von Wegen. Wir kennen bereits das RGB-Modell (Standard). Eine Alternative ist die Verwendung der Farben Cyan, Magenta, Gelb (yellow) und Schwarz (black) in dem CMYK-Konzept. Beide Modelle - RGB und CMYK - haben spezifische Vor- und Nachteile, auf die wir nicht genauer eingehen wollen, sind aber als wesentlichste Eigenschaft weitgehend Ausgabe-unabhängig. Die verschiedenen Techniken zur Spezifizierung von Farben werden Farbraum genannt.

Das Java-2D-API referenziert auf das RGB- und das CMYK-Modell als Farbraum-Typen. Dies ist eine Erweiterung des bisherigen Modells, das nur das RGB-Modell nutzt.

Die Farbklassen von Java 2D

Die Schlüssel-Farbmanagementklasse in dem Java-2D-API ist Color. Die Color-Klasse beschreibt eine Farbe als Zusammensetzung der beteiligten Farbkomponenenten in dem speziellen Farbraum. Die Color-Klasse hat eine Vielzahl von Methoden und Konstruktoren, die den Standard-RGB-Farbraum (sRGB - Informationen dazu gibt es unter http://www.w3.org/pub/WWW/Graphics/Color/sRGB.html) unterstützen. sRGB ist der Default-Farbraum von Java 2D. In dem Java-2D-API werden auch viele Funktionalitäten, die bisher in anderen Klassen untergebracht wurden, in diese Klasse verlagert.

Eine wichtige Subklasse von Color ist die ColorSpace-Klasse. Sie besitzt u.a. Methoden zur Konvertierung von Farbkomponenenten in einen Farbraum.

9.8.13 2D-Bildbearbeitung

Bildbearbeitung beinhaltet die Manipulation von Rasterbildern. Fast jeder der bekannten Effekte von Foto-editierenden Programmen können über die Java-2D-API-Bildbearbeitungsklassen oder deren Erweiterungsklassen realisiert werden.

Das Java-2D-API beinhaltet einen Satz von Klassen, die nahezu beliebige Operationen mit Bildobjekten durchführen können. So erlaubt beispielsweise BufferedImage extends java.awt.Image, auf jedes Pixel in einem Bild zuzugreifen.

Für affine Transformationen sind diverse Klassen vorhanden. Im Wesentlichen sind es AffineTransformOp und Subklassen, BilinearAffineTransformOp und NearestNeighborAffineTransformOp, BandCombineOp, ColorConvertOp, ConvolveOp, LookupOp, RescaleOp und ThresholdOp. Diese Klassen können für die verschiedensten geometrischen Transformationen verwendet werden.

9.8.14 Offscreen-Puffer unter Java 2D

Wir haben uns im Laufe unserer Grafikbehandlung in Java bereits mit der Technik des Offscreen-Puffers auseinander gesetzt. Es handelt sich um eine Technik, mit der ein grafisches Element außerhalb des sichtbaren Bereichs eines Bildschirms (offscreen) erstellt und dann auf den Bildschirm kopiert wird. Dies ist bei sehr zeitintensiven Erstellungsvorgängen von Grafiken sehr nützlich und konnte auch schon in früheren Java-Versionen durchgeführt werden.

Das java.awt-Package erleichert die Verwendung von Offscreen-Puffern, indem es das Zeichnen in einem solchen Puffer vollkommen identisch macht mit dem Zeichnen in einem Fenster. Alle Java-2D-API-Features zum Zeichnen können wie gewohnt verwendet werden. Zum Kopieren eines Offscreen-Puffers auf den Bildschirm wird einfach drawImage() aufgerufen.

Erstellen eines Offscreen-Puffers

Der einfachste Weg zur Erstellung eines Offscreen-Puffers unter Java 2D ist die Verwendung der Component.createImage()-Methode. Diese Methode kreiert ein Offscreen-zeichenbares Bild mit den angegebenen Dimensionen. Das Bild ist undurchsichtig und hat die Vordergrund- und Hintergrundfarben der Komponente.

Die Transparenz lässt sich nicht verändern.

Manipulation von Offscreen-Bilder

Die BufferedImage-Klasse erlaubt die direkte Manipulation von Pixeln in einem Bild. Diese Klasse unterstützt eine absolute Kontrolle über ein Bild, indem sowohl das Farbmodell, die Bilddaten allgemein und das Datenlayout zu beeinflussen sind. Dies funktioniert auch ohne Einschränkung mit einem BufferedImage-Objekt, denn dieses enthält wie jedes andere Bild einen Alphachannel.

9.8.15 2D-Graphics Devices

Alle grafischen Ausgabemedien (Graphics Devices) - Monitore und Drucker - werden durch ein GraphicsDevice-Objekt repräsentiert, das alle notwendigen Informationen einschließt. Das Java-2D-API erlaubt die Abfrage sämtlicher Attribute von der Umgebung, wo eine Applikation läuft. Dazu dient die GraphicsEnvironment-Klasse sowie GraphicsDevice und ihre zugehörigen GraphicsConfiguration-Objekte.

Über die GraphicsEnvironment-Klasse haben Sie Zugriff auf alle notwendigen Informationen der Laufzeitumgebung, die eine Liste von GraphicsDevice-Objekten beinhaltet, die Ausgabegeräte repräsentieren. Diese kann man zwar abfragen, aber das ist so gut wie nie notwendig.

Die GraphicsDevice-Klasse wird vom Java-2D-API zur Repräsentation eines aktuellen Ausgabegeräts verwendet. Normalerweise werden Sie auf diese Klasse nie zugreifen müssen. GraphicsDevice-Objekte werden standardmäßig mit dem aktuellen Ausgabegerät verbunden. Jedes GraphicsDevice-Objekt hat eine Liste von GraphicsConfiguration-Objekten zugeordnet. Sie können eine Referenz auf ein GraphicsDevice-Objekt entweder von GraphicsEnvironment oder GraphicsConfiguration bekommen, indem Sie die Methode getDeviceConfiguration() aufrufen.

Um eine Liste von den GraphicsConfiguration-Objekten zu bekommen, die den jeweiligen GraphicsDevice zugeordnet sind, können Sie analog GraphicsDevice.getConfigurations() verwenden.

9.9 Die Java-2D-API-Referenz

Alle Klassen in den folgenden Paketen sind Teile des Java-2D-API:

 java.awt.color 
 java.awt.font 
 java.awt.geom 
 java.awt.print 
 com.sun.image.codec.jpeg 
 java.awt.image.renderable

Zusätzlich werden einige Klassen in den java.awt und java.awt.image-Packages dazugezählt:

java.awt:

 java.awt.AlphaComposite 
 java.awt.BasicStroke 
 java.awt.Color 
 java.awt.Composite 
 java.awt.CompositeContext 
 java.awt.Font 
 java.awt.GradientPaint 
 java.awt.Graphics2D 
 java.awt.GraphicsConfiguration 
 java.awt.GraphicsDevice 
 java.awt.GraphicsEnvironment 
 java.awt.Paint 
 java.awt.PaintContext 
 java.awt.Rectangle 
 java.awt.Shape 
 java.awt.Stroke 
 java.awt.TexturePaint 
 java.awt.Toolkit 
 java.awt.Transparency

java.awt.image:

 java.awt.image.AffineTransformOp 
 java.awt.image.BandCombineOp 
 java.awt.image.BandedSampleModel 
 java.awt.image.BufferedImage 
 java.awt.image.BufferedImageFilter 
 java.awt.image.BufferedImageOp 
 java.awt.image.ByteLookupTable 
 java.awt.image.ColorConvertOp 
 java.awt.image.ColorModel 
 java.awt.image.ComponentColorModel 
 java.awt.image.ComponentSampleModel 
 java.awt.image.ConvolveOp 
 java.awt.image.DataBuffer 
 java.awt.image.DataBufferByte 
 java.awt.image.DataBufferInt 
 java.awt.image.DataBufferShort 
 java.awt.image.DirectColorModel 
 java.awt.image.IndexColorModel 
 java.awt.image.Kernel 
 java.awt.image.LookupOp 
 java.awt.image.LookupTable 
 java.awt.image.MultiPixelPackedSampleModel 
 java.awt.image.PackedColorModel 
 java.awt.image.Raster 
 java.awt.image.RasterformatException 
 java.awt.image.RasterOp 
 java.awt.image.RenderedImage 
 java.awt.image.RescaleOp 
 java.awt.image.SampleModel 
 java.awt.image.ShortLookupTable 
 java.awt.image.SinglePixelPackedSampleModel 
 java.awt.image.WritableRaster

9.10 Sound und Töne in Java

Zum Abschluss wollen wir uns noch mit den akustischen Möglichkeiten von Java beschäftigen. Diese sind meist als Spezialfall einer allgemeinen Multimedia-Animation zu verstehen. Java gibt Ihnen jedenfalls die Mittel zum Einsatz.

Unterstützt wurde ursprünglich nur das AU-Format. Dies ist ein extrem hochkomprimiertes Klangformat, das leider nicht allzu gute Tonqualität liefert. Allerdings konnten Sie schon länger bei Applets über Verknüpfungen zu externen Dateien auch andere Klangformate einbinden. Unter der aktuellen Java-Version wird die Audio-Unterstützung erweitert. Sowohl in Applikationen als auch in Applets lassen sich nun MIDI-Dateien (Typ 0 und Typ 1) sowie RMF-, WAVE-, AIFF-, und AU-Dateien in hoher Tonqualität abspielen und mischen.

Das Abspielen von Sound wird mit Mitteln bewerkstelligt, die wie die Unterstützung von Bildern in den Applet- und awt-Klassen integriert sind und wie gesagt als Spezialfall einer Multimedia-Animation anzusehen sind. Die Methoden und deren Syntax ähneln sehr stark der Situation bei Bildern und Animationen. Wir schauen uns nur den Applet-Fall an.

Sie können einem Applet jederzeit Klang hinzufügen, indem Sie die getAudioClip()-Methode verwenden, die zur Applet-Klasse gehört. Es gibt zwei Varianten:

public AudioClip getAudioClip(URL url):
public AudioClip getAudioClip(URL url, String  name)

Dies ist wieder die gleiche Situation wie bei der getImage()-Methode. Mit einem Argument (ein Objekt vom Typ URL) ist es die einfachere Variante mit einer hartcodierten Pfadangabe. Die zweite Variante mit den zwei Argumenten (ein Objekt vom Typ URL und eine Zeichenkette) lässt sich in Verbindung mit den zwei weiteren - uns bekannten - Methoden getDocumentBase() und getCodeBase() - Bestandteile der Applet-Klasse - äußerst anpassungsfähig einsetzen.

Beispiel:

Audioclip clip = getAudioClip(getDocumentBase(), "audio/history.au");

Kann Java eine angegebene Datei nicht finden, gibt die Methode getAudioClip() Null aus.

9.10.1 Das Abspielen und Stoppen eines Audio-Clips - play()

Sie können einen Audio-Clip mit der play()-Methode abspielen. Auch hier schauen wir uns die Methode der Applet-Klasse (Applet.play()) an. Es gibt wieder zwei Varianten:

public void play(URL  url)
public void play(URL  url, String name)

Wenn eine Klangdatei an der angegebenen URL nicht zu finden ist, passiert einfach nichts.

Das Anhalten eines Audio-Clips erfolgt über die stop()-Methode. Das permanente Wiederholen eines Audio-Clips erfolgt über die loop()-Methode.

Tonwiedergabe ist ein ideales Einsatzgebiet für Multithreading. Besonders, wenn die Tondatei in einer Endlosschleife wiedergegeben wird.

9.10.2 Die Sound-Möglichkeiten der JDK-Demos

Mit dem JDK 1.3 werden wunderbare Demo-Applikationen mitgeliefert, die sich nach einer vollständigen Installation in dem Verzeichnis demo befinden. In dessen Unterverzeichnis sound befindet sich zu den wirklich sehr umfangreichen Multimediamöglichkeiten von Java ein exzellentes Demo, das man mit java -jar JavaSound.jar starten kann und das eine Basis für interessante Experimente bieten sollte. Besonders wichtig - der Quellcode des Demos liegt im Unterverzeichnis src bei und ist eine wahre Fundgrube, die für eigene Zwecke ausgeschlachtet werden kann.

Abbildung 9.37:  Die Demo-Quelltexte

Abbildung 9.38:  Die Quelltexte bieten sich als ideale Studientexte für eigene Soundexperimente an.

Aber auch das reine Experimentieren mit dem Programm ist eine äußerst kurzweilige Angelegenheit, die die Motivation für eigene Programmierexperimente mit diesen genialen Techniken sicher steigert1. Das Demo zeigt in vier Registerblättern, wie Java mit Sound umgehen kann. Da ist einmal die Juke Box, die Musikdateien unterschiedlichster (oben beschriebener) Formate abspielen kann.

Abbildung 9.39:  Die Juke Box

Das zweite Registerblatt (und natürlich der dazu vorhandene Quelltext) zeigt die Möglichkeit der Aufnahme von Sounds.

Abbildung 9.40:  Die Aufnahme von Sounds

Das dritte Registerblatt demonstriert den Umgang mit MIDI über ein virtuelles Keyboard, das sogar mit reinem MouseOver bedient werden kann. Dazu kann man verschiedene Instrumente und Kanäle auswählen und all das machen, was mit einem MIDI-Keyboard möglich sein sollte.

Abbildung 9.41:  Das mit der Maus zu bedienende virtuelle MIDI-Keyboard

Ein Drumsynthesizer fehlt auch nicht.

Abbildung 9.42:  Der Drumsynthesizer

9.10.3 Das Java Media Framework

Die Soundmöglichkeiten von Java sind aber in der Java-2-Plattform nicht nur auf solche Basismöglichkeiten des JDK beschränkt. Multimedia allgemein (also auch wieder über reine Sounds hinausgehend) nimmt in der Java-2-Plattform großen Raum ein. Ein wesentlicher Bestandteil der Multimediafähigkeit von Java steckt in eine eignem API der Java-2-Plattform - dem Java Media Framework (JMF), das eine sehr komfortable Verbindung von verschiedenen Mediatypen mit Java-Applets und -Applikationen erlaubt. Das JMF-API unterstützt eine einfache und vereinheitlichte Synchronisation, Kontrolle, Verarbeitung und Präsentation von komprimierten, Zeit-basierenden Mediadaten. Dies beinhaltet sowohl Javastreams als auch MIDI, Audio und Video auf allen Java-Plattformen.

Das erste Framework 1.0 wurde von Sun, Silicon Graphics und Intel entwickelt und unter dem Namen Java Media Player bekannt. JMF 1.0 wurde durch die Folgeversion 1.1 u.a. um Screenshotfähigkeiten, Dateispeichern, RTP-Broadcast und der Manipulation von Mediadaten erweitert. Das aktuelle JMF 2 unterstützt MPEG-1 Layer 3 und bietet eine pluggable Codec-Architektur. Codec ist die Abkürzung für compressed und decompressed (co/dec), also die Kompression und Dekompression von Mediadaten, die für eine zeitkritische Verwendung im Internet unabdingbar sind. Das JMF 2.0 ist unter http://java.sun.com/products/java-media/jmf/ zu laden.

Abbildung 9.43:  Download des Java Media Frameworks

9.11 Zusammenfassung

Bereits in der ersten Version stellte Java eine Vielzahl von Techniken zur Verfügung. Diese basieren im Wesentlichen auf java.awt.Graphics bzw. sind Bestandteil der Image-Klasse, die auch zum Paket java.awt gehört. Darunter sind alle grundlegenden Grafikvorgänge und Fundamente für Animationen zu finden, die auch die Basis für die neuen Grafiktechniken sind. Die neuen Grafikmöglichkeiten werden noch einige Zeit nicht in allen Darstellungplattformen - insbesondere den Browsern - wiedergegeben werden können. Festhalten kann man aber, dass es unter der neuen JFC für sämtliche der Methoden, die java.awt.Graphics bzw. die Image-Klasse nutzen, einen Ersatz gibt und dessen Anwendung sich meist direkt ableiten lässt.

Das Java-2D-API erweitert die Grafikmöglichkeiten von Java um zahlreiche Möglichkeiten zur Behandlung von komplexen Formen, Texten und Bildern. Mit den Java-2D-API-Klassen, können Sie 2D-Grafiken in hoher Qualität, Text und Bilder in Ihren Applikationen und Applets vereinigen. Das Java-2D-API erlaubt die Ausgabe von Grafiken in hoher Qualität und Auflösung - unabhängig von dem physikalischen Ausgabegerät, stellt erweiterte Schriftarten- und Textunterstützung zur Verfügung und unterstützt ein einfaches und dennoch umfassendes Übersetzungsmodell.

1

Und Freizeitmusiker wie den Autor von der eigentlichen Arbeit abhält ;-).


© Copyright Markt+Technik Verlag, ein Imprint der Pearson Education Deutschland GmbH
Elektronische Fassung des Titels: Java 2 Kompendium, ISBN: 3-8272-6039-6 Kapitel: 9 Grafik und Animation