Programmieren in Java: Aufbau


Kapitel 3: Grafik und Sound


Inhaltsverzeichnis

Dieses Buch ist unter einer Creative Commons-Lizenz lizensiert.


3.1 Allgemeines

Eingeschränkte Multimedia-Fähigkeiten, 2D-Zeichnungen und Sound-Ausgabe

In diesem Kapitel lernen Sie, wie Sie Multimedia-Effekte in Java programmieren. Was sich hochtrabend anhört ist in Wirklichkeit recht bescheiden - jedenfalls in Java 1.1. Machen Sie sich darauf gefasst, dass die Möglichkeiten zur Grafik- und Sound-Ausgabe sehr dürftig sind.

Seit Java 1.2 gibt es die Java 2D API - eine Schnittstelle, um ansprechende 2-dimensionale Grafiken anzuzeigen. Die zentrale Klasse in Java 2D ist java.awt.Graphics2D. In Java 1.1, auf das viele Jahre lang Applet-Programmierer wegen der hohen Verbreitung dieser Version in Browsern angewiesen waren, war diese Schnittstelle zur Grafik-Programmierung leider nicht enthalten.

Was die Sound-Ausgabe betrifft ist Java 1.3 einen gehörigen Schritt weiter als die Vorgängerversionen. Seit Java 1.3 ist es möglich, Sound-Dateien verschiedener Formate abzuspielen. Programmierer, die mit Java-Versionen vor 1.3 arbeiteten, können nur Sound-Dateien im AU-Format laden - ein Format, das aus der Sun-Welt stammt und unter Microsoft Windows kaum verwendet wird. Das Abspielen wichtiger Dateiformate wie WAV oder MIDI blieb dem Java-1.1- wie auch dem Java-1.2-Entwickler vorenthalten.

Java 1.3 hat unter anderem die beiden Pakete java.sound.sampled und java.sound.midi eingeführt, in der die für die Sound-Bearbeitung und -Ausgabe notwendigen Klassen liegen. Unterstützt werden zum Beispiel die Formate AIFF, AU, WAV und MIDI.

Insgesamt bleibt festzuhalten, dass Java-1.1-Entwicklern nur sehr wenige Möglichkeiten zur Verfügung standen, Multimedia-Effekte einzusetzen. Das, was technisch in Java 1.1 möglich war, wird Ihnen im Folgenden vorgestellt. Gegen Ende des Kapitels erhalten Sie eine detaillierte Einführung in Java 2D.


3.2 Malen im Applet

Pixel setzen

Erinnern Sie sich an die Methode paint() der Klasse java.applet.Applet? Diese Methode bekommt standardmäßig vom Java-System ein Objekt vom Typ java.awt.Graphics übergeben, über das Sie direkt auf die Oberfläche des Java-Applets zugreifen können. In vielen Beispielen aus dem Buch Programmieren in Java: Einführung wurde lediglich die Methode drawString() aufgerufen, mit der Text ins Applet hineingezeichnet werden konnte. Sehen sich jedoch einmal folgendes Beispiel an.

import java.applet.*; 
import java.awt.*; 
import java.awt.event.*; 

public class MyApplet extends Applet implements MouseMotionListener 
{ 
  Graphics MyGraphics; 

  public void init() 
  { 
    MyGraphics = getGraphics(); 
    addMouseMotionListener(this); 
  } 

  public void mouseDragged(MouseEvent ev) 
  { 
    MyGraphics.drawLine(ev.getX(), ev.getY(), ev.getX(), ev.getY()); 
  } 

  public void mouseMoved(MouseEvent ev) { } 
} 

Sie erhalten nicht nur innerhalb der Methode paint() Zugriff auf das Objekt, das die Oberfläche des Applets repräsentiert, sondern auch, indem Sie die Methode getGraphics() aufrufen. Diese wird durch Vererbung von der Klasse java.awt.Component an die Klasse java.applet.Applet weitergegeben. Im obigen Beispiel wird das Objekt in der Referenzvariablen MyGraphics gespeichert.

Im Beispiel wird das Interface java.awt.event.MouseMotionListener implementiert. Damit kann das Applet Ereignisse abfangen, die eintreten, wenn die Maus bewegt wird. mouseDragged() wird aufgerufen, wenn der Anwender klickt und dann mit gehaltener Taste die Maus bewegt. mouseMoved() wird ausgeführt, wenn der Anwender die Maus bewegt, ohne eine Taste gedrückt zu halten.

Wenn der Anwender eine Maustaste drückt und dann die Maus bewegt, wird innerhalb der Methode mouseDragged() auf das Objekt zugegriffen, das die Oberfläche des Applets repräsentiert, und mit der Methode drawLine() eine Linie in das Applet hineingezeichnet. Anfangs- und Endpunkt der Linie werden dort festgelegt, wo sich die Maus momentan befindet - genaugenommen wird also keine Linie gezeichnet, sondern ein Punkt gesetzt. Die Koordinaten des Anfangs- und Endpunkts sind ja die gleichen.

Wenn Sie das Beispiel ausführen, klicken Sie mit der Maus ins Applet und bewegen Sie sie dann mit gehaltener Maustaste. Sie sehen, es handelt sich um eine kleines Malprogramm.

