DOC-07-01 处理事件

本章描述了JavaFX程序中的事件以及事件的处理。将学习有关事件类型、事件目标、事件捕获、事件冒泡(Event Bubbling)以及事件处理系统的底层架构。

事件被用来将用户的动作通知给你的程序并且使程序能够响应该事件。JavaFX平台提供了捕获事件、跟踪事件、将事件路由到其目标和使程序能够按需处理事件的结构。

事件

事件(Event)表示程序所感兴趣的事情的发生,比如鼠标的移动或者是某个按键的按下。在JavaFX中,事件是javafx.event.Event类或其任何子类的实例。JavaFX提供了多种事件,包括DragEvent、KeyEvent、MouseEvent、ScrollEvent等。你可以通过继承Event类来实现你自己的事件。

每个事件中都包含了表1-1中描述的信息。

表1-1 事件属性

属性 描述
事件类型(Event type) 发生事件的类型
源(Source) 事件的来源,表示该事件在事件派发链中的位置。事件通过派发链传递时,“源”会随之发生改变。
目标(Target) 发生动作的节点,在事件派发链的末尾。“目标”不会改变,但是如果某个事件过滤器在事件捕获阶段消费了该事件,“目标”将不会收到该事件。

事件子类提供了一些额外的信息,这些信息与事件的类型有关。例如,MouseEvent类包含哪个按钮被点击、按钮被点击的次数以及鼠标的位置等信息。

事件类型

事件类型(Event Type)是EventType类的实例。事件类型对单个事件类的多种事件进行了细化归类。例如,KeyEvent类包含如下事件类型:

● KEY_PRESSED

● KEY_RELEASED

● KEY_TYPED

事件类型是一个层级结构。每个事件类型有一个名称和一个父类型。例如,按键被按下的事件名叫KEY_PRESSED,其父类型是KeyEvent.ANY。顶级事件类型的父类型是null。图1-1展示了该层级结构的一个子集。

图1-1 事件类型层级机构

7_1_1 event_type_hierarchy

在该层级机构中顶级的事件类型是Event.ROOT,相当于Event.ANY。在子类型中,事件类型“ANY”用来表示该事件类中的任何事件类型。例如,为了给任何类型的键盘事件(Key Event)提供相同的响应,可以使用KeyEvent.ANY作为事件过滤器(Event Filter)或事件处理器(Event Handler)的事件类型。为了只在按键被释放时才响应,则可以使用KeyEvent.KEY_RELEASED作为过滤器或处理器的事件类型。

事件目标

一个事件的目标可以是任何实现了EventTarget接口的类的实例。buildEventDispatchChain方法的具体实现创建了事件派发链,事件必须经过该派发链到达事件目标。

Window、Scene和Node类均实现了EventTarget接口,这些类的子类也均继承了此实现。因此,在你的UI中的大多数元素都有它们已经定义好了的派发链,这使得你可以聚焦在如何响应事件上而不必关心创建事件派发链的事情。

如果你创建了一个响应用户动作的自定义UI控件,并且该控件是Window、Scene或者Node的子类,通过继承机制,你的控件也成为了一个事件目标。如果你的控件或你的控件中的某元素不是Window、Scene或者Node的子类,你必须要为该控件或元素实现EventTarget接口。例如,MenuBar控件通过继承成为了事件目标,但是一个菜单栏的MenuItem元素必须要实现EventTarget接口以便接收事件。

事件分发流程

事件分发流程包括如下几个步骤:

● 选择目标

● 构造路径

● 捕获事件

● 事件冒泡

目标选择

当一个动作发生时,系统根据内部规则决定哪一个Node是事件目标。规则如下:

● 对于键盘事件,事件目标是已获取焦点的Node。

● 对于鼠标事件,事件目标是光标所在位置处的Node;对于合成的(Synthesized)鼠标事件,触摸点被当做是光标所在位置。

● 对于在触摸屏上产生的连续的手势事件,事件目标是手势开始时所有触碰位置的中心点处的Node。对于在非触摸屏(例如触控板)上产生的间接手势,事件目标是光标所在位置的Node。

● 对于由在触摸屏上划动而产生的划动(swipe)事件,事件目标在所有手指的全部路径的中心处的Node。对于间接划动事件,事件目标是光标所在位置处的Node。

● 对于触摸事件,每个触摸点的默认事件目标是第一次按下时所在位置处的Node。在Event Filter或者Event Handler中可通过ungrab()、grab()或者grab(Node)方法来为触摸点指定不同的事件目标。

如果有多个Node位于光标或者触摸处,最上层的Node将被作为事件目标。例如,如果用户点击或触摸了图1-2中的三角形,那么该三角形即为事件目标,而不是包含该三角形和圆的矩形。

图1-2 UI事件目标样例

7_1_2 node_image

当鼠标按键按下时事件目标即被选定,所有随后的鼠标事件都将被分发到同样的事件目标上,一直到鼠标按键被释放时为止。手势事件也类似,从该手势的开始直到完成,手势事件都会被分发到手势开始时识别出的事件目标。对于触摸事件,默认会将事件分发到每个触摸点的初始事件目标,除非通过ungrab()、grab()或grab(node)方法修改了事件目标。

