Tit   Inh   Ind   1   2   3   4   5   6   7   8   9   10   11   12   13   14   15   16   17   18   19   20   21   22   23   24   25   26   27   28   29   30   31   32   <<   <   >   >> 

2.6 Schritt 5: Mausereignisse



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.

Aus Performance-Gründen wurden die Ereignisempfänger für Mausklick- und Mausbewegungsereignisse getrennt. Dadurch werden Programme, die sich lediglich für Mausklicks interessieren, nicht ständig mit Ereignissen bombardiert, wenn der Mauszeiger auf dem Bildschirm bewegt wird. Wir wollen uns zunächst die Klasse MyMouseListener ansehen, die für Mausklickereignisse zuständig ist.

 Tip 

MyMouseListener enthält die Methode mousePressed, die genau dann aufgerufen wird, wenn der Anwender eine der Maustasten drückt.

001 public void mousePressed(MouseEvent event)
002 {
003    sourcefield = getFieldFromCursor(event.getX(), event.getY());
004    if (sourcefield.x == -1 || sourcefield.y == -1) {
005       swapRandomization();
006       repaint();
007    }
008    lastpoint.x = -1;
009    lastpoint.y = -1;
010 }
Listing 2.11: Die Methode mousePressed im Schiebepuzzle

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:

001 /**
002  * Liefert den zur Mausposition passenden horizontalen und
003  * vertikalen Index des darunterliegenden Steins. Liegt der
004  * Punkt auf dem Rahmen, wird (-1,-1) zurückgegeben.
005  */
006 private Point getFieldFromCursor(int x, int y)
007 {
008    Insets insets = getInsets();
009    Point topleft = new Point();
010    topleft.x     = insets.left + bordersize;
011    topleft.y     = insets.top  + bordersize;
012    Point ret     = new Point(-1, -1);
013    if (x >= topleft.x) {
014       if (x < topleft.x + 4 * fieldsize.width) {
015          if (y >= topleft.y) {
016             if (y < topleft.y + 4 * fieldsize.height) {
017                ret.x = (x - topleft.x) / fieldsize.width;
018                ret.y = (y - topleft.y) / fieldsize.height;
019                drawoffset.x = x - topleft.x -
020                               ret.x * fieldsize.width;
021                drawoffset.y = y - topleft.y -
022                               ret.y * fieldsize.height;
023             }
024          }
025       }
026    }
027    return ret;
028 }
029 
030 
031 /**
032  * Kehrt die Steineordnung um: falls sie sortiert sind,
033  * werden sie gemischt und umgekehrt.
034  */
035 private void swapRandomization()
036 {
037    //Sind die Felder sortiert?
038    boolean sorted = true;
039    for (int i = 0; i <= 15; ++i) {
040       if (aFields[i / 4][i % 4] != i) {
041          sorted = false;
042          break;
043       }
044    }
045    //Neu mischen bzw. sortieren
046    randomizeField(sorted);
047 }
Listing 2.12: Lokale Methoden im Schiebepuzzle

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

 Hinweis 

001 /**
002  * Maustaste losgelassen.
003  */
004 public void mouseReleased(MouseEvent event)
005 {
006    if (sourcefield.x != -1 && sourcefield.y != -1) {
007       Point destfield;
008       destfield = getFieldFromCursor(event.getX(),event.getY());
009       if (destfield.x != -1 && destfield.y != -1) {
010          if (aFields[destfield.y][destfield.x] == 15) {
011             if (areNeighbours(sourcefield, destfield)) {
012                aFields[destfield.y][destfield.x] =
013                   aFields[sourcefield.y][sourcefield.x];
014                aFields[sourcefield.y][sourcefield.x] = 15;
015             }
016          }
017       }
018       repaint();
019    }
020    sourcefield.x = -1;
021    sourcefield.y = -1;
022 }
Listing 2.13: Die Methode mouseReleased des Schiebepuzzles

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:

001 /**
002  * Testet, ob die durch p1 und p2 bezeichneten Spielsteine
003  * Nachbarn sind.
004  */
005 private boolean areNeighbours(Point p1, Point p2)
006 {
007    int aNeighbours[][] = {{-1,0},{0,-1},{0,1},{1,0}};
008    for (int i = 0; i < aNeighbours.length; ++i) {
009       if (p1.x + aNeighbours[i][0] == p2.x) {
010          if (p1.y + aNeighbours[i][1] == p2.y) {
011             return true;
012          }
013       }
014    }
015    return false;
016 }
Listing 2.14: Test benachbarter Spielsteine im Schiebepuzzle

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:

001 /**
002  * Maus wurde bei gedrückter Taste bewegt.
003  */
004 public void mouseDragged(MouseEvent event)
005 {
006    if (sourcefield.x != -1 && sourcefield.y != -1) {
007       Graphics g = getGraphics();
008       g.setXORMode(getBackground());
009       g.setColor(Color.black);
010       //Das zuletzt gezeichnete Rechteck entfernen
011       if (lastpoint.x != -1) {
012          g.drawRect(
013             lastpoint.x - drawoffset.x,
014             lastpoint.y - drawoffset.y,
015             fieldsize.width,
016             fieldsize.height
017          );
018       }
019       //Neues Rechteck zeichnen
020       g.drawRect(
021          event.getX() - drawoffset.x,
022          event.getY() - drawoffset.y,
023          fieldsize.width,
024          fieldsize.height
025       );
026       lastpoint.x = event.getX();
027       lastpoint.y = event.getY();
028       g.dispose();
029    }
030 }
Listing 2.15: Die Methode mouseDragged im Schiebepuzzle

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.

Programme, die ein Drag & Drop implementieren, visualisieren dies in der Regel so, daß sie während der Drag-Operation eine vereinfachte Version des Quellobjekts anzeigen und synchron mit der Maus mitführen. Um dabei nicht jeweils ein vollständiges Neuzeichnen erforderlich zu machen, bietet der Device-Kontext einen XOR-Modus an, bei dem alle Zeichenoperationen die jeweilige Pixelfarbe des Untergrundes invertieren. Aus einem schwarzen Pixel wird ein weißes und umgekehrt, und farbige Pixel werden in eine gut zu unterscheidende Komplementärfarbe verwandelt. Wird im XOR-Modus eine Zeichenfunktion zweimal nacheinander aufgerufen, so hat der Bildschirm nach dem zweiten Aufruf exakt dasselbe Aussehen wie vor dem ersten.

 Tip 

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 Kapitel 18 und Kapitel 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. Seit dem JDK 1.2 gibt es ein eigenes Drag & Drop-API, mit dem Daten zwischen verschiedenen Anwendungen oder mit dem Betriebssystem ausgetauscht werden können. Das API wird in diesem Buch nicht behandelt.

 Hinweis 


 Tit   Inh   Ind   1   2   3   4   5   6   7   8   9   10   11   12   13   14   15   16   17   18   19   20   21   22   23   24   25   26   27   28   29   30   31   32   <<   <   >   >> 
Go To Java 2, Addison Wesley, Version 1.0.2, © 1999 Guido Krüger, http://www.gkrueger.com