Advanced Java Services | Das EventHandling ab 1.1 |
Mit der Version 1.1 wurde ein neues Eventmodell eingeführt, das Event-Delegation-Modell. Wir nennen
es hier Sender-Empfänger-Modell.
Ein Objekt e möchte von einem anderen Objekt s eine Nachricht erhalten. In dieser Situation nennen wir s einen Sender oder ein Sendeobjekt und e einen Empfänger oder ein Empfängerobjekt. Die Nachricht soll dann gesendet werden, wenn für das Objekt s ein bestimmtes Ereignis eintritt. Zu einem Ereignis gibt es eine oder mehrere zu sendende Nachrichten, die genau definiert sind. Ebenso gibt es verschiedene Ereignistypen. Jeder Ereignistyp hat also einen Satz von Nachrichten, die genau zu diesem Ereignistyp gehören.
Zu jedem Ereignistyp gibt es in Java eine passende Klasse aus dem Package java.awt.event oder javax.swing.event. Die Namen dieser Klassen enden alle auf "Event", also z.Bsp. ActionEvent, MouseEvent, FocusEvent, PropertyChangeEvent usw. Alle diese Klassen leiten sich letzten Endes von der Klasse EventObject ab, die die Basisklasse für alle Eventklssen darstellt. Es gibt auch eine Klasse Event, diese existiert jedoch nur noch aus Gründen der Abwärtskompabilität. Sie stellt sicher, daß alte Programme, die das Ereignismodell von JDK1.0 verwenden immer noch laufen können.
Zu jeder Eventklasse gibt es einen Satz von Nachrichten, die gesendet werden können, wenn ein Event eintritt. Diese Nachrichten stehen in den zum Ereignis gehörenden sog. ListenerInterfaces. So gibt es etwa zum ActionEvent ein Interface ActionListener, zum MouseEvent ein Interface MouseListener, zum FocusEvent ein Interface FocusListener, zum PropertyChangeEvent ein Interface PropertyChangeListener usw.
Es kann vorkommen, daß es zu einem Event viele Nachrichten existieren, die versendet werden können. In solchen Fällen kann es zu einem Event mehrere ListenerInterfaces geben. Jedes dieser Interfaces stellt dann ein Untergruppe der möglichen Nachrichten dar. So gibt es etwa zum MouseEvent die beiden Interfaces MouseListener und MouseMotionListener und zum WindowEvent sogar drei Listeners, nämlich WindowListener, WindowFocusListener und WindowStateListener.
Wirft man einen Blick (oder auch mehrere) in die Dokumentation der ListenerInterfaces, so sieht man, daß jedes Interface einen Satz von Methoden vereinbart, was ja auch der Zweck eines Interfaces ist. Tritt nun für ein Sendeobjekt ein bestimmter Event ein, so ruft dieser Sender bestimmte Methoden des Empfängerobjektes auf. Dabei handelt es sich um solche Methoden, die in dem zum Event gehörenden EventListener Interface(s) vereinbart sind. Um für den Fall eines Events diese Methoden aufrufen zu können, verwaltet der Sender eine Liste von Empfängern. Tritt nun der Event ein werden alle Empfänger benachrichtigt.
Sender sind Objekte. Meist, aber nicht notwendigerweise, gehören die Objekte zu Klassen der graphischen
Oberfläche (AWT oder Swing). Diese Klassen sind bereits so eingerichtet, daß ihre Objekte Nachrichten
versenden können. Klassen, deren Objekte Nachrichten senden können erkennt man daran, daß sie bestimmte
Methoden bereitstellen. Die Namen dieser Methoden werden nach einem festen Muster gebildet und beginnen
alle mit "add" gefolgt von dem Namen des Interfaces, in dem die Nachrichten vereinbart sind, also etwa
addActionListener oder addMouseListener oder addFocusListener oder addPropertyChangeListener usw.
Eine Übersicht über die Events und die zugehörigen ListenerInterfaces finden Sie unter
AWTEvents bzw. unter SwingEvent.
Unser Modell sieht also folgendermaßen aus:
In diesem Abschnitt wollen wir das Modell konkret implementieren.
Was muß man tun, damit ein Sendeobjekt Nachrichten senden kann ?
Nichts !
Ein Buttonobjekt z.B. reagiert auf einen Buttondruck automatisch mit einem Actionereignis.
Was muß man tun, damit ein Empfängerobjekt Nachrichten empfangen kann ?
Etwas mehr !
Das Empfangen geht nicht so automatisch wie das Senden. Hier muß man selbst aktiv werden.
Fangen wir mit einem Button als Sender an.
Ein Empfängerobjekt muß sich beim Button registrieren lassen um Nachrichten zu erhalten.
Dies geschieht mit der Methode addActionListener(ActionListener lis), die die Klasse Button
bereitstellt. Da der Methode ein Objekt vom Typ eine Interfaces übergeben wird, muß unser
Empfängerobjekt typverträglich zum Typ ActionListener sein, oder anders gesagt, die
zugehörige Klasse muß das Interface ActionListener implementieren. So eine Klasse nennen wir
in Zukunft Empfängerklasse. Jede Klasse, die dieses Interface implementiert ist eine
Empfängerklasse.
Hier ganz schematisch die Schritte, die in einem Klassenentwurf durchzuführen sind.
Jedes Objekt der nach dem obigen Schema gebildeten Klasse ist nun ein Empfängerobjekt, natürlich nur für den zu XxxListener gehörenden Satz von Nachrichten. Das Objekt muß sich nun noch beim Sender, von dem es Nachrichten haben will, registrieren. Dabei kann sich ein Empfängerobjekt durchaus bei mehreren Sendeobjekten registrieren. Es enthält dann Nachrichten von mehreren Sendern. Das umgekehrte ist genau so normal. Ein Sender sendet seine Nachrichten an mehrere Empfänger.
sender.addXxxListener(empfänger) ; // Sender-Empfänger-Verbindung herstellen. // optional sender.addXxxListener(empfänger2) ; sender2.addXxxListener(empfänger) ;
Will man etwa Actionereignisse empfangen, so sieht das codiert so aus.
import java.awt.event.*; class ActionHandler implements ActionListener { public void actionPerformed(Actionevent e) { // Diese Methode wird vom Sender gerufen } }
In irgendeiner anderen Klasse (oder auch derselben) gibt es dann ein Buttonobjekt. Falls wir ein Empfängerobjekt registrieren, dann bekommt dieses Objekt Actionnachrichten von diesem Button.
class Xyz { public void methode() { Button knopf = new Button(); // Button konfigurieren knopf.addActionListener( new ActionHandler() ); // Empfänger registrieren: Sender-Empfänger-Verbindung herstellen } }
Die folgende Frage ist einfach zu beantworten.
Jede Klasse, die man selbst entwirft. Außerdem kann sie jeden beliebigen Status haben. Sie kann eine anonyme Klasse, eine lokale Klasse, eine innere Klasse oder eine top-level-Klasse sein, je nachdem welches Design man bevorzugt.
Hier nun einige Beispiele von Empfängerklassen.
class Xyz { public void methode() { Button knopf = new Button(); // Button konfigurieren knopf.addActionListener( new ActionListener() { public void actionPerformed(ActionEvent e) { // Reaktion auf Buttonclick programmieren. } } ); } }
Hier wird eine anonyme Klasse Empfängerklasse. Wie üblich und notwendig bei anonymen Klassen wird gleichzeitig ein Objekt angelegt, das als Empfängerobjekt registriert wird. Sehr beliebt, aber vom objektorientierten Standpunkt anfechtbar. Sie wird hier die Ereignisbehandlung mit der graphischen Konfiguration vermischt. Für kurzen Programmcode und einfache Reaktionen ok, ungeeignet dagegen für komplexes Ereignishandling. Vorteilhaft ist, daß es zu dem Empfängerobjekt (zwangsläufig) nur einen Sender gibt, man muß also nicht zwischen mehreren Sendern unterscheiden. Oft wird aus der Reaktionsmethode eine andere Methode aufgerufen. In so einem Fall kann man auch gleich eine innere Klasse schreiben.
class Xyz { public void methode() { // Button konfigurieren Button knopf = new Button(); // Sender-Empfänger-Verbindung knopf.addActionListener( new ActionHandler() ); } // some other stuff // Innere Klasse als EventHandler class ActionHandler implements ActionListener { public void actionPerformed(ActionEvent e) { // Reaktion auf Buttonclick programmieren. } } }
Hier hat man eine saubere Trennung zwischen user interface und Ereignisbehandlung. Will man auf Ereignisse von mehreren Sendern reagieren, dann kann man entweder mehrere Handler schreiben und so eine 1:1 Beziehung zwischen Sender und Empfänger herstellen. Es gibt jedoch auch bei mehreren Sendern und nur einem Empfänger die Möglichkeit, den Sender zu ermitteln, was weiter unten beschrieben wird.
class Xyz { public void methode() { // Button konfigurieren Button knopf = new Button(); // Sender-Empfänger-Verbindung knopf.addActionListener( new ActionHandler() ); } // some other stuff } // extra Klasse als EventHandler class ActionHandler implements ActionListener { actionPerformed(ActionEvent e) { // Reaktion auf Buttonclick programmieren. } }
Die Klasse EventObject stellt hierzu die wichtige Methode Object getSource() bereit, die damit für alle Events verfügbar ist. Die Methode gibt eine Referenz auf das aktuelle Sendeobjekt zurück.
public void actionPerformed(ActionEvent e) { Object quelle = e.getSource(); if(quelle==knopf1) { // Reaktion auf ersten Button programmieren. } else if (quelle==knopf2) { // Reaktion auf zweiten Button programmieren. } else { // Reaktion auf dritten Button programmieren. } }
Längere else-if Ketten sind allerdings nicht sehr schön. Der Code wird schwerer durchschaubar. Man kann sich wieder mit Methoden behelfen, die aus den einzelnen Blöcken aufgerufen werden. Hat man zudem sehr unterschiedliche Reaktionen, so ist es oft besser, man schreibt eine für eine Reaktion maßgeschneiderte Empfängerklasse.
Wenn man auf einen Button klickt, dann ist es egal, an welcher Stelle man auf den Button klickt,
Hauptsache man klickt. Ähnliches gilt für das Anhaken einer Checkbox oder das Auswählen eines Elementes
aus einer Liste. Zu solchen Ereignissen gehört nur eine Nachricht, man hat den Button angeklickt oder
man hat ein Element aus einer Liste ausgwählt. So sendet ein Button Actionereignisse, indem er für alle
Listener die eine Methode actionPerformed(ActionEvent e) aufruft und so den ActionEvent
übermittelt. Solche Ereignisse heißen High-Level Ereignisse.
Es gibt aber auch komplexere Ereignisse, zu denen mehr als eine Nachricht gehören. Ein gutes Beispiel
ist der MouseEvent. Er gehört zu den Low-Level Events. Zu einem MouseEvent gehören insgesamt sieben
Nachrichten, die auf zwei Listeners aufgeteilt wurden, MouseListener und MouseMotionListener. Tritt
in einer Komponente ein Mausereignis ein, so werden zu registrierten MouseListenern fünf Methoden
aufgerufen und zu registrierten MouseMotionListenern zwei Methoden. Die MouseListener erfahren u.a.,
ob die Maus nur gedrückt wurde (mousePressed) oder ob der User die Maus geklickt hat (mouseClicked)
oder ob das Gebiet der dargestellten Komponente verlassen hat (mouseExited). MouseMotionListener werden
darüber informiert, ob die Maus bei gedrückter Maustaste bewegt wurde (mouseDragged) oder lediglich
bewegt wurde (mouseMoved). Über den so übermittelten MouseEvent erfährt der Listener z.B. auch noch
die Koordinaten, an denen der Event ausgelöst worden ist und welche Maustaste der User verwendet hat.
Informieren Sie sich in der Tabelle AWT Listeners and Events
über ActionEvent und MouseEvent. Wir wollen hier kurz die Implementierung eines MouseListeners
schildern.
Im Grunde ist der Vorgang der gleiche wie oben bei unserem Buttonbeispiel. Es ist nur ein wenig aufwendiger. Angenommen wir wollen die Maus über einem JLabel kontrollieren. Ein JLabel sendet wie jede Komponente Mausereignisse.
public class Xyz { JLabel label = new JLabel(); public Xyz() { // JLabel konfigurieren label.addMouseListener( new MouseHandler() ); // Empfänger registrieren für Mausnachrichten label.addMouseMotionListener( new MouseMotionHandler() ); // Empfänger registrieren für MouseMotion-Nachrichten } }
Hier die Listenerklasse für MouseListener-Objekte
class MouseHandler implements MouseListener { public void mouseClicked(MouseEvent e) { // Reaktionscode } public void mouseEntered(MouseEvent e) { // Reaktionscode } public void mouseExited(MouseEvent e) { // Reaktionscode } public void mousePressed(MouseEvent e) { // Reaktionscode } public void mouseReleased(MouseEvent e) { // Reaktionscode } }
Und hier die Listenerklasse für MouseMotionListener-Objekte
class MouseMotionHandler implements MouseMotionListener { public void mouseDragged(MouseEvent e) { // Reaktionscode } public void mouseMoved(MouseEvent e) { // Reaktionscode } }
Ob MouseHandler und MouseMotionHandler innere Klassen sind oder top-level, das ist wieder Ansichtssache. Wenn man will kann man auch nur eine Klasse schreiben, die beide Interfaces implementiert.
public class Xyz { JLabel label = new JLabel(); public Xyz() { // JLabel konfigurieren label.addMouseListener( new MouseEventHandler() ); // Empfänger registrieren für Mausnachrichten label.addMouseMotionListener( new MouseEventHandler() ); // Empfänger registrieren für MouseMotion-Nachrichten } class MouseEventHandler implements MouseListener, MouseMotionListener { public void mouseClicked(MouseEvent e) { // Reaktionscode } public void mouseEntered(MouseEvent e) { // Reaktionscode } // und weitere drei MouseListenermethoden // ... public void mouseDragged(MouseEvent e) { // Reaktionscode } public void mouseMoved(MouseEvent e) { // Reaktionscode } } // end inner class }
Die nächste Graphik bringt eine Übersicht über die Eventklassen. Da es eine große Anzahl von Eventklassen gibt, wird dort nur ein Ausschnitt gezeigt.
Oft will man bei Low-Level Ereignissen nicht auf alle Nachrichten reagieren. Vielleicht ist man bei einem MouseEvent nur an einer mousePressed Nachricht interessiert. Trotzdem muß ein Listener natürlich alle Methoden des Interfaces implementieren, die dann als leere Methoden kodiert werden. Das ist nicht weiter schlimm, aber ein wenig lästig. Für einige der Low-Level Events stellt die API deswegen sog. Adapterklassen zur Verfügung. Diese Klassen übernehmen die Implementierung eines Listenerinterfaces. Hier z.B. der Quellcode der Klasse MouseAdapter.
public abstract class MouseAdapter implements MouseListener { /** * Invoked when the mouse has been clicked on a component. */ public void mouseClicked(MouseEvent e) {} /** * Invoked when a mouse button has been pressed on a component. */ public void mousePressed(MouseEvent e) {} /** * Invoked when a mouse button has been released on a component. */ public void mouseReleased(MouseEvent e) {} /** * Invoked when the mouse enters a component. */ public void mouseEntered(MouseEvent e) {} /** * Invoked when the mouse exits a component. */ public void mouseExited(MouseEvent e) {} }
Wie man sieht, wird sie als abstract deklariert und implementiert alle Methoden des zugehörigen Interfaces als leere Methoden. Ein Ereignishandler wird dann als Ableitung der Adapterklasse angelegt und überschreibt dann diejenigen Methoden, auf deren Nachrichten man reagieren will. Ist man etwa nur an mousePressed Nachrichten interessiert, so kann man eine Variation des letzten Beispiels auch so codieren.
//import... public class Xyz extends JFrame { JLabel label = new JLabel(); // ... public Xyz() { // JLabel konfigurieren label.addMouseListener( new MouseEventHandler() ); // Empfänger registrieren für Mausnachrichten // ... } // ... class MouseEventHandler extends MouseAdapter { public void mousePressed(MouseEvent e) { // Reaktionscode } } // end inner class }
Ableitungen von Adapterklassen werden gerne als innere Klassen realisiert. In der obigen Situation ist
etwa die Klasse Xyz bereits eine Ableitung von JFrame und kann daher keine Ableitung mehr von
MouseAdapter sein. Will man die Ereignisbehandlung in der gleichen Klasse haben muß man irgendeine
Form einer nested class wählen.
Eine Übersicht über Adapterklassen gewinnt man aus den Tabellen
AWT Listeners and Events und
Swing Listeners and Events.