构造路径

初始的事件路径是由事件派发链决定的,派发链是在被选中的事件目标的buildEventDispatchChain方法实现中创建的。例如,如果用户点击了图1-2中的三角形,初始路径如图1-3中的灰色节点所示。当场景图中的一个节点被选中作为事件目标时,那么Node类的buildEventDispatchChain方法的默认实现中设置的初始事件路径即是从Stage到其自身的一条路径。

图1-3 事件派发链

7_1_3 dispatch_chain

由于路径上的Event Filter和Event Handler均会处理事件,因此路径可能会被修改。同样的,如果Event Filter或者Event Handler在任何时间点消费掉了事件,则在初始路径上的一些节点可能不会收到该事件。

事件捕获阶段

在事件捕获阶段,事件被程序的根节点派发并通过事件派发链向下传递到目标节点。如果使用图1-3中所示的事件派发链,在事件捕获阶段将从Stage节点传递到Triangle节点。

如果派发链中的任何节点为所发生的事件类型注册了Event Filter,则该Event Filter将会被调用。当Event filter执行完成以后,对应的事件会向下传递到事件派发链中的下一个节点。如果该节点未注册过滤器,事件将被传递到事件派发链中的下一个节点。如果没有任何过滤器消费掉事件,则事件目标最终将会接收到该事件并处理之。

事件冒泡阶段

当到达事件目标并且所有已注册的过滤器都处理完事件以后,该事件将顺着派发链从目标节点返回到根节点。如果使用图1-3中所示的事件派发链,事件在冒泡阶段将从Triangle节点传递到Stage节点。

如果在事件派发链中有节点为特定类型的事件注册了Event Handler,则在对应类型的事件发生时对应的Event Handler将会被调用。当Event Handler执行完成后,对应的事件将会向上传递给事件派发链中的上一个节点。如果么有任何Handler消费掉事件,则根节点最终将接收到对应的事件并且完成处理过程。

事件处理

事件处理功能由Event Filter和Event Handler提供,两者均是EventHandler接口的实现。如果你想要在某事件发生时通知应用程序,就需要为该事件注册一个Event Filter或Event Handler。Event Filter和Event Handler之间主要区别在于两者被执行的时机不同。

Event Filter  Event Filter在事件捕获阶段执行。父节点的事件过滤器可以为多个子节点提供公共的事件处理,并且如果需要的话,也可以消费掉事件以阻止子节点收到该事件。当某事件被传递并经过注册了Event Filter的节点时,为该事件类型的注册的Event Filter就会被执行。

一个节点可以注册多个Event Filter。Event Filter执行的顺序取决于事件类型的层级关系。特定事件类型的Event Filter会先于通用事件类型的过滤器执行。例如,MouseEvent.MOUSE_PRESSED事件的Event Filter会在InputEvent.ANY事件的Event Filter之前执行。同层级的Event Filter的执行顺序并未指定。

Event Handler

Event Handler在事件冒泡阶段执行。如果子节点的Event Handler未消耗掉对应的事件,那么父节点的Event Handler就可以在子节点处理完成以后来处理该事件,并且父节点的Event Handler还可以为多个子节点提供公共的事件处理过程。当某事件返回并经过注册了Event Handler的节点时,为该事件类型注册的Event Handler就会被执行。

一个节点可以注册多个Event Handler。Event Handler执行的顺序取决于事件类型的层级。特定事件类型的Event Handler会先于通用事件类型的Event Handler执行。例如,KeyEvent.KEY_TYPED事件的过滤器会在InputEvent.ANY事件的处理器执行。同层级的Event Handler的执行顺序并未指定,不过有一种例外情况:用第2章《使用快捷方法(Working with Convenience Methods)》章节中介绍的快捷方法注册的Event Handler会在最后执行。

事件的消费

事件可以被Event Filter或 Event Handler在事件派发链中的任意节点上通过调用consume()方法消耗掉。该方法表示事件的处理已经完成,并且事件派发链的遍历也应该终止。

在Event Filter中消费掉对应的事件会阻止事件派发链上的任何子节点再响应该事件。在Event Handler中消费掉对应的事件会阻止事件派发链中的任何父处理器再处理该事件。但是,如果消费掉该事件的节点为该事件注册了多个Event Filter或者Event Handler,这些同级的Event Filter或者Event Handler仍然会被执行。

例如,使用图1-3中的事件派发链,假设Pane节点有注册了一个KeyEvent.KEY_PRESSED事件的Event Filter和一个InputEvent.ANY事件的Event Filter。如果KEY_PRESSED事件的过滤器消费掉了对应事件,InputEvent事件的Event Filter会被执行,而Triangle节点不会接收到该事件。

需要注意的是,JavaFX中UI控件的默认Event Handler通常会消费掉大部分的录入事件。

附加资源

如果要了解关于事件处理的更多信息,请参考javafx.event包的JavaFX API文档

打赏一下
支付宝
微信
除非注明,博客文章均为原创,转载请标明文章地址
本文地址: http://www.javafxchina.net/blog/2015/09/doc0701-event/
百度已收录