JavaFX listenerek használata controllerben

A JavaFX controllerben az eseménykezelők segítségével lehet reagálni a felhasználói műveletekre, például egy gomb megnyomására, egérmozgásra, kattintásra vagy billentyűzeteseményre. Az FXML-elemek a controller mezőihez kapcsolhatók, az eseménykezelők pedig az initialize() metódusban vagy közvetlenül az FXML-ben regisztrálhatók.

A legegyszerűbb megoldás a vezérlő eseménykezelő-metódusának használata, például az onAction eseményhez kapcsolódó metódus esetén. Összetettebb esetekben az EventHandler példánya újra felhasználható több vezérlőn és több eseménytípusnál.

A JavaFX eseménykezelés alapjai

Az eseménykezelők lehetővé teszik egy esemény kezelését az eseményfeldolgozás buborékolási szakaszában. Az eseményútvonal buborékolási szakasza az a szakasz, amelyben az esemény a célnodustól a stage node felé halad.

Az eseményszűrőkhöz hasonlóan egy node-hoz egy vagy több kezelő tartozhat, illetve egyetlen kezelő sem. Ha a node nem tartalmaz kezelőt, az esemény eljut a gyökérnode-hoz, és a folyamat befejeződik.

Ha az eseményküldési lánc valamelyik node-ja kezelőt tartalmaz, a kezelő végrehajtódik. Egyetlen kezelő több node-hoz és több eseménytípushoz is használható.

Ha egy gyermeknode eseménykezelője nem fogyasztja el az eseményt, akkor a szülőnode eseménykezelője a gyermek feldolgozása után reagálhat az eseményre, és közös eseményfeldolgozást biztosíthat több gyermeknode számára.

JavaFX és a controller kapcsolata

Ez felelős a felhasználói felület elemeinek és az alkalmazáslogikának az összekapcsolásáért. Az alkalmazás indításakor egy gombnak kell megjelennie középen „Click me” felirattal, az ablak alján pedig egy tesztcímkének.

Java 8, 9 és 10 esetén nincs szükség további beállításra a JavaFX-könyvtárral való munkához. A projekt a JDK 11-től kezdve kikerült a JDK-ból, ezért a szükséges függőségeket hozzá kell adni a pom.xml fájlhoz.

FüggőségSzerepe
org.openjfx:javafx-controlsJavaFX vezérlők használata
org.openjfx:javafx-fxmlFXML-felületek és controllerek használata
org.openjfx:javafx-maven-pluginJavaFX-alkalmazás Mavenből történő indítása

FXML-elrendezés és controller

Először hozzunk létre egy egyszerű FXML-elrendezést, amely egy gombot tartalmaz:

A gyökérelemben az fx:controller tulajdonság kapcsolja össze az FXML-fájlt a controller osztállyal.

Az elemek azonosításához az FXML-ben fx:id használható. A controllerben ugyanilyen nevű, @FXML annotációval ellátott mezőkön keresztül érhetők el.

A controller osztályban például a következő mezők kapcsolhatók az FXML-elemekhez:

A button.setText("Click me") utasítás az inicializálási szakaszban beállítja a gomb feliratát.

Listener hozzáadása az initialize metódusban

A legegyszerűbb esetben a gomb eseménykezelőjét az initialize() metódusban lehet beállítani:

A setOnAction() metódushoz egy EventHandler<ActionEvent> objektum rendelhető.

A kezelő egyetlen handle() metódussal rendelkező funkcionális interfész, ezért az eseménykezelő lambda-kifejezéssel is megadható:

Ez a forma rövidebb és könnyebben olvasható, miközben ugyanazt a műveletet hajtja végre, mint a névtelen EventHandler osztály.

button.setOnAction(event -> logger.info("OnAction {}", event));

Amikor a felhasználó rákattint a gombra, új naplóbejegyzés jelenik meg:

INFO c.b.b.e.ButtonEventHandlerController - OnAction javafx.event.ActionEvent[source=Button[id=searchButton, styleClass=button]'Click me']

