深度探索Linux操作系统 —— 构建桌面环境2:https://developer.aliyun.com/article/1598093
6、构建窗口装饰
仅给窗口 “落户” 还是不够的,接下来我们还需要为窗口构建装饰。除了起到美化作用外,这些装饰还是用户和应用的窗口之间的桥梁。用户可以通过标题栏移动窗口位置,可以通过边框改变窗口尺寸,可以点击最大化按钮将窗口最大化,可点击最小化按钮将窗口最小化,可以点击关闭按钮关闭窗口。
在创建了窗口对象后,函数 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、绘制装饰窗口
在 7.1.6 节中,winman 创建了各个装饰窗口,但是并没有为各装饰窗口绘制内容。事实上,即使 winman 想去绘制,也是有心无力。基于 X 的原理,X 服务器并不保存窗口的内容,在窗口可见时,X 服务器会向应用报告 Expose 事件,应用收到这个事件后,开始绘制。否则即使应用在创建窗口时自说自话地进行了绘制,也会被丢掉。
因此,在函数 wm_new_client 中,在构建了窗口装饰后,调用了窗口对象中函数指针 show 指向的函数,请求 X 服务器进行显示。对于标准窗口来说,请求 X 服务器显示窗口是 normal_client_show 。
8、配置窗口
通常,我们在编写具有图形界面的应用程序时,在显示图形界面之前,一定会设置窗口的位置、尺寸或者边框宽度等。虽然读者可能反驳说,我们有时并没有设置这些啊?实际上,那是因为如 GTK、QT 等图形库已经帮我们做了。另外一种情况是在应用运行的某个中间时刻,应用也可能会改变窗口的这些信息。
X 将这些信息统称为窗口配置,包括窗口的位置、宽度和高度,边框的宽度以及在栈中的位置。
在上述两种情况下,应用都将产生配置请求,X 服务器也都会将它们重定向给窗口管理器。那么窗口管理器如何区分这两种情况呢?winman 是这样处理的,当收到 X 服务器重定向来的配置请求时, winman 调用函数 wm_find_client_by_window 遍历窗口栈,如果窗口栈中没有一个窗口对象与发送请求的窗口匹配,就说明这个窗口尚未被管理,否则说明这个窗口已经被管理了。
对于尚未纳入管理的应用的窗口,winman 当然不能贸然管理,谁知道未来它是否需要管理呢。所以直接请求 X 服务器满足其需要。winman 从事件 XConfigureRequestEvent 中提取信息,不加任何修改,完全照搬原来的配置请求,使用 Xlib 的函数 XConfigureWindow 直接代替应用向 X 服务器发出配置请求。
对于已被管理的窗口,winman 调用具体窗口对象中处理配置的函数进行具体的配置。
9、移动窗口
对于一个典型的桌面应用来说,用户可以通过拖动窗口的标题栏来移动窗口。这里所谓的 “拖动” 的具体动作是:在标题栏上按下鼠标左键并保持,然后移动鼠标,直到释放鼠标。
因此,整个移动窗口的过程可以划分为三个阶段:
1)用户在标题栏上按下鼠标左键并保持;
2)用户移动鼠标;
3)用户释放鼠标,移动结束。
为此,结构体 Client 中设计了布尔变量 moving ,当用户在标题栏内按下鼠标左键时,moving 将被置为 True 。当鼠标移动事件发生时,如果变量 moving 为 True,那么我们就可以断定用户是在移动窗口。一旦用户释放了鼠标,winman 将 moving 更改为 False 。
10、改变窗口大小
改变窗口大小与移动窗口的操作逻辑上基本相同,这里只简要讨论实现的逻辑,就不再列出具体代码了,请读者自行参考随书光盘中附带的源代码。
在用户按下鼠标事件时,将鼠标指针所在的标识移动区域的窗口,也就是结构体 Client 中以 rsz_ 开头的窗口,记录到窗口对象的成员 resizing_area 中。
然后,当收到鼠标移动事件时,如果窗口对象的成员 resizing_area 非 0,那就说明用户正在试图改变窗口大小。根据 resizing_area 与 8 个标识移动区域的窗口对比,推断出用户正在如何更改窗口的大小,然后计算出窗口改变后的几何信息,请求 X 服务器改变窗口大小。
当鼠标释放时,将 resizing_area 清 0 。
11、切换窗口
我们以图 7-12 所示的场景为例来讨论窗口之间的切换。应用 A 和应用 B 分别为两个 X 应用,图中使用虚线标识的是应用创建的窗口,实线标出的是窗口管理器创建的窗口。A1 是应用 A 的标准类型的顶层窗口,A2 是对话框类型的顶层窗口,且 A2 是窗口 A1 的临时窗口。B1 是应用 B 的标准类型的顶层窗口,B2 是对话框类型的顶层窗口,且 B2 是窗口 B1 的临时窗口。初始状态 X 时,应用 A 是当前活动的应用;在状态为 Y 时,应用 B 被切换为当前的活动应用。
12、最大化/最小化/关闭窗口
本节我们讨论最大化、最小化及关闭窗口的相关知识。
1. 最小化窗口
最小化窗口本质上就是取消窗口的显示,Xlib 为此提供了相应的函数 XUnmapWindow 。当取消某个窗口的显示时,同时也需要取消其临时窗口的显示。
2. 最大化/恢复窗口
所谓的最大化/恢复窗口,本质上就是使用 Xlib 的类似如 XMoveResizeWindow 的函数调整窗口位置和大小,winman 中代码中对应的实现是 maximize_window 函数。
唯一需要指出的就是, winman 自定义了属性 _CUSTOM_WM_RESTORE_GEOMETRY,在最大化之前将窗口的几何信息,包括位置、高度和宽度,都记录到窗口的属性中。为什么要在窗口的属性中记录,不是都已经记录到窗口对象中了吗?试想一下,如果不在窗口的属性中记录,而只是记录在窗口管理器中,一旦窗口管理器异常退出,那么一切状态信息将随着窗口管理器灰飞烟灭。为了窗口管理器再次启动时能获得这些信息,将这些信息保存在窗口中是一个合理的办法。
类似地,在最大化/恢复窗口时,winman 也更新了窗口的另外两个属性 _NET_WM_STATE_MAXIMIZED_VERT 和 _NET_WM_STATE_MAXIMIZED_HORZ 。
3. 关闭窗口
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 数组,其中包括多个属性。
13、管理已存在的窗口
在窗口管理器启动之前,可能有一些应用已经在运行。因此,在窗口管理器启动时,需要管理这些已存在的窗口。这就是 winman 的初始化时调用函数 init_clients 的目的。
二、任务条和桌面
从最初出现在桌面环境中发展到现在,任务条的风格也在不断地发生改变,但依然是桌面环境的重要组件之一,只不过表现形式并不一定是千篇一律。
典型的任务条从左至右包括 “开始按钮” 、“快速启动栏” 、“任务项” 以及 “通知区域”。 用户通过 “开始按钮” 可以启动应用程序;“快速启动栏” 中放置用户常用的一些程序;每个启动的任务都有一个 “任务项” ;“通知区域” 主要用来显示一些系统状态,比如显示当前的输入法、网络状态等。
除了任务条外,一般的桌面环境都有一个背景,并且在这个背景上面可以显示一些快捷方式,可以显示一些很有个性的小插件。
本章中,我们实现了一个简单的任务条和一个桌面。不同的桌面环境,实现这些组件的逻辑不尽相同,有的是放在一个完整的程序中,有的是每个组件是一个单独的程序,我们采用后者。我们通过这两个程序向读者展示使用图形库(GTK)编程。相比于 Xlib,GTK 的编程理解起来要容易得多,而且 GTK 的官方文档写得也非常详尽,所以我们就不浪费篇幅讨论有关 GTK 的编程了,这里仅讨论其中与窗口管理器相关的部分。
1、标识任务条的身份
虽然任务条也是一个普通 X 应用,但是作为桌面环境中重要的一个组件,还是有一些特殊的地方。比如,在我们构建的桌面环境中,窗口管理器将其停靠在屏幕的最下方。但是任务条如何向 winman 亮明自己的任务身份呢?读者一定已经猜到了:属性。任务条自定义了属性 _CUSTOM_WM_WINDOW_TYPE_TASKBAR ,在启动时,其将窗口的属性 _NET_WM_WINDOW_TYPE 设置为属性 _CUSTOM_WM_WINDOW_TYPE_TASKBAR 。
2、更新任务条上的任务项
前面我们看到,在 winman 中,每当为一个窗口 “落户” 时,winman 都将更新根窗口的属性 _NET_CLIENT_LIST_STACKING 。因此,任务条利用的就是这个机制,监测根窗口属性的变化,从而跟踪系统中任务的变化,相关代码如下:
在任务条初始化时,其将选择根窗口事件掩码 PropertyChangeMask ,并设置根窗口的属性变化事件的回调函数为 root_window_event_filter 。如此,一旦根窗口的属性发生变化时,任务条都将洞悉。
每当根窗口的属性 _NET_CLIENT_LIST_STACKING 发生变化时,函数 taskbar_setup_items 就读取根窗口的该属性的值,获取目前系统中全部的窗口列表。然后遍历这个列表,更新任务栏。为了简单,该函数做了很多简化,比如
只要窗口类型是 _NET_WM_WINDOW_TYPE_NORMAL ,并且也没有判断窗口是否是其他窗口的临时窗口,任务条就为其在任务条上创建一个任务项。
作为桌面环境的核心组件之一,在桌面环境启动时,任务条是首先启动的核心组件之一。理论上,这个时候还没有应用启动,但是不排除系统运行过程中,任务条重新启动,谁也不能保证程序完全没有 bug 。因此,无论如何,任务条还是有必要在启动时获取系统中正在运行的任务,并为它们在任务条上建立相应的任务项。
3、激活任务
任务条的另外一个主要任务就是将最小化的,或者将非活动的窗口激活为当前活动窗口。
EWMH 规范规定,如果一个 X 应用希望激活另外一个窗口,可以通过向根窗口发送消息 _NET_ACTIVE_WINDOW 来实现。因此,在我们的任务条中,当用户点击任务按钮时,在回调函数中将向根窗口发送 ClientMessage 事件,其中的消息类型为 _NET_ACTIVE_WINDOW 。
4、高亮显示当前活动任务
当某个任务成为当前活动任务时,任务条需要将对应的任务项特殊标识一下。那么任务条如何知道当前任务已经发生变化了呢?前面我们看到,在 winman 中,每当为将一个窗口设置为当前活动窗口时,winman 都将更新根窗口的属性 _NET_ACTIVE_WINDOW 。看到这里,读者一定明白了,任务条的处理过程与 7.2.2 节基本完全相同。
5、显示桌面
当用户按下快速启动栏上的显示桌面按钮时,将把桌面显示到所有窗口的最前面。本章讨论到这里,我想读者应该已经大致可以猜出这个故事的脚本了:
1)任务条向根窗口发送类型为 ClientMessage 的事件,EWMH 规范规定这个事件中的消息类型为 _NET_SHOWING_DESKTOP 。
2)winman 请求 X 服务器将将桌面这个组件显示到窗口栈的最上面。winman 中的实现与切换窗口基本完全相同。
6、桌面
相比于任务条,这个示例的桌面程序要简单很多。而且,经过了前面任务条的讨论,我想读者应该不需要笔者再过多的啰唆了。同普通应用对比,其比较特殊的地方之一就是,要向窗口管理器亮明自己的身份,代码如下所示:
// 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 主题描述安装在正确的目录下。