Die Klasse java.awt.Graphics stellt eine ganze Reihe an Methoden zur Verfügung, mit denen Sie einfache geometrische Figuren zeichnen können.

  • draw3DRect() zeichnet ein 3-dimensionales Rechteck. Sie können Ort und Größe des Rechtecks über Parameter angeben. Außerdem legen Sie über einen weiteren Paramter fest, ob das Rechteck hervorstehen oder in der Oberfläche versunken sein soll.

  • fill3DRect() zeichnet ein 3-dimensionales Rechteck wie draw3DRect(), füllt jedoch das Rechteck mit der momentanen Kontextfarbe aus.

  • drawArc() zeichnet einen Kreisbogen. Sie können Ort, Größe und genaue Form über Parameter festlegen.

  • fillArc() zeichnet einen Kreisbogen wie drawArc() und füllt ihn mit der momentanen Kontextfarbe aus.

  • drawLine() zeichnet eine Linie in der aktuellen Kontextfarbe. Sie geben als Parameter die Koordinaten des Anfangs- und Endpunktes an.

  • drawOval() zeichnet einen Kreis oder ein Oval. Sie können Ort und Größe über Parameter festlegen.

  • fillOval() zeichnet wie drawOval() einen Kreis oder ein Oval, nur dass er in der aktuellen Kontextfarbe ausgefüllt wird.

  • drawRect() zeichnet ein Rechteck. Sie geben als Parameter Ort und Größe des Rechtecks an.

  • fillRect() zeichnet wie drawRect() ein Rechteck, nur dass es diesmal mit der aktuellen Kontextfarbe ausgefüllt wird.

  • clearRect() löscht ein Rechteck. Sie geben als Parameter Ort und Größe des zu löschenden Rechtecks an. Der Löschvorgang sieht so aus, dass einfach ein neues Rechteck in der momentanen Hintergrundfarbe des Kontexts gezeichnet und damit andere gezeichnete Figuren übermalt werden.

  • drawRoundRect() zeichnet ein Rechteck mit abgerundeten Ecken. Sie geben Ort, Größe und Form der Ecken als Parameter an.

  • fillRoundRect() zeichnet wie drawRoundRect() ein Rechteck mit abgerundeten Ecken, füllt es jedoch mit der aktuellen Kontextfarbe.

  • drawString() zeichnet einen Text, den Sie als Objekt vom Typ java.lang.String übergeben. Die Koordinaten, an denen der Text startet, werden ebenfalls als Parameter der Methode übergeben.

  • setClip() blendet ein rechteckiges Fenster ein, dessen Ort und Größe Sie über Parameter festlegen. Zeichen-Operationen werden nur dann sichtbar, wenn sie ganz oder teilweise innerhalb dieses Fensters stattfinden. Werden geometrische Formen außerhalb dieses Fensters gezeichnet, werden diese nicht sichtbar. Bei diesem Fenster handelt es sich also um eine Art Ausschnitt. Es handelt sich hierbei nicht um ein sichtbares Fenster, wie Sie es von Windows-Anwendungen her kennen.

Während es sich beim Zugriff auf ein Objekt vom Typ java.awt.Graphics um eine Art Low-Level-Programmierung handelt, gehört das Arbeiten mit AWT- oder den Swing-Klassen zum Aufbau von Benutzerschnittstellen zur High-Level-Programmierung. Grundsätzlich arbeiten beide Techniken gut zusammen. Dennoch könnte es unliebsame Überraschungen geben, wenn sich beide Systeme direkt überschneiden. Hier ist Ausprobieren und Testen angesagt.


3.3 Bilder anzeigen

Grafiken laden und einblenden

Um Grafiken in einem Java-Applet anzuzeigen, muss die entsprechende Datei erstmal vom Applet geladen werden. Das allseits bekannte Problem ist, dass Ladezeiten im Internet mitunter nicht gerade berauschend sind. Und je größer die Grafikdatei, die im Applet eingeblendet werden soll, umso länger dauert der Ladevorgang. Damit das Applet während der gesamten Ladedauer der Datei nicht stillsteht, gibt es eine spezielle Klasse java.awt.MediaTracker. Man erstellt ein Objekt vom Typ dieser Klasse und beauftragt das Objekt, eine oder mehrere Grafiken zu laden. In der Zwischenzeit kann das Applet Initialisierungsprozesse durchführen und andere Arbeiten erledigen. Das heißt, das Applet kann parallel zum Laden der Grafiken weiterlaufen.

So schön sich die Theorie anhört - die Praxis erfordert tiefergehende Kenntnisse bezüglich Threads. In diesem Zusammenhang spielt das Interface java.lang.Runnable eine wichtige Rolle. Threads gehören zum Kompliziertesten, was die Entwicklung von Anwendungen zu bieten hat, und können daher im Rahmen dieses Buchs nicht näher erläutert werden. Das folgende Beispiel demonstriert daher den Einsatz der Klasse java.awt.MediaTracker ohne Verwendung von Threads. Das bedeutet, dass das Applet bei einer sehr großen Datei während der Ladezeit wartet und keine anderen Arbeiten erledigen kann. Eine Lösung in Form von Threads wird wie gesagt vom Java-System angeboten, sprengt jedoch den Rahmen dieses Buchs.

import java.applet.*; 
import java.awt.*; 

public class MyApplet extends Applet 
{ 
  Image MyImage; 

  public void init() 
  { 
    MediaTracker MyTracker = new MediaTracker(this); 
    MyImage = getImage(getCodeBase(), "highscore.jpg"); 
    MyTracker.addImage(MyImage, 0); 
    try 
    { 
      MyTracker.waitForAll(); 
    } catch (InterruptedException ex) { } 
  } 

  public void paint(Graphics g) 
  { 
    int x = (getSize().width - MyImage.getWidth(this)) / 2; 
    int y = (getSize().height - MyImage.getHeight(this)) / 2; 
    g.drawImage(MyImage, x, y, this); 
  } 
} 

