JavaFx自定义事件

关于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);
    }
}

最终效果展示:

猜你喜欢

转载自blog.csdn.net/kanyun123/article/details/127814063
今日推荐