ZetCode

JavaFX 事件

最后修改于 2023 年 10 月 18 日

GUI 应用程序是事件驱动的。应用程序在其生命周期中会响应不同类型的事件。事件由用户(鼠标点击)、应用程序(计时器)或系统(时钟)生成。

事件是关于变化的通知。它封装了事件源中的状态变化。应用程序内的已注册的事件过滤器和事件处理程序会接收事件并提供响应。

JavaFX 中的每个事件都有三个属性

事件源 是状态发生变化的对象;它生成事件。事件目标 是事件的目的地。 事件类型 为同一 Event 类的事件提供了额外的分类。

事件源对象将处理事件的任务委托给事件处理程序。当事件发生时,事件源会创建一个事件对象,并将其发送给每个已注册的处理程序。

JavaFX 事件处理程序

EventHandler 处理特定类或类型的事件。事件处理程序被设置为事件源。它有一个 handle 方法,我们在其中放置对生成的事件做出反应的代码。

com/zetcode/EventHandlerEx.java
package com.zetcode;

import javafx.application.Application;
import javafx.application.Platform;
import javafx.scene.Scene;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.MenuItem;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;

public class EventHandlerEx extends Application {

    @Override
    public void start(Stage stage) {

        initUI(stage);
    }

    private void initUI(Stage stage) {

        var root = new HBox();

        var conMenu = new ContextMenu();
        var noopMi = new MenuItem("No op");
        var exitMi = new MenuItem("Exit");

        conMenu.getItems().addAll(noopMi, exitMi);

        exitMi.setOnAction(event -> Platform.exit());

        root.setOnMousePressed(event -> {
            if (event.isSecondaryButtonDown()) {
                conMenu.show(root, event.getScreenX(),
                        event.getScreenY());
            }
        });

        var scene = new Scene(root, 300, 250);

        stage.setTitle("EventHandler");
        stage.setScene(scene);
        stage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }
}

该示例使用两个 EventHandlers 来处理两个不同的 Events

var conMenu = new ContextMenu();

ContextMenu 是一个包含菜单项列表的弹出控件。

var noop = new MenuItem("No op");
var exit = new MenuItem("Exit");
var.getItems().addAll(noop, exit);

创建了两个 MenuItems 并将它们添加到上下文菜单中。

exitMi.setOnAction(event -> Platform.exit());

使用 setOnAction 方法,我们为 ActionEvent 设置一个事件处理程序。 EventHandlerhandle 方法使用 Platform.exit 方法退出应用程序。

root.setOnMousePressed(event -> {
    if (event.isSecondaryButtonDown()) {
        conMenu.show(root, event.getScreenX(),
                event.getScreenY());
    }
});

使用 setOnMousePressed 方法,我们为 MouseEvent 设置一个事件处理程序。当我们单击鼠标右键(通常是右键)时,上下文菜单会显示在屏幕上;它显示在鼠标点击的 x 和 y 坐标下方。

JavaFX 事件属性

以下程序探讨了 MouseEvent 的属性。由于用户与鼠标交互而发生的事件。

com/zetcode/EventSourceEx.java
package com.zetcode;

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.layout.Pane;
import javafx.scene.shape.Rectangle;
import javafx.stage.Stage;

public class EventSourceEx extends Application {

    @Override
    public void start(Stage stage) {

        initUI(stage);
    }

    private void initUI(Stage stage) {

        var root = new Pane();

        var rect = new Rectangle(30, 30, 80, 80);
        rect.setOnMouseClicked(e -> {

            System.out.println(e.getSource());
            System.out.println(e.getTarget());
            System.out.println(e.getEventType());
            System.out.format("x:%f, y:%f%n", e.getSceneX(), e.getSceneY());
            System.out.format("x:%f, y:%f%n", e.getScreenX(), e.getScreenY());
        });

        root.getChildren().addAll(rect);

        var scene = new Scene(root, 300, 250);

        stage.setTitle("Event properties");
        stage.setScene(scene);
        stage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }
}