Wenn Sie obiges Beispiel ausführen, wird das Highscore-Logo zentriert innerhalb des Java-Applets eingeblendet.

Innerhalb der Methode init() wird ein Objekt vom Typ java.awt.MediaTracker angelegt. Dem Konstruktor wird das Schlüsselwort this übergeben, um anzudeuten, dass das Java-Applet selber diejenige Komponente ist, die auf die Grafik wartet und auf der die Grafik nachher gezeichnet werden soll.

Obwohl die Klasse java.awt.MediaTracker heißt, können Sie mit ihr übrigens nur Grafiken laden. Der Name der Klasse ist jedenfalls etwas irritierend, nachdem eben nicht verschiedene Medien geladen werden können. Grafiken können vom Typ JPG oder GIF sein.

Nachdem das Objekt erzeugt wurde, wird die Methode getImage() aufgerufen. Diese Methode ist innerhalb der Klasse java.applet.Applet definiert. Ihr werden im Beispiel zwei Parameter übergeben: Zum einen der Rückgabewert der Methode getCodeBase(), der ein Objekt vom Typ java.net.URL ist, zum anderen ein Dateiname als String. Diese beiden Parameter ergeben zusammengesetzt den absoluten Pfad auf eine Grafik. Der Dateiname lautet highscore.jpg, und der Pfad ist gleichbedeutend mit dem Pfad der Klassendatei. Genau diesen gibt nämlich die Methode getCodeBase() zurück. Es wird also auf eine Datei zugegriffen, die im gleichen Verzeichnis liegt wie die Datei MyApplet.class.

getImage() gibt ein Objekt vom Typ java.awt.Image zurück, das in der Eigenschaft MyImage gespeichert wird. Es liegt also jetzt eine Referenzvariable auf eine Grafik vor, nur ist diese Grafik noch nicht geladen. Das übernimmt der MediaTracker.

Über den Aufruf der Methode addImage() wird der MediaTracker darauf vorbereitet, diese Grafik zu laden. Der zweite Parameter ist ein Wert vom Typ int. Er stellt einen ID-Wert dar, über den beispielsweise jederzeit überprüft werden kann, ob eine bestimmte Grafik bereits komplett geladen ist. Es ist nämlich kein Problem, die Methode addImage() öfter aufzurufen, so dass der MediaTracker mehrere Bilder gleichzeitig laden wird. Indem jedem Bild ein anderer ID-Wert zugewiesen wird, können später die Ladezustände einzelner Bilder überprüft werden.

Während die Methode addImage() den MediaTracker nur vorbereitet, startet nun der Aufruf waitForAll() den Ladevorgang tatsächlich. Das heißt, der MediaTracker lädt nun alle Grafiken, die ihm zuvor mit addImage() gemeldet worden sind. waitForAll() kehrt erst dann zurück, wenn alle Grafiken vollständig geladen wurden. Je nachdem, wie groß die Grafiken sind, kann nun also eine gewisse Zeit vergehen. Der Trick mit den Threads ist, dass man den Aufruf dieser Methode in einen anderen Thread legt, um im ursprünglichen Thread weiterarbeiten zu können. Während der eine Thread auf die Rückkehr der Methode waitForAll() wartet, kann das Applet im anderen Thread munter weiterarbeiten. Obiges Beispiel verwendet jedoch keinen Thread, so dass das Applet darauf wartet, dass waitForAll() irgendwann zurückkehrt.

Ist die Grafik vom MediaTracker geladen worden, passiert auch schon nichts mehr in init(). Wie von Applets gewohnt wird nach der Initialisierung paint() aufgerufen, damit sich das Applet neu zeichnet. Innerhalb von paint() wird die Koordinate errechnet, an der die linke obere Ecke der Grafik im Applet liegen soll. Nachdem die Grafik zentriert ausgerichtet werden soll, wird die genaue Position errechnet, indem man von der Höhe und Breite des Applets jeweils die Höhe und Breite der Grafik subtrahiert und dann durch 2 dividiert.

Zuguterletzt wird die Methode drawImage() für das Objekt aufgerufen, das die Oberfläche des Applets repräsentiert. Mit dieser Methode wird nun tatsächlich die Grafik sichtbar, die als erster Parameter beim Aufruf angegeben wird. Der zweite und dritte Parameter sind die Koordinaten, an denen die linke obere Ecke der Grafik liegen soll. Der vierte Parameter ist ein Objekt vom Typ java.awt.image.ImageObserver. Es handelt sich hierbei um ein Interface. Im Beispiel wird das Schlüsselwort this angegeben, obwohl die Klasse MyApplet das Interface gar nicht implementiert. Das muss sie auch nicht, da dies eine der Elternklasse, nämlich java.awt.Component, schon getan hat. Wenn Sie dafür sorgen, dass zum Zeitpunkt des Malvorgangs die komplette Grafik bereits geladen ist, müssen Sie sich über den vierten Parameter und das Interface java.awt.image.ImageObserver keine Gedanken machen.

Als abschließender Tipp: Die Klasse java.awt.Graphics bietet eine Methode an, die ebenfalls drawImage() heißt, jedoch zwei weitere Parameter vom Typ int erwartet. Über diese Parameter kann die Größe der Grafik festgelegt werden. Die Grafik wird hierbei automatisch gestreckt oder gestaucht, so dass sie den vorgegebenen Platz komplett ausfüllt und nicht überschreitet.


3.4 Sound abspielen

Java für den Lautsprecher

