7.1 窗口管理器

本质上,窗口就是显示器上对应的一块区域。对于一个运行多任务的操作系统来讲,在一个有限的屏幕上可以同时存在多个窗口,因此,用户希望多个窗口之间可以协调布局和平共享同一个屏幕。可以将特定窗口切换为当前活动窗口;可以按需改变窗口尺寸;可以最大化、最小化以及关闭窗口。但是X的设计哲学是只提供机制,不提供策略,X服务器只提供窗口操作相关的函数,但不管如何去操作窗口。于是诞生了另外一个特殊的X应用:窗口管理器。

7.1.1 基本原理

1.X的窗口

X将所有窗口组织为一棵树。X服务器启动后,将默认创建一个窗口,这个窗口充满整个屏幕,作为整个窗口树的根,称为根窗口(Root Window),所有应用的顶层窗口(Top-level Window)都是根窗口的子窗口。

假设在X中运行两个应用A和B,A包含2个窗口,B应包含3个窗口,窗口之间的布局如图7-1所示。

7.1 窗口管理器 - 图1

图 7-1 窗口布局示意图

它们之间的树形关系如图7-2所示。

7.1 窗口管理器 - 图2

图 7-2 窗口树形关系示意图

窗口管理器仅管理应用的顶层窗口,即如图7-2中的"Top Window A"和"Top Window B"。一个应用可能有多个顶层窗口,除了应用的主窗口之外,对话框一般也是一个顶层窗口。而对于顶层窗口的子窗口,则由应用自己管理。

2.窗口装饰

在第6章中,我们看到,无论是基于Xlib的程序,还是使用GTK编写的程序,在没有窗口管理器的情况下,它们的窗口都以“素颜”示人,只是一个“裸”窗口。一个典型的桌面应用的窗口,一般而言,包括一个标题栏,标题栏上还可能显示窗口的名称、最大化、最小化和关闭按钮。另外,窗口一般还有一个边框。用户可以通过标题栏移动窗口,可以在边框处拖动鼠标改变窗口尺寸,可以分别通过最大化、最小化和关闭按钮最大化、最小化、关闭窗口。这些组件除了具备功能外,还具备美化的作用,比如可以设置窗口边框的颜色、阴影效果等,因此,它们也被称为窗口装饰。

显然,窗口装饰不应该由各个应用负责,暂且不提重复劳动,单单一致性就是个大问题。如果任由应用自己绘制,最后将导致窗口标题栏等装饰五花八门。因此,在X中,将窗口装饰提取为公共部分,由窗口管理器统一负责。通常的实现方式是:窗口管理器创建一个窗口,我们称这个窗口为Frame,作为根窗口的子窗口,但是作为应用的顶层窗口的父窗口。其他装饰,或者直接绘制在Frame窗口上,或者创建新的装饰窗口,但是这些装饰窗口也作为Frame的子窗口,本章我们开发的窗口管理器采用后者。应用的顶层窗口和Frame窗口之间的关系如图7-3所示。

7.1 窗口管理器 - 图3

图 7-3 顶层窗口和Frame窗口的关系

3.拦截事件

X服务器维护一个事件队列,在该队列中按顺序保存着发生的各个事件,并周期地分发给应用。每个应用可以选择对发生在某些窗口上的哪些事件感兴趣,如果多个应用对同一个事件感兴趣,X服务器将复制该事件的多个副本,并将其分发给各个对其感兴趣的应用,如图7-4所示。

7.1 窗口管理器 - 图4

图 7-4 事件队列

Xlib提供了函数XSelectInput,应用程序可以使用该函数选择接收指定窗口的事件,其函数原型如下:

7.1 窗口管理器 - 图5

其中参数w表示接收发生在窗口w上的事件,event_mask表示对哪些事件感兴趣,如ButtonPressMask表示希望接收窗口w的ButtonPress事件。

在这些事件掩码中,有一个比较特殊—SubstructureRedirectMask,其含义是:当某个应用选定了某个窗口的SubstructureRedirectMask时,该窗口的子窗口(Substructure)发送给X服务器的MapRequest、ConfigureRequest和CirculateRequest三类请求,都将被重定向给这个应用,这就是X的"Substructure Redirection"机制。