Mi történik több setOnAction hívás esetén?

Ugyanahhoz a gombhoz egymás után több kattintási kezelőt is hozzá lehet rendelni:

button.setOnAction(event -> logger.info("OnAction {}", event));button.setOnAction(event -> logger.info("OnAction2 {}", event));

A gombra kattintva azonban csak az „OnAction 2” üzenet jelenik meg. Ez azért történik, mert a második setOnAction hívás az első eseménykezelőt a másodikra cserélte.

Ha ugyanarra az eseménytípusra több műveletet szeretnénk elhelyezni, azokat egyetlen kezelőben kell meghívni, vagy alacsonyabb szintű eseménykezelő-regisztrációt kell használni.

Különböző eseménytípusok kezelése

Más eseménytípusok is kezelhetők, például egérlenyomás, egérfelengedés, húzás és billentyűzetesemények.

A gomb fölé vitt kurzor esetén árnyék jeleníthető meg, a kurzor távozásakor pedig az effektus eltávolítható:

Effect shadow = new DropShadow();searchButton.setOnMouseEntered(e -> searchButton.setEffect(shadow));searchButton.setOnMouseExited(e -> searchButton.setEffect(null));

A setOnMouseEntered() eseménykezelője akkor fut le, amikor a kurzor belép a gomb területére. A setOnMouseExited() eseménykezelője akkor fut le, amikor a kurzor elhagyja a gombot.

Egy eseménykezelő újrafelhasználása

Bizonyos esetekben ugyanazt az eseménykezelőt többször szeretnénk használni. Létrehozható például egy olyan kezelő, amely a jobb egérgomb megnyomásakor növeli a gomb betűméretét:

EventHandler<MouseEvent> rightClickHandler = event -> { if (MouseButton.SECONDARY.equals(event.getButton())) { button.setFont(new Font(button.getFont().getSize() + 1)); }};

Ez a kezelő önmagában még nem rendelkezik működéssel, mert nem kapcsoltuk eseményhez.

Ugyanaz az eseménykezelő hozzárendelhető a gomb és a címke egérlenyomási eseményéhez:

button.setOnMousePressed(rightClickHandler);label.setOnMousePressed(rightClickHandler);

Az alkalmazás tesztelésekor a gombra vagy a címkére a jobb egérgombbal kattintva a betűméret növekszik.

EventHandler használata közvetlenül a controllerben

A node eseménykezelőjének hozzáadásához a Node osztály addEventHandler() metódusával kell regisztrálni a kezelőt.

EventHandler<javafx.scene.input.MouseEvent> eventHandler = new EventHandler<javafx.scene.input.MouseEvent>() { @Override public void handle(javafx.scene.input.MouseEvent e) { System.out.println("Hello World"); circle.setFill(Color.DARKSLATEBLUE); } };circle.addEventHandler( javafx.scene.input.MouseEvent.MOUSE_CLICKED, eventHandler);

Ugyanígy az eseménykezelő eltávolítható a removeEventHandler() metódussal:

circle.removeEventHandler(MouseEvent.MOUSE_CLICKED, eventHandler);

Ez a megoldás akkor hasznos, ha a kezelőt később el kell távolítani, vagy több node-hoz és eseményhez ugyanazt a kezelőobjektumot szeretnénk felhasználni.

Eseménykezelés és eseményfogyasztás

Ha egy gyermeknode eseménykezelője nem fogyasztja el az eseményt, egy szülőnode eseménykezelője is feldolgozhatja azt.

A kezelőben az event.consume() hívás megakadályozza, hogy az esemény további feldolgozásra jusson az eseményútvonal következő elemeihez.

EventHandler<MouseEvent> handler = new EventHandler() { @Override public void handle(MouseEvent event) { if (event.getSource() == btn) { st.play(); } if (event.getSource() == btn1) { st.pause(); } event.consume(); }};

Ebben a példában az esemény forrása alapján különböztethető meg a lejátszás és a szüneteltetés gombja.

Lejátszás és szüneteltetés kezelővel

