1 处理事件
本主题描述了JavaFX应用程序中的事件和事件处理。了解事件类型、事件目标、事件捕获、事件冒泡以及事件处理系统的底层架构。
事件用于通知应用程序用户采取的操作,并使应用程序能够对事件做出响应。JavaFX平台提供了捕获事件、将事件路由到其目标并使应用程序能够根据需要处理事件的结构。
事件
事件代表应用程序中感兴趣的某个事件的发生,例如鼠标移动或按键按下。在JavaFX中,事件是javafx.event.Event
类的实例或Event
的任何子类。JavaFX提供了几种事件,包括DragEvent
、KeyEvent
、MouseEvent
、ScrollEvent
等。您可以通过扩展Event
类来定义自己的事件。
每个事件都包含在表1-1中描述的信息。
表1-1 事件属性
属性 | 描述 |
---|---|
事件类型 |
发生的事件类型。 |
源 |
事件在事件分发链中的位置的起源。随着事件在链上传递,源会发生变化。 |
目标 |
发生操作的节点,也是事件分发链中的最后一个节点。目标不会改变,但是如果事件过滤器在事件捕获阶段消耗了事件,目标将不会接收到事件。 |
事件子类提供了特定于事件类型的其他信息。例如,MouseEvent
类包括有关哪个按钮被按下、按钮被按下的次数以及鼠标的位置等信息。
事件类型
事件类型是EventType
类的实例。事件类型进一步分类了单个事件类的事件。例如,KeyEvent
类包含以下事件类型:
-
KEY_PRESSED
-
KEY_RELEASED
-
KEY_TYPED
事件类型是层次结构的。每个事件类型都有一个名称和一个超类型。例如,按下键盘的事件的名称是KEY_PRESSED
,超类型是KeyEvent.ANY
。顶级事件类型的超类型为null。 图1-1显示了层次结构的一个子集。
层次结构中的顶级事件类型是Event.ROOT
,等同于Event.ANY
。在子类型中,事件类型ANY
用于表示事件类中的任何事件类型。例如,要对任何类型的键盘事件提供相同的响应,请将KeyEvent.ANY
作为事件过滤器或事件处理程序的事件类型。要仅在释放键时响应,请使用KeyEvent.KEY_RELEASED
事件类型作为过滤器或处理程序的事件类型。
事件目标
事件的目标可以是实现了EventTarget
接口的任何类的实例。 buildEventDispatchChain
的实现创建了事件必须经过的事件分发链以达到目标。
Window
、Scene
和Node
类实现了EventTarget
接口,这些类的子类继承了该实现。因此,您用户界面中的大多数元素都有其分发链定义,使您能够专注于响应事件,而不必担心创建事件分发链。
如果您创建了一个自定义的用户界面控件,该控件响应用户操作,并且该控件是Window
、Scene
或Node
的子类,则通过继承,您的控件是一个事件目标。如果您的控件或控件的元素不是Window
、Scene
或Node
的子类,则必须为该控件或元素实现EventTarget
接口。例如,MenuBar
控件是通过继承成为目标,但是菜单栏的MenuItem
元素必须实现EventTarget
接口才能接收事件。
事件传递过程
事件传递过程包括以下步骤:
-
目标选择
-
路由构建
-
事件捕获
-
事件冒泡
目标选择
当发生一个动作时,系统根据内部规则确定目标节点:
-
对于键盘事件,目标节点是具有焦点的节点。
-
对于鼠标事件,目标节点是光标所在位置的节点。对于合成的鼠标事件,触摸点被视为光标的位置。
-
对于由触摸屏上的手势生成的连续手势事件,目标节点是手势开始时所有触摸点的中心点所在的节点。对于由触摸屏以外的设备(如触摸板)上的手势生成的间接手势事件,目标节点是光标所在位置的节点。
-
对于由触摸屏上的滑动生成的滑动事件,目标节点是所有手指路径的中心点所在的节点。对于间接滑动事件,目标节点是光标所在位置的节点。
-
对于触摸事件,每个触摸点的默认目标节点是首次按下的位置所在的节点。可以使用事件过滤器或事件处理器中的
ungrab()
、grab()
或grab(
node)
方法来指定不同的目标节点。
如果光标或触摸点上有多个节点,最上层的节点被视为目标节点。例如,如果用户点击或触摸图1-2中显示的三角形,三角形是目标节点,而不是包含圆和三角形的矩形。
当鼠标按钮被按下并且目标节点被选择后,所有后续的鼠标事件都会传递给同一个目标节点,直到按钮被释放。类似地,对于手势事件,从手势开始到手势完成,手势事件都会传递给开始时确定的目标节点。触摸事件的默认行为是将事件传递给每个触摸点最初确定的初始目标节点,除非使用ungrab()
、grab()
或grab(
node)
方法修改目标节点。
路由构建
初始事件路由由在所选事件目标的buildEventDispatchChain()
方法的实现中创建的事件分发链确定。例如,如果用户点击图1-2中显示的三角形,则初始路由由图1-3中的灰色节点显示。当将场景图节点选为事件目标时,在Node
类的buildEventDispatchChain()
方法的默认实现中设置的初始事件路由是从舞台到自身的路径。
随着事件过滤器和事件处理器沿着路由处理事件,路由可以被修改。此外,如果事件过滤器或事件处理器在任何点消耗了事件,则初始路由上的某些节点可能不会接收到事件。
事件捕获阶段
在事件捕获阶段,事件由应用程序的根节点分发,并沿着事件分发链传递到目标节点。使用图1-3中显示的事件分发链,事件在事件捕获阶段从舞台节点传递到三角形节点。
如果链中的任何节点注册了与发生的事件类型相对应的事件过滤器,则会调用该过滤器。当过滤器完成时,事件会传递给链中的下一个节点。如果节点没有注册过滤器,则事件会传递给链中的下一个节点。如果没有过滤器消耗事件,则事件目标最终接收并处理事件。
事件冒泡阶段
在到达事件目标并且所有注册的过滤器都处理完事件后,事件会沿着分发链从目标节点返回到根节点。使用图1-3中显示的事件分发链,事件在事件冒泡阶段从三角形节点返回到舞台节点。
如果链中的任何节点注册了与遇到的事件类型相对应的处理器,则会调用该处理器。当处理器完成时,事件会返回给链中的下一个节点。如果节点没有注册处理器,则事件会返回给链中的下一个节点。如果没有处理器消耗事件,则根节点最终接收事件并完成处理。
事件处理
事件处理由事件过滤器和事件处理器提供,它们是EventHandler
接口的实现。如果希望应用程序在事件发生时收到通知,请为事件注册一个过滤器或处理器。过滤器和处理器之间的主要区别在于它们的执行时机。
事件过滤器
事件过滤器在事件捕获阶段执行。父节点的事件过滤器可以为多个子节点提供共同的事件处理,并且如果需要,可以消耗事件以防止子节点接收事件。为发生的事件类型注册的过滤器在事件通过注册过滤器的节点时执行。
一个节点可以注册多个过滤器。每个过滤器的调用顺序基于事件类型的层次结构。特定事件类型的过滤器在通用事件类型的过滤器之前执行。例如,MouseEvent.MOUSE_PRESSED
事件的过滤器在InputEvent.ANY
事件的过滤器之前调用。在同一级别上执行两个过滤器的顺序没有指定。
事件处理器
事件处理器在事件冒泡阶段执行。如果子节点的事件处理器不消耗事件,父节点的事件处理器可以在子节点处理事件后对事件进行处理,并且可以为多个子节点提供共同的事件处理。为发生的事件类型注册的处理器在事件返回到注册处理器的节点时执行。
一个节点可以注册多个处理器。每个处理器的调用顺序基于事件类型的层次结构。特定事件类型的处理器在通用事件类型的处理器之前执行。例如,KeyEvent.KEY_TYPED
事件的处理器在InputEvent.ANY
事件的处理器之前调用。在同一级别上执行两个处理器的顺序没有指定,但通过使用便捷方法注册的处理器会最后执行。
事件的消费
事件可以在事件分发链的任何位置被事件过滤器或事件处理器消费,通过调用consume()
方法。这个方法表示事件的处理已经完成,并且事件分发链的遍历结束。
在事件过滤器中消费事件会阻止事件的任何子节点对事件的处理。在事件处理器中消费事件会停止事件在父处理器上的进一步处理。然而,如果消费事件的节点对同一个事件注册了多个过滤器或处理器,那么这些同级的过滤器或处理器仍然会被执行。
例如,假设在图1-3所示的事件分发链中,Pane节点对KeyEvent.KEY_PRESSED
事件注册了一个事件过滤器,并对InputEvent.ANY
事件注册了一个事件过滤器。如果按下键盘的事件过滤器消费了事件,那么输入事件的过滤器会被执行,而Triangle节点不会接收到事件。
请注意,JavaFX UI控件的默认处理器通常会消费大部分输入事件。