Wenn Sie die Möglichkeiten der Grafik-Programmierung in Java 1.1 bisher nicht so berauschend fanden, dann wird Ihnen die Vorstellung der Sound-Fähigkeiten auch nicht gefallen. Aber immerhin können Sie als Applet-Programmierer überhaupt Sound abspielen. Der Entwickler von Java-Applications kann dies nämlich nicht. Die Sound-Ausgabe hängt direkt mit der Klasse java.applet.Applet zusammen, und die wird bekanntlich nur von Applet-Programmierern verwendet. Programmieren Sie eine Anwendung, die außerhalb eines Browsers läuft, in Java 1.1, können Sie keinen Sound abspielen.

Applet-Programmierer können zwar Sounds in ihre Anwendung miteinbeziehen, aber freuen Sie sich nicht zu früh: Sie können in Java 1.1 ausschließlich Sound-Dateien im AU-Format spielen. Nachdem dieses Format unter Microsoft Windows kaum genutzt wird, benötigen Sie wahrscheinlich Konverter, um Ihre Sounds in das AU-Format umwandeln und dann in Ihrem Java-Applet abspielen zu können.

Das AU-Format hat leider noch mehr Nachteile: Die Qualität des Sounds ist sehr schlecht verglichen mit anderen Sound-Formaten. Und die Dateien selber sind auch nicht gerade klein, beanspruchen also zusätzliche Downloadzeit und die Nerven des Anwenders. Die Verwendung von Sound in Java-1.1-Applets ist also grundsätzlich sehr fraglich.

Möchten Sie Ihr Applet dennoch mit Sound aufpeppen, gehen Sie wie im folgenden Beispiel vor. Das Beispiel bedient sich einer Sound-Datei, die dem Java Tutorial von Sun Microsystems entliehen wurde.

import java.applet.*; 
import java.awt.*; 
import java.awt.event.*; 

public class MyApplet extends Applet implements ActionListener 
{ 
  AudioClip Space; 
  Button Play; 

  public void init() 
  { 
    Space = getAudioClip(getCodeBase(), "space.au"); 
    Play = new Button("Sound abspielen"); 
    Play.addActionListener(this); 
    add(Play); 
  } 

  public void actionPerformed(ActionEvent ev) 
  { 
    Space.play(); 
  } 
} 

Um eine Sound-Datei abspielen zu können, muss diese erst geladen werden. Das übernimmt die Methode getAudioClip() der Klasse java.applet.Applet. Dieser Methode werden im Beispiel zwei Parameter übergeben, und zwar ein Objekt vom Typ java.net.URL, das von der Methode getCodeBase() zurückgegeben wird, und ein Dateiname als String. Beide Parameter ergeben zusammen die absolute Pfadangabe auf die Sound-Datei. Vergessen Sie nicht: Obwohl von Sound-Dateien die Rede ist und die Methode getAudioClip() aussieht, als können Sie beliebige Sound-Dateien laden, müssen Sie in Java 1.1 Sound im AU-Format verwenden.

getAudioClip() gibt ein Objekt vom Typ java.applet.AudioClip zurück. Es handelt sich hierbei um ein Interface. Das Objekt wird in einer Referenzvariablen gespeichert, die eine Eigenschaft der Klasse MyApplet ist. Mit dem Aufruf von getAudioClip() startet übrigens gleichzeitig der Ladevorgang der Sound-Datei.

Innerhalb von init() wird eine Schaltfläche vom Typ java.awt.Button eingefügt. Bei einem Mausklick wird die Methode actionPerformed() ausgeführt, in der auf das Objekt vom Typ java.applet.AudioClip zugegriffen und die Methode play() aufgerufen wird. Damit wird die entsprechende Sound-Datei genau einmal abgespielt. Vergessen Sie nicht, den Lautsprecher anzumachen, wenn Sie obiges Beispiel ausführen.

Das Interface java.applet.AudioClip stellt folgende drei Methoden zur Verfügung.

  • play() spielt eine Sound-Datei genau einmal ab.

  • loop() spielt eine Sound-Datei wiederholt ab, und zwar unendlich oft.

  • stop() stoppt eine Sound-Datei, die wiederholt abgespielt wird.

Es gibt übrigens noch eine andere Möglichkeit, Sound-Dateien abzuspielen. Die hat jedoch den entscheidenden Nachteil, dass jedes Mal, wenn der Sound gespielt werden soll, die entsprechende Datei neu geladen werden muss. Daher wird in diesem Kapitel nur der Zugriff über das Interface java.applet.AudioClip vorgestellt, weil dies die eindeutig bessere Methode zum Abspielen von Sound ist.


3.5 Java 2D

2-dimensionales Paradies

Mit Veröffentlichung der Java-Version 1.2 wurde eine ganze Reihe neuer Klassen eingeführt, die zusammen die sogenannte Java 2D API bilden. Die Klassen der Java 2D API ermöglichen den Einsatz stark verbesserter Zeichenoperationen, um 2-dimensionale Grafiken zu erstellen. Während Sie bis einschließlich Java 1.1 wie im Kapitel bereits vorgestellt lediglich Linien und ein paar einfache geometrische Figuren zeichnen können, haben Sie dank Java 2D ab Java 1.2 eine sehr viel größere Flexibilität: So können Sie zum Beispiel nicht mehr nur die Farbe einer Linie einstellen, sondern auch die Dicke. Sie können angeben, ob die Linie durchgezogen gezeichnet werden soll oder aber gestrichelt, gepunktet oder ein beliebiges anderes Muster erzeugt werden soll. Sie können definieren, ob die Endpunkte von Linien abgerundet oder eckig sein sollen. Sie können sogar festlegen, ob Funktionen wie Anti-Aliasing aktiviert werden sollen, wodurch Grafiken weicher gezeichnet und Übergänge optimiert werden.

