JavaFX: Ereignisse und Animationen

Grafische Benutzeroberflächen mit JavaFX
Ereignisse und Animationen
Ereignisgesteuerte Programmierung
Graphische Benutzeroberflächen wie JavaFX basieren auf
einem ereignisgesteuerten Ansatz.
I
Beim Start eines JavaFX-Programms wird die
start-Methode eines Application-Objekts ausgeführt.
I
In start werden die GUI-Elemente zusammengesetzt und
initialisiert.
I
Die Applikation wird dann angezeigt.
I
Der weitere Programmablauf wird durch Ereignisse
gesteuert.
Ereignisgesteuerte Programmierung
Die main-Methode ist vordefiniert und implementiert eine
Event-Loop.
Pseudocode:
while (nicht alle Fenster geschlossen) {
Warte auf Ereignis (Maus bewegt, Taste gedrueckt,
Timer abgelaufen,...);
Informiere Steuerelemente von eingetretenem Ereignis;
Aktualisiere die Anzeige;
}
Ereignisverarbeitung in JavaFX
Ereignisse werden durch Objekte der Klasse Event
repräsentiert.
I
physikalische Events, z.B. Maus bewegt
– InputEvent extends Event
– MouseEvent extends InputEvent
– ScrollEvent extends InputEvent
– KeyEvent extends InputEvent
– ...
I
logische Events, z.B. Button betätigt
– ActionEvent extends Event
– ...
Ablauf der Ereignisbehandlung
Ablauf der Ereignisbehandlung bei Mausklick auf Slider.
Objektdiagramm
Anzeige
BorderPane
Stage
Center: Pane
Scene
BorderPane
Pane HBox
Label Slider Label
Bottom: HBox
Label
Slider
Label
Ablauf der Ereignisbehandlung
I
Informationen über den Mausklick werden
in ein MouseEvent-Objekt kodiert
Stage
Scene
BorderPane
Pane HBox
Label Slider Label
Ablauf der Ereignisbehandlung
I
Informationen über den Mausklick werden
in ein MouseEvent-Objekt kodiert
I
Der MouseEvent wird zum Zielobjekt (der
geklickte Slider) durchgereicht. In jedem
Schritt werden Event-Filter angewendet.
(Beispiel: Verschlucken des Events)
Stage
Scene
BorderPane
Pane HBox
Label Slider Label
Ablauf der Ereignisbehandlung
I
Informationen über den Mausklick werden
in ein MouseEvent-Objekt kodiert
I
Der MouseEvent wird zum Zielobjekt (der
geklickte Slider) durchgereicht. In jedem
Schritt werden Event-Filter angewendet.
(Beispiel: Verschlucken des Events)
I
Das Event-Objekt wird wieder nach oben
durchgereicht. In jedem Schritt werden
Event-Handler aufgerufen.
(Beispiel: Ändern des Slider-Werts)
Stage
Scene
BorderPane
Pane HBox
Label Slider Label
Ablauf der Ereignisbehandlung
I
Informationen über den Mausklick werden
in ein MouseEvent-Objekt kodiert
I
Der MouseEvent wird zum Zielobjekt (der
geklickte Slider) durchgereicht. In jedem
Schritt werden Event-Filter angewendet.
(Beispiel: Verschlucken des Events)
I
I
Das Event-Objekt wird wieder nach oben
durchgereicht. In jedem Schritt werden
Event-Handler aufgerufen.
(Beispiel: Ändern des Slider-Werts)
Event-Filter und -handler können beliebig
vom Benutzer definiert werden.
Stage
Scene
BorderPane
Pane HBox
Label Slider Label
Event-Handler
Zur Ereignisbehandlung implementiert man das Interface
EventHandler.
class SayClickHandler implements EventHandler<MouseEvent> {
@Override
public void handle(MouseEvent event) {
System.out.println("Click!");
}
}
Jedes Node-Objekt hat eine Liste von aktiven Event-Handlern.
Ein eigener Event-Handler kann mittels addEventHandler
aktiviert werden.
Pane pane = new Pane();
pane.addEventHandler(MouseEvent.MOUSE_CLICKED, new SayClickHandler());
Beispiel: Hinzufügen einer Linie durch Klick
public class Main extends Application {
@Override
public void start(Stage primaryStage) {
Pane pane = new Pane();
pane.addEventHandler(MouseEvent.MOUSE_CLICKED, new AddLineHandler(pane));
Scene scene = new Scene(pane, 300, 300);
primaryStage.setScene(scene);
primaryStage.show();
}
}
class AddLineHandler implements EventHandler<MouseEvent> {
private Pane pane;
AddLineHandler(Pane pane) { this.pane = pane; }
@Override
public void handle(MouseEvent event) {
Line line = new Line(0, 0, event.getX(), event.getY());
pane.getChildren().add(line);
}
}
Beispiel: Hinzufügen einer Linie durch Klick
Gleiches Beispiel mit anonymer innerer Klasse:
public class Main extends Application {
@Override
public void start(Stage primaryStage) {
Pane pane = new Pane();
pane.addEventHandler(MouseEvent.MOUSE_CLICKED,
new EventHandler<MouseEvent>() {
@Override
public void handle(MouseEvent event) {
Line line = new Line(0, 0, event.getX(), event.getY());
pane.getChildren().add(line);
}
});
Scene scene = new Scene(pane, 300, 300);
primaryStage.setScene(scene);
primaryStage.show();
}
}
Beispiel: Hinzufügen einer Linie durch Klick
Gleiches Beispiel mit Java-8-Lambda:
public class Main extends Application {
@Override
public void start(Stage primaryStage) {
Pane pane = new Pane();
pane.addEventHandler(MouseEvent.MOUSE_CLICKED,
event -> {
Line line = new Line(0, 0, event.getX(), event.getY());
pane.getChildren().add(line);
});
Scene scene = new Scene(pane, 300, 300);
primaryStage.setScene(scene);
primaryStage.show();
}
}
Event-Handler: Abkürzungen
Zur Vereinfachung des Programmtexts gibt es zahlreiche
abkürzende Methoden zum Hinzufügen von Event-Handlern.
Beispiel:
node.setOnMouseClicked(handler);
hat den gleichen Effekt wie
node.addEventHandler(MouseEvent.MOUSE_CLICKED, handler);
Event-Filter
Jedes Node-Objekt hat eine Liste von aktiven Event-Filtern.
Mit addEventFilter können eigene Event-Filter aktiviert werden.
Beispiel:
Pane pane = new Pane();
pane.addEventFilter(MouseEvent.MOUSE_CLICKED, new ForgetFilter());
class ForgetFilter implements EventHandler<MouseEvent> {
@Override
public void handle(MouseEvent event) {
// Verschlucke den event. Er wird nicht an die Kinder weitergeleitet.
event.consume();
}
}
Event-Filter unterscheiden sich von Event-Handlern nur im
Zeitpunkt der Ausführung.
Ablauf der Ereignisbehandlung
I
Ein aufgetretenes Ereignis wird als Event-Objekt kodiert.
I
Das Event-Objekt muss zu seinem Zielobjekt (z.B. die Node,
auf die geklickt wurde) geschickt werden.
I
Dazu wird eine Dispatch-Kette zum Zielobjekt erstellt.
Das ist normalerweise der Pfad von der Wurzel (Stage) zum
Zielobjekt.
I
Das Event-Objekt wird entlang der Dispatch-Kette zum
Zielobjekt durchgereicht. In jedem Schritt werden die
handle-Methoden der Event-Filter des erreichten Objekts
aufgerufen.
I
Das Event-Objekt wird wieder nach oben durchgereicht. In
jedem Schritt werden die handle-Methoden der
Event-Handler des erreichten Objekts angewendet.
Beispiele: Behandlung von Mausereignissen
Typen von Mausereignissen
Verschiedene Typen von Mouse-Events
I
MouseEvent.MOUSE_CLICKED
I
MouseEvent.MOVED
I
MouseEvent.MOUSE_PRESSED
I
MouseEvent.MOUSE_DRAGGED
I
...
Zum Experimentieren können alle Maus-Events an einer Node
angezeigt werden.
node.addEventHandler(MouseEvent.ANY,
event -> {
System.out.println(event);
});
Beispiel: Linien mit Start- und Endpunkt
Ziel: Linienzüge sollen mit der Maus zur Scene hinzugefügt
werden.
I
Klick mit linker Maustaste: Beginnen eines Linienzugs
I
jeder weitere Klick mit linker Maustaste: Verlängerung des
Linienzugs
I
Klick mit rechter Maustaste: Beenden des Linienzugs
Beispiel: Linien mit Start- und Endpunkt
Mögliche Lösung
Benutze Node-Klasse Polyline, die einen Linienzug darstellt.
Bei Linksklick:
1. Wenn noch kein Linienzug angefangen ist, dann lege ein
neues Polyline-Objekt an und füge es zur Scene hinzu.
2. Hänge an das aktive Polyline-Objekt einen Punkt mit den
Koordinaten des Klicks an.
Bei Rechtsklick:
I
Wenn ein Linienzug angefangen ist, dann hänge den
Klickpunkt an und erkläre den Linienzug für beendet.
Beispiel: Linien mit Start- und Endpunkt
public class Linienzug extends Application {
private Polyline currentLine = null; // null heisst: kein aktiver Linienzug
public void start(Stage primaryStage) {
Pane pane = new Pane();
pane.addEventHandler(MouseEvent.MOUSE_CLICKED,
event -> {
if (event.getButton() == MouseButton.PRIMARY) {
if (currentLine == null) {
currentLine = new Polyline();
pane.getChildren().add(currentLine);
}
currentLine.getPoints().add(event.getX());
currentLine.getPoints().add(event.getY());
} else if (event.getButton() == MouseButton.SECONDARY) {
if (currentline != null) {
currentLine.getPoints().add(event.getX());
currentLine.getPoints().add(event.getY());
}
currentLine = null;
}
});
primaryStage.setScene(new Scene(pane, 500, 500));
primaryStage.show();
}
Beispiel: Linien mit Start- und Endpunkt
Verbesserung: Zeige das nächste Segment schon als rote
Linie an, wenn man die Maus nur bewegt.
I
Füge dazu eine rote Linie zur Scene hinzu, wenn man einen
Linienzug beginnt.
I
Bei jeder Mausbewegung wird der Endpunkt der Linie auf
den Punkt des Mauszeigers gesetzt.
I
Bei Beendigung des Linienzugs wird die Linie wieder
entfernt.
Beispiel: Linien mit Start- und Endpunkt
Neue Instanzvariable:
private Line newSegment = null;
Neue Event-Handler:
pane.addEventHandler(MouseEvent.MOUSE_CLICKED, event -> {
if (event.getButton() == MouseButton.PRIMARY) {
pane.getChildren().remove(newSegment);
newSegment = new Line(event.getX(), event.getY(),
event.getX(), event.getY());
newSegment.setStyle("-fx-stroke: red");
pane.getChildren().add(newSegment);
} else if (event.getButton() == MouseButton.SECONDARY) {
pane.getChildren().remove(newSegment);
newSegment = null;
}
});
pane.addEventHandler(MouseEvent.MOUSE_MOVED, event -> {
if (newSegment != null) {
newSegment.setEndX(event.getX());
newSegment.setEndY(event.getY());
}
});
Beispiel: Verschieben von Objekten
Ziel: Verschieben von Nodes durch Ziehen mit der Maus
Mögliche Lösung
I
Bei Drücken des Mouse-Buttons merkt man sich die Position
des Mauszeigers.
I
Bei einem Mouse-Drag-Ereignis wird die Node um so viel
verschoben, wie sich die Maus bewegt hat.
(aktuelle Mauspositon - gemerkte Position)
I
Nach dem Mouse-Drag-Ereignis merkt man sich wieder die
Postion des Mauszeigers.
Beispiel: Verschieben von Objekten
public class Verschieben extends Application {
Point2D lastMousePos;
public void start(Stage primaryStage) {
Circle circle = new Circle(100, 100, 50);
circle.setFill(Color.CORAL);
circle.addEventHandler(MouseEvent.MOUSE_PRESSED,
e -> lastMousePos = new Point2D(e.getSceneX(), e.getSceneY()));
circle.addEventHandler(MouseEvent.MOUSE_DRAGGED, e -> {
circle.setTranslateX(circle.getTranslateX()
+ (e.getSceneX() - lastMousePos.getX()));
circle.setTranslateY(circle.getTranslateY()
+ (e.getSceneY() - lastMousePos.getY()));
lastMousePos = new Point2D(e.getSceneX(), e.getSceneY());
});
Rectangle rectangle = new Rectangle(10, 10, 30, 30);
Pane pane = new Pane(circle, rectangle);
primaryStage.setScene(new Scene(pane, 500, 500));
primaryStage.show();
}
Beispiel: Verschieben von Objekten
Koordinaten eines Events im Event-Handler:
I
getX, getY: Koordinaten in Bezug auf das Objekt, auf dem
der Event gerade behandelt wird.
I
getSceneX, getSceneY: Koordinaten in Bezug auf die
gesamte Scene.
Im Beispiel werden Scene-Koordinaten benutzt, da das Objekt
selbst immer wieder verschoben wird.
Zur Umrechnung zwischen verschiedenen
Koordinatensystemen gibt es nützliche Hilfsfunktionen, z.B.
Point2D sceneToLocal(double x, double y)
(Klasse in Node) zur Umrechnung eines Punkts von
Scenen-Koordinaten in Koordinaten bzgl. einer Node.
Behandlung von logischen Ereignissen
Beispiel: Button
Logische Ereignisse werden analog zu physikalischen
behandelt.
I
Ereignisse werden durch Klasse ActionEvent kodiert
I
Behandlung mittels EventHandler<ActionEvent>
I
Event-Handler hinzufügen mit
addEventHandler(ActionEvent.ACTION, handler) oder mit
abkürzenden Methoden
Button button = new Button("Ok");
button.setOnAction(
event -> {
// wird aufgerufen, wenn der Knopf betaetigt wurde
System.out.println("Ok gedrueckt");
});
// button.setOnAction(handler) ist Abkuerzung fuer
// button.addEventHandler(ActionEvent.ACTION, handler )
Timer und Animationen
Timeline
Beispiel: Verschieben eines Rechtecks mit Position (0,0)
1 Sek.
1 Sek.
Idee: Spezifikation der Position zu bestimmten Zeitpunkten,
automatische Ausführung
I
Wert der X-Position nach 1 Sekunde: 100
I
Wert der Y-Position nach 1 Sekunde: 0
I
Wert der X-Position nach 2 Sekunden: 100
I
Wert der Y-Position nach 2 Sekunde: 50
Timeline
Spezifikation mit einer Folge von Keyframes:
I
ein Keyframe definiert den Zielzustand bestimmter Werte
einem bestimmeten Zeitpunkt
I
mehrere Keyframes werden zu einer Timeline
zusammengesetzt
I
automatische Animation durch lineare Interpolation
Wert
Keyframe 1
Keyframe 2
X-Position
Y-Position
Zeit
Timeline
In JavaFX:
// nach einer Sekunde
KeyFrame f1 = new KeyFrame(Duration.seconds(1),
new KeyValue(rectangle.translateXProperty(), 100),
new KeyValue(rectangle.translateYProperty(), 0));
// nach zwei Sekunden
KeyFrame f2 = new KeyFrame(Duration.seconds(2),
new KeyValue(rectangle.translateXProperty(), 100),
new KeyValue(rectangle.translateYProperty(), 50));
// beide Keyframes in eine Timeline zusammensetzen
Timeline tl = new Timeline();
tl.getKeyFrames().addAll(f1, f2);
// Eigenschaften der Timeline setzen:
tl.setCycleCount(Timeline.INDEFINITE); // endlos wiederholen
tl.setAutoReverse(true); // Animation nach Ende umgedreht wiederholen
// Animation starten
tl.play();
Timeline
Keyframes werden durch Objekte der Klasse KeyFrame
repräsentiert.
Jedes KeyFrame-Objekt hat eine Liste von Werten.
I
Werte sind KeyValue-Objekte.
Beispiel: Spezifikation des Zielwerts für eine Property
new KeyValue(rectangle.translateXProperty(), 100)
“Die Property der X-Position soll den Wert 100 haben.”
I
Die Liste aller Werte eines KeyFrame erhält man mit
keyframe.getValues() und kann dazu Werte hinzufügen.
I
Werte können auch direkt im Konstruktor von KeyFrame
übergeben werden.
Vorgefertigte Animationen
Die Klasse Transition und ihre Unterklassen stellen
vorgefertigte Animationen zur Verfügung.
I
PathTransition: Bewegung einer Node entlang eines
Pfades
I
FadeTransition: Ausblenden einer Node
I
StrokeTransition: Veränderung der Linienfarbe
I
...
Wiederholte Aktionen
Die Werte in einem KeyFrame können nicht nur Zielwerte für
Properties sein.
Das Erreichen eines Keyframe ist ein logisches Ereignis, auf
das man mit einem EventHandler reagieren kann.
Beispiel: Aufruf einer Methode alle 400ms.
EventHandler<ActionEvent> handler = new EventHandler<ActionEvent>() {
@Override
public void handle(ActionEvent event) {
// wird alle 400 Millisekunden ausgefuehrt
...
}
};
KeyFrame f = new KeyFrame(Duration.millis(400), handler);
Timeline timer = new Timeline(f);
timer.setCycleCount(Timeline.INDEFINITE);
timer.play();
Vorsicht: Java Timer
Wiederholte Aktionen in JavaFX stets mit Timeline ausführen!
I
Animationen werden nicht nebenläufig ausgeführt.
I
Animationen werden über die Event-Loop gesteuert.
I
JavaFX benutzt immer einen einzigen Ausführungsthread.
Grund: Nebenläufigkeit macht Synchronisierung aufwändig,
hat aber nur geringen Nutzen.
I
JavaFX-Objekte dürfen nicht aus einem anderen
Ausführungsthread verändert werden.
Achtung: Die Timer-Klasse von Java benutzt einen neuen
Thread und funktioniert deshalb nicht direkt mit JavaFX.
Vorsicht: Java Timer
Funktioniert nicht:
Button button = new Button("Ok");
// Nach einer Sekunde wird button.setText("ko") aufgerufen,
// aber aus einem falschen Ausfuehrungsthread
new Timer().schedule(new TimerTask() {
@Override
public void run() {
button.setText("ko");
}
}, 1000);
Ausnahme bei Ausführung:
Exception in thread "Timer-0" java.lang.IllegalStateException:
Not on FX application thread; currentThread = Timer-0
Nebenläufigkeit
Ein anderer Thread kann Platform.runLater aufrufen.
Button button = new Button("Ok");
new Timer().schedule(new TimerTask() {
@Override
public void run() {
// Bitte den JavaFX-Thread, die folgende Methode "run" in
// sobald moeglich in der Event-Loop auszufuehren.
Platform.runLater(new Runnable() {
@Override
public void run() {
button.setText("ko");
}
});
}
}, 1000);
Nebenläufigkeit nur einsetzen, wenn wirklich nötig.