关于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<UserEvent> ANY = new EventType<>(Event.ANY, "ANY");
public static final EventType<UserEvent> 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<UserEvent> ANY = new EventType<>(Event.ANY, "ANY");
/**
* 点击+1 事件类型
*/
public static final EventType<UserEvent> CLICKED = new EventType<>(ANY,"CLICKED");
/**
* 随机数 事件类型
*/
public static final EventType<UserEvent> 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);
}
}
最终效果展示: