AWT 焦点子系统
在 Java 2 标准版 JDK 1.4 之前,AWT 焦点子系统是不够的。它遭受了重大的设计和 API 问题,以及一百多个公开的错误。许多这些错误是由平台不一致引起的,或者是重量级的本机焦点系统与轻量级的 Java 焦点系统之间的不兼容。
AWT 焦点实现的一个最严重的问题是无法查询当前焦点组件。这种查询不仅没有API,而且由于架构不够完善,这种信息甚至没有代码维护。
几乎同样糟糕的是 Window(不是 Frame 或 Dialog)的轻量级子级无法接收键盘输入。存在此问题是因为 Windows 从未收到 WINDOW_ACTIVATED 事件,因此永远无法激活,并且只有活动的 Windows 才能包含焦点组件。
此外,许多开发人员注意到 FocusEvent 和 WindowEvent 的 API 不足,因为它们没有提供一种方法来确定参与焦点或激活更改的“相反”组件。例如,当 Component 收到 FOCUS_LOST 事件时,它无法知道哪个 Component 正在获得焦点。由于 Microsoft Windows 免费提供此功能,因此从 Microsoft Windows C/C++ 或 Visual Basic 迁移到 Java 的开发人员因遗漏而感到沮丧。
为了解决这些和其他缺陷,我们在 JDK 1.4 中为 AWT 设计了一个新的焦点模型。主要的设计更改是构建新的集中式 KeyboardFocusManager 类和轻量级焦点架构。与焦点相关、平台相关的代码量已被最小化,并被 AWT 中完全可插入和可扩展的公共 API 所取代。虽然我们试图保持与现有实现的向后兼容,但我们被迫进行微小的不兼容更改,以得出优雅且可行的结论。我们预计这些不兼容性对现有应用程序的影响很小。
本文档是新 API 和在新模型中仍然相关的现有 API 的正式规范。结合与焦点相关的类和方法的 javadoc,该文档应该使开发人员能够创建实质性的 AWT 和 Swing 应用程序,这些应用程序具有自定义但跨平台一致的焦点行为。本文档包含以下部分:
- KeyboardFocusManager 概述
- KeyboardFocusManager和浏览器上下文
- KeyEventDispatcher
- FocusEvent 和 WindowEvent
- 事件传递
- 相反的组件和窗口
- 临时焦点事件
- 焦点遍历
- 焦点遍历策略
- 焦点遍历策略提供者
- 程序化遍历
- 可聚焦性
- 可聚焦窗口
- 请求焦点
- 焦点和 PropertyChangeListener
- 焦点和 VetoableChangeListener
- Z-顺序
- 替换 DefaultKeyboardFocusManager
- 与以前版本的不兼容性
KeyboardFocusManager 概述
焦点模型集中在单个类 KeyboardFocusManager 周围,它为客户端代码提供一组 API,用于查询当前焦点状态、启动焦点更改以及使用自定义调度程序替换默认焦点事件调度。客户端可以直接查询焦点状态,也可以注册一个 PropertyChangeListener,它会在焦点状态发生更改时接收 PropertyChangeEvents。
KeyboardFocusManager 介绍了以下主要概念及其术语:
- “焦点所有者”——通常接收键盘输入的组件。
- “永久焦点所有者”——最后一个永久接收焦点的组件。 “焦点所有者”和“永久焦点所有者”是等价的,除非临时焦点更改当前生效。在这种情况下,“永久焦点所有者”将在临时焦点更改结束时再次成为“焦点所有者”。
- “焦点窗口”——包含“焦点所有者”的窗口。
- “活动窗口”——作为“聚焦窗口”的框架或对话框,或者作为“聚焦窗口”所有者的第一个框架或对话框。
- “焦点遍历”——用户无需移动光标即可更改“焦点所有者”的能力。通常,这是使用键盘(例如,使用 TAB 键)或可访问环境中的等效设备来完成的。客户端代码也可以以编程方式启动遍历。正常的焦点遍历可以是“前进”到“下一个”组件,也可以“后退”到“前一个”组件。
- “焦点遍历循环”——组件层次结构的一部分,这样正常的“向前”(或“向后”)焦点遍历将遍历焦点循环中的所有组件,但不会遍历其他组件。这个循环提供了从循环中的任意组件到它的“下一个”(向前遍历)和“上一个”(向后遍历)组件的映射。
- "Traversable Component" -- 处于焦点遍历循环中的组件。
- "Non-traversable Component" -- 不在焦点遍历循环中的组件。请注意,不可遍历的组件仍然可以通过其他方式获得焦点(例如通过直接焦点请求)。
- “焦点循环根”——容器,它是特定“焦点遍历循环”的组件层次结构的根。当“焦点所有者”是特定循环内的组件时,正常的向前和向后焦点遍历不能将“焦点所有者”移动到组件层次结构中的焦点循环根之上。相反,定义了两个额外的遍历操作,“向上循环”和“向下循环”,以允许键盘和编程导航在焦点遍历循环层次结构中上下移动。
- “焦点遍历策略提供者”- 具有“FocusTraversalPolicyProvider”属性为真的容器。该容器将用于获取焦点遍历策略。这个容器没有定义新的焦点循环,只是修改了它的子元素“向前”和“向后”遍历的顺序。可以在容器上使用
setFocusTraversalPolicyProvider设置焦点遍历策略提供程序。
默认情况下,每个 Window 和 JInternalFrame 都是“焦点循环根”。如果它是唯一的焦点循环根,那么它的所有可聚焦后代都应该在它的焦点循环中,并且它的焦点遍历策略应该通过确保在正常的向前(或向后)遍历期间到达所有对象来强制执行它们。另一方面,如果 Window 或 JInternalFrame 的后代也是焦点循环根,那么每个这样的后代都是两个焦点循环的成员:一个是它的根,另一个是它最近的焦点循环- 根的祖先。为了遍历属于这种“后代”焦点循环根的焦点循环的可聚焦组件,首先遍历(向前或向后)到达后代,然后使用“向下循环”操作依次到达,它的后代。
这是一个示例:
假设如下:
- A是
Window,这意味着它必须是焦点循环根。 - B和D是
Container是焦点循环根。 - C是一个
Container,它不是焦点循环根。 - G, H, E, 和F都是
Components。
- A是一个根,并且A, B, C, 和F是成员A的循环。
- B是一个根,并且B, D, 和E是成员B的循环。
- D是一个根,并且D, G, 和H是成员D的循环。
KeyboardFocusManager 是一个抽象类。 AWT 在 DefaultKeyboardFocusManager 类中提供了默认实现。
KeyboardFocusManager和浏览器上下文
一些浏览器将不同代码库中的小程序划分为不同的上下文,并在这些上下文之间建立隔离墙。每个线程和每个组件都与特定的上下文相关联,不能干扰线程或访问其他上下文中的组件。在这种情况下,每个上下文将有一个 KeyboardFocusManager。其他浏览器将所有小程序置于相同的上下文中,这意味着所有小程序将只有一个全局的 KeyboardFocusManager。此行为依赖于实现。有关详细信息,请参阅浏览器的文档。然而,无论有多少上下文,每个 ClassLoader 都不会超过一个焦点所有者、焦点窗口或活动窗口。
KeyEventDispatcher 和 KeyEventPostProcessor
虽然用户的 KeyEvents 通常应该传递给焦点所有者,但在极少数情况下这是不可取的。输入方法是一个专用组件的示例,它应该接收 KeyEvents,即使它的关联文本组件是并且应该保持焦点所有者。
KeyEventDispatcher 是一个轻量级接口,允许客户端代码预先监听特定上下文中的所有 KeyEvent。实现该接口并向当前 KeyboardFocusManager 注册的类的实例将在将它们分派给焦点所有者之前接收 KeyEvent,从而允许 KeyEventDispatcher 重新定位事件、使用它、自行分派它或进行其他更改。
为了保持一致性,KeyboardFocusManager 本身是一个 KeyEventDispatcher。默认情况下,当前的 KeyboardFocusManager 将成为所有未由注册的 KeyEventDispatcher 调度的 KeyEvent 的接收器。当前的 KeyboardFocusManager 不能作为 KeyEventDispatcher 完全注销。但是,如果 KeyEventDispatcher 报告它已调度 KeyEvent,则不管它实际上是否已调度,KeyboardFocusManager 都不会对该 KeyEvent 采取进一步的操作。 (虽然客户端代码可以将当前的 KeyboardFocusManager 注册为 KeyEventDispatcher 一次或多次,但没有明显的理由证明这是必要的,因此不推荐这样做。)
客户端代码还可以使用 KeyEventPostProcessor 接口在特定上下文中后监听 KeyEvents。向当前 KeyboardFocusManager 注册的 KeyEventPostProcessors 将在 KeyEvents 被调度到焦点所有者并由其处理后接收 KeyEvents。 KeyEventPostProcessors 还将接收 KeyEvents,否则这些事件将被丢弃,因为应用程序中当前没有组件拥有焦点。这将允许应用程序实现需要全局 KeyEvent 后处理的功能,例如菜单快捷方式。
与 KeyEventDispatcher 一样,KeyboardFocusManager 也实现了 KeyEventPostProcessor,并且类似的限制适用于其在该能力中的使用。
FocusEvent 和 WindowEvent
AWT 在两个不同的 java.awt.event 类中定义了以下六种对焦点模型至关重要的事件类型:
WindowEvent.WINDOW_ACTIVATED:当它成为活动窗口时,此事件将被分派到框架或对话框(但绝不是非框架或对话框的窗口)。WindowEvent.WINDOW_GAINED_FOCUS:当窗口成为焦点窗口时,此事件将被分派到该窗口。只有可聚焦的 Windows 才能接收此事件。FocusEvent.FOCUS_GAINED:当组件成为焦点所有者时,此事件将分派给组件。只有可聚焦的组件才能接收此事件。FocusEvent.FOCUS_LOST:当组件不再是焦点所有者时,此事件将分派给组件。WindowEvent.WINDOW_LOST_FOCUS:当窗口不再是焦点窗口时,此事件将被分派到窗口。WindowEvent.WINDOW_DEACTIVATED:当它不再是活动窗口时,此事件将被分派到框架或对话框(但绝不是非框架或对话框的窗口)。
事件传递
如果焦点不在 java 应用程序中并且用户单击可聚焦的子组件a非活动框架b,以下事件将按顺序发送和处理:
- b将收到
WINDOW_ACTIVATED事件。 - 下一个,b将收到
WINDOW_GAINED_FOCUS事件。 - 最后,a将收到
FOCUS_GAINED事件。
- a将收到
FOCUS_LOST事件。 - b将收到
WINDOW_LOST_FOCUS事件。 - b将收到
WINDOW_DEACTIVATED事件。 - d将收到
WINDOW_ACTIVATED事件。 - d将收到
WINDOW_GAINED_FOCUS事件。 - c将收到
FOCUS_GAINED事件。
此外,每个事件类型将与其相反的事件类型一一对应地被调度。例如,如果一个组件接收到一个 FOCUS_GAINED 事件,在任何情况下它都不能在没有介入 FOCUS_LOST 事件的情况下接收另一个 FOCUS_GAINED 事件。
最后,重要的是要注意这些事件仅供参考。例如,不可能通过在处理前面的 FOCUS_LOST 事件时请求焦点返回到失去焦点的组件来阻止传递挂起的 FOCUS_GAINED 事件。虽然客户端代码可能会发出这样的请求,但挂起的 FOCUS_GAINED 仍将被交付,随后事件将焦点转移回原始焦点所有者。
如果绝对有必要抑制 FOCUS_GAINED 事件,客户端代码可以安装一个 VetoableChangeListener 来拒绝焦点更改。参见 焦点和 VetoableChangeListener。
相反的组件和窗口
每个事件都包含有关焦点或激活更改中涉及的“相反”组件或窗口的信息。例如,对于 FOCUS_GAINED 事件,相反的 Component 是失去焦点的 Component。如果焦点或激活更改发生在本机应用程序、不同 VM 或上下文中的 Java 应用程序,或者没有其他组件,则相反的组件或窗口为空。可以使用 FocusEvent.getOppositeComponent 或 WindowEvent.getOppositeWindow 访问此信息。
在某些平台上,当两个不同的重量级 Component 之间发生焦点或激活变化时,无法识别相反的 Component 或 Window。在这些情况下,相反的 Component 或 Window 可能在某些平台上设置为 null,而在其他平台上设置为有效的非空值。但是,对于共享同一个重量级容器的两个轻量级组件之间的焦点变化,相反的组件将始终被正确设置。因此,纯 Swing 应用程序在使用顶级窗口中发生的焦点更改的相反组件时可以忽略此平台限制。
临时焦点事件
FOCUS_GAINED 和 FOCUS_LOST 事件被标记为临时的或永久的。
当组件失去焦点时发送临时 FOCUS_LOST 事件,但很快就会重新获得焦点。当焦点更改用作数据验证的触发器时,这些事件可能很有用。例如,文本组件可能希望在用户开始与另一个组件交互时提交其内容,并且可以通过响应 FOCUS_LOST 事件来完成此操作。但是,如果收到的 FocusEvent 是临时的,则不应进行提交,因为文本字段将很快再次获得焦点。
永久焦点转移通常是由于用户单击可选择的重量级组件、使用键盘或等效输入设备进行焦点遍历或调用 requestFocus() 或 requestFocusInWindow() 的结果。
临时焦点转移通常是由于显示菜单或弹出菜单、单击或拖动滚动条、通过拖动标题栏移动窗口或使另一个窗口成为焦点窗口而发生的。请注意,在某些平台上,这些操作可能根本不会生成任何 FocusEvent。在其他人身上,会发生临时的焦点转移。
当一个组件收到一个临时的 FOCUS_LOST 事件时,该事件的对立组件(如果有的话)可能会收到一个临时的 FOCUS_GAINED 事件,但也可能会收到一个永久的 FOCUS_GAINED 事件。显示菜单或弹出菜单,或者单击或拖动滚动条,应该会生成一个临时的 FOCUS_GAINED 事件。但是,更改焦点窗口将为新的焦点所有者产生一个永久性的 FOCUS_GAINED 事件。
Component 类包括 requestFocus 和 requestFocusInWindow 的变体,它们将所需的临时状态作为参数。但是,由于指定任意临时状态可能无法在所有本机窗口系统上实现,因此只能为轻量级组件保证此方法的正确行为。此方法不适用于一般用途,而是作为轻量级组件库(例如 Swing)的挂钩而存在。
焦点遍历
每个 Component 为给定的焦点遍历操作定义了自己的一组焦点遍历键。组件支持单独的一组键用于向前和向后遍历,也支持向上遍历一个焦点遍历周期。作为焦点循环根的容器也支持一组键,用于向下遍历一个焦点遍历循环。如果未为组件显式定义 Set,则该组件递归地从其父级继承 Set,并最终从当前 KeyboardFocusManager 上的上下文范围默认集继承。
使用 AWTKeyStroke API,客户端代码可以指定两个特定 KeyEvents KEY_PRESSED 或 KEY_RELEASED 中的哪一个,将发生焦点遍历操作。但是无论指定哪个KeyEvent,所有与焦点遍历键相关的KeyEvents,包括关联的KEY_TYPED事件,都会被消费掉,不会派发给任何Component。将 KEY_TYPED 事件指定为焦点遍历操作的映射,或者将同一事件映射到任何特定组件或 KeyboardFocusManager 的默认值的多个焦点遍历操作是运行时错误。
默认的焦点遍历键是依赖于实现的。 Sun 建议特定本机平台的所有实现都使用相同的密钥。对于 Windows 和 Unix,建议如下:
- 向前遍历到下一个组件:
TextAreas:CTRL-TAB在KEY_PRESSED
所有其他人TAB在KEY_PRESSED和CTRL-TAB在KEY_PRESSED - 向后遍历到前一个组件:
TextAreas:CTRL-SHIFT-TAB在KEY_PRESSED
所有其他人:SHIFT-TAB在KEY_PRESSED和CTRL-SHIFT-TAB在KEY_PRESSED - 向上遍历一个焦点遍历循环:<none>
- 向下遍历一个焦点遍历循环:<none>
组件可以使用 Component.setFocusTraversalKeysEnabled 集体启用和禁用所有焦点遍历键。当焦点遍历键被禁用时,组件接收这些键的所有 KeyEvents。启用焦点遍历键时,组件永远不会收到遍历键的 KeyEvents;相反,KeyEvents 会自动映射到焦点遍历操作。
对于正常的向前和向后遍历,AWT 焦点实现根据焦点所有者的焦点循环根或焦点遍历策略提供者的FocusTraversalPolicy 来确定下一个要关注的组件。如果焦点所有者是焦点循环根,那么在正常的焦点遍历期间,哪些组件代表要关注的下一个和前一个组件可能是不明确的。因此,当前KeyboardFocusManager 维护对“当前”焦点循环根的引用,它在所有上下文中都是全局的。当前焦点循环根用于解决歧义。
对于向上循环遍历,焦点所有者设置为当前焦点所有者的焦点循环根,当前焦点循环根设置为新焦点所有者的焦点循环根。但是,如果当前焦点所有者的焦点循环根是顶级窗口,则将焦点所有者设置为焦点循环根的默认组件以获取焦点,并且当前焦点循环根不变。
对于下循环遍历,如果当前焦点拥有者是一个焦点循环根,则将焦点拥有者设置为当前焦点拥有者默认要聚焦的组件,将当前焦点循环根设置为当前焦点拥有者。如果当前焦点所有者不是焦点循环根,则不会发生焦点遍历操作。
FocusTraversalPolicy
FocusTraversalPolicy 定义了遍历特定焦点循环根或焦点遍历策略提供程序中的组件的顺序。 FocusTraversalPolicy 的实例可以跨 Container 共享,允许这些 Container 实现相同的遍历策略。当焦点遍历循环层次结构发生变化时,无需重新初始化 FocusTraversalPolicies。
每个 FocusTraversalPolicy 必须定义以下五种算法:
- 给定一个焦点循环根和一个组件a在那个循环中,下一个组件之后a.
- 给定一个焦点循环根和一个组件a在那个循环中,之前的前一个组件a.
- 给定一个焦点循环根,即该循环中的“第一个”组件。 “第一个”组件是向前遍历时要关注的组件。
- 给定一个焦点循环根,即该循环中的“最后一个”组件。 “最后”组件是在反向遍历时要关注的组件。
- 给定一个焦点循环根,即该循环中的“默认”组件。当向下遍历到新的焦点遍历循环时,“默认”组件将是第一个获得焦点的组件。这可能与“第一个”组件相同,但不必如此。
FocusTraversalPolicy 可以选择为以下内容提供算法:
给定一个窗口,该窗口中的“初始”组件。当 Window 首次可见时,初始 Component 将最先获得焦点。默认情况下,这与“默认”组件相同。此外,Swing 提供了
FocusTraversalPolicy 的子类,InternalFrameFocusTraversalPolicy,它允许开发人员提供以下算法:
给定一个JInternalFrame,即JInternalFrame中的“初始”组件。首次选择JInternalFrame时,初始组件是第一个获得焦点的组件。默认情况下,这与JInternalFrame的默认组件相同。
FocusTraversalPolicy 使用 Container.setFocusTraversalPolicy 安装在 Container 上。如果未显式设置策略,则容器会从其最近的 focus-cycle-root 祖先继承其策略。顶层使用上下文默认策略初始化它们的焦点遍历策略。上下文默认策略是使用 KeyboardFocusManager 建立的。 setDefaultFocusTraversalPolicy。
AWT 提供了两个标准的FocusTraversalPolicy 实现供客户端代码使用。
ContainerOrderFocusTraversalPolicy:在焦点遍历循环中按照组件添加到其容器的顺序遍历组件。使用 accept(Component) 方法测试每个组件的适用性。默认情况下,组件只有在可见、可显示、启用和可聚焦时才适合。- 默认情况下,ContainerOrderFocusTraversalPolicy 隐式转移焦点向下循环。也就是说,在正常的前向焦点遍历中,焦点循环根之后遍历的Component将是焦点循环根的默认要聚焦的Component,而不管焦点循环根是可遍历还是不可遍历的Container(见下图) 。 1,2 下面)。这种行为提供了与在没有向上和向下循环遍历概念的情况下设计的应用程序的向后兼容性。
DefaultFocusTraversalPolicy:ContainerOrderFocusTraversalPolicy的子类,重新定义了适应性测试。如果客户端代码已通过覆盖Component.isFocusTraversable()或Component.isFocusable()或通过调用Component.setFocusable(boolean)显式设置组件的可聚焦性,则DefaultFocusTraversalPolicy的行为与ContainerOrderFocusTraversalPolicy完全相同。但是,如果组件依赖于默认的可聚焦性,那么DefaultFocusTraversalPolicy将拒绝所有具有不可聚焦对等的组件。
对等点的可聚焦性取决于实现。 Sun 建议特定本机平台的所有实现都构建具有相同可聚焦性的对等体。 Windows 和 Unix 的建议是 Canvases、Labels、Panels、Scrollbars、ScrollPanes、Windows 和轻量级组件具有不可聚焦的对等体,而所有其他组件具有可聚焦的对等体。这些建议用于 Sun AWT 实现。请注意,Component 的 peer 的 focusability 与 Component 本身的 focusability 不同,也不影响。
Swing 提供了两个额外的标准 FocusTraversalPolicy 实现供客户端代码使用。每个实现都是一个 InternalFrameFocusTraversalPolicy。
- SortingFocusTraversalPolicy:通过根据给定的 Comparator 对焦点遍历循环的 Components 进行排序来确定遍历顺序。使用 accept(Component) 方法测试每个组件的适用性。默认情况下,组件只有在可见、可显示、启用和可聚焦时才适合。
- 默认情况下,SortingFocusTraversalPolicy 隐式转移焦点向下循环。也就是说,在正常的前向焦点遍历中,焦点循环根之后遍历的Component将是焦点循环根的默认要聚焦的Component,而不管焦点循环根是可遍历还是不可遍历的Container(见下图)。 1,2 下面)。这种行为提供了与在没有向上和向下循环遍历概念的情况下设计的应用程序的向后兼容性。
- LayoutFocusTraversalPolicy:SortingFocusTraversalPolicy 的子类,它根据组件的大小、位置和方向对组件进行排序。根据它们的大小和位置,组件大致分为行和列。对于水平方向的容器,列从左到右或从右到左排列,行从上到下排列。对于垂直方向的容器,列从上到下排列,行从左到右或从右到左排列。在继续下一行之前,一行中的所有列都被完全遍历。
此外,适应性测试被扩展以排除具有或继承空 InputMaps 的 JComponents。
下图显示了隐式焦点转移:
pic.1
pic.2
假设如下:
- A, B和C是某个窗口(容器)中的组件
- R是窗口中的一个容器,它是B和C.除了,R是焦点循环根。
- B是焦点遍历循环中的默认组件R
- R图1中是可遍历的Container,图2中是不可遍历的Container。
- 在这种情况下,正向遍历将如下所示:
- 图一:A→R→B→C
- 图二:A→B→C
Swing 应用程序或混合的 Swing/AWT 应用程序,如果使用一种标准外观或任何其他从 BasicLookAndFeel 派生的外观,默认情况下将对所有容器使用 LayoutFocusTraversalPolicy。
默认情况下,所有其他应用程序(包括纯 AWT 应用程序)都将使用 DefaultFocusTraversalPolicy。
焦点遍历策略提供者
不是焦点循环根的容器可以选择提供自己的 FocusTraversalPolicy。为此,需要通过调用将 Container 的焦点遍历策略提供程序属性设置为 true
Container.setFocusTraversalPolicyProvider(boolean)
判断一个Container是否是一个焦点遍历策略提供者,应该使用下面的方法:
Container.isFocusTraversalPolicyProvider()
如果焦点遍历策略提供者属性设置在焦点循环根上,则它不被视为焦点遍历策略提供者,其行为与任何其他焦点循环根一样。
焦点循环根和焦点遍历策略提供者之间的主要区别在于,后者允许焦点进入和离开它们,就像所有其他容器一样。但是,焦点遍历策略提供者中的子项按照提供者的 FocusTraversalPolicy 确定的顺序遍历。为了使焦点遍历策略提供者能够以这种方式运行,FocusTraversalPolicies 以下列方式处理它们:
- 可以将焦点遍历策略提供程序传递给 FocusTraversalPolicy 方法,而不是焦点循环根。
- 在计算
FocusTraversalPolicy.getComponentAfter或FocusTraversalPolicy.getComponentBefore中的下一个或前一个组件时,- 如果组件是焦点遍历策略提供者的子项,则此组件的下一个和上一个是使用此焦点遍历策略提供者的 FocusTraversalPolicy 确定的。但是,为了使焦点离开提供者,应用以下规则:
- 如果某个时候
next找到的Component是焦点遍历策略提供者的firstComponent,则返回焦点遍历策略提供者之后的Component - 如果某个时刻
previous找到的Component是焦点遍历策略提供者的lastComponent,则返回焦点遍历策略提供者之前的Component
- 如果某个时候
- 在计算
FocusTraversalPolicy.getComponentAfter中的下一个 Component 时,- 如果获取的Component是不可遍历的Container,并且是焦点遍历策略提供者,则返回该提供者的默认Component
- 如果传递给
FocusTraversalPolicy.getComponentAfter方法的Component是一个可遍历的Container并且是一个焦点遍历策略提供者,则返回这个提供者的默认Component
- 在计算
FocusTraversalPolicy.getComponentBefore中的前一个 Component 时,- 如果获取的 Component 是一个 Container(可遍历或不可遍历)并且它是一个焦点遍历策略提供者,则返回该提供者的最后一个 Component
- 如果组件是焦点遍历策略提供者的子项,则此组件的下一个和上一个是使用此焦点遍历策略提供者的 FocusTraversalPolicy 确定的。但是,为了使焦点离开提供者,应用以下规则:
- FocusTraversalPolicy.getFirstComponent中计算第一个Component时,
- 如果获取的Component是不可遍历的Container,并且是焦点遍历策略提供者,则返回该提供者的默认Component
- 如果获取的 Component 是可遍历的 Container 并且它是焦点遍历策略提供者,则返回该 Container 本身
- 在FocusTraversalPolicy.getLastComponent中计算最后一个Component时,
- 如果获取的 Component 是一个 Container(可遍历或不可遍历)并且它是一个焦点遍历策略提供者,则返回该提供者的最后一个 Component
程序化遍历
除了用户启动的焦点遍历之外,客户端代码还可以通过编程方式启动焦点遍历操作。对于客户端代码,编程遍历与用户启动的遍历没有区别。启动编程遍历的首选方法是在 KeyboardFocusManager 上使用以下方法之一:
KeyboardFocusManager.focusNextComponent()KeyboardFocusManager.focusPreviousComponent()KeyboardFocusManager.upFocusCycle()KeyboardFocusManager.downFocusCycle()
这些方法中的每一个都启动与当前焦点所有者的遍历操作。如果当前没有焦点所有者,则不会发生遍历操作。此外,如果焦点所有者不是焦点循环根,则 downFocusCycle() 不执行遍历操作。
KeyboardFocusManager 还支持这些方法的以下变体:
KeyboardFocusManager.focusNextComponent(Component)KeyboardFocusManager.focusPreviousComponent(Component)KeyboardFocusManager.upFocusCycle(Component)KeyboardFocusManager.downFocusCycle(Container)
替代但等效的 API 是在 Component 和 Container 类本身上定义的:
Component.transferFocus()Component.transferFocusBackward()Component.transferFocusUpCycle()Container.transferFocusDownCycle()
KeyboardFocusManager 变体一样,这些方法中的每一个都启动遍历操作,就好像 Component 是焦点所有者一样,尽管它不一定是。
另请注意,直接或间接通过祖先隐藏或禁用焦点所有者,或者使焦点所有者不可显示或不可聚焦,会启动自动的前向焦点遍历。虽然隐藏任何轻量级或重量级祖先都会间接隐藏其子级,但只有禁用重量级祖先才会禁用其子级。因此,禁用焦点所有者的轻量级祖先不会自动启动焦点遍历。
如果客户端代码发起焦点遍历,并且没有其他组件需要关注,则焦点所有者保持不变。如果客户端代码通过直接或间接隐藏焦点所有者,或者通过使焦点所有者不可显示或不可聚焦来启动自动焦点遍历,并且没有其他要聚焦的 Component,则清除全局焦点所有者。如果客户端代码通过直接或间接禁用焦点所有者来启动自动焦点遍历,并且没有其他要关注的组件,则焦点所有者保持不变。
可聚焦性
可获得焦点的组件可以成为焦点所有者(“focusability”)并使用 FocusTraversalPolicy 参与键盘焦点遍历(“focus traversability”)。这两个概念没有区别;组件必须既可聚焦又可聚焦,或者两者都不是。组件通过 isFocusable() 方法表达这种状态。默认情况下,所有组件都从此方法返回 true。客户端代码可以通过调用 Component.setFocusable(boolean) 来更改此默认值。
可聚焦窗口
为了支持调色板窗口和输入方法,客户端代码可以防止窗口成为焦点窗口。通过传递性,这可以防止 Window 或其任何后代成为焦点所有者。不可聚焦的 Windows 可能仍然拥有可聚焦的 Windows。默认情况下,每个框架和对话框都是可聚焦的。每个不是 Frame 或 Dialog 的窗口,但其最近拥有的 Frame 或 Dialog 正在屏幕上显示,并且在其焦点遍历周期中至少有一个 Component,默认情况下也是可聚焦的。要使窗口不可聚焦,请使用 Window.setFocusableWindowState(false)。
如果窗口不可聚焦,则当 KeyboardFocusManager 看到窗口的 WINDOW_GAINED_FOCUS 事件时,将强制执行此限制。此时,拒绝焦点更改并将焦点重置到不同的窗口。拒绝恢复方案与 VetoableChangeListener 拒绝焦点更改相同。参见 焦点和 VetoableChangeListener。
因为新的焦点实现要求通过 Window 所有者的子代代理用于 Window 或其后代的 KeyEvents,并且因为必须将此代理映射到 X11 上才能接收事件,所以最近拥有 Frame 或 Dialog 的 Window 不是showing 永远无法在 X11 上接收 KeyEvents。为了支持此限制,我们区分了窗口的“窗口可聚焦性”及其“窗口可聚焦性状态”。窗口的可聚焦性状态与窗口最近的所属框架或对话框的显示状态相结合,以确定窗口的可聚焦性。默认情况下,所有 Windows 的可聚焦状态都为 true。将 Window 的可聚焦状态设置为 false 可确保它不会成为焦点窗口,而不管其最近的拥有 Frame 或 Dialog 的显示状态如何。
Swing 允许应用程序创建具有空所有者的 JWindows。 Swing 构造所有此类 JWindows,以便它们属于私有的隐藏框架。因为此 Frame 的显示状态将始终为 false,所以构造为 null 所有者的 JWindow 永远不会成为获得焦点的 Window,即使它的 Window 可聚焦状态为 true。
如果获得焦点的 Window 变为不可聚焦的,那么 AWT 将尝试聚焦 Window 所有者的最近获得焦点的 Component。因此,窗口的所有者将成为新的焦点窗口。如果 Window 的所有者也是不可获得焦点的 Window,则焦点更改请求将递归地向上处理所有权层次结构。由于并非所有平台都支持跨窗口焦点更改(请参阅 请求焦点 ),因此所有此类焦点更改请求都可能会失败。在这种情况下,全局焦点所有者将被清除,焦点窗口将保持不变。
请求焦点
组件可以通过调用 Component.requestFocus() 请求它成为焦点所有者。仅当组件可显示、可聚焦、可见且其所有祖先(顶级窗口除外)可见时,才会启动到组件的永久焦点转移。如果不满足这些条件中的任何一个,请求将立即被拒绝。禁用的组件可能是焦点所有者;但是,在这种情况下,所有 KeyEvent 都将被丢弃。
如果 Component 的顶级 Window 不是获得焦点的 Window 并且平台不支持跨 Windows 请求焦点,则请求也将被拒绝。如果由于这个原因请求被拒绝,该请求将被记住并在用户稍后聚焦窗口时将被授予。否则,焦点更改请求也会更改焦点窗口。
无法同步确定是否已授予焦点更改请求。相反,客户端代码必须在组件上安装 FocusListener 并监视 FOCUS_GAINED 事件的传递。客户端代码在接收到此事件之前不得假设组件是焦点所有者。在 requestFocus() 返回之前,事件可能会也可能不会被传递。开发人员不得假设一种行为或另一种行为。
如果所有焦点更改请求都在 EventDispatchThread 上进行,则 AWT 支持提前输入。如果客户端代码请求焦点更改,并且 AWT 确定该请求可能由本机窗口系统授予,那么 AWT 将通知当前的 KeyboardFocusManager 应该将所有 KeyEvents 加入队列,时间戳晚于当前正在处理的事件的时间戳处理。在新组件成为焦点所有者之前,不会调度这些 KeyEvent。如果焦点更改在本机级别不成功,如果组件的对等体被处置,或者如果焦点更改被 VetoableChangeListener 否决,AWT 将取消延迟的调度请求。如果焦点更改请求是从 EventDispatchThread 以外的线程发出的,则 KeyboardFocusManagers 不需要支持提前输入。
由于 Component.requestFocus() 无法跨平台一致地实施,因此鼓励开发人员改用 Component.requestFocusInWindow()。此方法自动拒绝所有平台上的跨窗口焦点转移。通过消除焦点转移的唯一特定于平台的元素,此方法实现了一致的跨平台行为。
此外,requestFocusInWindow() 返回一个boolean。如果返回“false”,请求肯定会失败。如果返回“true”,请求将成功,除非它被否决,或者在本机窗口系统允许请求之前发生异常事件,例如处置组件的对等事件。同样,虽然返回值“true”表示请求可能成功,但开发人员绝不能假设此组件是焦点所有者,直到此组件收到 FOCUS_GAINED 事件。
如果客户端代码不希望应用程序中的任何组件成为焦点所有者,它可以调用方法 KeyboardFocusManager 。 clearGlobalFocusOwner() 在当前 KeyboardFocusManager 上。如果调用此方法时存在焦点所有者,则焦点所有者将收到一个永久性的FOCUS_LOST事件。在此之后,AWT 焦点实现将丢弃所有 KeyEvent,直到用户或客户端代码明确地将焦点设置到组件。
Component 类还支持 requestFocus 和 requestFocusInWindow 的变体,允许客户端代码指定临时状态。参见 临时焦点事件
焦点和 PropertyChangeListener
客户端代码可以通过 PropertyChangeListeners 监听上下文范围内焦点状态的变化,或组件中与焦点相关的状态的变化。
KeyboardFocusManager 支持以下属性:
focusOwner:焦点所有者focusedWindow:焦点窗口activeWindow:活动窗口defaultFocusTraversalPolicy:默认的焦点遍历策略forwardDefaultFocusTraversalKeys:默认设置FORWARD_TRAVERSAL_KEYSbackwardDefaultFocusTraversalKeys:默认设置BACKWARD_TRAVERSAL_KEYSupCycleDefaultFocusTraversalKeys:默认设置UP_CYCLE_TRAVERSAL_KEYSdownCycleDefaultFocusTraversalKeys:默认设置DOWN_CYCLE_TRAVERSAL_KEYScurrentFocusCycleRoot:当前焦点循环根
安装在当前 KeyboardFocusManager 上的 PropertyChangeListener 只会在 KeyboardFocusManager 的上下文中看到这些更改,即使焦点所有者、焦点窗口、活动窗口和当前焦点循环根组成了所有上下文共享的全局焦点状态。我们相信这比要求客户端代码在安装 PropertyChangeListener 之前通过安全检查更具侵入性。
组件支持以下与焦点相关的属性:
focusable:组件的可聚焦性focusTraversalKeysEnabled: 组件的焦点遍历键启用状态forwardFocusTraversalKeys:FORWARD_TRAVERSAL_KEYS的组件集backwardFocusTraversalKeys:组件的BACKWARD_TRAVERSAL_KEYS集upCycleFocusTraversalKeys:UP_CYCLE_TRAVERSAL_KEYS的组件集
除了 Component 属性外,Container 还支持以下与焦点相关的属性:
downCycleFocusTraversalKeys:DOWN_CYCLE_TRAVERSAL_KEYS的容器集focusTraversalPolicy: Container 的焦点遍历策略focusCycleRoot:容器的焦点循环根状态
除了 Container 属性外,Window 还支持以下与焦点相关的属性:
focusableWindow:窗口的可聚焦窗口状态
另请注意,安装在 Window 上的 PropertyChangeListener 永远不会看到 focusCycleRoot 属性的 PropertyChangeEvent。 Window 始终是焦点循环根;此属性无法更改。
焦点和 VetoableChangeListener
KeyboardFocusManager 还支持 VetoableChangeListener 的以下属性:
- “focusOwner”:焦点所有者
- “focusedWindow”:聚焦窗口
- “activeWindow”:活动窗口
在更改反映在 KeyboardFocusManager 中之前,VetoableChangeListeners 会收到状态更改的通知。相反,PropertyChangeListeners 会在更改反映后收到通知。因此,所有 VetoableChangeListeners 将在任何 PropertyChangeListener 之前得到通知。
VetoableChangeListeners 必须是幂等的,并且必须否决特定焦点更改的损失和获得事件(例如,FOCUS_LOST 和 FOCUS_GAINED)。例如,如果 VetoableChangeListener 否决了 FOCUS_LOST 事件,则不需要 KeyboardFocusManager 来搜索 EventQueue 并删除关联的未决 FOCUS_GAINED 事件。相反,KeyboardFocusManager 可以自由尝试派发此事件,VetoableChangeListener 也有责任否决它。此外,在处理 FOCUS_GAINED 事件期间,KeyboardFocusManager 可能会尝试通过合成另一个 FOCUS_LOST 事件来重新同步全局焦点状态。必须像第一个 FOCUS_LOST 事件一样否决此事件。
KeyboardFocusManager 在通知 PropertyChangeListener 状态更改时可能不持有任何锁。但是,VetoableChangeListeners 放宽了此要求。因此,客户端定义的 VetoableChangeListener 应避免在 vetoableChange(PropertyChangeEvent) 内获取额外的锁,因为这可能会导致死锁。如果焦点或激活更改被拒绝,KeyboardFocusManager 将启动拒绝恢复,如下所示:
- 如果聚焦或活动窗口更改被拒绝,则聚焦或活动窗口将重置为之前聚焦或活动窗口的窗口。如果没有这样的窗口,
KeyboardFocusManager将清除全局焦点所有者。 - 如果焦点所有者更改被拒绝,则焦点所有者将重置为以前是焦点所有者的组件。如果那不可能,那么它将被重置为焦点遍历周期中前一个焦点所有者之后的下一个组件。如果这也不可能,那么
KeyboardFocusManager将清除全局焦点所有者。
VetoableChangeListener s 必须小心避免否决由于否决拒绝恢复而启动的焦点更改。未能预见到这种情况可能会导致无限循环的否决焦点更改和恢复尝试。
Z-顺序
在某些本机窗口系统上,窗口的 Z 顺序会影响其聚焦或活动(如果适用)状态。在 Microsoft Windows 上,最顶层的窗口自然也是焦点窗口。然而,在 Solaris 上,许多窗口管理器使用点到焦点模型,在确定焦点窗口时忽略 Z 顺序。当聚焦或激活Windows 时,AWT 遵循本机平台的 UI 要求。因此,Z-order相关方法的焦点行为如:
Window.toFront()Window.toBack()Window.show()Window.hide()Window.setVisible(boolean)Window.dispose()Frame.setState(int)
Window.toFront():
微软Windows:如果可能,窗口将移到前面。虽然我们总是能够将此窗口移动到同一 VM 中其他窗口的前面,但 Windows 98 和 Windows 2000 不允许应用程序将其任何窗口置于前面,除非该应用程序的窗口之一已经在前台.在这种情况下,Windows 将改为在任务栏中闪烁窗口图标。如果窗口移到前面,它将成为焦点和(如果适用)活动窗口。
索拉里斯:窗口移到前面。在点焦点窗口管理器中,如果窗口是光标下方的最顶层窗口,则该窗口将成为焦点窗口。在点击聚焦窗口管理器中,聚焦的窗口将保持不变。Window.toBack():
微软Windows:窗口移到后面。但是请注意,Microsoft Windows 坚持拥有的 Window 始终位于其所有递归所有者的前面。因此,在完成此操作后,Window 可能不是 Z 顺序中最低的 Java Window。如果窗口或其任何所有者是获得焦点的窗口,则获得焦点的窗口将重置为 VM 中最顶层的窗口。
索拉里斯:窗口移到后面。与 Microsoft Windows 一样,一些窗口管理器坚持认为拥有的窗口始终位于其所有递归所有者的前面。因此,在完成此操作后,Window 可能不是 Z 顺序中最低的 Java Window。如果 Window 是获得焦点的 Window,如果它不再是光标下最顶层的 Window,它将在点到焦点窗口管理器中失去焦点。在点击聚焦窗口管理器中,聚焦的窗口将保持不变。Window.show()/Window.setVisible(true)/Frame.setState(NORMAL):
微软Windows:窗口移到前面并成为焦点窗口。
索拉里斯:窗口移到前面。在指向焦点的焦点窗口管理器中,如果窗口现在是光标下最顶层的窗口,则该窗口将获得焦点。在点击聚焦窗口管理器中,窗口将成为焦点窗口。Window.hide()/Window.setVisible(false)/Window.dispose()/ Frame.setState(ICONIFIED):
微软Windows:如果窗口是焦点窗口,焦点窗口将重置为操作系统选择的窗口,或者重置为无窗口。该窗口可能位于本机应用程序中,也可能位于另一个 VM 中的 Java 应用程序中。
索拉里斯:如果窗口是焦点窗口,在指向焦点的窗口管理器中,光标下的最顶层窗口将成为焦点窗口。在点击聚焦窗口管理器中,聚焦的窗口被重置为窗口管理器选择的窗口。该窗口可能位于本机应用程序中,也可能位于另一个 VM 中的 Java 应用程序中。
替换 DefaultKeyboardFocusManager
KeyboardFocusManager 可在浏览器上下文级别插入。客户端代码可以子类化 KeyboardFocusManager 或 DefaultKeyboardFocusManager 以修改与焦点相关的 WindowEvents、FocusEvents 和 KeyEvents 的处理和调度方式,并检查和修改全局焦点状态。与 FocusListener 或 WindowListener 相比,自定义 KeyboardFocusManager 还可以在更基本的层面上拒绝焦点更改。
在给予开发人员对焦点模型的最终控制权的同时,替换整个 KeyboardFocusManager 是一个困难的过程,需要对对等焦点层有透彻的了解。幸运的是,大多数应用程序不需要这么多的控制。鼓励开发人员在完全替换 KeyboardFocusManager 之前使用 KeyEventDispatchers、KeyEventPostProcessors、FocusTraversalPolicies、VetoableChangeListeners 和本文档中讨论的其他概念。
首先请注意,由于在其他上下文中不受阻碍地访问组件代表了一个安全漏洞,因此 SecurityManager 必须授予新权限“replaceKeyboardFocusManager”,然后客户端代码才可以用任意子类实例替换 KeyboardFocusManager。由于安全检查,替换 KeyboardFocusManager 不是将部署在具有 SecurityManager 的环境中的应用程序的选项,例如浏览器中的小程序。
安装后,KeyboardFocusManager 实例可以通过一组受保护的函数访问全局焦点状态。 KeyboardFocusManager 只有安装在调用线程的上下文中才能调用这些函数。这确保了恶意代码无法绕过 KeyboardFocusManager.setCurrentFocusManager 中的安全检查。 KeyboardFocusManager 应该始终使用全局焦点状态而不是上下文焦点状态。不这样做将导致 KeyboardFocusManager 的错误行为。
KeyboardFocusManager 的主要职责是调度以下事件:
- 所有
KeyEvents - 所有
FocusEvents WindowEvent.WINDOW_GAINED_FOCUSWindowEvent.WINDOW_LOST_FOCUSWindowEvent.WINDOW_ACTIVATEDWindowEvent.WINDOW_DEACTIVATED
KeyboardFocusManager 提供上述所有事件,但 WINDOW_ACTIVATED 和 WINDOW_DEACTIVATED 除外。 KeyboardFocusManager 必须在适当的时候合成 WINDOW_ACTIVATED 和 WINDOW_DEACTIVATED 事件并相应地定位它们。
KeyboardFocusManager 可能需要将对等层提供的事件重新定位到它自己的焦点所有者或焦点窗口的概念:
- 必须将 KeyEvent 重定向到焦点所有者。因为对等层不知道任何轻量级组件,所以 KeyEvents 将从针对焦点所有者的重量级 Container 的对等层到达,而不是焦点所有者。
FOCUS_LOST事件必须重定向到焦点所有者。同样,这是必要的,因为对等层不知道轻量级组件。WINDOW_LOST_FOCUS事件必须重定向到聚焦窗口。 Window 类的实现可能会导致本机焦点窗口与 Java 焦点窗口不同。
KeyboardFocusManager 必须确保正确的事件排序,以及事件与其相反事件类型之间的一对一对应关系。对等层不做任何这些保证。例如,对等层可以在 WINDOW_GAINED_FOCUS 事件之前发送 FOCUS_GAINED 事件。 KeyboardFocusManager 负责确保在 FOCUS_GAINED 事件之前调度 WINDOW_GAINED_FOCUS 事件。
在通过 KeyboardFocusManager 重新发送事件之前。 redispatchEvent,KeyboardFocusManager 必须尝试更新全局焦点状态。通常,这是使用 KeyboardFocusManager.setGlobal* 方法之一完成的;然而,一个实现可以自由地实现它自己的方法。尝试更新后,KeyboardFocusManager 必须验证全局焦点状态更改未被拒绝。当对相应的 getGlobal* 方法的调用返回与刚刚设置的值不同的值时,将检测到拒绝。拒绝发生在三种标准情况下:
- 如果
KeyboardFocusManager尝试将全局焦点所有者设置为不可聚焦的组件。 - 如果
KeyboardFocusManager尝试将全局聚焦窗口设置为非聚焦窗口。 - 如果更改被已安装的
VetoableChangeListener拒绝。
KeyboardFocusManager 的客户端定义实现可以调整一组焦点转移,这些焦点转移通过覆盖全局焦点状态的访问器和增变器方法而被拒绝。
如果更改全局焦点状态的请求被拒绝,KeyboardFocusManager 必须丢弃提示焦点更改请求的事件。事件所针对的组件不得接收该事件。
KeyboardFocusManager 也有望启动拒绝恢复,如 焦点和 VetoableChangeListener 中所述。
最后,KeyboardFocusManager 必须处理以下一组特殊情况:
- 在处理
WINDOW_GAINED_FOCUS事件时,KeyboardFocusManager必须将焦点设置到窗口的相应子组件。如果 Window 的子 Component 先前请求了焦点,但由于平台不支持跨 Window 的焦点更改请求而焦点更改被拒绝,则应将焦点设置到该子 Component。否则,如果 Window 从未获得焦点,则应将焦点设置为要获得焦点的 Window 的初始 Component。如果 Window 以前获得焦点,则焦点应设置为 Window 最近的焦点所有者。 KeyboardFocusManager必须确保对应的组件或窗口与本机窗口平台允许的一样准确。例如,KeyboardFocusManager可能需要将相反的组件重定向到对等层最初指定的重量级组件的轻量级子组件。
如果对等层声明对面的 Component 或 Window 是null,则KeyboardFocusManager传播此值是可以接受的。null表示很可能没有其他组件或窗口参与焦点或激活更改。由于平台限制,此计算可能会受到启发式的影响,并且可能不正确。然而,这种启发式将是对等层可能做出的最佳猜测。- 必须丢弃组件或窗口失去焦点或激活自身的焦点和激活更改。
- 必须丢弃由对等层发布的声称活动窗口已失去焦点的事件。 Window 类的对等实现可能会生成这些虚假事件。
与以前版本的不兼容性
跨平台变化:
- 所有组件的默认焦点可遍历性现在为“真”。以前,某些组件(特别是所有轻量级组件)的默认焦点可遍历性为“false”。请注意,尽管有此更改,但所有 AWT 容器的
DefaultFocusTraversalPolicy将保留以前版本的遍历顺序。 - 聚焦非聚焦可遍历(即非聚焦)组件的请求将被拒绝。以前,此类请求已获批准。
- 如果窗口不可见,
Window.toFront()和Window.toBack()现在不执行任何操作。以前,该行为依赖于平台。 - 安装在
Component上的 KeyListeners 将不再看到映射到焦点遍历操作的KeyEvent,并且将不再为此类事件调用Component.handleEvent()。以前,AWT 组件看到这些事件并有机会在 AWT 启动焦点遍历之前使用它们。需要此功能的代码应该禁用其Component上的焦点遍历键并自行处理焦点遍历。或者,代码可以使用AWTEventListener或KeyEventDispatcher预听所有KeyEvent。
特定于 Microsoft Windows 的更改:
Window.toBack()在 Z 顺序更改后将聚焦窗口更改为最顶层窗口。requestFocus()现在在所有情况下都允许跨窗口焦点更改请求。以前,重量级的请求被批准,但轻量级的请求被拒绝。