在示例中,我们有一个矩形形状。我们为鼠标点击事件类型添加一个事件处理程序。

rect.setOnMouseClicked(e -> {
...
});

setOnMouseClicked 为鼠标点击事件类型添加一个事件处理程序。处理程序是一个匿名内部类。

System.out.println(e.getSource());
System.out.println(e.getTarget());
System.out.println(e.getEventType());

这三个是通用属性,适用于所有事件。 getSource 方法返回事件最初发生的对象。 getTarget 方法返回此事件的事件目标。在我们的例子中,事件源和事件目标是相同的——矩形。 getEventType 方法返回 MouseEvent 的事件类型。在我们的例子中,它返回 MOUSE_CLICKED 值。

System.out.format("x:%f, y:%f%n", e.getSceneX(), e.getSceneY());
System.out.format("x:%f, y:%f%n", e.getScreenX(), e.getScreenY());

这四个属性是此事件特有的。我们打印鼠标点击的 x 和 y 坐标,相对于场景和屏幕。

JavaFX 通用处理程序

在下一个示例中,我们创建一个通用事件处理程序,用于监听所有类型的事件。

com/zetcode/GenericHandlerEx.java
package com.zetcode;

import javafx.application.Application;
import javafx.event.Event;
import javafx.event.EventHandler;
import javafx.event.EventType;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.layout.StackPane;
import javafx.stage.Stage;

public class GenericHandlerEx extends Application {

    @Override
    public void start(Stage stage) {

        initUI(stage);
    }

    private void initUI(Stage stage) {

        var root = new StackPane();

        var btn = new Button("Button");
        btn.addEventHandler(EventType.ROOT, new GenericHandler());

        root.getChildren().add(btn);

        var scene = new Scene(root, 300, 250);

        stage.setTitle("Generic handler");
        stage.setScene(scene);
        stage.show();
    }

    public static void main(String[] args) {
        launch(args);
    }

    private class GenericHandler implements EventHandler<Event> {

        @Override
        public void handle(Event event) {

            System.out.println(event.getEventType());
        }
    }
}

此示例有一个按钮控件。一个通用处理程序被连接到按钮。

var btn = new Button("Button");
btn.addEventHandler(EventType.ROOT, new GenericHandler());

addEventHandler 方法将事件处理程序注册到按钮节点,以用于指定的事件类型。 EventType.ROOT 代表所有事件类型。

private class GenericHandler implements EventHandler<Event> {

    @Override
    public void handle(Event event) {

        System.out.println(event.getEventType());
    }
}

处理程序在其 handle 方法中将事件类型打印到控制台。

JavaFX 多个源

可以将单个事件处理程序添加到多个源。可以使用 getSource 方法确定事件的源。

com/zetcode/MultipleSourcesEx.java
package com.zetcode;

import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.layout.AnchorPane;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

public class MultipleSourcesEx extends Application {

    private Label lbl;

    @Override
    public void start(Stage stage) {

        initUI(stage);
    }

    private void initUI(Stage stage) {

        var root = new AnchorPane();

        var vbox = new VBox(5);

        var btn1 = new Button("Close");
        var btn2 = new Button("Open");
        var btn3 = new Button("Find");
        var btn4 = new Button("Save");

        var mbh = new MyButtonHandler();

        btn1.setOnAction(mbh);
        btn2.setOnAction(mbh);
        btn3.setOnAction(mbh);
        btn4.setOnAction(mbh);

        vbox.getChildren().addAll(btn1, btn2, btn3, btn4);

        lbl = new Label("Ready");

        AnchorPane.setTopAnchor(vbox, 10d);
        AnchorPane.setLeftAnchor(vbox, 10d);
        AnchorPane.setBottomAnchor(lbl, 10d);
        AnchorPane.setLeftAnchor(lbl, 10d);

        root.getChildren().addAll(vbox, lbl);

        var scene = new Scene(root, 350, 200);

        stage.setTitle("Multiple sources");
        stage.setScene(scene);
        stage.show();
    }

