发送到按钮控件的 JavaFX 事件:为什么有两个 MOUSE_ENTERED / MOUSE_EXITED 事件?

问题描述 投票:0回答:1

我对发送到 JavaFX 按钮控件的事件进行了一些实验,具体取决于鼠标指针发生的情况,并生成了一个 分层状态机图(大约是因为我的编辑器没有任何语义概念)这个:

这一切都非常简单,只需从开始状态跟踪行为并看看会发生什么。这里有一个微妙之处,因为按钮控件内的标签也可以发挥作用,它会生成

MOUSE_ENTERED_TARGET
MOUSE_EXITED_TARGET
事件(我向 ChatGPT 询问了这一点,它打印了一个漂亮的答案,但完全错误)

Button control hierarchical state machine

我唯一想知道的一点是 - 为什么我总是收到两个

MOUSE_ENTERED
或两个
MOUSE_EXITED
事件,一个带有
consumed = false
,一个带有
consumed = true

示例代码

这是(几乎最小的)示例。它只是创建一个按钮,我们可以手动触发该按钮以查看生成了哪些事件:

文件

com.example.stack_overflow.Main.java

package com.example.stack_overflow;

import javafx.application.Application;
import javafx.event.ActionEvent;
import javafx.event.Event;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.input.MouseEvent;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Pane;
import javafx.scene.layout.StackPane;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;

import java.util.Objects;

class PaneBuilder {

    private final StackPane stackPane;

    private static void log(String text) {
        System.out.println(text);
    }

    public PaneBuilder() {
        stackPane = buildCenteringStackPaneAroundButton(buildButton());
    }

    private static Button buildButton() {
        final var button = new Button("Click Me!");
        button.setMinWidth(120); // I think these are "120pt", why is there no "unit"?
        final var desc = "Button";
        button.addEventHandler(ActionEvent.ACTION, event -> {
            handleActionEvent(desc, event);
        });
        button.addEventHandler(MouseEvent.MOUSE_PRESSED, event -> {
            handleMouseEvent(desc, event);
        });
        button.addEventHandler(MouseEvent.MOUSE_RELEASED, event -> {
            handleMouseEvent(desc, event);
        });
        button.addEventHandler(MouseEvent.MOUSE_CLICKED, event -> {
            handleMouseEvent(desc, event);
        });
        button.addEventHandler(MouseEvent.MOUSE_ENTERED, event -> {
            handleMouseEvent(desc, event);
        });
        button.addEventHandler(MouseEvent.MOUSE_EXITED, event -> {
            handleMouseEvent(desc, event);
        });
        button.addEventHandler(MouseEvent.MOUSE_ENTERED_TARGET, event -> {
            handleMouseEvent(desc, event);
        });
        button.addEventHandler(MouseEvent.MOUSE_EXITED_TARGET, event -> {
            handleMouseEvent(desc, event);
        });
        button.armedProperty().addListener((obs, oldVal, newVal) -> log(desc + ": armed status changes: " + oldVal + " -> " + newVal));
        return button;
    }

    public Pane getPane() {
        return stackPane;
    }

    private static StackPane buildCenteringStackPaneAroundButton(Button button) {
        final var stackPane = new StackPane();
        stackPane.setAlignment(Pos.CENTER);
        final var hbox = buildHBoxAroundVBoxAroundButton(button);
        final var insets = new Insets(20, 20, 20, 20); // I guess those are "20points"
        StackPane.setMargin(hbox, insets);
        stackPane.getChildren().add(hbox);
        return stackPane;
    }

    private static HBox buildHBoxAroundVBoxAroundButton(Button button) {
        final var hbox = new HBox();
        hbox.setAlignment(Pos.CENTER);
        hbox.getChildren().add(buildVBoxAroundButton(button));
        return hbox;
    }

    private static VBox buildVBoxAroundButton(Button button) {
        final var vbox = new VBox();
        vbox.setAlignment(Pos.CENTER);
        vbox.getChildren().add(button);
        return vbox;
    }

    private static void appendEventDesc(StringBuilder buf, Event event) {
        buf.append("\n   Type          : " + event.getEventType());
        buf.append("\n   Class         : " + event.getClass().getName());
        buf.append("\n   Consumed      : " + event.isConsumed());
        buf.append("\n   Source class  : " + event.getSource().getClass().getName());
        buf.append("\n   Event         : " + Objects.toIdentityString(event));
    }

    public static void handleMouseEvent(String desc, MouseEvent event) {
        final var buf = new StringBuilder("Mouse event in " + desc);
        appendEventDesc(buf, event);
        log(buf.toString());
    }

    public static void handleActionEvent(String desc, ActionEvent event) {
        final var buf = new StringBuilder("Action event in " + desc);
        appendEventDesc(buf, event);
        log(buf.toString());
    }
}

