In Java kann man entweder Applets oder Applikationen schreiben. Während es sich bei Applikationen um eigenständige Programme handelt, die mit Hilfe des Java-Interpreters gestartet werden können, laufen Applets innerhalb eines Browsers. Sie wurden erfunden, um interaktive Web-Seiten zu entwickeln, die neben Text auch Grafiken, Dialoge und andere Programmfunktionalitäten enthalten können.
Unser Beispielprogramm ist als Applet ausgelegt und sollte daher in jedem Browser ausgeführt werden können, der Java 1.1 beherrscht. Zum Zeitpunkt der Drucklegung kann dies allerdings lediglich HotJava von Sun. Netscape Navigator und Microsoft Internet Explorer werden aber bald folgen.
Jedes Java-Programm - egal ob Applet oder Applikation - besteht aus einer Reihe von Quelldateien mit der Erweiterung .java. Diese werden vom Compiler in Bytecode übersetzt und in .class-Dateien gespeichert, die dann vom Interpreter ausgeführt werden können. Java ist eine vollständig objektorientierte Sprache, und zu jeder Klasse wird eine eigene .class-Datei angelegt. Unser Beispielprogramm besteht lediglich aus einer einzigen Quelldatei Puzzle.java, die insgesamt drei Klassen enthält:
|
|
Überlicherweise wird nur eine Klasse pro Quelldatei definiert. In diesem Fall aber wurden die Klassen MyMouseListener und MyMouseMotionListener lokal zur Hauptklasse deklariert und daher in derselben Quelldatei belassen. In jedem Fall darf nur eine Klasse innerhalb der Quelldatei als public deklariert und so anderen Programmen zur Verfügung gestellt werden. |
Die Grundstruktur unseres Programmes sieht so aus:
/**
* @(#)Puzzle.java 1.000 97/07/23
*
* Copyright (c) 1997 Guido Krueger. All Rights Reserved.
*
* Dieses Applet ist die Implementierung eines Schiebepuzzles
* mit 4 x 4 Feldern. Auf den zunächst unsortierten Spielsteinen
* werden die Bestandteile eines Images angezeigt, die dann per
* Drag & Drop sortiert werden können. Der einzig erlaubte Zug
* besteht darin, einen Stein in die benachbarte Lücke zu
* verschieben. Durch Klicken auf den Rahmen kann die Sortierung
* umgekehrt werden.
*
* Das applet-Tag erwartet folgende Parameter:
*
* bordersize = Breite des Spielfeldrandes
* src = Name der Bilddatei (gif oder jpeg)
*
*/
import java.awt.*;
import java.awt.event.*;
import java.applet.*;
import java.util.*;
public class Puzzle
extends Applet
{
//Variablendeklarationen
public void init()
{
//Initialisierung des Applets
}
public void update(Graphics g)
{
//Wird überlagert, um die Grafikdarstellung
//flimmerfrei zu machen
}
public void paint(Graphics g)
{
//Grafikausgabe, ruft paintBorder und
//paintField auf
}
private void paintBorder(Graphics g)
{
}
private void paintField(Graphics g)
{
//Zeichnet die Spielsteine auf dem Brett.
}
private void prepareImage()
{
//Lädt das Bild.
}
private void randomizeField(boolean unordered)
{
//Mischt die Steine auf dem Spielfeld.
}
class MyMouseListener
extends MouseAdapter
{
public void mousePressed(MouseEvent event)
{
//Maustaste wurde gedrückt.
}
public void mouseReleased(MouseEvent event)
{
//Maustaste wurde losgelassen
}
private Point getFieldFromCursor(int x, int y)
{
//Liefert den zur Mausposition passenden horizontalen und
//vertikalen Index des darunterliegenden Steins. Liegt der
//Punkt auf dem Rahmen, wird (-1,-1) zurückgegeben.
return null;
}
private boolean areNeighbours(Point p1, Point p2)
{
//Testet, ob die durch p1 und p2 bezeichneten Spielsteine
//Nachbarn sind.
return false;
}
private void swapRandomization()
{
//Kehrt die Steineordnung um: falls sie sortiert sind,
//werden sie gemischt, andernfalls werden sie sortiert.
}
}
class MyMouseMotionListener
extends MouseMotionAdapter
{
public void mouseDragged(MouseEvent event)
{
//Maus wurde bei gedrückter Taste bewegt.
}
}
}
Wir erkennen die drei Klassendefinitionen, die durch das Schlüsselwort class eingeleitet werden, und eine Reihe von Methoden, die innerhalb der Klassen definiert wurden. Anstelle des Methodenrumpfs haben wir hier lediglich einen Kommentar eingefügt, der ihre Aufgabe beschreibt. In den nachfolgenden Abschnitten werden alle Methoden näher beschrieben.
Am Anfang des Programms befindet sich ein umfangreicher Kommentar, der seine Aufgabe beschreibt. Dieser Kommentar wurde als Dokumentationskommentar angelegt, der mit dem Werkzeug javadoc zur Erzeugung von Sourcecode-Dokumentationen verwendet werden kann. Die nachfolgenden import-Anweisungen zeigen dem Java-Compiler, welches die Pakete sind, aus denen das Programm Klassen und Methoden verwenden will. Anders als in C oder C++ gibt es in Java keine separaten Headerfiles, sondern der Compiler liest zum Übersetzungszeitpunkt die .class-Dateien der eingebundenen Pakete.
Der abgebildete Programmrumpf ist bereits übersetzbar und kann als Applet gestartet werden. Die Übersetzung erfolgt mit Hilfe des Compilerprogramms javac, das mit folgendem Kommando aufgerufen wird:
javac Puzzle.html
Nach der Übersetzung existieren drei Klassendateien Puzzle.class, Puzzle$MyMouseMotionListener.class und Puzzle$MyMouseListener.class, die im selben Verzeichnis wie die Quelldatei liegen. Mit Hilfe des AppletViewers könnte das übersetzte Programm nun gestartet werden. Zuvor benötigen wir allerdings noch eine passende HTML-Datei, die das Applet einbindet. Diese werden wir im nächsten Abschnitt vorstellen.
Weitere Informationen zur Strukturierung von Programmen und zur Einbindung von Paketen finden sich in Kapitel 8. Kapitel 7 liefert eine ausführliche Erklärung der objektorientierten Programmierung und erklärt die Verwendung von Klassen und Methoden. Die Entwicklung von Applets wird in Kapitel 25 erklärt, das sich ausschließlich diesem Thema widmet. Kapitel 26 erläutert die Verwendung von javadoc zur Generierung von HTML-Dokumenten aus Java-Quelltexten und beschreibt weitere Optionen des Compilers javac.
Um das Beispielprogramm nach dem Übersetzen zu starten, wird der Appletviewer benötigt. Der Appletviewer ist ein Programm, das eine einfache HTML-Datei interpretiert, die darin befindlichen APPLET-Tags extrahiert und die zugehörigen Applets aufruft. Ein Applet erfordert zum Start also immer eine HTML-Datei, in die es eingebunden wird. Dazu wird ein oder mehrere APPLET-Tags eingebunden, die den Namen der zu verwendenden Klassendatei, die Größe des Applets und seine Parameter angeben. Alle übrigen HTML-Anweisungen werden vom Appletviewer, der natürlich kein vollständiger Web-Browser ist, ignoriert.
Eine einfache HTML-Datei, die unser Applet lädt, sieht so aus:
<html> <head> <title>Puzzle</title> </head> <body> <h1>Puzzle</h1> <applet code=Puzzle.class width=278 height=348> <param name="bordersize" value=30> <param name="src" value="mine.gif"> Hier steht das Applet Puzzle.class </applet> </body> </html>
Sie enthält die üblichen Formalien einer HTML-Datei wie HTML-, HEAD- und BODY-Tags und das APPLET-Tag zum Aufruf unseres Applets. Die erforderlichen Parameter code, width und height geben dabei den Namen der Klassendatei sowie die Breite und Höhe des zur Ausgabe verfügbaren Bildschirmbereichs an. Die mit dem PARAM-Tag definierten Parameter bordersize und name werden an das Applet weitergereicht und von diesem zur Einstellung der Rahmengröße und zur Auswahl der Bilddatei verwendet. Das Applet kann mit folgendem Kommando gestartet werden:
AppletViewer Puzzle.html
Da alle Methoden leer sind, hat das Applet natürlich noch keinerlei benutzerdefinierte Funktionalität und es wird eine leere Ausgabefläche angezeigt (siehe Abbildung 2.3):
Abbildung 2.3: Ein leeres Applet
Ein Applet wird immer aus der Klasse Applet abgeleitet. Sie besitzt eine Reihe von Methoden, die zu Initialisierungszwecken überlagert werden können. Die Methode init ist eine davon. Sie wird lediglich ein einziges Mal aufgerufen und dient dazu, Einmalinitialisierungen vorzunehmen. Es gibt andere Methoden, die bei jedem Aufruf der zugehörigen HTML-Seite oder beim Verlassen derselben aufgerufen werden.
public void init()
{
aFields = new int[4][4];
sourcefield = new Point(-1, -1);
lastpoint = new Point(-1, -1);
drawoffset = new Point(0,0);
bordersize = Integer.parseInt(getParameter("bordersize"));
if (bordersize < 1 || bordersize > 50) {
bordersize = 5;
}
setBackground(Color.lightGray);
addMouseListener(new MyMouseListener());
addMouseMotionListener(new MyMouseMotionListener());
prepareImage();
randomizeField(true);
}
In unserem Fall initialisiert init eine Reihe von Variablen bzw. erzeugt die erforderlichen Objektreferenzen. Objekte werden - von wenigen Ausnahmen abgesehen - immer mit Hilfe des new-Operators erzeugt, der von dem Klassennamen und der Liste der Parameter, die an den Konstruktor übergeben werden, gefolgt wird. Alle hier initialisierten Variablen wurden als Instanzvariablen im Kopf der Klasse deklariert (dort, wo im obigen Rumpf der Kommentar "//Variablendeklarationen" steht):
int aFields[][]; //Brett mit allen Feldern Image image; //Bildspeicher int bordersize; //Randbreite Dimension fieldsize; //Größe eines Feldes Dimension imagesize; //Größe des Bildes Point sourcefield; //Bei Mausklick ausgewähltes Feld Point lastpoint; //Ursprung des letzten Rechtecks Point drawoffset; //Offset zur Mausdragposition
Hier sind sowohl einfache Typen als auch Objektvariablen zu finden. Als Instanzmerkmale sind sie nur innerhalb der Klasse Puzzle sichtbar, andere Klassen haben auf sie keinen direkten Zugriff.
|
|
Innerhalb von init wird auch der Applet-Parameter border gelesen, der mit Hilfe des PARAM-Tags in der HTML-Datei übergeben wurde. Mit diesem sehr allgemeinen Parameterübergabemechanismus ist es möglich, beliebige Argumente an Applets zu übergeben und sie so in weiten Bereichen konfigurierbar zu machen. |
Die übrigen Anweisungen in init sind Methodenaufrufe. Mit addMouseListener und addMouseMotionListener werden die Objekte zur Behandlung von Mausereignissen erzeugt und registriert. Weiterhin wird durch Aufruf von setBackground die Hintergrundfarbe durch Übergabe eines Color-Objekts auf hellgrau eingestellt. Die anderen Methoden, prepareImage und randomizeField, sind lokale Methoden der Klasse Puzzle, die wir weiter unten erläutern.
Weiterführende Informationen zur Applet-Programmierung und den verschiedenen Parametern des APPLET-Tags finden sich in Kapitel 25. Die Definition von Variablen wird in Kapitel 4 erläutert, das Instanzieren von Objekten und der Aufruf von Methoden in Kapitel 7. In Kapitel 5 und 6 werden Ausdrücke und Anweisungen behandelt. Methoden zur Grafikausgabe finden sich in Kapitel 14 und den folgenden Kapiteln.
Während der Initialisierung wird zunächst die Methode prepareImage aufgerufen, um die Bilddatei zu laden:
/**
* Lädt das Bild.
*/
private void prepareImage()
{
//Bild laden
image = getImage(getDocumentBase(),getParameter("src"));
MediaTracker mt = new MediaTracker(this);
mt.addImage(image, 0);
try {
//Warten, bis das Image vollständig geladen ist,
mt.waitForAll();
} catch (InterruptedException e) {
//nothing
}
imagesize = new Dimension();
imagesize.height = image.getHeight(this);
imagesize.width = image.getWidth(this);
}
Der eigentliche Ladevorgang ist ganz einfach und erfordert lediglich den Aufruf der Methode getImage. Diese erwartet als erstes Argument die Basisadresse des Bildes, in diesem Fall das mit getDocumentBase ermittelte Verzeichnis, aus dem die aktuelle HTML-Seite geladen wurde. Als zweiter Parameter wird der Name der Bilddatei übergeben, die hier durch Aufruf von getParameter aus dem Applet-Parameter "src" gewonnen wird.
Etwas aufwendig wird die Methode, um auf den Abschluß des Ladevorgangs zu warten. getImage geht davon aus, daß Bilder oftmals über langsame Netzwerkverbindungen geladen werden müssen und führt das Laden daher asynchron mit Hilfe eines eigenen Threads aus. Da wir am Ende der Methode die Größe des Bildes ermitteln müssen, um die Berechnung der Teilbilder auf den Spielsteinen korrekt durchführen zu können, muß das Programm warten, bis das Bild vollständig geladen ist. Dazu instanziert es ein Objekt der Klasse MediaTracker, das in der Lage ist, den Ladevorgang eines oder mehrer Bilder durch Aufruf von waitForAll zu überwachen. Nach Abschluß des Ladevorgangs wird eine Ausnahme vom Typ InterruptedException gesendet, nach der das Programm fortfährt. Anschließend wird das fertige Bild in der Instanzvariablen image abgelegt.
Nach dem Laden des Bildes wird durch Aufruf von randomizeField die Anordnung der Spielsteine durcheinandergebracht. Die Spielsteine werden durch das zweidimensionale Array aFields repräsentiert, das eine Größe von 4 mal 4 Elementen hat. Arrays sind in Java Objekte, die wie andere Objekte dynamisch instanziert werden müssen. Unser Programm hat dies in init mit Hilfe der folgenden Anweisung erledigt:
aFields = new int[4][4];
In jedem Element von aFields steht einer der Werte 0 bis 15, der anzeigt, welches der 16 Teilbilder an der entsprechenden Position angezeigt werden soll. Das letzte Bild (also Nummer 15) repräsentiert die Lücke, die von den Anzeigeroutinen als schwarzes Rechteck dargestellt wird. randomizeField sortiert zunächst das Array, indem die Zahlen von 0 bis 15 in aufsteigender Reihenfolge hineingeschrieben werden. Anschließend werden die Steine vermischt, indem zwanzigmal nacheinander je zwei zufällig ausgewählte Elemente miteinander vertauscht werden:
/**
* Mischt die Steine auf dem Spielfeld.
*/
private void randomizeField(boolean unordered)
{
int i, j, k, tmp;
//Zuerst sortieren...
for (i = 0; i <= 15; ++i) {
aFields[i / 4][i % 4] = i;
}
//Dann mischen...
if (unordered) {
Random rand = new Random(System.currentTimeMillis());
for (i = 0; i < 20; ++i) {
j = Math.abs(rand.nextInt()) % 16;
k = Math.abs(rand.nextInt()) % 16;
tmp = aFields[j / 4][j % 4];
aFields[j / 4][j % 4] = aFields[k / 4][k % 4];
aFields[k / 4][k % 4] = tmp;
}
}
}
Zufallszahlengeneratoren in Java sind Objekte der Klasse Random. Der Zufallszahlengenerator, der in diesem Fall mit der aktuellen Systemzeit initialisiert wird, ist in der Lage, positive und negative Zufallszahlen für Ganz- und Fließkommazahlen zu erzeugen.
|
|
Die Klasse Math stellt eine Reihe von Methoden zur Verfügung, die bei der Verarbeitung von numerischen Werten hilfreich sind. Sie besitzt überwiegend Klassenmethoden, die ohne zugeordnetes Objekt aufgerufen werden können. Klassenmethoden sind das Pendant zu globalen Funktionen anderer Programmiersprachen, müssen aber immer zusammen mit dem Namen der Klasse, in der sie deklariert wurden, aufgerufen werden. |
Weiterführende Informationen zum Laden und Anzeigen von Bildern finden sich in Kapitel 24, dort wird auch das Konzept der Klasse MediaTracker genau erläutert. Die Behandlung von Ausnahmen und das Exception-Konzept werden in Kapitel 9 vorgestellt. Die Instanzierung von Arrays wird in Kapitel 4 behandelt und die Methoden der Klasse Math stammen aus Kapitel 7, in dem auch die Verwendung von Klassenmethoden beschrieben wird. Der Zufallszahlengenerator und die Methoden der Klasse System werden in Kapitel 12, das sich mit Utility-Klassen beschäftigt, vorgestellt.
Die Grafikausgabe erfolgt in allen Java-Programmen durch Aufruf der Methode paint. Ausnahmen davon sind lediglich Terminalausgaben, die vom Programm durch Aufruf von System.out.println erzeugt werden können. paint wird in abgeleiteten Klassen überlagert und sorgt für die ordnungsgemäße Darstellung der Bildschirmausgabe. Die Methode bekommt ein Objekt vom Typ Graphics übergeben, das einen Grafikkontext darstellt. Dieser stellt die Methoden zur Ausgabe von Text und Grafik zur Verfügung und kann am besten als abstraktes Ausgabegerät aufgefaßt werden. Ein Grafikkontext wird auch dann verwendet, wenn Druckausgaben erzeugt werden sollen oder die Ausgabe in einem Offscreen-Image erfolgt. In unserem Fall ruft paint die beiden Methoden paintBorder und paintField auf:
public void paint(Graphics g)
{
paintBorder(g);
paintField(g);
}
An beide Methoden wird der Grafikkontext übergeben, auf dem die Ausgabeoperationen vorgenommen werden. paintBorder ist dafür verantwortlich, den Rahmen um das Spielfeld zu zeichnen:
/**
* Zeichnet den Rahmen des Spielbretts.
*/
private void paintBorder(Graphics g)
{
Insets insets = getInsets();
Dimension size = getSize();
size.height -= insets.top + insets.bottom;
size.width -= insets.left + insets.right;
fieldsize = new Dimension();
fieldsize.width = (size.width - (2 * bordersize)) / 4;
fieldsize.height = (size.height - (2 * bordersize)) / 4;
g.setColor(Color.black);
g.drawRect(
insets.left,
insets.top,
size.width - 1,
size.height - 2
);
g.drawRect(
insets.left + bordersize,
insets.top + bordersize,
4 * fieldsize.width,
4 * fieldsize.height
);
}
Dazu ermittelt die Methode durch Aufruf von getInsets zunächst die Größe aller Randelemente wie Fensterrahmen, Menüzeile oder Statuszeile. Da das Java-Koordinatensystem in der linken oberen Ecke des zugrundliegenden Fensters beginnt (und nicht in der linken oberen Ecke des Client-Bereichs), müssen diese Werte berücksichtigt werden, damit nicht Teile der Ausgabe hinter den Randelementen verschwinden.Des weiteren ermittelt die Methode durch Aufruf von getSize die Größe des Applet-Fensters, um zusammen mit der als Parameter übergebenen Rahmengröße die Abmessung eines einzelnen Spielsteins zu berechnen. Diese wird der Instanzvariable fieldsize vom Typ Dimension zugewiesen und bei der späteren Anzeige des Spielfelds verwendet.
Anschließend wird die Zeichenfarbe auf schwarz geschaltet und die beiden Rechtecke zur Begrenzung des Rahmens gezeichnet. Bei diesem Rahmen handelt es sich natürlich nicht um die oben erwähnten Rahmenelemente des Java-Fensters, sondern um den Rand, den wir zur Begrenzung des Spielfelds selbst zeichnen. Würde innerhalb von paint lediglich paintBorder aufgerufen, so hätte das Applet das in Abbildung 2.4 gezeigte Aussehen.
Abbildung 2.4: Die Rahmendarstellung
Die Darstellung des Spielfelds erfolgt in der Methode paintField, die ebenfalls den Grafikkontext aus paint übergeben bekommt. paintField bestimmt durch Aufruf von getInsets zunächst die linke obere Ecke des Client-Bereichs und setzt dann die Zeichenfarbe auf schwarz. Anschließend wird das Array aFields zeilenweise durchlaufen und für jede der 16 Positionen auf dem Bildschirm die Nummer des darauf befindlichen Spielsteins ermittelt. Falls es sich um die Nummer 15 handelt, wird mit fillRect ein schwarzes Rechteck in der Größe eines Spielsteins gezeichnet, um die Lücke darzustellen. Andernfalls wird der Spielstein gezeichnet. Dazu geht das Programm in drei Schritten vor:
Hier ist der Quelltext von paintField:
/**
* Zeichnet die Spielsteine auf dem Brett.
*/
private void paintField(Graphics g)
{
int imagenumber, image_i, image_j;
Insets insets = getInsets();
Point topleft = new Point();
topleft.x = insets.left + bordersize;
topleft.y = insets.top + bordersize;
g.setColor(Color.black);
for (int i = 0; i <= 3; ++i) {
for (int j = 0; j <= 3; ++j) {
imagenumber = aFields[i][j];
if (imagenumber == 15) {
//Lücke zeichnen
g.fillRect(
topleft.x + j * fieldsize.width,
topleft.y + i * fieldsize.height,
fieldsize.width,
fieldsize.height
);
} else {
//Image darstellen
image_i = imagenumber / 4;
image_j = imagenumber % 4;
g.drawImage(
image,
topleft.x + j * fieldsize.width,
topleft.y + i * fieldsize.height,
topleft.x + j * fieldsize.width + fieldsize.width,
topleft.y + i * fieldsize.height + fieldsize.height,
image_j * (imagesize.width / 4),
image_i * (imagesize.height / 4),
image_j * (imagesize.width/4) + imagesize.width/4,
image_i * (imagesize.height/4) + imagesize.height/4,
this
);
//Rahmen
g.drawRect(
topleft.x + j * fieldsize.width,
topleft.y + i * fieldsize.height,
fieldsize.width,
fieldsize.height
);
//Beschriftung
g.drawString(
"" + imagenumber,
topleft.x + j * fieldsize.width + 2,
topleft.y + i * fieldsize.height + 12
);
}
}
}
}
|
|
Mit den beiden bisher beschriebenen Methoden wäre die Grafikausgabe eigentlich schon komplett. Die zusätzlich vorhandene Methode update, die normalerweise nicht überlagert werden muß, dient dazu, das Bildschirmflackern zu vermindern: |
public void update(Graphics g)
{
Image dbImage;
Graphics dbGraphics;
//Double-Buffer initialisieren
dbImage = createImage(getSize().width,getSize().height);
dbGraphics = dbImage.getGraphics();
//Hintergrund löschen
dbGraphics.setColor(getBackground());
dbGraphics.fillRect(0,0,getSize().width,getSize().height);
//Vordergrund zeichnen
dbGraphics.setColor(getForeground());
paint(dbGraphics);
//Offscreen-Image anzeigen
g.drawImage(dbImage,0,0,this);
dbGraphics.dispose();
}
Ihre Arbeitsweise beruht auf dem Einsatz eines Offscreen-Images, das zur Zwischenspeicherung der Grafikausgabe verwendet wird. Das Verfahren wird auch als Doppelpufferung bezeichnet, weil ein zweiter Bildschirmpuffer zur Ausgabe verwendet wird. update wird in der Aufrufkette, die bei einem Neuzeichnen des Bildschirms ausgelöst wird, unmittelbar vor paint aufgerufen. Ihre Aufgabe besteht darin, zunächst den Hintergrund mit der aktuellen Hintergrundfarbe zu füllen, dann die Vordergrundfarbe zu aktivieren und schließlich paint aufzurufen.
Genau das erledigt auch unsere Variante von update. Alle Zeichenoperationen spielen sich dabei allerdings in einem separaten Puffer ab, der erst nach kompletter Fertigstellung in den Bildschirm eingeblendet wird. Das unschöne Bildschirmflackern, das dadurch entsteht, daß unmittelbar vor der Darstellung dunkler Flächen die darunterliegende helle Fläche des Hintergrunds neu gezeichnet wird, kann so vermieden werden. Der Nachteil ist die etwas verringerte Ausgabegeschwindigkeit und der recht hohe Speicherbedarf, denn bei jedem Neuzeichnen wird ein neues Offscreen-Image erzeugt.
Weiterführende Informationen zur Grafikausgabe finden sich in Kapitel 14 und den folgenden Kapiteln. Kapitel 14 erläutert dabei insbesondere die grundlegenden Zeichenfunktionen und beschreibt den Grafikkontext. Kapitel 15 erläutert die Textausgabe und Kapitel 16 das Farbsystem von Java . Die zeilenorientierte Ausgabe von einfachen Texten wird in Kapitel 3 erläutert und in Kapitel 12 noch einmal aufgegriffen. In Kapitel 24 werden verschiedene Techniken zur Reduzierung des Bildschirmflackerns vorgestellt.
Das Programm reagiert auf Mausereignisse in zweierlei Weise. Erstens wird nach einem Klick auf den Rahmen die Sortierung des Spielfelds umgekehrt, und zweitens erlaubt das Programm das Verschieben der Spielsteine per Drag & Drop.
Um auf Mausereignisse zu reagieren, definieren wir die beiden Klassen MyMouseListener und MyMouseMotionListener, die in der init-Methode des Applets instanziert und durch Aufruf von addMouseListener bzw. addMouseMotionListener als Ereignisempfänger registriert werden. Seit der Version 1.1 des JDK gibt es in Java das Delegation Based Event Handling, bei dem die Ereignisempfänger von den Ereignisquellen getrennt sind und nur dann Nachrichten erhalten, wenn sie sich zuvor beim Ereignissender registriert haben. Dies erlaubt eine verbesserte Strukturierung großer Programme.
MyMouseListener enthält die Methode mousePressed, die genau dann aufgerufen wird, wenn der Anwender eine der Maustasten drückt.
public void mousePressed(MouseEvent event)
{
sourcefield = getFieldFromCursor(event.getX(), event.getY());
if (sourcefield.x == -1 || sourcefield.y == -1) {
swapRandomization();
repaint();
}
lastpoint.x = -1;
lastpoint.y = -1;
}
Zunächst wird durch Aufruf von getFieldFromCursor überprüft, ob der Mausklick innerhalb des Spielfelds lag. Ist das nicht der Fall, gibt die Methode ein Point-Objekt mit den Koordinaten (-1, -1) zurück, und das Programm ruft swapRandomization auf, um die Ordnung des Spielfelds umzukehren. Es geht in diesem Fall davon aus, daß der Mausklick auf dem Rahmen erfolgte. Sowohl getFieldFromCursor als auch swapRandomization sind lokale Methoden der Klasse MyMouseListener:
/**
* Liefert den zur Mausposition passenden horizontalen und
* vertikalen Index des darunterliegenden Steins. Liegt der
* Punkt auf dem Rahmen, wird (-1,-1) zurückgegeben.
*/
private Point getFieldFromCursor(int x, int y)
{
Insets insets = getInsets();
Point topleft = new Point();
topleft.x = insets.left + bordersize;
topleft.y = insets.top + bordersize;
Point ret = new Point(-1, -1);
if (x >= topleft.x) {
if (x < topleft.x + 4 * fieldsize.width) {
if (y >= topleft.y) {
if (y < topleft.y + 4 * fieldsize.height) {
ret.x = (x - topleft.x) / fieldsize.width;
ret.y = (y - topleft.y) / fieldsize.height;
drawoffset.x = x - topleft.x -
ret.x * fieldsize.width;
drawoffset.y = y - topleft.y -
ret.y * fieldsize.height;
}
}
}
}
return ret;
}
/**
* Kehrt die Steineordnung um: falls sie sortiert sind,
* werden sie gemischt und umgekehrt.
*/
private void swapRandomization()
{
//Sind die Felder sortiert?
boolean sorted = true;
for (int i = 0; i <= 15; ++i) {
if (aFields[i / 4][i % 4] != i) {
sorted = false;
break;
}
}
//Neu mischen bzw. sortieren
randomizeField(sorted);
}
swaprandomization ist sehr einfach aufgebaut. In einer Schleife wird zunächst überprüft, ob alle Spielsteine in aufsteigender Reihenfolge angeordnet sind. Abhängig vom Ergebnis wird randomizeField mit dem Sortieren oder Vermischen der Spielsteine beauftragt.
getFieldFromCursor bestimmt zunächst die linke obere Ecke des Client-Bereichs und die Größe des Spielfelds. Anschließend wird überprüft, ob zum Zeitpunkt des Mausklicks der Mauszeiger innerhalb dieses Bereichs gelegen hat. Ist dies nicht Fall, wird (-1, -1) zurückgegeben, andernfalls wird der Index des Spielsteins ermittelt, über dem der Mauszeiger stand. Dazu wird der jeweilige x- bzw. y-Offset in die Client-Area durch die Breite bzw. Länge eines einzelnen Spielsteins dividiert.
|
|
Anschließend wird der x- und y-Abstand des Mauszeigers von der linken oberen Ecke des unter dem Mauszeiger befindlichen Spielsteins ermittelt und in drawoffset gespeichert. Dieser Wert wird benötigt, um beim Ziehen der Maus das Rechteck konsistent mit dem Mauszeiger mitführen zu können. Das zweite Mausereignis, auf das unser Programm reagieren muß, tritt auf, wenn die Maustaste losgelassen wird. In diesem Fall wird die Methode mouseReleased aufgerufen: |
/**
* Maustaste losgelassen.
*/
public void mouseReleased(MouseEvent event)
{
if (sourcefield.x != -1 && sourcefield.y != -1) {
Point destfield;
destfield = getFieldFromCursor(event.getX(), event.getY());
if (destfield.x != -1 && destfield.y != -1) {
if (aFields[destfield.y][destfield.x] == 15) {
if (areNeighbours(sourcefield, destfield)) {
aFields[destfield.y][destfield.x] =
aFields[sourcefield.y][sourcefield.x];
aFields[sourcefield.y][sourcefield.x] = 15;
}
}
}
repaint();
}
sourcefield.x = -1;
sourcefield.y = -1;
}
mouseReleased überprüft zunächst, ob der zugehörige Mausklick innerhalb des Spielfelds ausgelöst wurde. Ist dies nicht der Fall (beispielsweise, weil die Maus vom Rand ins Spielfeld gezogen wurde), erfolgt keine weitere Bearbeitung. Andernfalls wird durch erneuten Aufruf von getFieldFromCursor der Spielstein ermittelt, auf dem die Maus gelandet ist. Falls dieser innerhalb des Spielfelds liegt, wird geprüft, ob die Maus auf der Lücke gelandet ist. Ist auch das der Fall, wird die Methode areNeighbours aufgerufen, um festzustellen, ob der Stein, auf dem die Maustaste gedrückt wurde, und die Lücke, auf der sie losgelassen wurde, Nachbarn auf dem Spielfeld sind:
/**
* Testet, ob die durch p1 und p2 bezeichneten Spielsteine
* Nachbarn sind.
*/
private boolean areNeighbours(Point p1, Point p2)
{
int aNeighbours[][] = {{-1,0},{0,-1},{0,1},{1,0}};
for (int i = 0; i < aNeighbours.length; ++i) {
if (p1.x + aNeighbours[i][0] == p2.x) {
if (p1.y + aNeighbours[i][1] == p2.y) {
return true;
}
}
}
return false;
}
Nur, wenn alle diese Bedingungen erfüllt sind, handelt es sich um eine gültige Operation und der Spielstein darf verschoben werden. Dazu wird im Array aFields einfach an die Zielposition der Wert des Spielsteins, auf dem die Maus gedrückt wurde, abgelegt und der Inhalt des Quellsteins anschließend mit 15, also der Nummer der Lücke, überschrieben. Damit die Aktion auf dem Bildschirm sichtbar wird, ruft die Methode anschließend repaint auf und das Fenster wird neu aufgebaut.
Um während des Ziehens der Maus einen visuellen Effekt zu erzielen, wird schließlich noch die Methode mouseDragged in der Klasse MyMouseMotionListener implementiert. Diese wird immer dann aufgerufen, wenn eine Mausbewegung bei gedrückter Maustaste erfolgt:
/**
* Maus wurde bei gedrückter Taste bewegt.
*/
public void mouseDragged(MouseEvent event)
{
if (sourcefield.x != -1 && sourcefield.y != -1) {
Graphics g = getGraphics();
g.setXORMode(getBackground());
g.setColor(Color.black);
//Das zuletzt gezeichnete Rechteck entfernen
if (lastpoint.x != -1) {
g.drawRect(
lastpoint.x - drawoffset.x,
lastpoint.y - drawoffset.y,
fieldsize.width,
fieldsize.height
);
}
//Neues Rechteck zeichnen
g.drawRect(
event.getX() - drawoffset.x,
event.getY() - drawoffset.y,
fieldsize.width,
fieldsize.height
);
lastpoint.x = event.getX();
lastpoint.y = event.getY();
g.dispose();
}
}
mouseDragged überprüft zunächst, ob das Ziehen der Maus wirklich innerhalb des Spielfeldes gestartet wurde. Ist dies nicht der Fall, wäre die in sourcefield gespeicherte Position des Startfeldes (-1, -1) und mouseDragged würde keine weiteren Aktionen vornehmen. Ist dies aber der Fall, beschafft sich die Methode durch Aufruf von getGraphics zunächst einen Grafikkontext, um (außerhalb von paint) im Client-Bereich des Fensters Ausgaben vornehmen zu können.
Wir rufen also zunächst setXORMode auf, um den Grafikkontext in den XOR-Modus zu versetzen. Mit der erwähnten Technik zeichnen wir einen rechteckigen Rahmen, der im Abstand von drawoffset zur Mausposition liegt und synchron mit ihr bewegt wird. Erfolgt ein neuer Aufruf von mouseDragged, wird der beim vorigen Aufruf gezeichnete Rahmen erneut gezeichnet und damit unsichtbar gemacht. Anschließend wird ein Rahmen an der neuen Position gezeichnet und diese Position für den nächsten Aufruf in der Variable lastpoint gespeichert. Zum Schluß werden die vom Grafikkontext belegten Ressourcen durch Aufruf von dispose zurückgegeben. Dieser Aufruf ist nur nötig, wenn der Grafikkontext durch Aufruf von getGraphics selbst beschafft wurde. Abbildung 2.5 zeigt die Bildschirmausgabe während einer Drag-Operation.
Abbildung 2.5: Die Darstellung des Drag & Drop
Weiterführende Informationen zur Ereignisbehandlung finden sich in den Kapiteln 18 und 19. Dort werden die Grundlagen des Delegation Based Event Handling erläutert, konkrete Architekturvorschläge vorgestellt und die möglichen Ereignistypen und die zur Reaktion darauf erforderlichen Klassen, Interfaces und Methoden vorgestellt. Die Anwendung des XOR-Modus findet sich nur in diesem Beispiel, sie wird im Buch nicht weiter vertieft.