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.
© Copyright 2025 ExpyDoc