In Java 2D ist java.awt.Graphics2D die zentrale Klasse. Diese Klasse ist von java.awt.Graphics abgeleitet und definiert zahlreiche neue Methoden. Im Folgenden wird das aus Abschnitt 3.2, „Malen im Applet“ vorgestellte Beispielprogramm so umgeschrieben, das es auf Java 2D basiert. Außerdem wird nicht mehr mit den AWT-Klassen gearbeitet, sondern Swing eingesetzt. Da Swing ab Java 1.2 zur Verfügung steht, werden die fortgeschrittenen Klassen für Java 2D genutzt, obwohl Java 2D auch ohne Probleme mit AWT-Klassen zusammenarbeitet. Wenn Sie Java 2D einsetzen wollen, heißt das also nicht, dass Sie Swing einsetzen müssen.

import javax.swing.*; 
import java.awt.*; 
import java.awt.event.*; 
import java.awt.geom.*; 

public class MyApplet extends JApplet implements MouseMotionListener 
{ 
  Graphics2D MyGraphics2D; 

  public void init() 
  { 
    MyGraphics2D = (Graphics2D)getGraphics(); 
    addMouseMotionListener(this); 
  } 

  public void mouseDragged(MouseEvent ev) 
  { 
    MyGraphics2D.draw(new Line2D.Double(ev.getX(), ev.getY(), ev.getX() + 1, ev.getY())); 
  } 

  public void mouseMoved(MouseEvent ev) { } 
} 

Wie Sie sehen hat sich der Code nur wenig geändert. Nachdem in Java 2D die zentrale Klasse java.awt.Graphics2D ist, müssen Sie irgendwie Zugriff auf ein Objekt dieser Klasse bekommen. Sie erhalten den Zugriff über die bereits bekannte Methode getGraphics(). Die Methode getGraphics() liefert zwar genaugenommen eine Referenz auf ein Objekt vom Typ java.awt.Graphics zurück. Da java.awt.Graphics2D jedoch eine Kindklasse von java.awt.Graphics ist, kann durch Casting der Datentyp geändert werden. Indem vor den Methodenaufruf von getGraphics() die Klasse java.awt.Graphics2D in Klammern gestellt wird, wird die Rerefenz, die getGraphics() zurückgibt, vom Typ java.awt.Graphics in den Datentyp java.awt.Graphics2D umgewandelt. Diese Datentypumwandlung wird als Casting bezeichnet. Somit steht nun über die Referenzvariable MyGraphics2D ein Zugriff auf die Oberfläche des Java-Applets zur Verfügung, was die Nutzung der Java-2D-Schnittstelle zuläßt.

Die Klasse java.awt.Graphics2D bietet letztendlich vier wichtige Methoden an, um beliebige 2-dimensionale Zeichnungen zu erstellen.

  • draw() zeichnet geometrische Figuren, indem nur Linien erstellt werden und Flächen leer bleiben.

  • fill() zeichnet geometrische Figuren, indem Linien erstellt und zusätzlich Flächen der geometrischen Figuren ausgemalt werden.

  • drawString() gibt eine Zeichenkette innerhalb einer Oberfläche an einer bestimmten Position aus.

  • drawImage() zeichnet eine Grafik innerhalb einer Oberfläche an einer bestimmten Position.

Im obigen Beispiel wird innerhalb der Methode mouseDragged() die Methode draw() der Klasse java.awt.Graphics2D aufgerufen. Dieser Methode muss nun ein Objekt einer Klasse übergeben werden, die eine geometrische Figur beschreibt. Java 2D definiert zahlreiche geometrische Figuren im Paket java.awt.geom. Dort ist unter anderem eine Klasse Line2D.Double definiert. Diese Klasse wird verwendet, um eine ganz normale Linie zu zeichnen. Sie müssen nun lediglich ein Objekt der Klasse Line2D.Double erstellen und dem Konstruktor entsprechende Koordinatenpositionen des Start- und Endpunktes der Linie übergeben. Wenn Sie das Objekt dann an draw() weitergeben, wird die entsprechende Linie gezeichnet.

Nachdem auch in diesem Beispiel lediglich Punkte und keine Linien gezeichnet werden sollen, werden als Start- und Endpunkt der Linie die Koordinaten des Mauszeigers übergeben. Jedoch wird dem x-Wert des Endpunktes der Wert 1 hinzuaddiert. Der Grund ist der, dass eine Linie in Java 2D nur dann gezeichnet wird, wenn sich der Start- und Endpunkt unterscheiden. Fallen beide Punkte zusammen, sehen Sie nichts. Sie könnten natürlich auch ohne Probleme den Wert 1 einem anderen Parameter des Konstruktors hinzuaddieren - Hauptsache, Start- und Endpunkt sind verschieden.

Das Beispiel funktioniert nun mit Java 2D genauso wie das zu Beginn des Kapitels vorgestellte Beispiel, das mit der Klasse java.awt.Graphics entwickelt wurde. Das heißt, es sind immer noch die gleichen Unzulänglichkeiten vorhanden wie im ursprünglichen Beispielprogramm. So haben Sie vielleicht bereits bemerkt, dass eine Zeichnung verschwindet, wenn Ihr Applet von einem anderen Fenster verdeckt wird und dann wieder von Ihnen sichtbar gemacht wird. In dem Moment, in dem die Oberfläche des Applets wieder zum Vorschein kommt, wird automatisch vom Browser für Ihr Applet die Methode paint() aufgerufen. Da Sie paint() gar nicht selber definiert haben, enthält also Ihr Applet die Methode paint() in der Form, wie sie in der Elternklasse java.applet.Applet definiert ist. Und diese Standarddefinition von paint() führt keinerlei Code aus. Der Methodenrumpf ist also leer. Dies gilt auch für Java-Applets, die mit Swing realisiert sind, da die Klasse javax.swing.JApplet von java.applet.Applet abgeleitet ist.