JavaFX-objektumhoz, például körhöz, animációs átmenet rendelhető. Egy eseménykezelő meghatározhatja, mikor induljon el és mikor szüneteljen az átmenet, például egy gombra kattintva.

A kör és a két gomb létrehozása után egy ScaleTransition objektum hozható létre:

ScaleTransition st = new ScaleTransition();st.setNode(c);st.setDuration(Duration.millis(1000));st.setByX(1);st.setByY(1);st.setAutoReverse(true);st.setCycleCount(50);

A kezelő hozzárendelhető mindkét gomb egérkattintási eseményéhez:

btn.setOnMouseClicked(handler);btn1.setOnMouseClicked(handler);

A lejátszás gomb megnyomásakor az animáció elindul, a szüneteltetés gomb megnyomásakor pedig szünetel.

Billentyűzetesemény kezelése controllerből

A billentyűzeteseményekhez a KeyEvent használható. Egy szövegmezőhöz rendelt kezelő például egy forgó animációt indíthat el, amikor a felhasználó karaktert ír be.

EventHandler<KeyEvent> eventHandlerTextField = new EventHandler<KeyEvent>() { @Override public void handle(KeyEvent event) { rotateTransition.play(); } };textField.addEventHandler( KeyEvent.KEY_TYPED, eventHandlerTextField);

A KEY_TYPED esemény minden beírt karakter esetén feldolgozható. A kezelőben a rotateTransition.play() elindítja a forgatást.

Egérkattintás kezelése és animáció leállítása

Az egérkattintás eseményét egy másik node, például egy 3D-s doboz is kezelheti:

EventHandler<javafx.scene.input.MouseEvent> eventHandlerBox = new EventHandler<javafx.scene.input.MouseEvent>() { @Override public void handle(javafx.scene.input.MouseEvent e) { rotateTransition.stop(); } };box.addEventHandler( javafx.scene.input.MouseEvent.MOUSE_CLICKED, eventHandlerBox);

Ebben az esetben a dobozra kattintás leállítja a korábban elindított forgó animációt.

Teljes JavaFX-példa controller nélküli eseménykezeléssel

Az alábbi programban egy szövegmezőbe írt karakter elindítja a 3D-s doboz forgását, a dobozra kattintás pedig leállítja a forgatást:

import javafx.animation.RotateTransition;import javafx.application.Application;import javafx.event.EventHandler;import javafx.scene.Group;import javafx.scene.PerspectiveCamera;import javafx.scene.Scene;import javafx.scene.control.TextField;import javafx.scene.input.KeyEvent;import javafx.scene.paint.Color;import javafx.scene.paint.PhongMaterial;import javafx.scene.shape.Box;import javafx.scene.text.Font;import javafx.scene.text.FontWeight;import javafx.scene.text.Text;import javafx.scene.transform.Rotate;import javafx.stage.Stage;import javafx.util.Duration;public class EventHandlersExample extends Application { @Override public void start(Stage stage) { Box box = new Box(); box.setWidth(150.0); box.setHeight(150.0); box.setDepth(100.0); box.setTranslateX(350); box.setTranslateY(150); box.setTranslateZ(50); Text text = new Text( "Type any letter to rotate the box, and click on the box to stop the rotation" ); text.setFont(Font.font(null, FontWeight.BOLD, 15)); text.setFill(Color.CRIMSON); text.setX(20); text.setY(50); PhongMaterial material = new PhongMaterial(); material.setDiffuseColor(Color.DARKSLATEBLUE); box.setMaterial(material); RotateTransition rotateTransition = new RotateTransition(); rotateTransition.setDuration(Duration.millis(1000)); rotateTransition.setNode(box); rotateTransition.setAxis(Rotate.Y_AXIS); rotateTransition.setByAngle(360); rotateTransition.setCycleCount(50); rotateTransition.setAutoReverse(false); TextField textField = new TextField(); textField.setLayoutX(50); textField.setLayoutY(100); EventHandler<KeyEvent> eventHandlerTextField = new EventHandler<KeyEvent>() { @Override public void handle(KeyEvent event) { rotateTransition.play(); } }; textField.addEventHandler( KeyEvent.KEY_TYPED, eventHandlerTextField ); EventHandler<javafx.scene.input.MouseEvent> eventHandlerBox = new EventHandler<javafx.scene.input.MouseEvent>() { @Override public void handle( javafx.scene.input.MouseEvent e ) { rotateTransition.stop(); } }; box.addEventHandler( javafx.scene.input.MouseEvent.MOUSE_CLICKED, eventHandlerBox ); Group root = new Group(box, textField, text); Scene scene = new Scene(root, 600, 300); PerspectiveCamera camera = new PerspectiveCamera(false); camera.setTranslateX(0); camera.setTranslateY(0); camera.setTranslateZ(0); scene.setCamera(camera); stage.setTitle("Event Handlers Example"); stage.setScene(scene); stage.show(); } public static void main(String args[]) { launch(args); }}