窗口管理器恰恰利用了这个机制,对根窗口选择了SubstructureRedirectMask,从而截获了应用的顶层窗口的请求。其中最关键的是MapRequest,在窗口请求X服务器显示时,其将向X服务器发送MapRequest请求。在截获了MapRequest后,窗口管理器创建Frame窗口,作为根窗口的子窗口,然后暗渡陈仓,将应用的顶层窗口从根窗口脱离,而将其作为Frame窗口的子窗口,同时也创建其他窗口装饰。都伪装好后,窗口管理器再以Frame窗口的身份,请求X服务器显示Frame窗口。应用的顶层窗口作为Frame窗口的子窗口,当Frame窗口得以显示后,其自然也被显示。在某种意义上,窗口管理器通过Frame窗口控制了应用的顶层窗口,从而达到管理它们的目的。

在应用的顶层窗口作为Frame窗口的子窗口后,窗口管理器还是要关心它们发送给X服务器与窗口管理相关的请求,因此,如同设置根窗口的SubstructureRedirectMask,窗口管理器也需要设置Frame窗口的SubstructureRedirectMask。

不知读者是否考虑过这样一个问题:既然X服务器将其他应用的MapRequest请求重定向给窗口管理器,那么窗口管理器同样也作为X服务器的一个客户程序,它也需要向X服务器发送MapRequest请求,比如请求显示Frame等装饰窗口。如此这般,X服务器岂不是将窗口管理器发送给它的请求再重定向给窗口管理器?如此往复,岂不是形成了死循环?

为此,窗口提供了一个属性:override_redirect。如果窗口的这个属性值为True,则其明确告知X服务器自己不需要窗口管理器的管理,X服务器就不会将这个窗口的请求重定向给窗口管理器。我们常用的鼠标右键菜单就是一个典型的将属性override_redirect设置为True的窗口。因此,窗口管理器在创建Frame等装饰窗口时,可以通过将它们的这个属性设置为True来解决我们刚刚谈到的死循环问题。事实上,即使不设置这个属性,也不会形成死循环,X的开发者已经考虑了这个问题。

窗口管理器除了关心应用的顶层窗口的SubstructureRedirectMask涉及的请求外,另外还要获得它们的某些通知事件。其中一个就是UnmapNotify,在收到这个通知后,窗口管理器需要清理所有为该窗口创建的对象,包括窗口装饰等。所以除了事件掩码SubstructureRedirectMask外,窗口管理器还要选择根窗口和Frame窗口的事件掩码SubstructureNotifyMask。

4.窗口间通信

在一个标准的桌面环境下,存在多个不同的应用程序,除了普通的应用程序外,还有构成基本桌面环境的组件,如任务条等。而且,每个应用的窗口布局策略不尽相同,比如普通的X应用一般带有窗口装饰,但是我们有看到过构成桌面环境的任务条装饰着标题栏,并且标题栏上有最大化/最小化以及关闭等按钮吗?显然,这类组件不需要窗口装饰。我们还以任务条为例,在某些桌面环境上,任务条可以放置在屏幕的上方、下方、左侧以及右侧。再比如,对话框的窗口装饰中通常是没有最大化按钮的。

显然,窗口管理器需要获得窗口的相关信息,才能根据这些信息决定如何为这些窗口在同一个屏幕上协调的布局以及如何装饰这些窗口。为此,X提供了多种窗口间通信的机制,属性(Property)是窗口管理器和应用的窗口之间使用的主要通信机制。

X默认定义了一些属性,这些属性在窗口管理器规范中约定,但是应用也可以自定义属性。在X中,每个窗口都附着一个属性表,表中每一行大致就是属性的名字和其对应的值。应用可以设置自己创建的窗口的属性,也可以读取或者改变其他应用的窗口的属性,从而达到不同窗口间通信的目的。

属性保存在X服务器端。每个属性都有一个名字,为了便于使用属性,属性的名字是可读性更好的ASCII字符串而不是一串数字。然而,如果应用程序使用属性的名字引用属性,势必要通过套接字传递属性的名字给X服务器。但是字符串的数据量明显大于一个固定长度的整数,而且,还有一点,字符串的长度是可变的,也给协议的实现增加了复杂度。为此,X又为每个属性起了个小名,这个小名是一个整型数,与属性的名字间是一一对应的关系,X将其称为Atom,在应用与服务器之间通信时,使用这个小名而不是可变长度的字符串。

