深度探索Linux操作系统 —— 构建桌面环境1:https://developer.aliyun.com/article/1598092
(3)示例
1. 创建 configure
我们将这个迷你窗口管理器命名为 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:
2. 生成 Makefile
窗口管理器的源码保存
窗口管理器的源码保存在顶层目录下的子目录 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
3、主要数据结构
在 winman 中,将为每个被管理的窗口创建一个对象,记录其相关信息,为此我们抽象了结构体 Client 。另外,我们抽象了结构体 WinMan ,其中记录了一些全局信息。在讨论具体的实现前,我们先来了解一下这两个数据结构。读者不必全部理解每一项的含义,后面具体遇到时,读者可以再回到这里,前后结合进行理解。
1. 结构体 Client
结构体 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 链接到窗口栈中。
与窗口操作相关的一些函数的实现,后面章节中我们会讨论。
2. 结构体 WinMan
结构体 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 分别指向当前活动的窗口、构成桌面环境的桌面组件以及任务条组件。
4、初始化
谈及初始化,大多给人的印象是进行一些琐碎的准备工作,但是 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; }
下面介绍该函数主要执行的操作。
(1)连接 X 服务器
窗口管理器与普通的 X 应用并无本质区别,只是具有一点点特权,它也是 X 服务器的一个客户端。从服务器和客户端的体系架构角度而言,应用程序当然需要和服务器建立连接后才能通信,为此,Xlib 提供了函数 XOpenDisplay 用于建立它们之间的连接。
(2)错误处理
Xlib 将错误分为两类:一类错误是不可恢复的,这类错误是致命的,一旦发生后,应用基本不可能执行下去了,如应用程序和服务器的连接断开了,除了终止应用程序外别无选择;另外一类是协议错误,比如当应用读取某个窗口的属性时,这个窗口可能在服务器中已经不存在了。显然,这类错误不是致命的,应用完全可以自己决定是忽略错误还是终止执行。
Xlib 为这两类错误都设置了默认的错误处理函数,它们的行为均是打印错误提示并终止应用。但是 Xlib 也分别提供了接口 XSetIOErrorHandler 和 XSetErrorHandler 允许应用设置自己的致命错误和协议错误处理函数。winman 设置了协议错误处理函数为 error_handler ,当发生协议错误时,error_handler 只打印错误信息,并不终止执行。
(3)创建 Atoms
函数 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 自定义的属性。
(4)拦截事件
函数 XSelectInput 可以说是窗口管理器的画龙点睛之笔了。winman 按照前面的讨论,选择了根窗口的 “SubstructureRedirectMask” 和 “SubstructureNotifyMask” 。
(5)管理已存在的窗口
在窗口管理器启动之前,系统上可能已经有 X 应用在运行。因此,winman 启动时需要管理这些已存在的窗口,函数 init_clients 就是做这件事的,其具体细节请参见 7.1.13 节。
(6)事件循环
初始化完成后,窗口管理器进入事件循环,函数 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 事件时才进行绘制。
5、为窗口“落户”
一旦收到 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 目的就是分别更新这两个属性。
深度探索Linux操作系统 —— 构建桌面环境3:https://developer.aliyun.com/article/1598094