public class Main extends Application {

    @Override
    public void start(Stage stage) {
        stage.setTitle("Button Example");
        stage.setScene(new Scene(new PaneBuilder().getPane()));
        stage.sizeToScene();
        stage.show();
    }

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

}

以及构建和运行上述内容的 POM。该程序必须使用

JavaFX Maven 插件
的 Maven 目标 javafx:run

运行
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.example</groupId>
    <artifactId>stack_overflow</artifactId>
    <version>1.0-SNAPSHOT</version>

    <!-- RUNNING VIA MAVEN + JAVAFX PLUGIN IN IDE: -->
    <!--   'Main Menu' > 'Run' > 'Run Maven Goal' > 'Plugins' > 'JavaFx Maven Plugin' > 'javafx:run' -->
    <!--   This will invoke the goal "javafx:run" of the "javafx-maven-plugin". -->
    <!--   With the 'Run New Maven Goal' menu entry, you can define that goal, and it will appear in the -->
    <!--   context menu as 'right-mouse-button menu' > 'run maven' > 'javafx:run' -->

    <!-- RUNNING VIA MAVEN IN COMMAND LINE w/o leaving the IDE (no need for OpenJDX SDK): -->
    <!-- Right-click on the project window and select "Open Terminal at the current Maven module path". -->
    <!-- Enter the command "mvn javafx:run" or for debugging output "mvn -X javafx:run". -->
    <!-- This will invoke the goal "javafx:run" of the "javafx-maven-plugin". -->

    <properties>

        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>

        <!-- This is the latest OpenJavaFX version on 2024-09-26 (version of 2024-09-16) -->
        <!-- https://mvnrepository.com/artifact/org.openjfx/javafx-controls -->

        <javafx.version>23</javafx.version>

        <javafx.plugin.version>0.0.8</javafx.plugin.version>
        <compiler.plugin.version>3.13.0</compiler.plugin.version>
        <exec.plugin.version>3.4.1</exec.plugin.version>
        <dependency.plugin.version>3.8.0</dependency.plugin.version>
        <main.class>com.example.stack_overflow.Main</main.class>

        <java.compiler.source.version>21</java.compiler.source.version>
        <java.compiler.target.version>21</java.compiler.target.version>

    </properties>

    <dependencies>
        <!-- https://mvnrepository.com/artifact/org.openjfx/javafx-controls -->
        <dependency>
            <groupId>org.openjfx</groupId>
            <artifactId>javafx-controls</artifactId>
            <version>${javafx.version}</version>
        </dependency>
        <!-- https://mvnrepository.com/artifact/org.openjfx/javafx-fxml -->
        <dependency>
            <groupId>org.openjfx</groupId>
            <artifactId>javafx-fxml</artifactId>
            <version>${javafx.version}</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>

            <!-- Standard Java Compiler Plugin -->
            <!-- https://maven.apache.org/plugins/maven-compiler-plugin/ -->
            <!-- https://mvnrepository.com/artifact/org.apache.maven.plugins/maven-compiler-plugin -->

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>${compiler.plugin.version}</version>
                <configuration>
                    <source>${java.compiler.source.version}</source>
                    <target>${java.compiler.target.version}</target>
                </configuration>
            </plugin>

            <!-- Special Plugin to run JavaFX programs -->
            <!-- https://github.com/openjfx/javafx-maven-plugin -->
            <!-- https://mvnrepository.com/artifact/org.openjfx/javafx-maven-plugin -->
            <!-- If you run the plugin's goal on the command line with the '-X' option like so: -->
            <!-- mvn -X javafx:run -->
            <!-- you will see the command line the plugin builds, which looks as follows, with ++ replaced by double dash -->

            <plugin>
                <groupId>org.openjfx</groupId>
                <artifactId>javafx-maven-plugin</artifactId>
                <version>${javafx.plugin.version}</version>
                <configuration>
                    <mainClass>${main.class}</mainClass>
                    <options>
                        <option>-ea</option>
                    </options>
                </configuration>
            </plugin>

        </plugins>
    </build>
</project>

运行此程序会弹出此窗口:

enter image description here

如果我们现在用鼠标指针沿着指示的红色路径移动,然后单击鼠标并再次将鼠标指针移出按钮:

enter image description here

我们在

armed
属性的值和按钮发出的事件中得到以下变化。请注意双
MOUSE_ENTERED
,其中
consumed
=
false
,然后是
true

进入按钮控制:

Mouse event in Button
   Type          : MOUSE_ENTERED
   Class         : javafx.scene.input.MouseEvent
   Consumed      : false
   Source class  : javafx.scene.control.Button
   Event         : javafx.scene.input.MouseEvent@2ebee20a
