关于JavaFx自定义事件:
JavaFX Documentation Projecthttps://fxdocs.github.io/docs/html5/index.html#_event_handling上面的文档已经做了简要说明,但是在实际应用中发现其并不够详细,搜索现有网上的自定义事件其内容大都并不十分清晰,因此写篇博客站在我的角度描述一下这个问题,我这里使用的JDK8。
首先我的需求:
如图所示,需求十分清晰,就是做一个点击按钮计数器,当点击按钮,下面的计数器的数字会发生变化。
实现方式一:
- import javafx.application.Application;
- import javafx.scene.Scene;
- import javafx.scene.control.Button;
- import javafx.scene.control.Label;
- import javafx.scene.layout.VBox;
- import javafx.stage.Stage;
-
- import java.util.concurrent.atomic.AtomicInteger;
-
- public class EventTestApp extends Application {
-
- public static AtomicInteger atomicInteger = new AtomicInteger();
-
- public static final String labelPrefix = "点击次数:";
-
- @Override
- public void start(Stage primaryStage) throws Exception {
- Button btn = new Button();
- btn.setText("点击加一");
- Label label = new Label(labelPrefix + "0");
- btn.setOnAction(event -> {
- // 设置Label显示文字
- label.setText(labelPrefix + atomicInteger.incrementAndGet());
- });
-
- VBox root = new VBox();
- root.getChildren().addAll(btn, label);
-
- Scene scene = new Scene(root, 300, 250);
-
- primaryStage.setTitle("javaFx自定义事件测试");
- primaryStage.setScene(scene);
- primaryStage.show();
- }
-
- public static void main(String[] args) {
- launch(args);
- }
- }
实现方式非常简单,就是当按钮发生点击时引用Label实例,然后设置Label的值,同时也可以看到一些缺陷: 就是Label的初始化必须在Button的前面。
实现方式二:
- import javafx.application.Application;
- import javafx.scene.Scene;
- import javafx.scene.control.Button;
- import javafx.scene.control.Label;
- import javafx.scene.layout.VBox;
- import javafx.stage.Stage;
-
- import java.util.concurrent.atomic.AtomicInteger;
-
- public class EventTestApp extends Application {
-
- public static AtomicInteger atomicInteger = new AtomicInteger();
-
- public static final String labelPrefix = "点击次数:";
-
- @Override
- public void start(Stage primaryStage) throws Exception {
- Button btn = new Button();
- btn.setText("点击加一");
-
- btn.setOnAction(event -> {
- UserEvent userEvent = new UserEvent(UserEvent.CLICKED);
- // 发射自定义事件
- btn.fireEvent(userEvent);
- });
-
- Label label = new Label(labelPrefix + "0");
- // 添加自定义事件处理方法
- label.addEventFilter(UserEvent.ANY,event -> {
- // 设置Label显示文字
- label.setText(labelPrefix + atomicInteger.incrementAndGet());
- });
-
- VBox root = new VBox();
- root.getChildren().addAll(btn, label);
-
-
-
- Scene scene = new Scene(root, 300, 250);
-
- primaryStage.setTitle("javaFx自定义事件测试");
- primaryStage.setScene(scene);
- primaryStage.show();
- }
-
- public static void main(String[] args) {
- launch(args);
- }
- }
- import javafx.event.Event;
- import javafx.event.EventType;
-
- public class UserEvent extends Event {
- public static final EventType
ANY = new EventType<>(Event.ANY, "ANY"); -
- public static final EventType
CLICKED = new EventType<>(ANY,"CLICKED"); -
-
- public UserEvent(EventType extends Event> eventType) {
- super(eventType);
- }
-
-
- }
由于这种方式我们使用了自定义事件,因此创建了自定义事件的类,当按钮点击时,创建自定义事件,然后发送事件,同时在Lable初始化后监听了该事件,此时我们发现Button与Lable实现了解耦。
然而一切看起来十分美好,但是当点击按钮时,却发现Label并未做出任何反应,此时按照开头文档,应该是没有问题的才对。
开启Debug模式,调试
-
- // 发射自定义事件
- btn.fireEvent(userEvent);
看他做了什么。
简单的调试过后,我们发现其实调用的是 com.sun.javafx.event.EventUtil.fireEvent()方法,他其实是一个静态方法。
EventUtil类
- public static Event fireEvent(EventTarget eventTarget, Event event) {
- // 通过调试发现,此时event.getTarget()为null,而eventTarget为button
- if (event.getTarget() != eventTarget) {
- // 顾名思义,似乎是一个事件拷贝的方法
- event = event.copyFor(event.getSource(), eventTarget);
- // 重要的是当该方法执行完成event.getTarget()竟然指向了button,也就是说事件发射源和接收者指向了同一个组件
- }
-
- if (eventDispatchChainInUse.getAndSet(true)) {
- // the member event dispatch chain is in use currently, we need to
- // create a new instance for this call
- return fireEventImpl(new EventDispatchChainImpl(),
- eventTarget, event);
- }
-
- try {
- return fireEventImpl(eventDispatchChain, eventTarget, event);
- } finally {
- // need to do reset after use to remove references to event
- // dispatchers from the chain
- eventDispatchChain.reset();
- eventDispatchChainInUse.set(false);
- }
- }
我把调试的发现写在了代码里,由于事件发射源与事件接收者是同一个组件,那么我就可以直接给button添加EventHandler。
于是代码更新为:
- import javafx.application.Application;
- import javafx.scene.Scene;
- import javafx.scene.control.Button;
- import javafx.scene.control.Label;
- import javafx.scene.layout.VBox;
- import javafx.stage.Stage;
-
- import java.util.concurrent.atomic.AtomicInteger;
-
- public class EventTestApp extends Application {
-
- public static AtomicInteger atomicInteger = new AtomicInteger();
-
- public static final String labelPrefix = "点击次数:";
-
- @Override
- public void start(Stage primaryStage) throws Exception {
- Button btn = new Button();
- btn.setText("点击加一");
-
- btn.setOnAction(event -> {
- System.out.println("btn发送事件");
- UserEvent userEvent = new UserEvent(UserEvent.CLICKED);
- // 发射自定义事件
- btn.fireEvent(userEvent);
- });
- btn.addEventHandler(UserEvent.ANY,event -> {
- System.out.println("btn接收到事件");
- });
- Label label = new Label(labelPrefix + "0");
- // 添加自定义事件处理方法
- label.addEventFilter(UserEvent.ANY,event -> {
- // 设置Label显示文字
- label.setText(labelPrefix + atomicInteger.incrementAndGet());
- System.out.println("Label接收到事件");
- });
-
- VBox root = new VBox();
- root.getChildren().addAll(btn, label);
-
-
-
- Scene scene = new Scene(root, 300, 250);
-
- primaryStage.setTitle("javaFx自定义事件测试");
- primaryStage.setScene(scene);
- primaryStage.show();
- }
-
- public static void main(String[] args) {
- launch(args);
- }
- }
运行代码,并且点击按钮,得到结果是
发现果然是button把事件发送给了自己。
那么问题就来了,为什么button会把事件发送给自己,而不是Label呢,其实通过调试我们也可以发现,一个重要的参数是EventTarget。而EventTarget是一个接口。
EventTarget类
- public interface EventTarget {
-
- EventDispatchChain buildEventDispatchChain(EventDispatchChain tail);
- }
然后找EventTarget的子类,发现都是一些control下的类,比如Button、Pane、Box等组件。
那么是不是我们在发射组件时指定EventTarget就可以了呢?已知,EventTarget的子类时control下的类,那么我们的Label应该也是EventTarget的实例
于是启动类变更为:
- import javafx.application.Application;
- import javafx.event.Event;
- import javafx.scene.Scene;
- import javafx.scene.control.Button;
- import javafx.scene.control.Label;
- import javafx.scene.layout.VBox;
- import javafx.stage.Stage;
-
- import java.util.concurrent.atomic.AtomicInteger;
-
- public class EventTestApp extends Application {
-
- public static AtomicInteger atomicInteger = new AtomicInteger();
-
- public static final String labelPrefix = "点击次数:";
-
- @Override
- public void start(Stage primaryStage) throws Exception {
- Button btn = new Button();
- btn.setText("点击加一");
- Label label = new Label(labelPrefix + "0");
- btn.setOnAction(event -> {
- System.out.println("btn发送事件");
- UserEvent userEvent = new UserEvent(UserEvent.CLICKED);
- // 发射自定义事件
- btn.fireEvent(userEvent);
- Event.fireEvent(label,userEvent);
- });
- btn.addEventHandler(UserEvent.ANY,event -> {
- System.out.println("btn接收到事件");
- });
-
- // 添加自定义事件处理方法
- label.addEventFilter(UserEvent.ANY,event -> {
- // 设置Label显示文字
- label.setText(labelPrefix + atomicInteger.incrementAndGet());
- System.out.println("Label接收到事件");
- });
-
- VBox root = new VBox();
- root.getChildren().addAll(btn, label);
- Scene scene = new Scene(root, 300, 250);
- primaryStage.setTitle("javaFx自定义事件测试");
- primaryStage.setScene(scene);
- primaryStage.show();
- }
-
- public static void main(String[] args) {
- launch(args);
- }
- }
启动,点击按钮:
发现Label监听到了事件,但是缺点也是十分明显,就是Label的初始化依然在Button的前面,似乎又回到开始的地方,那么有没有现成的方案去获取
EventTarget呢,其实是有的,通过kookup()方法进行查找,不过在查找对应EventTarget之前,需要先将EventTarget设置一个标识,即通过setId()方法。此时启动类变更为:
- import javafx.application.Application;
- import javafx.event.Event;
- import javafx.scene.Node;
- import javafx.scene.Scene;
- import javafx.scene.control.Button;
- import javafx.scene.control.Label;
- import javafx.scene.layout.VBox;
- import javafx.stage.Stage;
-
- import java.util.concurrent.atomic.AtomicInteger;
-
- public class EventTestApp extends Application {
-
- public static AtomicInteger atomicInteger = new AtomicInteger();
-
- public static final String labelPrefix = "点击次数:";
-
- @Override
- public void start(Stage primaryStage) throws Exception {
- Button btn = new Button();
- btn.setText("点击加一");
-
- btn.setOnAction(event -> {
- System.out.println("btn发送事件");
- UserEvent userEvent = new UserEvent(UserEvent.CLICKED);
- // 发射自定义事件
- // btn.fireEvent(userEvent);
- Node lookup = btn.getScene().lookup("#test-label");
- Event.fireEvent(lookup,userEvent);
- });
- btn.addEventHandler(UserEvent.ANY,event -> {
- System.out.println("btn接收到事件");
- });
-
- Label label = new Label(labelPrefix + "0");
- // 设置EventTarget的id
- label.setId("test-label");
- // 添加自定义事件处理方法
- label.addEventFilter(UserEvent.ANY,event -> {
- // 设置Label显示文字
- label.setText(labelPrefix + atomicInteger.incrementAndGet());
- System.out.println("Label接收到事件");
- });
-
- VBox root = new VBox();
- root.getChildren().addAll(btn, label);
- Scene scene = new Scene(root, 300, 250);
- primaryStage.setTitle("javaFx自定义事件测试");
- primaryStage.setScene(scene);
- primaryStage.show();
- }
-
- public static void main(String[] args) {
- launch(args);
- }
- }
启动,执行结果为:
此时Label组件执行了,而button的监听并没有执行,这说明如果指定了EventTarget那么只有指定的EventTarget才能被触发,当然如何让button也能执行呢,其实我们只需要发送多次事件即可,如同代码中的注释。
同时可以看到在使用lookup()方法之前,需要先执行getScene()方法,这是因为寻找EventTarget是从上到下寻找,因此我们从Scene开始找就一定可以找到。
关于javaFx的结构图,我从网上找了一张。
同时采用了lookup()方法,组件间不需要相互持有引用,因此组件初始化顺序就变得灵活了。
以上我们的代码是直接写在启动类中的,其实这并不符合我们的开发直觉,因为不管CS程序还是BS程序,都有一些公共的区域,比如页头区域,页尾,按钮组区域等,因此我们需要把内容单独写到一个组件中。
自定义一个组件,把内容放在自定义组件中。
- import javafx.scene.control.Button;
- import javafx.scene.control.Label;
- import javafx.scene.layout.VBox;
-
- import java.util.concurrent.atomic.AtomicInteger;
-
- public class CustomPane extends VBox {
-
- public static AtomicInteger atomicInteger = new AtomicInteger();
-
- public static final String labelPrefix = "点击次数:";
-
- public CustomPane() {
- Button btn = new Button();
- btn.setText("点击加一");
- btn.setOnAction(event -> {
- System.out.println("btn发送事件");
- UserEvent userEvent = new UserEvent(UserEvent.CLICKED);
- // 发射自定义事件
- btn.fireEvent(userEvent);
- });
- btn.addEventHandler(UserEvent.ANY,event -> {
- System.out.println("btn接收到事件");
- });
- Label label = new Label(labelPrefix + "0");
- // 添加自定义事件处理方法
- label.addEventFilter(UserEvent.ANY,event -> {
- // 设置Label显示文字
- label.setText(labelPrefix + atomicInteger.incrementAndGet());
- System.out.println("Label接收到事件");
- });
-
- getChildren().addAll(btn, label);
- // 自定义组件添加监听
- addEventHandler(UserEvent.ANY,e->{
- System.out.println("自定义组件接收到事件");
- // 设置Label显示文字
- label.setText(labelPrefix + atomicInteger.incrementAndGet());
- });
- }
- }
启动类精简为:
- import javafx.application.Application;
- import javafx.scene.Scene;
- import javafx.stage.Stage;
-
- public class EventTestApp extends Application {
-
-
-
- @Override
- public void start(Stage primaryStage) throws Exception {
- Scene scene = new Scene(new CustomPane(), 300, 250);
- primaryStage.setTitle("javaFx自定义事件测试");
- primaryStage.setScene(scene);
- primaryStage.show();
- }
-
- public static void main(String[] args) {
- launch(args);
- }
- }
启动,点击按钮得到结果:
Label没有监听到事件在意料之中,因为上面代码在发射事件时并没有指定EventTarget,但是在自定义组件的代码里同时设置了监听,结果也监听到了。并且由于Label是在自定义组件中进行初始化的,那么自定义组件本身自然可以也引用的到Label。
那么为什么自定义组件本身也可以监听的到事件呢?
继续调试:
在com.sun.javafx.event.EventUtil类中找到该方法:
- private static Event fireEventImpl(EventDispatchChain eventDispatchChain,
- EventTarget eventTarget,
- Event event) {
- // eventTarget.buildEventDispatchChain(eventDispatchChain)是个十分重要的方法,即构建事件分发链。
- // 构建好后的分发链包含了Buuton的父节点即自定义组件,因此自定义组件也可以监听事件,可以试试在启动类
- // 中添加addEventHandler(),其实是添加不上的。
- final EventDispatchChain targetDispatchChain =
- eventTarget.buildEventDispatchChain(eventDispatchChain);
- return targetDispatchChain.dispatchEvent(event);
- }
我们看看是如何构建事件分发链的。
javafx.scene.Node类
- public EventDispatchChain buildEventDispatchChain(
- EventDispatchChain tail) {
-
- if (preprocessMouseEventDispatcher == null) {
- preprocessMouseEventDispatcher = (event, tail1) -> {
- event = tail1.dispatchEvent(event);
- if (event instanceof MouseEvent) {
- preprocessMouseEvent((MouseEvent) event);
- }
-
- return event;
- };
- }
-
- tail = tail.prepend(preprocessMouseEventDispatcher);
-
- // prepend all event dispatchers from this node to the root
- Node curNode = this;
- do {
- if (curNode.eventDispatcher != null) {
- final EventDispatcher eventDispatcherValue =
- curNode.eventDispatcher.get();
- if (eventDispatcherValue != null) {
- tail = tail.prepend(eventDispatcherValue);
- }
- }
- // 重点是这个方法
- final Node curParent = curNode.getParent();
- curNode = curParent != null ? curParent : curNode.getSubScene();
- } while (curNode != null);
-
- if (getScene() != null) {
- // prepend scene's dispatch chain
- tail = getScene().buildEventDispatchChain(tail);
- }
-
- return tail;
- }
这个方法是Node类即节点类,我们在方法里面看到了getParent()方法,疑问也就可以解答了,在构建了事件分发链时取了父节点。同时我们在看看节点链类的大致结构:
com.sun.javafx.event.EventDispatchChainImpl
- public class EventDispatchChainImpl implements EventDispatchChain {
- /** Must be a power of two. */
- private static final int CAPACITY_GROWTH_FACTOR = 8;
-
- private EventDispatcher[] dispatchers;
-
- private int[] nextLinks;
-
- private int reservedCount;
- private int activeCount;
- private int headIndex;
- private int tailIndex;
-
- public EventDispatchChainImpl() {
- }
-
- /** 略 */
-
- /**
- * 重点方法 事件分发
- */
- @Override
- public Event dispatchEvent(final Event event) {
- if (activeCount == 0) {
- return event;
- }
-
- // push current state
- final int savedHeadIndex = headIndex;
- final int savedTailIndex = tailIndex;
- final int savedActiveCount = activeCount;
- final int savedReservedCount = reservedCount;
-
- final EventDispatcher nextEventDispatcher = dispatchers[headIndex];
- headIndex = nextLinks[headIndex];
- --activeCount;
- // 重点
- final Event returnEvent =
- nextEventDispatcher.dispatchEvent(event, this);
-
- // pop saved state
- headIndex = savedHeadIndex;
- tailIndex = savedTailIndex;
- activeCount = savedActiveCount;
- reservedCount = savedReservedCount;
-
- return returnEvent;
- }
-
- }
里面包含了需要分发的数组。还有一个分发函数dispatchEvent()。它最终又调用了其他方法。
第三种方式:
通过第二种方式,其实就已经讲明白了事件分发是怎么回事,其实第三种方式是用来说明我认为最合适的方式,由于前面已经说明了EventTarget,那么我们在日常开发时,尽量要做到组件化,比如按钮组就是一个组件,里面是很多的按钮,内容显示区是单独的一个组件,因此,我们创建两个自定义组件,一个组件专门用来放置按钮,一个组件专门用来控制Label,按钮组件发射事件,Label组件监听组件并响应变化。
自定义按钮组件
- import com.sun.javafx.event.EventUtil;
- import javafx.geometry.Insets;
- import javafx.geometry.Pos;
- import javafx.scene.Node;
- import javafx.scene.control.Button;
- import javafx.scene.layout.Background;
- import javafx.scene.layout.BackgroundFill;
- import javafx.scene.layout.CornerRadii;
- import javafx.scene.layout.HBox;
- import javafx.scene.paint.Color;
-
- /**
- * 自定义Button组件
- */
- public class ButtonPane extends HBox {
-
-
- public ButtonPane() {
- setPrefHeight(30);
- setAlignment(Pos.CENTER);
- // 设置子组件间距
- setSpacing(30);
- Color blue = Color.BLUE;
- // 四个角半径即填充有弧度
- CornerRadii cornerRadii = new CornerRadii(0);
- Insets insets = new Insets(0, 0, 0, 0);
- BackgroundFill backgroundFill = new BackgroundFill(blue, cornerRadii, insets);
- setBackground(new Background(backgroundFill));
-
- Button btn = new Button("点击+1");
- Button btn2 = new Button("生成随机数");
-
- btn.setOnAction(event -> {
- // 注意构造方法中选择正确的事件类型
- UserEvent userEvent = new UserEvent(UserEvent.CLICKED);
- Node eventTarget = btn.getScene().lookup("#LabelPane");
- // 发射自定义事件(点击+1)
- EventUtil.fireEvent(eventTarget, userEvent);
- });
-
- btn2.setOnAction(event -> {
- // 注意构造方法中选择正确的事件类型
- UserEvent userEvent = new UserEvent(UserEvent.RANDOM);
- Node eventTarget = btn2.getScene().lookup("#LabelPane");
- // 发射自定义事件(随机数)
- EventUtil.fireEvent(eventTarget, userEvent);
- });
-
- getChildren().addAll(btn, btn2);
- }
- }
自定义Label组件
- import javafx.geometry.Insets;
- import javafx.geometry.Pos;
- import javafx.scene.control.Label;
- import javafx.scene.layout.Background;
- import javafx.scene.layout.BackgroundFill;
- import javafx.scene.layout.CornerRadii;
- import javafx.scene.layout.HBox;
- import javafx.scene.paint.Color;
-
- import java.util.Random;
- import java.util.concurrent.atomic.AtomicInteger;
- /**
- * 自定义Label组件
- */
- public class LabelPane extends HBox {
-
- public static AtomicInteger atomicInteger = new AtomicInteger();
-
- public static final String labelPrefix = "点击次数:";
-
- public static final String label2Prefix = "随机数:";
-
- public LabelPane() {
- setId("LabelPane");
- setPrefHeight(30);
- setAlignment(Pos.CENTER);
- // 设置子组件间距
- setSpacing(30);
- Color green = Color.GREEN;
- // 四个角半径即填充有弧度
- CornerRadii cornerRadii = new CornerRadii(0);
- Insets insets = new Insets(0, 0, 0, 0);
- BackgroundFill backgroundFill = new BackgroundFill(green, cornerRadii, insets);
- setBackground(new Background(backgroundFill));
-
- Label label = new Label(labelPrefix + "0");
- Label label2 = new Label(label2Prefix + "0");
- // 添加自定义事件处理方法(点击+1),注意此处类型要设置正确
- addEventHandler(UserEvent.CLICKED, event -> {
- // 设置Label显示文字
- label.setText(labelPrefix + atomicInteger.incrementAndGet());
- });
- // 添加自定义事件处理方法(随机数),注意此处类型要设置正确
- addEventHandler(UserEvent.RANDOM, event -> {
- // 设置Label显示文字
- label2.setText(label2Prefix + new Random().nextInt());
- });
-
- getChildren().addAll(label, label2);
- }
- }
自定义事件类:
- import javafx.event.Event;
- import javafx.event.EventType;
-
- /**
- * 自定义事件类
- */
- public class UserEvent extends Event {
- public static final EventType
ANY = new EventType<>(Event.ANY, "ANY"); -
- /**
- * 点击+1 事件类型
- */
- public static final EventType
CLICKED = new EventType<>(ANY,"CLICKED"); -
- /**
- * 随机数 事件类型
- */
- public static final EventType
RANDOM = new EventType<>(ANY,"RANDOM"); -
-
- public UserEvent(EventType extends Event> eventType) {
- super(eventType);
- }
-
-
- }
启动类
- import javafx.application.Application;
- import javafx.scene.Scene;
- import javafx.scene.layout.VBox;
- import javafx.stage.Stage;
-
- public class EventTestApp extends Application {
-
- @Override
- public void start(Stage primaryStage) throws Exception {
- VBox root = new VBox();
- // 初始化自定义组件
- ButtonPane buttonPane = new ButtonPane();
- LabelPane labelPane = new LabelPane();
- // 将初始化好的组件,放入到根布局中
- root.getChildren().addAll(buttonPane, labelPane);
- // 根布局设置到场景中
- Scene scene = new Scene(root, 300, 250);
- primaryStage.setTitle("javaFx自定义事件测试");
- primaryStage.setScene(scene);
- primaryStage.show();
- }
-
- public static void main(String[] args) {
- launch(args);
- }
- }
最终效果展示: