Linux 焦点模型分析 |
|
冯 遥遥 ([email protected]), 软件工程师, IBM 中国软件开发中心
2008 年 1 月 21 日
本 文以 SUSE 10 为例,通过实例帮助读者理解 Linux 环境下焦点系统与鼠标、键盘输入的关系,并讨论其与 Windows 环境下焦点系统的区别。另外,本文还将介绍 X server 、 window manager 以及应用程序之间如何相互配合以管理焦点。最后本文将简要介绍 GTK 的焦点模型。 本文面向 Linux 应用程序开发人员,以及 GUI 应用程序移植人员。帮助读者深入理解 Linux 系统的焦点系统与管理方式。
概述
无 论是 Windows 应用程序还是 Linux 应用程序的开发人员,焦点( focus )都是一个非常常见的一个概念。那么焦点究竟是什么呢?简单的说,焦点决定了由哪个窗口或者控件接收键盘输入信息,因此,它又被称作输入焦点。对于用户来 说,最直观的感觉是,有光标闪动的窗口或者被高亮的控件就有焦点。
很多初级应用程序员或者 Windows 用户有这样一个误解,认为凡是鼠标点击的窗口就是焦点窗口。当出现有的窗口或者控件点击后没有反应的现象时,就认为是焦点出现了问题。事实上,焦点仅仅控 制着键盘的输入,而鼠标输入与焦点没有直接关系。用户之所以有这样的误解是由于另一个概念,系统的焦点模式(focus mode )。焦点模式决定了鼠标如何使一个窗口获得焦点。一般来说,焦点模式被分为三种:
不同的系统对焦点模式的支持不同,所使用的焦点模型也有很大的区别。
|
Linux 与 Windows 焦点系统
Linux 使用的焦点模型与 Windows 有着很大的区别。对于大多数用户来说,Windows 的焦点模型较为直观、易于理解,因此,本节中我们将以Windows 上的焦点模型为引子,着重介绍 Windows 与 Linux 上焦点系统的基本概念,并讨论两个平台上焦点系统的区别。
Windows 上的焦点系统
Windows 上默认采用 click-to-focus 的焦点模式。这是因为 Windows 操作系统采用的资源管理器 explorer.exe 只支持这一种焦点模式。这也是造成之前所提到的用户认为鼠标点击的窗口就是焦点窗口的错觉的原因之一。
现在有一些基于 X 的 Windows 窗口管理器,如 blackbox for Windows 等,可以替代explorer。这些窗口管理器就可以支持以上提到的焦点模式。
正如前面所介绍的,焦点决定了哪个窗口可以获得键盘输入。那么,介绍系统的焦点模型就不能不提到键盘输入。下图展示的就是 Windows 上的键盘输入模型。
当 键盘中的一个键被按下或者被释放时,键盘驱动会收到键盘中断,获得该按键的扫描码( scan code )。这是一个与硬件相关的数值。驱动会根据键盘布局将这个扫描码转换成设备无关的虚拟键盘码( virtual-key code ),并生成一个键盘消息( WM_KEYDOWN 或者 WM_KEYUP 消息)放在系统输入队列中。在任何给定的时刻,只有一个线程与系统输入队列连接。系统会将这个消息从系统输入队列中取出,发送给这个线程的输入消息队列。 该线程的消息循环又会从本线程的消息队里取出这个消息,传递给合适的窗口处理过程。这样的输入模型保证了一个线程的行为不会对其它前程产生影响。例如,如 果一个线程挂起了,不会妨碍其他线程接收键盘输入。
那么,哪个线程是“与系统输入队列连接的线程”呢,哪个窗口又是这个“合适的窗口”呢?Windows 有它自己的管理方式。
在 Windows 上,窗口消息是以线程为单位进行管理的。每个进程可能有多个线程在执行,每个线程都可以创建窗口。用户当前正在使用的顶层窗口被称为前景窗口( foreground window ),它位于所有窗口的上面。而创建该窗口的线程就被称为前景线程( foreground thread )。相应地,别的窗口被称为背景窗口( background window ),创建它们的线程则被称为背景线程( background thread )。应用程序可以使用 SetForegroundWindow 来设置前景窗口。用户也可以用鼠标,或者 ALT+TAB,ALT+ESC 来切换前景窗口。
每个线程内部还维护着自己的活动窗口( active window )和焦点窗口( focus window )。焦点窗口( focus window )实际上是一个窗口的临时的属性。拥有焦点的窗口可以从线程的消息队列中获得键盘消息。焦点窗口的顶层窗口被称为活动窗口( ActiveWindow )。程序员可以使用 SetFocus 和 SetActiveWindow 来为该线程设置焦点窗口和活动窗口。
但 是,焦点窗口只是一个局部的概念,并不是所有的焦点窗口都可以获得键盘事件。只有前景线程的焦点窗口才能从系统队列中得到键盘事件,而前景线程中的活动窗 口是前景窗口。在任何时刻系统中都只可能有一个被激活的窗口,这就是前景窗口。这也就回答了上一节中的问题:与系统队列相连接的线程就是前景线程,而那个 可以得到键盘事件的窗口就是前景线程的焦点窗口。当然,Windows 还提供了 AttachThreadInput 方法来合并两个线程的输入队列。本文主要介绍焦点系统,对输入也就不做过多介绍了。
当一个线程的焦点窗口从一个窗口改变到另一个窗口的时候,失去焦点的窗口会收到系统发出的 WM_KILLFOCUS 消息,而得到焦点的窗口会收到 WM_SETFOCUS 消息。
当另一个窗口被激活时,系统会向这两个窗口发送 WM_ACTIVATE ,并用 wParam 来通知窗口是被激活了或者去激活了。如果被激活的窗口属于另一个应用,那么系统将给这两个应用发送 WM_ACTIVATEAPP 消息。
事实上,Windows 上采用的是一套简单、易于理解的焦点系统。Linux 的焦点系统远比Windows 的复杂得多。
Linux 上的焦点系统
几 乎所有 Linux 系统都支持前面介绍的三种焦点模式。以 SUSE 10的 GNOME 桌面系统为例。SUSE 10上焦点模式的默认焦点模式也是采用 click-focus 模式。同时,用户也可以通过 Control Center->Windows 来修改系统所使用的焦点模式(见图2)。
“Window Selection”下的复选框如果没有被选中,如图中所示,则使用的是 click-focus 模式,也就是默认的焦点模式。如果用户将此复选框选中,那么系统将采用 sloppy focus 模式。
用户所设置的焦点模式由 GConf 管理保存,它所对应的键值为“/apps/metacity/general/focus_mode”。用户也可以通过 gconf-editor 修改此键对应的值达到修改焦点模式的目的(见图3)。
从 GConf 中对这个键的描述我们可以看出,该键支持的值有“click”、“sloppy”和“mouse”,分别对应了焦点模式中的 click-focus, sloppy focus 和 focus-follow-mouse。
Linux 上的界面系统使用的是 X 窗口系统。X 是一个面向网络的窗口系统,它采用了 Client-Server 的模型。Client 可以看做是X系统下运行的应用程序。X Server 为各个 Client 提供了界面方面的服务。Client 和 Server 可以不在同一台机器上运行,它们通过网络发送请求和消息。
Windows 上的焦点是一个局部的概念,每个线程都拥有自己的焦点窗口。但是在 X 系统中,焦点是一个全局的概念。整个系统中只有一个 X 窗口拥有 X 焦点。应用程序可以通过 XSetInputFocus 来设置焦点窗口。与 Windows 不同的是,在 Linux 上不可见的窗口是不能被设置为焦点窗口的。
在 X 系统中, X Server 会在窗口获得或者失去焦点时发送 FocusIn 和 FocusOut 事件通知Client。但是和 Windows 不同的是,并不是所有窗口都能在焦点切换时收到通知,只有创建时选择了 FocusChangeMask 的窗口才能接收到焦点变化事件。以下代码就是创建一个这样的窗口。
|
X 系统有着非常特殊的键盘输入模型。在一般情况下(没有应用调用了 XGrabKey 或者XGrabKeyboard 的情况下),X 系统下键盘输入不仅与焦点有关,还与鼠标位置相关。如果当前鼠标位于焦点窗口之外,那么,和 Windows 一样,X Server 会将键盘事件发送给焦点窗口。但是,如果鼠标位于焦点窗口的子窗口上,那么,键盘事件会直接发送给这个子窗口。这样的输入模型往往使 Linux 的初级用户尤其是习惯了 Windows 焦点模型的用户相当困惑。
以图4为例,当前窗口 A 拥有 X 焦点,窗口 B 是窗口 A 的子窗口。
当鼠标在 P1 位置时,它位于窗口 A 范围之外,那么键盘的 KeyPress/KeyRelease 事件会发给 A 窗口;
当鼠标在 P2 位置时,虽然该位置处于窗口 A 范围内,但是不属于任何 A 窗口的子窗口上,因此,键盘事件也会发给 A 窗口;
当鼠标位于 P3 位置时,由于 P3 位于窗口 A 的子窗口 B 上,因此,键盘事件将直接发给窗口 B。
由于 X 系统上输入模型的特殊性,基于 X 系统开发的较为高级的图形库一般会将 X焦点设置在顶层窗口上,而系统内部维护着自己的逻辑焦点。这些图形库会统一处理发送到本应用的键盘事件,将键盘事件再分发给拥有逻辑焦点的控件。
关于 X 系统上焦点管理的具体规范将在下一章中进行详细的讨论。
|
X 系统上焦点的管理
本章将具体讨论 X 系统上焦点的管理与控制方法和规范。
X Server 与窗口管理器( window manager )
上 一章介绍了在 Linux 上的界面系统使用的是 X 窗口系统。在这个系统中,X Server 起了很重要的作用,它管理着系统中各种资源,包括窗口、光标、字体以及所有的图形元素,它使得这些资源可以以统一的格式在各个 Client 之间通过网络共享。但是,这些资源以何种风格显示出来却是由窗口管理器( window manager )控制的。
窗 口管理器是运行在 X 系统上的一种特殊的应用程序,它实际上也是一个 X Client。它为 X 系统上运行的窗口程序提供了一套统一的外观风格机制。例如,它决定了窗口的标题栏使用蓝色还是灰色;它规定了窗口的滚动条是扁还是圆;当两个窗口请求的位 置重叠时,它控制着这两个窗口具体显示位置是否重叠,等等。简单的说,在 X 系统上,所有的可见的控件都是窗口,窗口管理器就负责管理这些窗口。
一 般情况下,Client 需要配合窗口管理器,以使窗口达到预期的效果。ICCCM( Inter-Client Communication Conventions Manual )定义了这种配合的规范。目前流行的窗口管理器很多,比如,KWin,Metacity,FVWM,TWM,wm2等等。
虽 然这是 X 系统广泛支持的规范,但是,ICCCM 对窗口管理器的实现仅仅是一个参考各窗口管理器对 ICCCM 支持的程度也不同。而对于 ICCCM 没有定义到的功能,各个窗口管理器的实现更有着很大的差别。因此当应用程序在 X 系统下请求完成某个操作后,在收到相应前永远不能假设实际结果会是怎样。
ICCCM 定义的焦点模型
在 X 系统中,每个窗口都具有一些属性,被称为 property。这些属性的名称和类型都以atom 的形式存在。每个 atom 在系统中由唯一的整数标识。Client 可以自己定义的属性,也可以由 Server 创建,它们都由 X Server 统一进行维护,在各个 Client 之间共享。应用程序可以通过 XSetWindowProperty, XGetWindowProperty 等方法来访问这些属性。
ICCCM 通过对一些 property 的定义,制定了 X Client 之间通信的规范,包括 Client与 Client,Client 与窗口管理器,Client 和会话管理器,以及颜色特征的通信规范。在这里我们将主要介绍 ICCCM 就 Client 与窗口管理器关于焦点的规定。
ICCCM 规定,所有拥有顶层窗口的 Client 都必须通过设置指定的 property 来指示窗口管理器如何管理 Client。所有与窗口管理器相关的属性都以“WM_***”的形式出现,如 WM_NAME,WM_ICON_NAME 等等。其中两个属性与焦点管理相关,一个是 WM_HINTS,一个是 WM_PROTOCOLS。
WM_HINTS 为窗口管理器提供了 Client 的信息包括图标的信息和位置,初始状态,是否需要窗口管理器设置焦点。下表是 Xlib 中对 XWMHins 的声明。程序可以通过 XSetWMHints 来设置该属性值。
|
其中的 InputHint 标识了应用程序是否依赖窗口管理器获取焦点。只有当该域被设置为True 时,窗口管理器才会在希望将其设置为焦点窗口时主动调用 XSetInputFocus 将其设置为焦点窗口。例如,在 click-focus 模式下,当用户用鼠标点击了某个窗口,如果该标志被设置为 True ,窗口管理器会将此窗口设置为焦点窗口。但是如果此标志被置为 False ,那么窗口管理器将不会主动设置其为焦点窗口。
下面这段示例程序显示了如何为窗口设置 InputHint 标志。
|
WM_PROTOCOLS 实际上是一组 Atom。每一个属性的设置实际上决定了 Client 是否要参与窗口管理器的某些管理。在 ICCCM 中定义的标准协议 Atom 包括:WM_TAKE_FOCUS、WM_SAVE_YOURSELF 和 WM_DELETE_WINDOW。不同的窗口管理器还可以定义自己的协议。Client 可以通过 XSetWMProtocols 指定需要接受协议。如果窗口设置了其中的协议 Atom,当窗口管理器要进行某些操作的时候将会以 ClientMessage 事件的形式将此协议 Atom 发送给窗口,通知 Client 做出相应的行动。其中,WM_TAKE_FOCUS 协议规定了 Client 是否要参与到设置焦点窗口的行为中。
以下程序片断显示了用户如何设置 WM_TAKE_FOCUS 协议。
|
当 WM_TAKE_FOCUS 协议 Atom 被设置表明该窗口希望参与到焦点的管理中。那么,当窗口管理器打算将该窗口设置为焦点窗口时,会给窗口发送包含了 WM_TAKE_FOCUS 的 ClientMessage 事件。一般来说,当窗口收到这个事件后会为根据需要设置焦点窗口。
如果该 Atom 没有设置,那么是否设置焦点窗口就完全由窗口管理器决定。
ICCCM 根据窗口对属性的设置不同分为四种输入焦点模型( Input Focus Model ):无输入( No Input ),被动输入( Passive Input ),局部主动输入( Locally Active Input )以及全局主动输入( Globally Active Input )。每种模型对应的 input 域和 WM_TAKE_FOCUS atom 的设置如下:
输入模型 | InputHint 域 | WM_TAKE_FOCUS |
无输入 | False | 不设置 |
被动输入 | True | 不设置 |
局部主动输入 | True | 设置 |
全局主动输入 | False | 设置 |
从前面对 InputHint 域以及 WM_TAKE_FOCUS 的介绍,我们可以大致可以推测出这些模型的工作方式。被动输入和局部主动输入的 WM_HINTS 的 InputHint 域设为 True,说明这种模型下,窗口依赖窗口管理器来获取输入焦点。而无输入和全局主动输入模型中,InputHint 域设置为 False,则表明窗口管理器不会把焦点设置到它们的顶层窗口上。而其中,局部主动和全局主动输入设置了 WM_TAKE_FOCUS,说明它们希望控制焦点的设置。
ICCCM 对 Client 如何响应窗口管理器的消息也做出了规定。ICCCM 规定 Client 只有在以下情况下才能将焦点设置到其子窗口上:在 WM_PROTOCOLS 属性中设置了 WM_TAKE_FOCUS,并且满足以下条件之一:
当 Client 在满足了以上条件后,可以通过 XSetInputFocus 发送设置焦点的请求。下面的程序片断展示的是 Client 如何响应窗口管理器发来的 WM_TAKE_FOCUS 通知。
|
也许读者已经注意到了,与 Windows 的 SetFocus/SetActiveWindow 不同,XSetInputFocus 参数除了有 display 和 window 外还有两个参数:revert_to 和 time。
清单6 XSetInputFocus 声明
XSetInputFocus(display, focus, revert_to, time)
Display *display;
Window focus;
int revert_to;
Time time; |
revert_to 参数用于指示窗口管理器如果窗口不可见了焦点将设置给哪个窗口。它可以设置为 RevertToParent,RevertToPointerRoot 和 RevertToNone。由于 RevertToPointerRoot和 RevertToNone 都不够安全,ICCCM 规定 Client 在调用 XSetInputFocus 时,应该把该参数设置为 RevertToParent。
time 参数指示了请求焦点的事件戳。如果使用 CurrentTime 作为 time 的值,则表示该请求使用 X Server 的事件戳。由于 XSetInputFocus 是向 X Server 发送获得焦点的请求,是一个异步的过程,如果直接使用 CurrentTime 可能产生同步问题。最好的办法是直接 WM_TAKE_FOCUS 消息的时间戳。
X Destop Group 在 ICCCM 的基础上还专门针对窗口管理器制定了一些扩展规范,称为Extended Window Manager Hints ( EWMH )spec。在本文中就不做过多讨论,有兴趣的读者可以参看 http://standards.freedesktop.org/wm-spec。
输入代理
输入代理是 X 焦点系统上一个非常有趣的概念。它是在 XEmbed 协议中提出的一项技术。
在“ X 系统上的键盘输入模型”一节中,我们介绍了 X 上特殊的键盘输入模型。键盘事件不仅与焦点相关还与鼠标位置相关。对于一般的应用而言,这样模型可以通过应用的逻辑焦点设置来进行控制。但是对于有嵌入窗口的应用程序而言,这样的输入模型会带来很多的问题。
以 上图为例,窗口 A、B 属于 Client1,而窗口 C 属于 Client2,窗口 B、C 是窗口 A 的子窗口。此时,窗口 A 拥有 X 焦点。在 X 系统上,当鼠标位于 P1 和 P2 位置时,键盘事件会被发送给窗口 A;如果鼠标在 P3 时,键盘事件会发送给窗口 B,但是由于 A 与 B 属于同一个 Client,应用程序可以根据自己维护的逻辑焦点将键盘事件发给正确的窗口;但是,如果鼠标位于 P4 位置时,此时,键盘事件将直接发送给 C 窗口,Client1 将得不到任何消息,也无法进行派发。为了解决这个问题,XEmbed 协议提供了“焦点代理”的概念。
它的具体实现是顶层窗口创建一个看不见,但是能够得到焦点的子窗口,称为焦点代 理( focus proxy )。它一般是一个1*1大小的窗口,这个窗口一般位于(-1,-1)的位置,并且没有子窗口。当其它窗口获得焦点时,Client 必须通过 XSetInputFocus 把焦点设置到这个代理上。由于这个焦点代理不是任何其它窗口的父窗口,而且鼠标永远不可能位于这个窗口上,因此所有的键盘事件必然发到这个焦点代理上,再 由它根据逻辑焦点派发键盘事件。
要实现“焦点代理”技术要求 Client 必须能够接收窗口管理器的 WM_TAKE_FOCUS 消息。换句话说,要求 Client 必须使用局部主动输入模型。
虽然焦点代理的技术是为了解决嵌入窗口的焦点管理问题,但是由于此技术能够很好的解决 X 系统下的焦点问题,给用户提供更好的体验,目前很多基于 X 系统的高级图形库都使用了这个技术。
除了输入代理的概念以外,XEmbed 协议还定义了窗口激活状态的概念,它类似于Windows 的活动窗口的概念,并为此相应的定义了一些消息。关于 XEmbed 协议的相关资料可以参看 http://standards.freedesktop.org/xembed-spec。
|
GTK+ 应用的焦点管理模型
GTK+ 是 GNOME 桌面系统主要采用的高级图形开发库。它为开发人员提供了一系列封装好的控件,极大的简化了开发人员的工作。本节将简要介绍一下 GTK+ 的焦点管理模型。
作 为一个高级图形开发库,GTK+ 考虑到用户体验以及窗口嵌入的需要,采用了输入代理技术,相应地,它使用的局部主动输入的模型。其使用体验与 Windows 上的应用非常类似。GTK+ 内部维护了自己的逻辑焦点,可通过 gtk_window_get_focus 和 gtk_window_set_focus 等方法设置其逻辑焦点控件。
GTK+ 沿用了 XEmbed 提出的窗口激活的概念。在 Windows 上,焦点窗口以线程为单位进行维护。而在 GTK+ 系统中,焦点窗口以顶层窗口为单位维护。只有被激活的顶层窗口的逻辑焦点控件才是应用程序的焦点。也就是说,用户可以调用 gtk_window_set_focus 为某 GtkWindow 设置焦点控件。但是,如果这个 GtkWindow 当前没有被激活,也就是说 X 焦点没有设在该窗口的焦点代理上,那么此时调用 GTK_WIDGET_HAS_FOCUS( widget )将返回 FALSE ,但是 gtk_window_get_focus 仍然返回此控件。
在 GTK+ 系统中,所有的消息都是以信号( signal )的形式发给相应控件。如果应用程序需要处理这些 signal,可以通过 g_signal_connect 和 g_signal_connect_after 将相应的回调处理函数与相应的信号关联起来,这些处理函数被称为 signal hander。GTK+ 的标准控件一般会为这些信号提供处理函数。当然,应用程序也可以添加自己的 signal hander。
对于 GTK+ 的焦点系统,当控件得到或者失去焦点时,会收到 focus_in_event 和focus_out_event。GtkWidget 对这两个信号原型的定义如下:
|
比如 gtkEntry 控件,它类似于 Windows 上的单行的 Edit 控件。当 focus_in_event 发送给 GtkEntry 时,GtkEntry 默认会设置它的输入法状态,并显示插入光标。应用程序还设置自己的 signal handler。这样的编程接口给程序员带来了很大的方便。下面的代码显示了如何为一个 GtkEntry 设置与 focus_in_event 的 signal handler。
|
|
总结
本 文主要介绍了焦点的基本概念,对比 Windows 焦点模型介绍了 Linux 系统上的焦点模型的特殊性,并着重介绍了 X 系统的一些焦点管理规范。可以看出,X 窗口系统的焦点模型比 Windows 上的焦点模型复杂很多。实际上,X 系统上的焦点系统,尤其在与窗口管理器的配合方面,还有着很多细节问题。尽管类似 GTK+ 这样的高级图形库屏蔽了 X 的复杂特性,但是,Linux 应用程序员只有了解了这些细节和规范才能编出更符合 X 风格的界面程序,也才能应对各种出乎意料的焦点问题。