Das Applet wird nun so erweitert, dass Zeichnungen dauerhaft im Applet gespeichert werden, so dass ein kurzzeitiges Verdecken des Applets nicht zu einem Löschen der Zeichnung führt. Da der Browser jeweils die Methode paint() aufruft, wenn ein Applet verdeckt war und wieder sichtbar wird, muss auch diese Methode nun in irgendeiner Form definiert werden. Diese Methode ist dafür verantwortlich, die ursprüngliche Zeichnung in die Applet-Oberfläche zu malen, wenn das Applet verdeckt war.

Außerdem sollen nun richtige Linien anstatt vereinzelter Punkte gezeichnet werden, die bei einer Mausbewegung mit gedrückter Taste erscheinen. Dazu wird einfach eine Linie zwischen dem Punkt, an dem sich der Mauszeiger aktuell befindet, und dem zuletzt erhaltenen Punkt gezeichnet anstatt wie bisher die Position des Mauszeigers als Start- und gleichzeitig als Endpunkt einer Linie zu verwenden.

import javax.swing.*; 
import java.awt.*; 
import java.awt.event.*; 
import java.awt.geom.*; 
import java.awt.image.*; 

public class MyApplet extends JApplet implements MouseMotionListener 
{ 
  BufferedImage bi; 
  Graphics2D big2; 
  int x, y; 
  boolean reset = true; 

  public void init() 
  { 
    bi = (BufferedImage)createImage(getWidth(), getHeight()); 
    big2 = bi.createGraphics(); 
    addMouseMotionListener(this); 
  } 

  public void paint(Graphics g) 
  { 
    Graphics2D g2 = (Graphics2D)g; 
    g2.drawImage(bi, null, 0, 0); 
  } 

  public void mouseDragged(MouseEvent ev) 
  { 
    if (reset) 
    { 
      reset = false; 
      x = ev.getX() - 1; 
      y = ev.getY(); 
    } 
    big2.draw(new Line2D.Double(x, y, ev.getX(), ev.getY())); 
    x = ev.getX(); 
    y = ev.getY(); 
    repaint(); 
  } 

  public void mouseMoved(MouseEvent ev) 
  { 
    reset = true; 
  } 
} 

Das Applet bedient sich einer Klasse BufferedImage, die im Paket java.awt.image liegt. Dieses Paket gehört ebenfalls zu Java 2D und enthält Klassen, um Grafiken zu erstellen und zu bearbeiten. Die Klasse java.awt.image.BufferedImage ist eine Kindklasse von java.awt.Image und stellt eine Art unsichtbare Grafik dar. Es handelt sich bei BufferedImage um eine gepufferte Grafik, die ausschließlich im Computerspeicher existiert und nicht am Bildschirm angezeigt wird. Alle Operationen, die mit diesen gepufferten Grafiken stattfinden, sind unsichtbar.

Ab sofort finden Zeichenvorgänge des Anwenders nicht mehr direkt auf der Applet-Oberfläche statt, sondern unsichtbar auf einer gepufferten Grafik. Dazu wird innerhalb von init() zuerst ein Objekt vom Typ BufferedImage erstellt. Dies geschieht über einen Aufruf der Methode createImage(). Da diese Methode eine Referenz vom Typ java.awt.Image zurückgibt, wird der Datentyp per Casting in java.awt.image.BufferedImage umgewandelt.

Da Zeichenoperationen immer ein Objekt vom Typ java.awt.Graphics oder java.awt.Graphics2D benötigen, wird für die gepufferte Grafik im nächsten Schritt die Methode createGraphics() aufgerufen. Diese liefert eine Referenz vom Typ java.awt.Graphics2D zurück, so dass kein Casting notwendig ist. Über die Referenzvariable big2 ist also nun jederzeit eine Zeichenoperation auf der gepufferten Grafik möglich.

Innerhalb der Methode mouseDragged() wird nun nicht mehr direkt auf die Applet-Oberfläche zugegriffen, sondern Zeichenoperationen finden über big2 auf der gepufferten Grafik statt. Damit eine Zeichenoperation auch sichtbar wird, ruft das Applet innerhalb von mouseDragged() nach der Zeichenoperation die Methode repaint() auf. Dies führt dazu, dass der Browser die Methode paint() des Applets aufruft. Sie dürfen paint() niemals direkt aufrufen, sondern machen dies immer über repaint(). Der Grund ist der, dass bei Zeichenoperationen in der Applet-Oberfläche über paint() mehr Dinge im Browser geschehen müssen als dass ein einfacher Aufruf von paint() durch den Programmierer ausreicht.

Was geschieht nun innerhalb von paint()? Fordert der Browser das Applet auf, die Oberfläche neu zu zeichnen - sei es, weil repaint() aufgerufen wurde, sei es, weil das Applet kurzzeitig verdeckt war und nun wieder sichtbar wird - dann wird innerhalb von paint() auf die gepufferte Grafik bi zugegriffen und diese als Parameter an die Methode drawImage() der Applet-Oberfläche übergeben. Das heißt, die gepufferte Grafik wird in die Applet-Oberfläche kopiert. Alles, was bisher vom Anwender gezeichnet und in der gepufferten Grafik festgehalten wurde, wird mit einem Schlag sichtbar.