    private class MyButtonHandler implements EventHandler<ActionEvent> {

        @Override
        public void handle(ActionEvent event) {

            var btn = (Button) event.getSource();
            lbl.setText(String.format("Button %s fired", btn.getText()));
        }
    }

    public static void main(String[] args) {
        launch(args);
    }
}

该示例有四个按钮和一个标签。一个事件处理程序被添加到所有四个按钮。触发按钮的名称显示在标签中。

var btn1 = new Button("Close");
var btn2 = new Button("Open");
var btn3 = new Button("Find");
var btn4 = new Button("Save");

这四个按钮将共享一个事件处理程序。

var mbh = new MyButtonHandler();

创建了 MyButtonHandler 的一个实例。它被实现为一个内部命名类。

btn1.setOnAction(mbh);
btn2.setOnAction(mbh);
btn3.setOnAction(mbh);
btn4.setOnAction(mbh);

使用 setOnAction 方法将处理程序添加到四个不同的按钮。

private class MyButtonHandler implements EventHandler<ActionEvent> {

    @Override
    public void handle(ActionEvent event) {

        var btn = (Button) event.getSource();
        lbl.setText(String.format("Button %s fired", btn.getText()));
    }
}

MyButtonHandlerhandle 方法中,我们确定事件的源,并使用源的文本标签构建消息。该消息使用其 setText 方法设置到标签控件。

Multiple sources
图:多个源

java.util.Timer

java.util.Timer 计划在后台线程中将来执行任务。 TimerTask 是一个可以由计时器计划为一次性或重复执行的任务。

com/zetcode/TimerEx.java
package com.zetcode;

import java.util.Timer;
import java.util.TimerTask;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Alert;
import javafx.scene.control.Button;
import javafx.scene.control.Spinner;
import javafx.scene.layout.HBox;
import javafx.stage.Stage;

public class TimerEx extends Application {

    int delay = 0;

    @Override
    public void start(Stage stage) {

        initUI(stage);
    }

    private void initUI(Stage stage) {

        var root = new HBox(10);
        root.setPadding(new Insets(10));

        var timer = new Timer();

        var spinner = new Spinner<>(1, 60, 5);
        spinner.setPrefWidth(80);

        var btn = new Button("Show message");
        btn.setOnAction(event -> {

            delay = (int) spinner.getValue();
            timer.schedule(new MyTimerTask(), delay*1000);
        });

        root.getChildren().addAll(btn, spinner);

        stage.setOnCloseRequest(event -> timer.cancel());

        var scene = new Scene(root);

        stage.setTitle("Timer");
        stage.setScene(scene);
        stage.show();
    }

    private class MyTimerTask extends TimerTask {

        @Override
        public void run() {

            Platform.runLater(() -> {

                var alert = new Alert(Alert.AlertType.INFORMATION);
                alert.setTitle("Information dialog");
                alert.setHeaderText("Time elapsed information");

                String contxt;

                if (delay == 1) {
                    contxt = "1 second has elapsed";
                } else {
                    contxt = String.format("%d seconds have elapsed",
                            delay);
                }

                alert.setContentText(contxt);
                alert.showAndWait();
            });
        }
    }

    public static void main(String[] args) {
        launch(args);
    }
}

该示例有两个控件:一个按钮和一个微调器。按钮启动一个计时器,该计时器在延迟后显示消息对话框。延迟由微调器控件选择。

var timer = new Timer();

创建了 Timer 的一个实例。

var spinner = new Spinner<>(1, 60, 5);

Spinner 控件用于选择延迟量。其参数是最小值、最大值和当前值。这些值以毫秒为单位。

btn.setOnAction(event -> {

    delay = (int) spinner.getValue();
    timer.schedule(new MyTimerTask(), delay*1000);
});