A program lefordításához és futtatásához a következő parancsok használhatók:

javac --module-path %PATH_TO_FX% --add-modules javafx.controls EventHandlersExample.javajava --module-path %PATH_TO_FX% --add-modules javafx.controls EventHandlersExample

A program egy szövegmezőt és egy 3D-s dobozt megjelenítő JavaFX-ablakot hoz létre. Ha karaktert írunk a szövegmezőbe, a 3D-s doboz forogni kezd, a dobozra kattintva pedig a forgás leáll.

Controller-metódus használata FXML-ben

Az eseménykezelő nem feltétlenül az initialize() metódusban regisztrálható. Egy FXML-elemhez controller-metódus is kapcsolható, amelyet az FXML eseményattribútuma hív meg.

A controllerben például létrehozható egy sendData nevű metódus, amelyben összegyűjtjük az adatokat. Ez a metódus egérkattintási esemény hatására hívható meg.

A másik controllerben egy receiveData nevű metódus töltheti fel a felhasználói adatokat tartalmazó objektumot. Ez a metódus szintén egérkattintási esemény hatására hívható meg.

Adatátadás két scene között

Az egyik leggyakoribb JavaFX-probléma annak megoldása, hogyan lehet a felhasználói adatokat az A scene-ből a B scene-be továbbítani. Két FXML-fájllal rendelkező JavaFX-alkalmazásban mindkét scene saját controllerrel rendelkezhet, és az A scene-ből adatot küldhetünk a B scene-be.

Az átadni kívánt információt egy külön osztály foglalhatja magába. Az A controllerben lévő sendData metódus összegyűjti az adatokat, a B controllerben lévő receiveData metódus pedig feldolgozza a fogadott objektumot.

Adatátadás a setUserData és getUserData segítségével

A JavaFX-ben a Node osztályból származó osztályok rendelkeznek a setUserData(Object) és a getUserData() metódussal. Ezek a metódusok lehetővé teszik tetszőleges objektum node-hoz csatolását későbbi használatra.

Bár erre a célra bármelyik node használható, célszerű magát a stage-et használni, mert az ugyanaz marad a scene-ek között.

A sendData metódusban az adatátadás lépései a következők:

  1. Az információ mentése egy user osztály példányába.
  2. A gombot reprezentáló node lekérése az eseményobjektumból.
  3. A stage példányának lekérése a node-ból és az aktuális scene bezárása.
  4. Az új scene betöltése az FXMLLoader osztály segítségével.
  5. Az információ átadása a stage-nek a setUserData függvénnyel.
  6. Új scene létrehozása és átadása a stage-nek.
  7. A stage ismételt láthatóvá tétele.

A receiveData metódusban le kell kérni a stage példányát, vissza kell nyerni az objektumot a getUserData függvénnyel, majd fel kell használni az információt.

Ez a megközelítés hasznos, ha gyors módra van szükség az adatok kétirányú továbbításához.

Controller példányának manuális beállítása

Az FXML-fájlok létrehozásakor az IDE általában automatikusan létrehozza a controllert, és az FXML gyökérelemében található fx:controller tulajdonsággal összekapcsolja őket.