Der Vorteil von Zeichenoperationen auf gepufferten Grafiken ist also der, dass Grafiken beliebig im Speicher bearbeitet werden können, Änderungen dauerhaft gespeichert werden und bei Bedarf die gepufferten Grafiken einfach auf eine Oberfläche kopiert werden können. Außerdem - ebenfalls wichtig in grafisch intensiven Anwendungen - sind Zeichenoperationen, die auf gepufferten Grafiken stattfinden, sehr viel schneller als Zeichenoperationen, die jeweils direkt auf sichtbaren Oberflächen stattfinden. Man bezeichnet das als Double-Buffering: Zeichenoperationen werden auf gepufferten Grafiken durchgeführt, die im Anschluss mit einem Kopiervorgang auf einer Oberfläche sichtbar gemacht werden. Die Technik des Double-Buffering ist zum Beispiel bei Animationen wichtig, um schnelle Überblendeffekte zu erreichen, die einen flüssigen Ablauf der Animation ermöglichen.

Die Eigenschaften x und y, die im Beispielprogramm eingesetzt werden, werden lediglich benötigt, um speichern zu können, an welcher Koordinatenposition der Mauszeiger das letzte Mal gefunden wurde, als mouseDragged() aufgerufen wurde. Die vorherige Koordinatenposition wird jeweils benötigt, um von dieser ausgehend eine Linie zur aktuellen Position zu zeichnen, wenn mouseDraggend() erneut aufgerufen wird. Da es sein kann, dass der Anwender nicht ununterbrochen Linien zeichnet, sondern seine Maus auch ohne gehaltene Taste bewegt, wird über die Eigenschaft reset festgehalten, ob bei einem Aufruf von mouseDragged() eine neue Linie gezeichnet wird oder eine bestehende Linie fortgesetzt wird. Im ersten Fall wird die aktuelle Mausposition als vorherige Mausposition gespeichert, weil schließlich irgendein Startwert benötigt wird. Dass von der x-Koordinaten der Wert 1 abgezogen wird liegt wieder daran, dass eine Linie in Java 2D nur dann gezeichnet wird, wenn Start- und Endpunkt unterschiedlich sind.

Abschließend wird das Beispiel um zwei Methodenaufrufe erweitert, um Ihnen zu zeigen, wie in Java 2D eine detaillierte Einstellung von Linieneigenschaften möglich ist.

import javax.swing.*; 
import java.awt.*; 
import java.awt.event.*; 
import java.awt.geom.*; 
import java.awt.image.*; 

public class MyApplet extends JApplet implements MouseMotionListener 
{ 
  BufferedImage bi; 
  Graphics2D big2; 
  int x, y; 
  boolean reset = true; 

  public void init() 
  { 
    bi = (BufferedImage)createImage(getWidth(), getHeight()); 
    big2 = bi.createGraphics(); 
    big2.setStroke(new BasicStroke(4, BasicStroke.CAP_ROUND, BasicStroke.JOIN_ROUND)); 
    big2.setPaint(Color.BLUE); 
    addMouseMotionListener(this); 
  } 

  public void paint(Graphics g) 
  { 
    Graphics2D g2 = (Graphics2D)g; 
    g2.drawImage(bi, null, 0, 0); 
  } 

  public void mouseDragged(MouseEvent ev) 
  { 
    if (reset) 
    { 
      reset = false; 
      x = ev.getX() - 1; 
      y = ev.getY(); 
    } 
    big2.draw(new Line2D.Double(x, y, ev.getX(), ev.getY())); 
    x = ev.getX(); 
    y = ev.getY(); 
    repaint(); 
  } 

  public void mouseMoved(MouseEvent ev) 
  { 
    reset = true; 
  } 
} 

Die Klasse java.awt.Graphics2D bietet folgende Methoden an, um Eigenschaften geometrischer Figuren zu beeinflussen.

  • setStroke() definiert, wie Linien gezeichnet werden sollen. Es können unter anderem Linienstärke und Muster angegeben werden.

  • setPaint() legt fest, wie geometrische Figuren ausgemalt werden. So ist es zum Beispiel möglich, Farbübergänge zu zeichnen.

  • setComposite() wird verwendet, wenn mehrere geometrische Figuren übereinander liegen.

  • setTransform() ermöglicht eine Transformation geometrischer Figuren. So können Figuren vergrößert, verkleinert, gedehnt, gestaucht oder auch gedreht werden.

  • setClip() legt einen Ausschnitt innerhalb einer Oberfläche fest, in dem Zeichenoperationen sichtbar werden. Alle Zeichenoperationen außerhalb des festgelegten Bereichs werden nicht sichtbar.

  • setFont() stellt eine Schriftart ein. Über Klassen im Paket java.awt.font, das ebenfalls zu Java 2D gehört, kann fast beliebig auf die Darstellung von Schriften eingewirkt werden.

  • setRenderingHints() macht es möglich, verschiedene System-Features zu nutzen, die eine Darstellung von Zeichnungen optimieren. So kann zum Beispiel Anti-Aliasing aktiviert werden, das weichere, fließende Übergänge ermöglicht.

Im obigen Code werden die beiden Methoden setStroke() und setPaint() verwendet. Während setStroke() ein Objekt vom Typ java.awt.Stroke erwartet, muss setPaint() ein Objekt vom Typ java.awt.Paint übergeben werden. Sowohl bei java.awt.Stroke als auch bei java.awt.Paint handelt es sich um Interfaces. Da die Klasse java.awt.Color seit Java 1.2 das Interface java.awt.Paint implementiert, kann Color.BLUE an setPaint() übergeben werden, um die Linienfarbe auf Blau zu setzen.

