深度探索Linux操作系统 —— 编译过程分析
深度探索Linux操作系统 —— 构建工具链
深度探索Linux操作系统 —— 构建内核
深度探索Linux操作系统 —— 构建initramfs
深度探索Linux操作系统 —— 从内核空间到用户空间
深度探索Linux操作系统 —— 构建根文件系统
深度探索Linux操作系统 —— 构建桌面环境
深度探索Linux操作系统 —— Linux图形原理探讨
计算机领域中的桌面环境(Desktop Environment)其实是一种比喻的说法,即图形用户界面就像物理书桌一样,其上可以放置文件夹、文档等。桌面最初用来特指个人计算机(PC),但是现在不只个人计算机有图形界面环境,服务器、嵌入式设备等基本都提供桌面环境。桌面环境包括窗口管理器、任务条等基本组件,除了这些基本的组件外,有的桌面环境还提供文件管理器、控制面板等。
桌面环境是操作系统中人机交互的关键部分,理解它的基本运作原理,无论是对理解操作系统,还是对开发应用程序,都有极大的帮助。我们处于这样一个追求个性的年代,无论是用于消费类电子设备的移动系统,还是用于 PC 的中规中矩的桌面系统,人们都已不再满足于千篇一律的桌面。打造一个全新的个性化桌面,绝不只是停留在更改个背景图、换个主题这个层面,我们需要更大的革新。但是如果对桌面环境的基本原理都不甚了解,那又何谈去开发打造具有创造性的用户交互。
因此,在本章中我们带领读者从头构建一个基本的桌面环境,包括窗口管理器、任务条以及一个显示桌面背景的组件。为了使读者更能深刻体会 X 的客户/服务器模型,窗口管理器基于 Xlib 编写,而任务条等组件则展示了使用 GTK 图形库的编程方法。
限于篇幅,我们没有将全部源代码全部贴到书中,所以请读者结合随书光盘中附带的源代码进行阅读。另外,本章虽然涉及 Xlib 和 GTK 编程,但是为了不干扰主线 —— 构建桌面环境,我们不会过多讨论它们的编程,其中涉及的 API ,如有必要请参考 Xlib 和 GTK 各自的参考手册。
本质上,窗口就是显示器上对应的一块区域。对于一个运行多任务的操作系统来讲,在一个有限的屏幕上可以同时存在多个窗口,因此,用户希望多个窗口之间可以协调布局和平共享同一个屏幕。可以将特定窗口切换为当前活动窗口;可以按需改变窗口尺寸;可以最大化、最小化以及关闭窗口。但是 X 的设计哲学是只提供机制,不提供策略,X 服务器只提供窗口操作相关的函数,但不管如何去操作窗口。于是诞生了另外一个特殊的 X 应用:窗口管理器。
X 将所有窗口组织为一棵树。X 服务器启动后,将默认创建一个窗口,这个窗口充满整个屏幕,作为整个窗口树的根,称为根窗口(Root Window),所有应用的顶层窗口(Top-level Window)都是根窗口的子窗口。
假设在 X 中运行两个应用 A 和 B ,A 包含 2 个窗口,B 应包含 3 个窗口,窗口之间的布局如图 7-1 所示。
它们之间的树形关系如图 7-2 所示。
窗口管理器仅管理应用的顶层窗口,即如图 7-2 中的 “Top Window A” 和 “Top Window B” 。一个应用可能有多个顶层窗口,除了应用的主窗口之外,对话框一般也是一个顶层窗口。而对于顶层窗口的子窗口,则由应用自己管理。
在第 6 章中,我们看到,无论是基于 Xlib 的程序,还是使用 GTK 编写的程序,在没有窗口管理器的情况下,它们的窗口都以 “素颜” 示人,只是一个 “裸” 窗口。一个典型的桌面应用的窗口,一般而言,包括一个标题栏,标题栏上还可能显示窗口的名称、最大化、最小化和关闭按钮。另外,窗口一般还有一个边框。用户可以通过标题栏移动窗口,可以在边框处拖动鼠标改变窗口尺寸,可以分别通过最大化、最小化和关闭按钮最大化、最小化、关闭窗口。这些组件除了具备功能外,还具备美化的作用,比如可以设置窗口边框的颜色、阴影效果等,因此,它们也被称为窗口装饰。
显然,窗口装饰不应该由各个应用负责,暂且不提重复劳动,单单一致性就是个大问题。如果任由应用自己绘制,最后将导致窗口标题栏等装饰五花八门。因此,在 X 中,将窗口装饰提取为公共部分,由窗口管理器统一负责。通常的实现方式是:窗口管理器创建一个窗口,我们称这个窗口为 Frame ,作为根窗口的子窗口,但是作为应用的顶层窗口的父窗口。其他装饰,或者直接绘制在 Frame 窗口上,或者创建新的装饰窗口,但是这些装饰窗口也作为 Frame 的子窗口,本章我们开发的窗口管理器采用后者。应用的顶层窗口和 Frame 窗口之间的关系如图 7-3 所示。
X 服务器维护一个事件队列,在该队列中按顺序保存着发生的各个事件,并周期地分发给应用。每个应用可以选择对发生在某些窗口上的哪些事件感兴趣,如果多个应用对同一个事件感兴趣,X 服务器将复制该事件的多个副本,并将其分发给各个对其感兴趣的应用,如图 7-4 所示。
Xlib 提供了函数 XSelectInput,应用程序可以使用该函数选择接收指定窗口的事件,其函数原型如下:
XSelectInput(Display *display, Window w, long event_mask)
其中参数 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 。
在一个标准的桌面环境下,存在多个不同的应用程序,除了普通的应用程序外,还有构成基本桌面环境的组件,如任务条等。而且,每个应用的窗口布局策略不尽相同,比如普通的 X 应用一般带有窗口装饰,但是我们有看到过构成桌面环境的任务条装饰着标题栏,并且标题栏上有最大化/最小化以及关闭等按钮吗?显然,这类组件不需要窗口装饰。我们还以任务条为例,在某些桌面环境上,任务条可以放置在屏幕的上方、下方、左侧以及右侧。再比如,对话框的窗口装饰中通常是没有最大化按钮的。
显然,窗口管理器需要获得窗口的相关信息,才能根据这些信息决定如何为这些窗口在同一个屏幕上协调的布局以及如何装饰这些窗口。为此,X 提供了多种窗口间通信的机制,属性(Property)是窗口管理器和应用的窗口之间使用的主要通信机制。
X 默认定义了一些属性,这些属性在窗口管理器规范中约定,但是应用也可以自定义属性。在 X 中,每个窗口都附着一个属性表,表中每一行大致就是属性的名字和其对应的值。应用可以设置自己创建的窗口的属性,也可以读取或者改变其他应用的窗口的属性,从而达到不同窗口间通信的目的。
属性保存在 X 服务器端。每个属性都有一个名字,为了便于使用属性,属性的名字是可读性更好的 ASCII 字符串而不是一串数字。然而,如果应用程序使用属性的名字引用属性,势必要通过套接字传递属性的名字给 X 服务器。但是字符串的数据量明显大于一个固定长度的整数,而且,还有一点,字符串的长度是可变的,也给协议的实现增加了复杂度。为此,X 又为每个属性起了个小名,这个小名是一个整型数,与属性的名字间是一一对应的关系,X 将其称为 Atom ,在应用与服务器之间通信时,使用这个小名而不是可变长度的字符串。
属性对应的 Atom 是动态创建的,当 X 服务器启动时,会为一些属性创建 Atom , 其他则是在首次使用时创建。 Xlib 提供了函数 XInternAtoms 和 XInternAtom 用来获取属性名对应的 Atom 。这两个函数基本相同,只不过一个是 “批发” ,一个是 “零售” ,相对于 XInternAtom 而言,XInternAtoms 减少了应用和服务器之间的通信次数。XInternAtoms 函数原型如下:
Status XInternAtoms(Display *display, char **names, int count,
Bool only_if_exists, Atom *atoms_return);
其中,参数 names 包含要转换的属性的名称,count 表示转换的数量,转换后的 Atom 存储在数组 atoms_return 中。如果属性的 Atom 已经存在了,则直接获取其值即可,否则,是否为属性创建 Atom 要根据参数 only_if_exists 的值而定。只有 only_if_exists 为 False 时,才创建 Atom 。
Xlib 提供了函数 XGetWindowProperty 和 XChangeProperty 来读写窗口的属性,我们以 XGetWindowProperty 为例来讨论一下如何读取窗口属性。
int XGetWindowProperty(Display *display, Window w, Atom property,
long long_offset, long long_length, Bool delete,
Atom req_type, Atom actual_type_return,
int *actual_format_return, unsigned long *nitems_return,
unsigned long *bytes_after_return,
unsigned char **prop_return);
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 中记录的是实际读取的属性的数量。
我们设想这样一种场景,如图 7-5 所示,假设 X 服务器上已经在运行两个 X 应用 A 和 B ,A 是当前活动的应用,B 是非活动应用。B 有两个顶层窗口,除了主窗口外,打开文件对话框也是一个顶层窗口,同时这个对话框也是应用 B 的临时(transient)窗口。正如其字面意义所言,所谓的 “transient” 就是临时的、短暂的,是一个相对的概念,是相对于某一窗口而言的。举个例子,如某些应用的 “打开文件” 对话框,是一个典型的临时窗口。但是如果某个应用的主窗口就是一个对话框,那么这个对话框就不是临时窗口了。
当用户想要将应用 B 切换为当前活动的应用时,常用的方法之一是使用鼠标点击 B 应用的窗口。这时窗口管理器拦截鼠标事件,然后请求 X 服务器重新排列窗口栈序,具体细节见 7.1.11 节。总之窗口管理器必须要能接收到鼠标事件,如果接收不到鼠标事件,一切都无从谈起。
Frame 等装饰窗口是窗口管理器创建的,因此窗口管理器可以自如控制,比如我们可以设置 Frame 窗口的事件掩码中包含 ButtonPressMask 。而对于应用的顶层窗口,我们肯定不能过多干涉。但是,我们又不能强制用户一定要点击到 Frame 窗口上未被应用顶层窗口覆盖的地方。而且一般情况下,用户一定是点击到顶层窗口或者其子窗口上,而不是 Frame 窗口上,毕竟 Frame 窗口未被应用顶层窗口遮挡的区域除了标题栏外,只有很小的边框了,也就是说能被点击到的区域很小。
根据 X 的事件传播机制,如果发生在一个窗口上的事件未被处理,在该窗口没有设置禁止事件继续向其父窗口传播的情况下,事件将沿着窗口树一直向着树的根部传播。很少有具有图形界面的程序不处理鼠标事件,否则就没有任何意义了,也就是说,鼠标事件几乎永远传递不到 Frame 窗口,都被应用自身消化了。如果不能接收鼠标事件,更何谈激活窗口了。那么怎么解决这个问题呢?
X 提供了鼠标捕捉机制,其又分为主动捕捉和被动捕捉。以图 7-5 为例,假设另外一个应用以被动机制捕捉应用 B 的顶层窗口时,当用户在应用 B 的顶层窗口范围内按下鼠标时,将激活捕捉机制,X 服务器将鼠标事件不再按照正常的事件传播路径传播了,而是转发给捕捉应用 B 的顶层窗口的 X 应用。窗口管理器恰恰是利用了这个机制,捕捉非活动窗口,从而捕获这些窗口的鼠标事件,实现不同应用间的切换。
Xlib 提供的用于捕捉的函数是 XGrabButton ,其原型如下:
XGrabButton(Display *display, unsigned int button,
unsigned int modifiers, Window grab_window, Bool owner_events,
unsigned int event_mask, int pointer_mode,
int keyboard_mode, Window confine_to, Cursor cursor);
其中各个参数意义如下:
◆ button 表示捕捉鼠标哪个键,比如是捕捉左键还是捕捉右键等。
◆ modifiers 表示是否要求同时按下键盘上某个按键才能捕捉,也就是我们所说的修饰键。
◆ event_mask 表示捕捉事件的掩码,即捕捉什么事件,是捕捉按下鼠标事件还是捕捉释放鼠标事件等。
◆ confine_to 表示是否需捕捉的区域限制在某个范围,也就是说,当事件发生时,只有鼠标在这个区域才可以捕捉。
◆ cursor 表示当捕捉发生时,是否需要使用特定的鼠标指针形状,以给用户一个友好的提示。
◆ owner_events 主要是用于当应用捕捉自身创建的窗口时使用,与窗口管理器无关。
◆ 参数 grab_window 是最核心的一个参数,理解了这个参数就基本理解了整个函数,这个参数就是表明当鼠标按键发生在哪个窗口时进行捕捉。
◆ 最后来解释参数 pointer_mode 。我们举个例子来解释这个参数,假设我们将捕捉比喻为窃,那么捕捉其他窗口的应用就是江洋大盗,被捕捉的窗口所属的应用就是受害人。不知读者是否有这样的疑问:当江洋大盗将事件窃走后,受害人还能否失而复得。X 再次将这个策略性的问题抛给了应用自己来决定。X 提供了两种捕捉模式:异步模式和同步模式。当使用异步模式时,受害人不要心存任何侥幸了。而当使用同步模式时,在取消对一个窗口的捕捉行为后,如果江洋大盗良心发现,X 则会给他一次浪子回头的机会。江洋大盗可以调用 Xlib 的函数 XAllowEvents 放行这个被截获的事件,这样受害者就可以失而复得了,但是可能不是那么新鲜了,要晚一点。
笔者没有找到一个恰当一点的词来表达 save-set 这个术语,所以我们就直接用英文了。根据其名字就可以猜出这是一个集合了。但是这个集合是做什么的呢?
我们设想这样一种情况,当窗口管理器异常终止时,窗口管理器创建的 Frame 等装饰窗口自然也被销毁。销毁这些窗口本身没有问题,但是它们带来了副作用:作为 Frame 窗口子窗口的应用的窗口也被销毁。这显然不是我们希望看到的。
每个 X 应用都有一个 save-set ,其中保存的就是就是窗口的列表。当应用异常断开到 X 服务器的连接时,X 服务器将首先检查应用的 save-set ,并安排根窗口领养 save-set 中的窗口,从而避免了在 save-set 中的这些窗口被销毁。
前面提到的窗口管理器的问题恰恰可以用这个方法解决。每当管理一个窗口时,窗口管理器就可以调用 Xlib 的函数 XAddToSaveSet 将其加入到自己的 save-set 中。一旦当窗口管理器异常终止,根窗口将领养应用的窗口,从而避免了 Frame 窗口被销毁时,应用的窗口也被销毁的命运。
开发者们开发了 GNU 构建系统(GNU Build System),或者叫 GNU 自动构建工具(GNU Autotools),为了行文方便,我们简称其为 Autotools 。Autotools 核心包括 Autoconf 和 Automake 。这里要准确理解 “自动构建工具” 的意义,所谓 Autotools,并不是自动完成整个配置编译过程,而是自动构建配置脚本 configure 和 Makefile 。
Autoconf 的准确含义是自动创建自动配置脚本( automatically create automatic configuration scripts)。怎么理解自动配置脚本呢?简单来讲,就是自动探测各种不同系统的各种特性,如是本地编译还是交叉编译,系统中使用的编译器、链接器等程序是什么,编译以及链接程序时需要的头文件、动态库以及它们所在的路径,等等,达到自动动态适配,而不是硬编码到脚本中。
可以这样概括 Autoconf 的工作过程:将多个 shell 片段最终合并为一个完整的 shell 脚本,即 configure 。Autoconf 使用宏来定义这些 shell 片段,开发者需要根据编译需要,使用这些宏组合 Autoconf 的元文件 configure.ac ,这个元文件曾经命名为 configure.in ,后来更改为 configure.ac ,但是 Autoconf 也向后兼容 configure.in 。然后 Autoconf 将元文件 configure.ac 中的宏展开为具体的配置脚本 configure 。
Autoconf 程序本身使用 shell 脚本编写,但是 Autoconf 并没有使用 shell 完成宏展开功能,而是借助了 GNU 的 M4 来完成宏的展开。简单来讲,M4 就是将输入的宏名转换为宏定义,也就是说,M4 的输入是宏名,而输出是 shell 脚本片段。 Autoconf 使用 M4 定义了一些内置的宏,并且基于 M4 之上又封装了一层宏,目的是为了更符合 Autoconf 的需求,Autoconf 封装的宏一般以 “AC_” 开头。其他程序可以使用 Autoconf 封装的这些宏,或者直接使用 M4 定义自己的宏,但是最终,本质上都是 M4 宏。
因为 M4 宏定义很多是第三方程序提供的,可能安装在系统的多个位置,因此 GNU 自动构建系统编写了程序 aclocal 负责将这些宏定义收集到文件 aclocal.m4,保存在源码的顶层目录下,供自动构建系统使用。
同 Autoconf 类似, Automake 的准确含义是 “automatically generate makefile.in”,开发人员只需编写一个简单的元文件,在其中描述必要的诉求:比如构建一个二进制程序,使用的源代码文件是什么,链接某某库等即可。其他的都交由 Automake 全权处理吧。 Automake 将创建一个标准的 Makefile 文件,包括补全开发者不愿意编写的那些琐碎的规则,如 install、clean、distclean、dist 等。
Automake 的输出事实上是一个 Makefile 模板,命名为 Makefile.in 。然后,configure 脚本使用探测到的值替换模板 Makefile.in 中的变量,创建最终的 Makefile 。显然,这种方式要比我们将所有的变量定义全部硬编码到 Makefile 中的做法可移植性更好。
综上,使用 GNU Autotools 创建 Makefile 的过程可以分为如下几个步骤:
1)编写元文件 configure.ac 。
2)执行 aclocal 。aclocal 将扫描 configure.ac 中使用的 M4 宏,并到系统中收集这些宏的定义,然后将这些宏定义复制到源码顶层目录下的 aclocal.m4 中。
3)调用 autoconf,将 configure.ac 中的宏展开为 shell 脚本形式的 configure 。
4)编写元文件 Makefile.am 。
5)调用 automake 。 automake 根据 Makefile.am 创建 Makefile 的模板文件 Makefile.in 。
6)执行脚本 configure。configure 探测系统环境,并使用探测到的值替换模板 Makefile.in 中的变量,生成具体的 Makefile 。
从上面的讨论可以看出,对于开发者来说,主要的工作就是创建元文件 configure.ac 和 Makefile.am ,其他的全部交给 Autotools 。Autotools 极大地减轻了程序开发人员的负担,将烦琐的编写的 Makefile 任务转嫁给了 Autotools 的开发和维护者。
既然 Autotools 有如此多的优点,所以即使我们的迷你窗口管理器很小,我们还是可以借助它感同身受一下 Autotools 带来的好处。我们这里绝非 “杀鸡用牛刀”,而是希望读者借助这个例子,可以切身体会一下 Autotools ,这样无论是在大型项目中使用 Autotools ,或者为 GNU 软件贡献源码,亦或基于使用 Autotools 的项目进行二次开发,都会大有益处。
我们将这个迷你窗口管理器命名为 winman,使用 winman 作为顶层目录的名字,在顶层目录下创建一个子目录 src 用来存放源代码。我们基于 Xlib ,使用 C 语言编写 winman 。因此,configure.ac 中除了 Autoconf 要求的必选的宏外,最重要的就是检查 C 编译器和 X 的库了,其内容如下:
// winman/configure.ac:
AC_INIT(winman, 0.1, baisheng_wang@163.com)
AM_INIT_AUTOMAKE(foreign)
AC_PROG_CC
PKG_CHECK_MODULES(X, x11)
AC_CONFIG_FILES(Makefile src/Makefile)
AC_OUTPUT
Autoconf 要求 configure.ac 以宏 AC_INIT 作为开头,该宏由 Autoconf 定义,接收一些基本信息,如软件包的名称,版本号,开发或者维护人员的 Email 等。制作发布的软件包时,将用到这些信息。
宏 AM_INIT_AUTOMAKE 由 Automake 定义,用来进行与 Automake 相关的初始化工作,只要使用 Automake,这个宏也是必选的。在默认情况下,Automake 会检查项目目录中是否包含 NEWS、README、ChangeLog 等文件,为简单起见,我们给 Automake 传递了 “foreign” 参数,明确告诉 Automake 我们的项目不需要包含这些文件。
宏 AC_PROG_CC 用来检测 C 编译器,根据该宏名的前缀 “AC_” 就可判断出其由 Autoconf 定义。其将在系统内搜索 C 编译器,并定义变量 CC 指向搜索到的 C 编译器。
接下来,我们使用软件包 pkg-config 提供的宏 PKG_CHECK_MODULES 检测 X 的库。该宏将定义两个变量,分别是宏的第一个参数加上后缀 “_CFLAGS” 和 “_LIBS” ,这里就是 X_CFLAGS 和 X_LIBS 。如果查看 congfigure 脚本就可以发现,这个宏定义的核心其实就是执行命令 “pkg-config --cflags x11” 和 “pkg-config --libs x11” 。
宏 AC_CONFIG_FILES 告诉 Automake 生成哪些 Makefile 模板文件 Makefile.in 。这里,需要在顶层目录和 src 目录下分别创建 Makefile.in 文件。
在 configure.ac 的最后, Autoconf 要求必须以宏 AC_OUTPUT 结束 configure.ac 。
准备好 configure.ac 后,我们使用如下命令生成脚本 configure:
窗口管理器的源码保存在顶层目录下的子目录 src 中,我们在顶层目录和子目录 src 下面分别需要编写 Automake 元文件 Makefile.am 。
顶层目录 winman 下的 Makefile.am 如下:
// winman/Makefile.am :
SUBDIRS = src
因为顶层目录下基本没有任何操作,所以该 Makefile.am 非常简单,只是通过变量 SUBDIRS 告诉 Automake ,需要递归编译子目录 src 。
子目录 src 下的 Makefile.am 如下:
// winman/src/Makefile.am:
bin_PROGRAMS = winman
winman_SOURCES = wm.h main.c ...
winman_CFLAGS = $(X_CFLAGS)
winman_LDADD = $(X_LIBS)
变量 bin_PROGRAMS 指定了编译最后创建的二进制可执行文件的名称,该变量由两部分构成,其中 “bin” 表示安装在目录 $prefix/bin 下,“PROGRAMS” 指明最后创建的文件是一个可执行文件。
winman_SOURCES 表示创建 winman 需要的源文件;winman_CFLAGS 和 winman_LDADD 分别表示编译链接时需要传递给编译器和链接器的参数。X_CFLAGS 和 X_LIBS 已经在前面的 configure.ac 中由宏 PKG_CHECK_MODULES 定义了。
Automake 的元文件已经准备就绪,我们使用下面的命令创建 Makefile 的模板 Makefile.in :
automake --add-missing -copy
其中选项 “–add-missing” 和 “–copy” 是告诉 automake 将其需要的一些脚本文件,比如 install-sh 等,直接复制到项目目录中,而不是建立这些脚本文件的链接。这么做是为了分发到其他系统时,避免因为脚本位置不同或者系统中没有安装相应脚本而导致编译链接失败。
上述命令执行后,将分别在顶层目录和子目录 src 下创建 Makefile 的模板 Makefile.in 。
最后,执行 confugure 脚本探测编译过程所需的各个变量,然后用探测到的具体的值替换 Makefile.in 中的变量,比如 X_CFLAGS、X_LIBS,生成 Makefile 文件:
./configure --prefix=/usr
在 winman 中,将为每个被管理的窗口创建一个对象,记录其相关信息,为此我们抽象了结构体 Client 。另外,我们抽象了结构体 WinMan ,其中记录了一些全局信息。在讨论具体的实现前,我们先来了解一下这两个数据结构。读者不必全部理解每一项的含义,后面具体遇到时,读者可以再回到这里,前后结合进行理解。
结构体 Client 中主要包含窗口属性信息以及与窗口操作相关的函数,定义如下:
我们结合图 7-6 来解释其中相关数据项。
◆ 每个窗口作为 X 服务器的一个资源,都有一个 ID 来唯一标识。这里的数据项 window 就是被管理的窗口的 ID 。
◆ wm 指向全局的结构体 WinMan 的对象。
◆ frame 是 Frame 窗口的 ID ;titlebar 是标题栏窗口的 ID ;minimize_btn 是最小化按钮对应的窗口的 ID ;maximize_restore_btn 是最大化/恢复按钮对应的窗口的 ID;close_btn 是关闭按钮对应的窗口的 ID 。acting_btn 是一个为了编程方便定义的辅助变量,用来记录用户点击了最大化、最小化以及关闭按钮中的哪一个。
◆ 以 “rsz_” ( resize 的简写)开头的 8 个窗口,是在 Frame 窗口上创建的 8 个不可见窗口,它们分别位于 Frame 窗口的 4 个边和 4 个角,目的是为了便于判断鼠标是否落在调整窗口尺寸的区域。也就是说,一旦用户的鼠标落入这个窗口区域,程序的鼠标将使用特定的指针,提示用户可以进行改变窗口的尺寸了。resizing_area 也是一个辅助变量,用来记录鼠标落在了调整窗口尺寸的 8 个区域的哪个区域,也就是哪个窗口上。
◆ 变量 moving 用来标识用户是否正在移动窗口,anchor_x、anchor_y 记录用户在标题栏上按下鼠标左键、准备开始移动时的位置,目的是为了计算鼠标按下位置与当前位置的距离。
◆ x、y、width、height 记录窗口的位置及大小。min_width、min_height 表示允许用户改变窗口尺寸时允许的最小值,主要目的是避免用户不小心将窗口缩小的太小,导致窗口 “丢失” 了。
◆ state 记录窗口的状态,winman 只处理最大化及其标准状态。restore_x、restore_y、restore_w、restore_h 分别记录窗口在最大化之前的位置和尺寸,以便从最大化恢复为标准尺寸时使用。
◆ trans_for 的目的是为了记录某个窗口是否是临时窗口,如果是临时窗口,那么它是谁的临时窗口。在讨论窗口切换时,我们将看到这个数据项的意义。
◆ 变量 ignore_unmap 涉及的内容有点复杂,将在 7.1.12 节详细讨论。
◆ 每个窗口通过指针 above 和 below 链接到窗口栈中。
与窗口操作相关的一些函数的实现,后面章节中我们会讨论。
结构体 WinMan 中包含了全局需要使用的变量,定义如下:
上述代码中各个参数含义如下:
◆ 第一个数据项 dpy 无需多说了,它是代表应用到 X 服务器的连接。一个 X 服务器可以支持多屏,每个屏上都会有一个根窗口。虽然我们不考虑多屏的情况,但是某些函数使用屏幕号和根窗口作为参数,为了避免每次使用时都要从 X 服务器获取,winman 在结构体中保留了一份副本,即数据项 screen 和 root ,分别代表屏幕号和根窗口 ID 。
◆ 数组 atoms 中记录的是用于窗口间通信的 Atom ,winman 也不希望应用反复的从 X 服务器获取这些 Atom ,所以将它们保存在 winman 的本地。
◆ winman 使用栈的方式记录窗口对象(即为每个窗口创建的结构体 Client 的实例),stack_top 和 stack_bottom 分别指向这个窗口栈的栈顶和栈底。距离用户最远的窗口记录在栈底,距离用户最近的窗口记录在栈顶。stack_items 记录栈中窗口对象的数量。
◆ active、desktop 以及 taskbar 分别指向当前活动的窗口、构成桌面环境的桌面组件以及任务条组件。
谈及初始化,大多给人的印象是进行一些琐碎的准备工作,但是 winman 中有几处却非常关键,相关代码如下:
// winman/src/main.c:
int main(int argc, char *argv[]) {
WinMan *wm;
wm = malloc(sizeof(WinMan));
memset(wm, 0, sizeof(WinMan));
if(!(wm->dpy = XOpenDisplay(NULL))) {
...
}
wm->screen = DefaultScreen(wm->dpy);
wm->root = RootWindow(wm->dpy, wm->screen);
XSetErrorHandler(error_handler);
atom_init(wm);
XSelectInput(wm->dpy, wm->root, SubstructureRedirectMask
| SubstructureNotifyMask);
init_clients(wm);
wm_event_loop(wm);
return 0;
}
下面介绍该函数主要执行的操作。
窗口管理器与普通的 X 应用并无本质区别,只是具有一点点特权,它也是 X 服务器的一个客户端。从服务器和客户端的体系架构角度而言,应用程序当然需要和服务器建立连接后才能通信,为此,Xlib 提供了函数 XOpenDisplay 用于建立它们之间的连接。
Xlib 将错误分为两类:一类错误是不可恢复的,这类错误是致命的,一旦发生后,应用基本不可能执行下去了,如应用程序和服务器的连接断开了,除了终止应用程序外别无选择;另外一类是协议错误,比如当应用读取某个窗口的属性时,这个窗口可能在服务器中已经不存在了。显然,这类错误不是致命的,应用完全可以自己决定是忽略错误还是终止执行。
Xlib 为这两类错误都设置了默认的错误处理函数,它们的行为均是打印错误提示并终止应用。但是 Xlib 也分别提供了接口 XSetIOErrorHandler 和 XSetErrorHandler 允许应用设置自己的致命错误和协议错误处理函数。winman 设置了协议错误处理函数为 error_handler ,当发生协议错误时,error_handler 只打印错误信息,并不终止执行。
函数 atom_init 使用 Xlib 的函数 XInternAtoms ,一次性创建后面用到的属性的 Atom,并将 Atom 保存在结构体 WinMan 的 atoms 数组中。相关代码如下:
// winman/src/main.c:
static void atom_init(WinMan *wm) {
char *atom_names[] = {
"_NET_WM_WINDOW_TYPE",
...
};
XInternAtoms(wm->dpy, atom_names, ATOM_COUNT, False, wm->atoms);
}
最初,X 标准协会制定了 ICCC 通信协议。后来,随着现代桌面的发展,又制定了 EWMH ,即对 ICCCM 进行的补充扩展。这两个协议中定义了大量的属性,在函数 atom_init 中,以 WM_ 开头的基本是 ICCCM 标准中定义的,以 _NET_ 开头的是 EWMH 中定义的。除了标准定义的属性外,应用也可以自定义属性,其中以 _CUSTOM_ 开头的就是 winman 自定义的属性。
函数 XSelectInput 可以说是窗口管理器的画龙点睛之笔了。winman 按照前面的讨论,选择了根窗口的 “SubstructureRedirectMask” 和 “SubstructureNotifyMask” 。
在窗口管理器启动之前,系统上可能已经有 X 应用在运行。因此,winman 启动时需要管理这些已存在的窗口,函数 init_clients 就是做这件事的,其具体细节请参见 7.1.13 节。
初始化完成后,窗口管理器进入事件循环,函数 wm_event_loop 调用 Xlib 的函数 XNextEvent 将窗口管理器对 X 的请求发送给 X 服务器,然后检查事件队列,调用相应的事件处理函数。接下来的的章节中,我们会陆续讨论这些事件处理函数。
需要特殊指出的是 Expose 事件,以图 7-7 所示窗口布局为例。
Window E 有四个区域分别被 Window A~D 遮挡,当 Window E 成为当前活动窗口时,X 服务器将为每一个被遮挡的部分都报告一个 Expose 事件,并将同一个动作引发的 Expose 事件连续的放到事件队列中。结构体 XExposeEvent 中的变量 count 就是用来记录一个 Expose 事件后面还有多少个 Expose 事件的。
因此,从效率的角度来讲,对于多个连续的 Expose 事件,应用应该忽略掉最后一个 Expose 事件前面所有的 Expose 事件,而在收到最后一个 Expose 事件时才进行绘制。
一旦收到 X 服务器转发来的 MapRequest ,就说明有应用的顶层窗口请求显示了,显然,这个时机是窗口管理器切入的最佳时机。winman 首先遍历窗口栈确认窗口是否已经被管理了,如果请求映射的窗口尚未被管理,则调用 wm_new_client 开始管理窗口,函数 wm_new_client 的代码如下:
// winman/src/main.c:
static Client* wm_new_client(WinMan *wm, Window win) {
Atom type;
int format, status;
unsigned long n, extra;
Atom *value = NULL;
Client *c = NULL;
status = XGetWindowProperty(wm->dpy, win,
wm->atoms[_NET_WM_WINDOW_TYPE],
0, 1, False, XA_ATOM, &type, &format, &n,
&extra, (unsigned char **) &value);
if (status == Success && type == XA_ATOM
&& format == 32 && value) {
if (value[0] == wm->atoms[_NET_WM_WINDOW_TYPE_NORMAL])
c = normal_client_new(wm, win);
else if (value[0] == wm->atoms[_NET_WM_WINDOW_TYPE_DIALOG])
...
}
...
c->reparent(c);
c->show(c);
return c;
}
该函数执行的主要操作如下:
1)如同我们每个人要有一个户口,在落户时需要提供各种自然人信息一样,窗口管理器也要收集窗口的各种 “自然人” 信息,为窗口在窗口管理器中 “落户” 。
2)绘制窗口装饰。
3)一切准备妥当后,申请 X 服务器显示应用的窗口,当然也包括窗口管理器附加的装饰。
这一节,我们先来讨论为窗口 “落户” 这一过程。
如前所述,在一个典型的 X 环境中,可能有多种类型的 X 应用程序,比如构成桌面环境的任务条等组件,以及普通的应用程序。即使是普通的应用的窗口,也可分为标准的窗口以及对话框等。显然,不同的类型的窗口需要区别对待,我们不能给任务条也加个标题栏,那样就会闹出笑话。
EWMH 规定窗口需要设置属性 _NET_WM_WINDOW_TYPE 来表明自己的类型,函数 wm_new_client 依据的就是 EWMH 这个规定来判别窗口的类型。因此,函数 wm_new_client 调用 Xlib 的函数 XGetWindowProperty 获取窗口的属性 _NET_WM_WINDOW_TYPE 的值,根据窗口的不同类型,创建不同类型的窗口对象。
下面,我们以标准窗口为例,讨论其 “落户” 过程。
// winman/src/normal_client.c:
Client* normal_client_new(Wingman *wm, Window win) {
Client *c;
XWindowAttributes attr;
XSizeHints *hints = NULL;
long dummy;
Window trans_for = None;
c = malloc(sizeof(Client));
memset(c, 0, sizeof(Client));
c->window = win;
c->wm = wm;
XSetWindowBorderWidth(wm->dpy, c->window, 0);
XGetWindowAttributes(wm->dpy, win, &attr);
c->x = attr.x;
c->y = attr.y;
c->width = attr.width;
c->height = attr.height;
if (!(hints = XAllocSizeHints()))
return;
if (XGetWMNormalHints(wm->dpy, c->window, hints, &dummy)) {
if (hints->flags & PMinSize) {
c->min_width = hints->min_width;
c->min_height = hints->min_height;
}
}
XFree(hints);
c->min_width = c->min_width > MIN_WIDTH ?
c->min_width : MIN_WIDTH ;
c->min_height = c->min_height > MIN_HEIGHT ?
c->min_height : MIN_HEIGHT;
ewmh_get_net_wm_state(c);
if (c->state & (NET_WM_STATE_MAXIMIZED_V
| NET_WM_STATE_MAXIMIZED_H))
custom_get_restore_size(c);
XGetTransientForHint(wm->dpy, c->window, &trans_for);
if (trans_for)
c->transfor = wm_find_client_by_window(wm, trans_for);
if (!c->trans_for) {
if (wm->active) {
XGrabButton(wm->dpy, Button1, 0, wm->active->window,
True, ButtonPressMask, GrabModeaSync,
GrabModeaSync, None, None);
Item *trans = normal_client_get_transients(wm->active);
Item *i;
for (i = trans; i; i = i->next) {
XGrabButton(c->wm->dpy, Button1, 0,
i->client->window, True, ButtonPressMask,
GrabModeaSync, GrabModeaSync, None, None);
}
list_free(&trans);
}
wm->active = c;
ewmh_set_net_active_window(wm->active);
}
c->configure = &normal_client_configure;
c->reparent = &normal_client_reparent;
...
stack_append_top(c);
ewmh_update_net_client_list_stacking(wm);
return c;
}
下面介绍函数 normal_client_new 执行的主要操作。
(1)设置窗口边框
通常,窗口可以请求 X 服务器绘制边框。但是为了统一,我们调用 XSetWindowBorderWidth 人为地将窗口的自身的边框设置为 0,而是在 Frame 窗口上为被管理的窗口绘制统一的边框。在 winman 中,为简单起见,边框的宽度采用了一个固定的值。但是窗口管理器可以尊重窗口的诉求,在绘制窗口边框前,读取窗口属性中设定的边框宽度。
(2)获取窗口几何尺寸
接下来,我们读取窗口的几何尺寸,包括位置、宽度和高度,以及窗口所允许的最小的尺寸。这里我们分别使用了 Xlib 的函数 XGetWindowAttributes 及 XGetWMNormalHints ,主要是因为 Xlib 不推荐通过 XGetWMNormalHints 获取的窗口的位置和大小,但是通过 XGetWindowAttributes 又不能获取窗口的最小宽度和最小高度。
(3)读取窗口状态
在 EWMH 中,规定了窗口的状态属性 _NET_WM_STATE 包括 _NET_WM_STATE_MODAL 、 _NET_WM_STATE_MAXIMIZED_VERT 、_NET_WM_STATE_MAXIMIZED_HORZ 、 _NET_WM_STATE_FULLSCREEN 等。
为简单起见, winman 中只示例处理了窗口最大化的状态。函数 ewmh_get_net_wm_state 调用 Xlib 的接口 XGetWindowProperty 读取窗口的属性 _NET_WM_STATE 。如果属性中包含 _NET_WM_STATE_MAXIMIZED_VERT 和 _NET_WM_STATE_MAXIMIZED_HORZ ,那么就说明窗口是处于最大化状态,则 winman 尝试读取窗口中的标准状态( Restore 状态)下窗口的位置和尺寸信息,以便窗口从最大化切换到标准状态时使用。
读者可能会问,结构体 Client 中数据项 restore_x、restore_y、restore_w、restore_h 不是记录了窗口在最大化之前的位置和尺寸吗?但是设想这样一个场景:当窗口处于最大化时,窗口管理器异常退出了,那么当窗口管理器再次启动时,这个数据如何初始化?这就是 winman 为窗口自定义属性 _CUSTOM_WM_RESTORE_GEOMETRY 的目的,winman 将这些信息保存在窗口中,只要窗口在,这些信息就可以从窗口中读出来,可谓是 “人在阵地就在” 。
(4)捕捉 “旧” 窗口
当新的窗口出现后,如果这个新窗口不是一个临时窗口,其将成为当前活动的窗口,而上一个活动窗口将退居二线。因此,winman 需要捕捉这个退居二线的窗口,以便可以将其顺利切换回来。而且,这个退居二线的窗口可能还有临时窗口,而且临时窗口可能还有临时窗口,因此,函数 normal_client_get_transients 遍历窗口栈,返回这个退居二线窗口的临时窗口组成的链,捕捉这个链上的所有窗口。
(5)设置窗口对象的函数指针
创建了窗口对象后,显然需要设置操作窗口的函数指针。根据不同的窗口类型,设置这些指针指向不同的函数实现。对于标准窗口,设置这些指针指向标准窗口的实现。
(6)更新根窗口的属性
新窗口的 “自然人” 信息收集完毕后,就可以给其 “落户” 了。函数 normal_client_new 调用 stack_append_top 将新创建的窗口对象压入 winman 自己维护的窗口栈。
为了让其他应用知晓又有新成员加入了,当然需要更新一些状态信息,比如任务条就时刻关注着系统中应用的变化情况。一个是记录当前活动窗口的属性 _NET_ACTIVE_WINDOW ;另外一个是记录 X 服务器中所有窗口的列表的属性 _NET_CLIENT_LIST_STACKING 。这两个属性都是 EWMH 标准规定的,它们都是根窗口的属性。函数 normal_client_new 中调用的两个子函数 ewmh_set_net_active_window 和 ewmh_update_net_client_list_stacking 目的就是分别更新这两个属性。
仅给窗口 “落户” 还是不够的,接下来我们还需要为窗口构建装饰。除了起到美化作用外,这些装饰还是用户和应用的窗口之间的桥梁。用户可以通过标题栏移动窗口位置,可以通过边框改变窗口尺寸,可以点击最大化按钮将窗口最大化,可点击最小化按钮将窗口最小化,可以点击关闭按钮关闭窗口。
在创建了窗口对象后,函数 wm_new_client 中调用窗口对象中函数指针 reparent 指向的函数来构建窗口装饰。对于标准窗口来说,构建窗口装饰的函数是 normal_client_reparent 。
// winman/src/normal_client.c:
static void normal_client_reparent(Client *c) {
XSetWindowAttributes attr;
WinMan *wm = c->wm;
int frame_x, frame_y;
XColor tc, sc;
if (normal_client_calc_geometry(c))
XMoveResizeWindow(wm->dpy, c->window, c->x, c->y,
c->width, c->height);
XAllocNamedColor(wm->dpy, DefaultColormap(wm->dpy,
wm->screen), LIGHTGRAY, &sc, &tc);
attr.background_pixel = sc.pixel;
attr.override_redirect = True;
attr.event_mask = SubstructureRedirectMask
| SubstructureNotifyMask
| ExposureMask
| ButtonPressMask
| ButtonReleaseMask
| Button1MotionMask;
c->frame = XCreateWindow(wm->dpy, wm->root,
c->x - BORDER_WIDTH,
c->y - BORDER_WIDTH - TITLEBAR_HEIGHT,
c->width + BORDER_WIDTH * 2,
c->height + TITLEBAR_HEIGHT + BORDER_WIDTH * 2, 0,
CopyFromParent, CopyFromParent, CopyFromParent,
CWOverrideRedirect | CWBackPixel | CWEventMask,
&attr);
XDefineCursor(wm->dpy, c->frame,
XCreateFontCursor(wm->dpy, XC_arrow));
attr.event_mask = ExposureMask;
c->titlebar = XCreateWindow(…);
...
c->rsz_ul_angle = XCreateWindow(wm->dpy, c->frame,
0, 0, RSZ_ANGLE_SIZE, RSZ_ANGLE_SIZE, 0,
CopyFromParent, InputOnly, CopyFromParent,
CWOverrideRedirect, &attr);
XDefineCursor(wm->dpy, c->rsz_ul_angle,
XCreateFontCursor(wm->dpy, XC_ul_angle));
XLowerWindow(wm->dpy, c->rsz_ul_angle);
...
XAddToSaveSet(wm->dpy, c->window);
XReparentWindow(wm->dpy, c->window, c->frame, BORDER_WIDTH,
TITLEBAR_HEIGHT + BORDER_WIDTH);
}
在 7.1.6 节中,winman 创建了各个装饰窗口,但是并没有为各装饰窗口绘制内容。事实上,即使 winman 想去绘制,也是有心无力。基于 X 的原理,X 服务器并不保存窗口的内容,在窗口可见时,X 服务器会向应用报告 Expose 事件,应用收到这个事件后,开始绘制。否则即使应用在创建窗口时自说自话地进行了绘制,也会被丢掉。
因此,在函数 wm_new_client 中,在构建了窗口装饰后,调用了窗口对象中函数指针 show 指向的函数,请求 X 服务器进行显示。对于标准窗口来说,请求 X 服务器显示窗口是 normal_client_show 。
通常,我们在编写具有图形界面的应用程序时,在显示图形界面之前,一定会设置窗口的位置、尺寸或者边框宽度等。虽然读者可能反驳说,我们有时并没有设置这些啊?实际上,那是因为如 GTK、QT 等图形库已经帮我们做了。另外一种情况是在应用运行的某个中间时刻,应用也可能会改变窗口的这些信息。
X 将这些信息统称为窗口配置,包括窗口的位置、宽度和高度,边框的宽度以及在栈中的位置。
在上述两种情况下,应用都将产生配置请求,X 服务器也都会将它们重定向给窗口管理器。那么窗口管理器如何区分这两种情况呢?winman 是这样处理的,当收到 X 服务器重定向来的配置请求时, winman 调用函数 wm_find_client_by_window 遍历窗口栈,如果窗口栈中没有一个窗口对象与发送请求的窗口匹配,就说明这个窗口尚未被管理,否则说明这个窗口已经被管理了。
对于尚未纳入管理的应用的窗口,winman 当然不能贸然管理,谁知道未来它是否需要管理呢。所以直接请求 X 服务器满足其需要。winman 从事件 XConfigureRequestEvent 中提取信息,不加任何修改,完全照搬原来的配置请求,使用 Xlib 的函数 XConfigureWindow 直接代替应用向 X 服务器发出配置请求。
对于已被管理的窗口,winman 调用具体窗口对象中处理配置的函数进行具体的配置。
对于一个典型的桌面应用来说,用户可以通过拖动窗口的标题栏来移动窗口。这里所谓的 “拖动” 的具体动作是:在标题栏上按下鼠标左键并保持,然后移动鼠标,直到释放鼠标。
因此,整个移动窗口的过程可以划分为三个阶段:
1)用户在标题栏上按下鼠标左键并保持;
2)用户移动鼠标;
3)用户释放鼠标,移动结束。
为此,结构体 Client 中设计了布尔变量 moving ,当用户在标题栏内按下鼠标左键时,moving 将被置为 True 。当鼠标移动事件发生时,如果变量 moving 为 True,那么我们就可以断定用户是在移动窗口。一旦用户释放了鼠标,winman 将 moving 更改为 False 。
改变窗口大小与移动窗口的操作逻辑上基本相同,这里只简要讨论实现的逻辑,就不再列出具体代码了,请读者自行参考随书光盘中附带的源代码。
在用户按下鼠标事件时,将鼠标指针所在的标识移动区域的窗口,也就是结构体 Client 中以 rsz_ 开头的窗口,记录到窗口对象的成员 resizing_area 中。
然后,当收到鼠标移动事件时,如果窗口对象的成员 resizing_area 非 0,那就说明用户正在试图改变窗口大小。根据 resizing_area 与 8 个标识移动区域的窗口对比,推断出用户正在如何更改窗口的大小,然后计算出窗口改变后的几何信息,请求 X 服务器改变窗口大小。
当鼠标释放时,将 resizing_area 清 0 。
我们以图 7-12 所示的场景为例来讨论窗口之间的切换。应用 A 和应用 B 分别为两个 X 应用,图中使用虚线标识的是应用创建的窗口,实线标出的是窗口管理器创建的窗口。A1 是应用 A 的标准类型的顶层窗口,A2 是对话框类型的顶层窗口,且 A2 是窗口 A1 的临时窗口。B1 是应用 B 的标准类型的顶层窗口,B2 是对话框类型的顶层窗口,且 B2 是窗口 B1 的临时窗口。初始状态 X 时,应用 A 是当前活动的应用;在状态为 Y 时,应用 B 被切换为当前的活动应用。
本节我们讨论最大化、最小化及关闭窗口的相关知识。
最小化窗口本质上就是取消窗口的显示,Xlib 为此提供了相应的函数 XUnmapWindow 。当取消某个窗口的显示时,同时也需要取消其临时窗口的显示。
所谓的最大化/恢复窗口,本质上就是使用 Xlib 的类似如 XMoveResizeWindow 的函数调整窗口位置和大小,winman 中代码中对应的实现是 maximize_window 函数。
唯一需要指出的就是, winman 自定义了属性 _CUSTOM_WM_RESTORE_GEOMETRY,在最大化之前将窗口的几何信息,包括位置、高度和宽度,都记录到窗口的属性中。为什么要在窗口的属性中记录,不是都已经记录到窗口对象中了吗?试想一下,如果不在窗口的属性中记录,而只是记录在窗口管理器中,一旦窗口管理器异常退出,那么一切状态信息将随着窗口管理器灰飞烟灭。为了窗口管理器再次启动时能获得这些信息,将这些信息保存在窗口中是一个合理的办法。
类似地,在最大化/恢复窗口时,winman 也更新了窗口的另外两个属性 _NET_WM_STATE_MAXIMIZED_VERT 和 _NET_WM_STATE_MAXIMIZED_HORZ 。
ICCCM 规范规定,当关闭窗口时,窗口管理器应该发送消息 WM_DELETE_WINDOW 给应用,而不是越俎代庖地请求 X 服务器去销毁应用的窗口。因为应用收到消息 WM_DELETE_WINDOW 后,可以做一些善后处理,然后在请求 X 服务器关闭窗口。
当然,有些应用程序不是很守规矩,尤其是早期使用 Xlib 编写的程序,它们不处理消息 WM_DELETE_WINDOW 。对于这类窗口,也只能采用简单粗暴的方法了,直接使用 Xlib 提供的函数 XKillClient 断开应用程序到X服务器的连接,这也就意味着整个 X 应用彻底退出执行。
那么窗口管理器如何得知应用是否处理了事件 WM_DELETE_WINDOW ? ICCCM 规范规定,如果窗口自己负责销毁,其应该在窗口的属性 WM_PROTOCOLS 中设置属性 WM_DELETE_WINDOW 。属性 WM_PROTOCOLS 的值是个 Atom 数组,其中包括多个属性。
在窗口管理器启动之前,可能有一些应用已经在运行。因此,在窗口管理器启动时,需要管理这些已存在的窗口。这就是 winman 的初始化时调用函数 init_clients 的目的。
从最初出现在桌面环境中发展到现在,任务条的风格也在不断地发生改变,但依然是桌面环境的重要组件之一,只不过表现形式并不一定是千篇一律。
典型的任务条从左至右包括 “开始按钮” 、“快速启动栏” 、“任务项” 以及 “通知区域”。 用户通过 “开始按钮” 可以启动应用程序;“快速启动栏” 中放置用户常用的一些程序;每个启动的任务都有一个 “任务项” ;“通知区域” 主要用来显示一些系统状态,比如显示当前的输入法、网络状态等。
除了任务条外,一般的桌面环境都有一个背景,并且在这个背景上面可以显示一些快捷方式,可以显示一些很有个性的小插件。
本章中,我们实现了一个简单的任务条和一个桌面。不同的桌面环境,实现这些组件的逻辑不尽相同,有的是放在一个完整的程序中,有的是每个组件是一个单独的程序,我们采用后者。我们通过这两个程序向读者展示使用图形库(GTK)编程。相比于 Xlib,GTK 的编程理解起来要容易得多,而且 GTK 的官方文档写得也非常详尽,所以我们就不浪费篇幅讨论有关 GTK 的编程了,这里仅讨论其中与窗口管理器相关的部分。
虽然任务条也是一个普通 X 应用,但是作为桌面环境中重要的一个组件,还是有一些特殊的地方。比如,在我们构建的桌面环境中,窗口管理器将其停靠在屏幕的最下方。但是任务条如何向 winman 亮明自己的任务身份呢?读者一定已经猜到了:属性。任务条自定义了属性 _CUSTOM_WM_WINDOW_TYPE_TASKBAR ,在启动时,其将窗口的属性 _NET_WM_WINDOW_TYPE 设置为属性 _CUSTOM_WM_WINDOW_TYPE_TASKBAR 。
前面我们看到,在 winman 中,每当为一个窗口 “落户” 时,winman 都将更新根窗口的属性 _NET_CLIENT_LIST_STACKING 。因此,任务条利用的就是这个机制,监测根窗口属性的变化,从而跟踪系统中任务的变化,相关代码如下:
在任务条初始化时,其将选择根窗口事件掩码 PropertyChangeMask ,并设置根窗口的属性变化事件的回调函数为 root_window_event_filter 。如此,一旦根窗口的属性发生变化时,任务条都将洞悉。
每当根窗口的属性 _NET_CLIENT_LIST_STACKING 发生变化时,函数 taskbar_setup_items 就读取根窗口的该属性的值,获取目前系统中全部的窗口列表。然后遍历这个列表,更新任务栏。为了简单,该函数做了很多简化,比如
只要窗口类型是 _NET_WM_WINDOW_TYPE_NORMAL ,并且也没有判断窗口是否是其他窗口的临时窗口,任务条就为其在任务条上创建一个任务项。
作为桌面环境的核心组件之一,在桌面环境启动时,任务条是首先启动的核心组件之一。理论上,这个时候还没有应用启动,但是不排除系统运行过程中,任务条重新启动,谁也不能保证程序完全没有 bug 。因此,无论如何,任务条还是有必要在启动时获取系统中正在运行的任务,并为它们在任务条上建立相应的任务项。
任务条的另外一个主要任务就是将最小化的,或者将非活动的窗口激活为当前活动窗口。
EWMH 规范规定,如果一个 X 应用希望激活另外一个窗口,可以通过向根窗口发送消息 _NET_ACTIVE_WINDOW 来实现。因此,在我们的任务条中,当用户点击任务按钮时,在回调函数中将向根窗口发送 ClientMessage 事件,其中的消息类型为 _NET_ACTIVE_WINDOW 。
当某个任务成为当前活动任务时,任务条需要将对应的任务项特殊标识一下。那么任务条如何知道当前任务已经发生变化了呢?前面我们看到,在 winman 中,每当为将一个窗口设置为当前活动窗口时,winman 都将更新根窗口的属性 _NET_ACTIVE_WINDOW 。看到这里,读者一定明白了,任务条的处理过程与 7.2.2 节基本完全相同。
当用户按下快速启动栏上的显示桌面按钮时,将把桌面显示到所有窗口的最前面。本章讨论到这里,我想读者应该已经大致可以猜出这个故事的脚本了:
1)任务条向根窗口发送类型为 ClientMessage 的事件,EWMH 规范规定这个事件中的消息类型为 _NET_SHOWING_DESKTOP 。
2)winman 请求 X 服务器将将桌面这个组件显示到窗口栈的最上面。winman 中的实现与切换窗口基本完全相同。
相比于任务条,这个示例的桌面程序要简单很多。而且,经过了前面任务条的讨论,我想读者应该不需要笔者再过多的啰唆了。同普通应用对比,其比较特殊的地方之一就是,要向窗口管理器亮明自己的身份,代码如下所示:
// desktop/src/main.c:
int main(int argc, char *argv[]) {
...
gtk_window_set_type_hint(GTK_WINDOW(win), GDK_WINDOW_TYPE_HINT_DESKTOP);
...
}
桌面程序使用标准的 EWMH 规范规定的属性 _NET_WM_WINDOW_TYPE_DESKTOP 标识该程序是一个桌面程序。GTK 中的函数 gtk_window_set_type_hint 就是对 Xlib 的函数 XChangeProperty 的更高层的封装,我们直接使用即可。
winman 将为桌面程序创建桌面窗口对象,并将其整个铺满在桌面背景上。同样,桌面窗口对象也不需要装饰,因此其函数 desktop_client_reparent 也是个空函数。其他细节,请读者参考随书光盘中附带的源代码。
至此,一个基本的桌面环境就已经搭建完毕了,读者将它们安装到 vita 系统,然后使用如下命令即可启动完整的桌面环境:
注意,桌面程序上的快捷方式 “Hello World” 的回调函数将到目录 /usr/bin 下寻找程序 hello_gtk ,所以请将这个程序复制到目录 /usr/bin 下。另外,也请确保程序 taskbar、desktop 使用的 css 主题描述安装在正确的目录下。