属性对应的Atom是动态创建的,当X服务器启动时,会为一些属性创建Atom,其他则是在首次使用时创建。Xlib提供了函数XInternAtoms和XInternAtom用来获取属性名对应的Atom。这两个函数基本相同,只不过一个是“批发”,一个是“零售”,相对于XInternAtom而言,XInternAtoms减少了应用和服务器之间的通信次数。XInternAtoms函数原型如下:

7.1 窗口管理器 - 图6

其中,参数names包含要转换的属性的名称,count表示转换的数量,转换后的Atom存储在数组atoms_return中。如果属性的Atom已经存在了,则直接获取其值即可,否则,是否为属性创建Atom要根据参数only_if_exists的值而定。只有only_if_exists为False时,才创建Atom。

Xlib提供了函数XGetWindowProperty和XChangeProperty来读写窗口的属性,我们以XGetWindowProperty为例来讨论一下如何读取窗口属性,该函数原型如下:

7.1 窗口管理器 - 图7

1)参数property指的就是准备读取的窗口w的属性,根据该参数类型也印证了X没有使用属性的名字,而是使用了占用字节数更少的属性的Atom。

2)属性的值可能是一个数组,比如窗口管理器规范EWMH规定属性_NET_WM_WINDOW_TYPE值就是一个Atom数组。数组就是在内存中的一块缓冲区了,从这个角度,就比较容易理解参数long_offset和long_length的意义了。XGetWindowProperty为获取窗口属性提供了更大的灵活性,调用者可以通过参数long_offset和long_length读取存储属性值的缓冲区中指定偏移处的指定长度的值,这两个参数均以32位为单位。

3)在读取窗口的属性后,可以通过参数delete告诉X服务器是否删除窗口的这个属性,这也是为了节省内存空间考虑。

4)XGetWindowProperty允许调用者传递参数req_type告诉服务器读取的属性值的类型,典型的包括XA_ATOM、XA_CARDINAL以及XA_STRING等,分别表示属性的值为Atom、32位整数以及字符串类型。当不确定属性的值的类型时,可以传递AnyPropertyType给X服务器,由X服务器将实际的类型通过参数actual_type_return返回给应用程序。

5)XGetWindowProperty收到X服务器的返回值后,将动态申请一块内存,保存读取到的属性的值,并使用指针prop_return指向这块内存。既然是动态申请的内存,使用后需要用Xlib的函数XFree将其释放。

6)XGetWindowProperty将实际读取的属性的值的类型保存在actual_type_return中;将实际读取的属性的值的格式保存在actual_format_return中,属性的值的格式可以是8、16或32三者之一,分别代表char、short以及long;如果读取操作仅读取了保存属性值的缓冲区中的部分数据,则XGetWindowProperty将保存属性值的缓冲区中剩余的尚未读取的字节数存储在bytes_after_return中;nitems_return中记录的是实际读取的属性的数量。

5.捕捉窗口

我们设想这样一种场景,如图7-5所示,假设X服务器上已经在运行两个X应用A和B,A是当前活动的应用,B是非活动应用。B有两个顶层窗口,除了主窗口外,打开文件对话框也是一个顶层窗口,同时这个对话框也是应用B的临时(transient)窗口。正如其字面意义所言,所谓的"transient"就是临时的、短暂的,是一个相对的概念,是相对于某一窗口而言的。举个例子,如某些应用的“打开文件”对话框,是一个典型的临时窗口。但是如果某个应用的主窗口就是一个对话框,那么这个对话框就不是临时窗口了。

7.1 窗口管理器 - 图8

图 7-5 切换应用

当用户想要将应用B切换为当前活动的应用时,常用的方法之一是使用鼠标点击B应用的窗口。这时窗口管理器拦截鼠标事件,然后请求X服务器重新排列窗口栈序,具体细节见7.1.11节。总之窗口管理器必须要能接收到鼠标事件,如果接收不到鼠标事件,一切都无从谈起。

Frame等装饰窗口是窗口管理器创建的,因此窗口管理器可以自如控制,比如我们可以设置Frame窗口的事件掩码中包含ButtonPressMask。而对于应用的顶层窗口,我们肯定不能过多干涉。但是,我们又不能强制用户一定要点击到Frame窗口上未被应用顶层窗口覆盖的地方。而且一般情况下,用户一定是点击到顶层窗口或者其子窗口上,而不是Frame窗口上,毕竟Frame窗口未被应用顶层窗口遮挡的区域除了标题栏外,只有很小的边框了,也就是说能被点击到的区域很小。

