GTK 输入和事件处理概述 [src]
本篇章详细地描述了 GTK 如何处理输入。如果您有兴趣了解如何将用户的按键或鼠标移动转换为 GTK 微件的更改,请阅读本章。如果您打算实现自己的微件,这款知识也会很有用。
设备和事件
所有计算机用户交互的最基本的输入设备是键盘和鼠标;除此之外,GTK 还支持触摸板、触摸屏以及更多非传统输入设备,例如数位板。在 GTK 中,每种此类输入设备都由一个 GdkDevice
对象表示。
为了简化在这些输入设备之间处理可变性,GTK 有一个逻辑设备和物理设备的概念。许多不同特性的具体物理设备(鼠标可能会有 2 个、3 个或 8 个按钮,键盘可能有不同的布局,可能有或没有数字小键盘块,等等)都表示为物理设备。每个物理设备都与虚拟逻辑设备关联。逻辑设备一直都是指针/键盘对——可以将这种对认为是一个“座位”。
GTK 微件通常处理逻辑设备,因此可以与任何指向设备或键盘一起使用。
当用户与输入设备交互(例如移动鼠标或按下键盘上的键)时,GTK 会从窗口系统接收事件。这些事件通常针对特定的外部表面——对于指针事件,即指针下的外部表面(抓取会对此进行更改),对于键盘事件,即具有键盘焦点的外部表面。
GDK 将这些原始窗口系统事件转换为 GdkEvents
。典型的输入事件有按钮单击、指针移动、按键或触摸事件。这些全部表示为 GdkEvents
,但可以通过查看其类型来区分不同的事件,方法是使用 gdk_event_get_event_type()。
某些事件(例如触摸事件或按钮按下-释放对)以“事件序列”方式相互连接,该序列唯一地标识与同一交互相关的事件。
当 GTK 创建 GdkSurface
时,它会连接到其 ::event 信号,该信号接收所有这些输入事件。外部表面有信号和属性,例如处理窗口管理相关事件。
事件传播
最初在 GTK 端接收输入事件的函数负责许多任务。
- 查找到获取事件的微件。
- 在焦点或悬停位置从一个微件更改到另一个微件时,生成交叉(即进入和离开)事件。
- 将事件发送给微件。
事件沿着微件层次结构向下和向上传播,分为三个阶段,目标指向一个目标微件。
对于关键事件,顶级窗口首先有机会激活辅助键与加速器。如果未消耗这些事件,事件传播的目标小组件是窗口当前的焦点小组件(请参阅 gtk_window_get_focus())。
对于指针事件,目标小组件通过在事件坐标处选择小组件来确定(请参阅 gtk_widget_pick())。
在第一阶段(“捕获”阶段),事件被从最上面的(顶级 GtkWindow 或抢占小组件)一直传递到目标小组件的每个小组件。使用 GTK_PHASE_CAPTURE 附加的事件控制器有机会对事件做出反应。
在“捕获”阶段之后,原本打算成为事件目标的小组件将运行使用 GTK_PHASE_TARGET 附加到它的事件控制器。这称为“目标”阶段,且仅在这个小组件上发生。
在最后一个阶段(“冒泡”阶段),事件从目标传递到最上面的每个小组件,并运行使用 GTK_PHASE_BUBBLE 附加的事件控制器。
不会将事件传递到不敏感或未映射的小组件。
在传播阶段的任何时候,控制器都可以指示已消耗已接收的事件,因此应停止传播。如果使用手势,当手势为自己的触摸序列(或指针事件)声明时,可能会发生这种情况。参阅下面的“手势状态”部分以详细了解手势和序列。
键盘输入
每个 GtkWindow 保持单一焦点位置(在 :focus-widget 属性中)。焦点小组件是发送到窗口的按键事件的目标小组件。只有将 :focusable 设置为 TRUE 的小组件才能成为焦点。通常,这些是输入控件,例如输入框或文本字段,但是按钮也可以获取焦点。
可以通过单击来为输入小组件提供焦点,但也可以使用某些按键事件来移动焦点(这称为“键盘导航”)。GTK 保留了 Tab 键将焦点移动到下一个位置,以及 Shift+Tab 键将其移回上一个位置。此外,许多容器还允许使用箭头键进行“方向导航”。
可以通过“激活”许多小组件来触发操作。例如,可以通过单击按钮或开关来激活它们,但也可以使用 Enter 或 Space 键通过键盘来激活它们。
除了键盘导航、激活和直接键入到输入框或文本视图中之外,GTK 小组件可以使用按键事件激活“快捷方式”。快捷方式通常充当移动焦点或激活当前没有焦点的某个小组件的快速方式。
传统上,GTK 支持不同类型的快捷方式
- 加速器可以激活任何其他快捷方式,而与焦点的位置无关,并且通常触发全局操作,例如 Ctrl+Q 以退出应用程序。
- 通常使用 Alt 作为某个字母的修饰符来触发辅助键。它们用于将标签与控件相关联的地方,并通过在标签中加上下划线字母来指示。作为特例,在菜单(即在 GtkPopoverMenu 中)内,无需修饰符就可以触发辅助键。
- 键绑定特定于各个小组件,例如输入框中的 Ctrl+C 或 Ctrl+V 可以复制到或从剪贴板粘贴。仅当小组件拥有焦点时,它们才会被激活。
GTK 在捕获阶段以全局范围处理加速器和辅助键,而以本地方式在目标阶段处理键绑定。
从本质上讲,所有快捷方式都表示为 GtkShortcut 实例,并且它们由 GtkShortcutController 进行管理。
请注意,GTK 并未执行任何操作将 macOS 上的主要快捷键修改符映射到 Command。如果您想让您的应用程序遵循 macOS 用户体验约定,则必须创建特定于 macOS 的键盘快捷键。Command 在 GTK 中命名为 Meta
(GDK_META_MASK
)。
文本输入
当需要实际的文本输入(例如,不仅仅是键盘快捷键)时,可以通过连接输入法上下文并侦听其 ::commit
信号向小组件添加输入法支持。要创建一个新的输入法上下文,请使用 gtk_im_multicontext_new(),要向其提供输入,请使用 gtk_event_controller_key_set_im_context()。
事件控制器和手势
事件控制器是独立的对象,可以在收到 GdkEvents
后执行特定操作。这些操作与小组件绑定,并且可以告知事件传播阶段,在该阶段它们将管理事件。
手势是一组特定的控制器,已准备好处理指针和/或触摸事件,每个手势实现都尝试识别接收到的事件中特定的操作,相应地通知状态/进度,以便小组件对这些事件做出反应。在多点触控手势中,每个交互触摸序列都将独立跟踪。
由于手势是“简单”的单元,因此将几个手势组合在一起以执行更高级别的操作并不少见,分组手势同时处理相同事件序列,并且这些序列在所有分组手势中共享相同的状态。分组的一些示例 可能是
- “拖动”和“滑动”手势可能需要分组。前者会在拖动时报告事件,而后者只有在识别完成后才会告知滑动 X/Y 速度。
- 将“拖动”手势与“平移”手势分组将有效地允许在平移方向上拖动,因为这两个手势共享状态。
- 如果同时需要“按”和“长按”,则需要进行分组。
快捷方式由 GtkShortcutController
处理,它是一个复杂的事件处理程序,可以自行激活快捷方式,或根据其范围将快捷方式传播到另一个控制器。
手势状态
手势针对每个单独的触摸序列都有一个“状态”概念。当首次收到触摸序列的事件时,触摸序列将具有“无”状态,这意味着该手势正在处理触摸序列以可能触发操作,但不会停止事件传播。
当手势进入识别阶段,或在较晚的时间点,小组件可以选择声明触摸序列(单独或分组),从而在事件在该小组件和传播阶段中的每个手势中运行后停止事件传播。每当出现这种情况时,触摸序列都会向下传播链取消,让它们知道不会再发送进一步的事件。
或者,或在以后的时间点,小组件可以选择否定触摸序列,从而再次让这些触摸序列进行事件传播。当这种情况发生在捕获阶段并且小组件中没有其他声明手势时,将模拟 GDK_TOUCH_BEGIN
/GDK_BUTTON_PRESS
事件并向下传播,以保持一致性。
对于给定的触摸序列,分组手势始终共享相同的状态,因此在一个手势上设置状态确实会将状态传给其他手势。它们也是互斥的,在其中一个手势组可能声明给定序列的小组件中。如果另一个手势组后来声明同一个序列,则第一个组将否定该序列。