在按钮的事件处理程序中,我们使用 getValue 方法获取微调器的当前值,并使用计时器的 schedule 方法计划任务。

stage.setOnCloseRequest(event -> timer.cancel());

当应用程序终止时,我们使用计时器的 cancel 方法取消计时器。

private class MyTimerTask extends TimerTask {

    @Override
    public void run() {

        Platform.runLater(() -> {

            var alert = new Alert(Alert.AlertType.INFORMATION);
            alert.setTitle("Information dialog");
            alert.setHeaderText("Time elapsed information");

            String contxt;

            if (delay == 1) {
                contxt = "1 second has elapsed";
            } else {
                contxt = String.format("%d seconds have elapsed",
                        delay);
            }

            alert.setContentText(contxt);
            alert.showAndWait();
        });
    }
}

runLater 方法在 JavaFX 应用程序线程上执行任务。我们显示一个消息对话框,通知经过的时间。

Time elapsed
图:经过的时间

JavaFX 移动窗口

以下示例显示应用程序窗口在屏幕上的位置。

com/zetcode/MovingWindowEx.java
package com.zetcode;

import javafx.application.Application;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.Label;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

public class MovingWindowEx extends Application {

    int x = 0;
    int y = 0;
    Label lbl_x;
    Label lbl_y;

    @Override
    public void start(Stage stage) {

        initUI(stage);
    }

    private void initUI(Stage stage) {

        var root = new VBox(10);
        root.setPadding(new Insets(10));

        var txt1 = String.format("x: %d", x);
        lbl_x = new Label(txt1);

        var txt2 = String.format("y: %d", y);
        lbl_y = new Label(txt2);

        root.getChildren().addAll(lbl_x, lbl_y);

        stage.xProperty().addListener(new ChangeListener<>() {

            @Override
            public void changed(ObservableValue<? extends Number> observable,
                                Number oldValue, Number newValue) {

                doChange(newValue);
            }

            private void doChange(Number newValue) {

                x = newValue.intValue();
                updateXLabel();
            }

        });

        stage.yProperty().addListener(new ChangeListener<>() {

            @Override
            public void changed(ObservableValue<? extends Number> observable,
                                Number oldValue, Number newValue) {

                doChange(newValue);
            }

            private void doChange(Number newValue) {

                y = newValue.intValue();
                updateYLabel();
            }

        });

        var scene = new Scene(root, 300, 250);

        stage.setTitle("Moving window");
        stage.setScene(scene);
        stage.show();
    }

    private void updateXLabel() {

        var txt = String.format("x: %d", x);
        lbl_x.setText(txt);
    }

    private void updateYLabel() {

        var txt = String.format("y: %d", y);
        lbl_y.setText(txt);
    }

    public static void main(String[] args) {
        launch(args);
    }
}

该示例在两个标签控件中显示当前的窗口坐标。要获取窗口位置,我们监听舞台的 xPropertyyProperty 的变化。

var txt1 = String.format("x: %d", x);
lbl_x = new Label(txt1);

var txt2 = String.format("y: %d", y);
lbl_y = new Label(txt2);

这两个标签显示应用程序窗口左上角的 x 和 y 坐标。

stage.xProperty().addListener(new ChangeListener<Number>() {

    @Override
    public void changed(ObservableValue<? extends Number> observable,
            Number oldValue, Number newValue) {

        doChange(newValue);
    }

    private void doChange(Number newValue) {

        x = newValue.intValue();
        updateXLabel();
    }

});

xProperty 存储舞台在屏幕上的水平位置。我们添加一个 ChangeListener 来监听属性的变化。每次修改该属性时,我们都会检索新值并更新标签。

private void updateYLabel() {

    var txt = String.format("y: %d", y);
    lbl_y.setText(txt);
}

标签使用 setText 方法进行更新。

Moving a window
图:移动窗口

JavaFX 教程的这一部分专门介绍了 JavaFX 事件。