根据X的事件传播机制,如果发生在一个窗口上的事件未被处理,在该窗口没有设置禁止事件继续向其父窗口传播的情况下,事件将沿着窗口树一直向着树的根部传播。很少有具有图形界面的程序不处理鼠标事件,否则就没有任何意义了,也就是说,鼠标事件几乎永远传递不到Frame窗口,都被应用自身消化了。如果不能接收鼠标事件,更何谈激活窗口了。那么怎么解决这个问题呢?

X提供了鼠标捕捉机制,其又分为主动捕捉和被动捕捉。以图7-5为例,假设另外一个应用以被动机制捕捉应用B的顶层窗口时,当用户在应用B的顶层窗口范围内按下鼠标时,将激活捕捉机制,X服务器将鼠标事件不再按照正常的事件传播路径传播了,而是转发给捕捉应用B的顶层窗口的X应用。窗口管理器恰恰是利用了这个机制,捕捉非活动窗口,从而捕获这些窗口的鼠标事件,实现不同应用间的切换。

Xlib提供的用于捕捉的函数是XGrabButton,其原型如下:

7.1 窗口管理器 - 图9

7.1 窗口管理器 - 图10

其中各个参数意义如下:

❑button表示捕捉鼠标哪个键,比如是捕捉左键还是捕捉右键等。

❑modifiers表示是否要求同时按下键盘上某个按键才能捕捉,也就是我们所说的修饰键。

❑event_mask表示捕捉事件的掩码,即捕捉什么事件,是捕捉按下鼠标事件还是捕捉释放鼠标事件等。

❑confine_to表示是否需捕捉的区域限制在某个范围,也就是说,当事件发生时,只有鼠标在这个区域才可以捕捉。

❑cursor表示当捕捉发生时,是否需要使用特定的鼠标指针形状,以给用户一个友好的提示。

❑owner_events主要是用于当应用捕捉自身创建的窗口时使用,与窗口管理器无关。

❑参数grab_window是最核心的一个参数,理解了这个参数就基本理解了整个函数,这个参数就是表明当鼠标按键发生在哪个窗口时进行捕捉。

❑最后来解释参数pointer_mode。我们举个例子来解释这个参数,假设我们将捕捉比喻为窃,那么捕捉其他窗口的应用就是江洋大盗,被捕捉的窗口所属的应用就是受害人。不知读者是否有这样的疑问:当江洋大盗将事件窃走后,受害人还能否失而复得。X再次将这个策略性的问题抛给了应用自己来决定。X提供了两种捕捉模式:异步模式和同步模式。当使用异步模式时,受害人不要心存任何侥幸了。而当使用同步模式时,在取消对一个窗口的捕捉行为后,如果江洋大盗良心发现,X则会给他一次浪子回头的机会。江洋大盗可以调用Xlib的函数XAllowEvents放行这个被截获的事件,这样受害者就可以失而复得了,但是可能不是那么新鲜了,要晚一点。

6.save-set

笔者没有找到一个恰当一点的词来表达save-set这个术语,所以我们就直接用英文了。根据其名字就可以猜出这是一个集合了。但是这个集合是做什么的呢?

我们设想这样一种情况,当窗口管理器异常终止时,窗口管理器创建的Frame等装饰窗口自然也被销毁。销毁这些窗口本身没有问题,但是它们带来了副作用:作为Frame窗口子窗口的应用的窗口也被销毁。这显然不是我们希望看到的。

每个X应用都有一个save-set,其中保存的就是就是窗口的列表。当应用异常断开到X服务器的连接时,X服务器将首先检查应用的save-set,并安排根窗口领养save-set中的窗口,从而避免了在save-set中的这些窗口被销毁。

前面提到的窗口管理器的问题恰恰可以用这个方法解决。每当管理一个窗口时,窗口管理器就可以调用Xlib的函数XAddToSaveSet将其加入到自己的save-set中。一旦当窗口管理器异常终止,根窗口将领养应用的窗口,从而避免了Frame窗口被销毁时,应用的窗口也被销毁的命运。