Das Interface java.awt.Stroke wird unter anderem von der Klasse java.awt.BasicStroke implementiert. Indem ein Objekt dieser Klasse erstellt wird, dem Konstruktor verschiedene Angaben zur Definition der Linie übergeben werden und dann das erstellte Objekt als Parameter der Methode setStroke() übergeben wird, kann recht einfach die Erscheinungsform der Linie beeinflusst werden. Obiges Bespiel legt Linienstärke 4, abgerundete Enden und runde Übergänge bei Aufeinandertreffen von zwei Linien fest.

Indem Sie für ein Objekt vom Typ java.awt.Graphics2D die verschiedenen Methoden aufrufen, legen Sie fest, wie geometrische Figuren gezeichnet werden sollen. Alle Zeichenvorgänge finden jeweils in der Form statt, wie Sie sie festgelegt haben. Wollen Sie irgendwann zum Beispiel die Linienfarbe ändern, nachdem Sie mehrere Linien in Blau gezeichnet haben, so müssen Sie lediglich erneut die Methode setPaint() für Ihr Objekt vom Typ java.awt.Graphics2D aufrufen und ihr einen neuen Farbwert übergeben. Alle Linien, die ab diesem Zeitpunkt gezeichnet werden, erscheinen dann in der neuen festgelegten Farbe.


3.6 Aufgaben

Übung macht den Meister

Sie können die Lösungen zu allen Aufgaben in diesem Buch als ZIP-Datei erwerben.

  1. Entwickeln Sie ein Java-Applet, auf dessen Oberfläche der Anwender Rechtecke zeichnen kann. Wenn der Anwender die Maustaste drückt, setzt er automatisch den linken oberen Eckpunkt des Rechtecks. Er kann dann bei gehaltener Maustaste die Maus nach rechts unten bewegen, wobei ihm ständig die Größe des Rechtecks immer ausgehend vom gesetzten Punkt angezeigt wird. Läßt er die Maustaste los, wird das Rechteck endgültig gezeichnet. Zeichnet er ein neues Rechteck, bleibt das vorherige Rechteck bestehen.

    Die Aufgabe erfordert Nachdenken und ein wenig Bastelarbeit. Sie benötigen zur Lösung nur das Interface java.awt.event.MouseMotionListener. Der Code der Lösung in diesem Buch ist gerade mal 40 Zeilen lang. Eventuell kann Ihnen die Klasse java.awt.Rectangle behilflich sein, wobei Sie die Aufgabe jedoch auch völlig ohne diese Klasse lösen können.

  2. Erweitern Sie das Java-Applet aus Aufgabe 1 dahingehend, dass der Anwender das Rechteck in eine beliebige Richtung ausgehend vom gesetzten Eckpunkt aufziehen kann. Der gesetzte Eckpunkt ist also nicht mehr zwangsläufig die linke obere Ecke des Rechtecks.

    Speichern Sie beispielsweise in einem Objekt vom Typ java.awt.Point die Koordinaten des gesetzten Eckpunkts. Wenn der Anwender die Maus bewegt, ermitteln Sie die aktuellen Koordinaten der Maus und errechnen, in welche Richtung ausgehend vom gesetzten Eckpunkt die Maus bewegt wurde. Errechnen Sie dann die Koordinaten der linken oberen Ecke und die Seitenlängen des Rechtecks, um es am Bildschirm anzeigen zu können.

  3. Entwickeln Sie basierend auf dem im Abschnitt 3.5, „Java 2D“ vorgestellten Beispielcode ein Java-Applet, das dem Anwender ermöglicht, selber eine Farbe für die zu zeichnenden Linien auszuwählen. Greifen Sie hierbei auf die Klasse javax.swing.JColorChooser zu und blenden Sie ein derartiges Steuerelement ein, wenn der Anwender mit der rechten Maustaste im Applet klickt. Hat der Anwender eine Farbe im Steuerelement ausgewählt, so werden alle neuen Linien in der gewählten Farbe gezeichnet.

    Das Applet muss nun das Interface java.awt.event.MouseListener implementieren, damit Sie bei Loslassen der rechten Maustaste ein Popup-Menü einblenden können. Popup-Menüs erzeugen Sie in Swing-Anwendungen mit Hilfe der Klasse javax.swing.JPopupMenu. Fügen Sie Ihrem Popup-Menü als einzigen Menüeintrag ein Objekt vom Typ javax.swing.JColorChooser hinzu. Bei einem rechten Mausklick erscheint dann das Steuerelement für die Farbauswahl. Um informiert zu werden, wenn der Anwender eine Farbe im Steuerelement ausgewählt hat, müssen Sie das Interface java.beans.PropertyChangeListener verwenden.

  4. Erweitern Sie das Java-Applet aus Aufgabe 3 dahingehend, dass der Anwender über eine Menüleiste jederzeit die Linienstärke ändern kann. Es sollen drei Menüeinträge angeboten werden, um dünne, mittelstarke und dicke Linien zeichnen zu können.

    Erstellen Sie eine Menüleiste mit javax.swing.JMenuBar. Fügen Sie dieser ein Menü mit javax.swing.JMenu hinzu. Erstellen Sie dann zum Beispiel drei Optionsschaltflächen mit javax.swing.JRadioButtonMenuItem, die Sie dem Menü hinzufügen und über die der Anwender jederzeit eine neue Linienstärke einstellen kann. Um auf Benutzereingaben auf den Menüeinträgen reagieren zu können, verwenden Sie zum Beispiel das Interface java.awt.event.ActionListener.