Ez egy új controllerpéldányt is létrehoz, de ha egy controllerben tárolt mezőnek szeretnénk adatot átadni, akkor a példányt manuálisan kell létrehozni.

Ehhez először el kell távolítani az előzőleg bemutatott tulajdonságot a SceneB.fxml fájlból. A SceneBController osztályban létre kell hozni egy user típusú attribútumot, valamint hozzá tartozó settert és gettert.

Az A controller sendData metódusában az első megközelítés első három lépését kell követni. Ezután létre kell hozni egy FXMLLoader objektumot, és tárolni kell benne az FXMLLoader.load() utasítás eredményét.

Új SceneBController példányt kell létrehozni, majd a setter segítségével át kell adni neki az adatot. Az FXMLLoader objektum setController() metódusával be kell állítani a controllerpéldányt, ezután a load() metódus eredménye eltárolható a root objektumban.

A további lépések megegyeznek a scene cseréjének korábbi lépéseivel. A receiveData metódusban ezután megjeleníthető a user mezőben tárolt információ.

Adatmegosztás Singleton osztállyal

A kontrollerek helyett létrehozható egy harmadik osztály is, amelyből mindkét controller hozzáfér az adatokhoz. Ehhez a két controllernek ugyanazt az új osztálypéldányt kell megosztania, ami a Singleton mintával valósítható meg.

A legegyszerűbb megvalósítás lépései a következők:

  1. Olyan osztály létrehozása, amelynek konstruktora privát, ezért nem hozhatók létre tetszőleges új példányok.
  2. Az osztály típusával megegyező konstans létrehozása és inicializálása.
  3. Publikus statikus függvény létrehozása a konstans lekéréséhez.

Ezek mellett létre kell hozni egy, a szükséges osztály típusába tartozó mezőt is, valamint hozzá settert és gettert.

Ha az osztály neve például UserHolder, akkor az A controller a statikus példány lekérése után a létrehozott setterrel tárolhatja az információt. A B controller ugyanebből a statikus példányból nyerheti vissza az objektumot.

Ez a megközelítés különösen konfigurációs nézeteknél lehet hasznos, mert a felhasználói beállítások a Singleton osztályban tárolhatók, és a program bármely részéről visszakereshetők.

Gyakori hibák listener használatakor

  • Ha ugyanahhoz az eseményhez egymás után több setOnAction() hívás tartozik, a későbbi hívás lecseréli a korábbi kezelőt.
  • Ha ugyanazt a kezelőt több vezérlőn szeretnénk használni, célszerű külön EventHandler példányt létrehozni és több eseményhez hozzárendelni.
  • Ha egy eseményt nem szeretnénk továbbengedni az eseményútvonalon, az event.consume() metódust kell meghívni.
  • Az FXML-elemek csak akkor érhetők el a controllerben, ha az fx:id és a controller mezőjének neve megfelel egymásnak.
  • Az FXML-fájlt és a controllert össze kell kapcsolni az fx:controller tulajdonsággal, kivéve azt az esetet, amikor a controllert manuálisan állítjuk be az FXMLLoader segítségével.
  • A listenerhez használt eseménytípusnak illeszkednie kell a kezelt eseményhez, például a billentyűzeteseményhez KeyEvent, az egérkattintáshoz pedig MouseEvent használható.

Listener-választás JavaFX controllerben

HelyzetJavasolt megoldás
Egyszerű gombműveletsetOnAction(event -> ...)
Egér belépése vagy kilépésesetOnMouseEntered() és setOnMouseExited()
Billentyűzet kezeléseaddEventHandler(KeyEvent.KEY_TYPED, ...)
Ugyanaz a kezelés több vezérlőnKözös EventHandler példány
Kezelő későbbi eltávolításaaddEventHandler() és removeEventHandler()
Adatátadás scene-ek közöttsetUserData(), manuális controller vagy Singleton
JavaFX controller eseménykezelési folyamat

JavaFX Java GUI Tutorial - 32 - Controllers in FXML

tags: #javafx #listener #in #controller