Mouse event in Button
   Type          : MOUSE_ENTERED
   Class         : javafx.scene.input.MouseEvent
   Consumed      : true
   Source class  : javafx.scene.control.Button
   Event         : javafx.scene.input.MouseEvent@2ebee20a

输入按钮控件的标签:

Mouse event in Button
   Type          : MOUSE_ENTERED_TARGET
   Class         : javafx.scene.input.MouseEvent
   Consumed      : false
   Source class  : javafx.scene.control.Button
   Event         : javafx.scene.input.MouseEvent@5d41df9f

点击,注意

armed
状态的变化。

Mouse event in Button
   Type          : MOUSE_PRESSED
   Class         : javafx.scene.input.MouseEvent
   Consumed      : false
   Source class  : javafx.scene.control.Button
   Event         : javafx.scene.input.MouseEvent@3df998ed
Button: armed status changes: false -> true
Mouse event in Button
   Type          : MOUSE_RELEASED
   Class         : javafx.scene.input.MouseEvent
   Consumed      : false
   Source class  : javafx.scene.control.Button
   Event         : javafx.scene.input.MouseEvent@241a11d5
Action event in Button
   Type          : ACTION
   Class         : javafx.event.ActionEvent
   Consumed      : false
   Source class  : javafx.scene.control.Button
   Event         : javafx.event.ActionEvent@3d8fce01
Button: armed status changes: true -> false
Mouse event in Button
   Type          : MOUSE_CLICKED
   Class         : javafx.scene.input.MouseEvent
   Consumed      : false
   Source class  : javafx.scene.control.Button
   Event         : javafx.scene.input.MouseEvent@505036ef

退出按钮控件的标签:

Mouse event in Button
   Type          : MOUSE_EXITED_TARGET
   Class         : javafx.scene.input.MouseEvent
   Consumed      : false
   Source class  : javafx.scene.control.Button
   Event         : javafx.scene.input.MouseEvent@2d496725

退出按钮控制:

Mouse event in Button
   Type          : MOUSE_EXITED
   Class         : javafx.scene.input.MouseEvent
   Consumed      : false
   Source class  : javafx.scene.control.Button
   Event         : javafx.scene.input.MouseEvent@59a94fe0
Mouse event in Button
   Type          : MOUSE_EXITED
   Class         : javafx.scene.input.MouseEvent
   Consumed      : true
   Source class  : javafx.scene.control.Button
   Event         : javafx.scene.input.MouseEvent@59a94fe0
javafx
1个回答
0
投票

这在

MouseEvent
javadoc 中进行了解释。

鼠标进入/退出处理

当鼠标进入某个节点时,该节点会得到 MOUSE_ENTERED 事件,当 当它离开时,它会收到 MOUSE_EXITED 事件。这些事件仅传递 到进入/退出的节点,并且似乎不经过 捕获/冒泡阶段。这是最常见的用例。

当需要捕获或冒泡时,有 MOUSE_ENTERED_TARGET/MOUSE_EXITED_TARGET 事件。这些事件去 通常通过捕获/冒泡阶段。这意味着家长可以 当鼠标进入任一目标时接收 MOUSE_ENTERED_TARGET 事件 父母本身或其一些孩子。为了区分 在这两种情况下,可以测试事件目标与 节点。

这两种类型紧密相连:MOUSE_ENTERED/MOUSE_EXITED 是 MOUSE_ENTERED_TARGET/MOUSE_EXITED_TARGET 的子类型。捕捉过程中 阶段,MOUSE_ENTERED_TARGET 被传递给父母。当 事件被传递到事件目标(实际发生事件的节点) 已输入),其类型切换为 MOUSE_ENTERED。那么类型是 在冒泡阶段切换回 MOUSE_ENTERED_TARGET。它是 还有一个事件只是切换类型,所以如果它被过滤或消耗, 它影响两个事件变体。 由于子类型关系, MOUSE_ENTERED_TARGET 事件处理程序将接收 MOUSE_ENTERED 事件达到目标。

我将最后一句加粗以示强调。

你写:

button.addEventHandler(MouseEvent.MOUSE_EXITED, event -> {
    handleMouseEvent(desc, event);
});
button.addEventHandler(MouseEvent.MOUSE_EXITED_TARGET, event -> {
    handleMouseEvent(desc, event);
});

根据文档,当鼠标退出按钮时,两个事件处理程序都将通过

MOUSE_EXITED
事件触发。 您正在处理各种类型的事件。 MOUSED_EXITED_TARGET 事件也是 MOUSE_EXITED 类型的事件,因此当您有两个事件的处理程序时,当鼠标退出目标时,两个事件都会被触发。

您可以从输出中看到,虽然只生成了一个事件(只是处理了多次),因为两个事件处理程序的输出都引用相同的事件对象:

Event         : javafx.scene.input.MouseEvent@59a94fe0
© www.soinside.com 2019 - 2024. All rights reserved.