7.1.6 构建窗口装饰

仅给窗口“落户”还是不够的,接下来我们还需要为窗口构建装饰。除了起到美化作用外,这些装饰还是用户和应用的窗口之间的桥梁。用户可以通过标题栏移动窗口位置,可以通过边框改变窗口尺寸,可以点击最大化按钮将窗口最大化,可点击最小化按钮将窗口最小化,可以点击关闭按钮关闭窗口。

在创建了窗口对象后,函数wm_new_client中调用窗口对象中函数指针reparent指向的函数来构建窗口装饰。对于标准窗口来说,构建窗口装饰的函数是normal_client_reparent,代码如下:

7.1.6 构建窗口装饰 - 图1

7.1.6 构建窗口装饰 - 图2

函数normal_client_reparent执行的主要操作如下:

(1)调整窗口位置和尺寸

在显示窗口前,winman调用函数normal_client_calc_geometry对窗口的位置和尺寸进行了一些合理性检查。对于某些不合理的值,函数normal_client_calc_geometry会进行简单的修正,比如不能允许窗口显示在屏幕的可见范围之外。如果经过函数normal_client_calc_geometry计算发现窗口确实需要修正,normal_client_reparent则调用Xlib的函数XMoveResizeWindow请求X服务器对窗口进行调整。

(2)创建Frame窗口

正所谓“皮之不存,毛将焉附”,Frame作为承载窗口装饰的载体,normal_client_reparent首先调用Xlib的函数XCreateWindow来创建这个窗口。Frame的父窗口是根窗口;几何尺寸基于应用的顶层窗口的尺寸,并为容纳窗口边框以及标题栏等预留出空间;其他如窗口类型等属性均采用与根窗口相同的值即可,也就是代码中的为函数XCreateWindow传递参数CopyFromParent的意图。

winman通过结构体XSetWindowAttributes设置了Frame窗口的另外三个属性:override_redirect、background_pixel和event_mask。

override_redirect表示窗口是否接受窗口管理。Frame窗口,包括后面的标题栏等其他装饰窗口,当然不再需要窗口管理器管理,因此,这个属性设置为True。

X服务器中可以创建一个或者多个颜色映射(Colormap),每个颜色映射就是一个数组,数组中的每个元素代表一个颜色。颜色映射数组的大小,由显示器的色深决定。应用使用颜色时,首先要根据颜色的名字,在颜色映射中找到对应的索引,X将这个过程称为分配颜色如图7-8所示。

7.1.6 构建窗口装饰 - 图3

图 7-8 X颜色映射

函数normal_client_reparent使用Xlib的函数XAllocNamedColor分配了一个颜色,然后将这个颜色通过属性background_pixel指定为Frame窗口的背景色。

winman通过属性event_mask选择接收Frame窗口的事件。winman依然需要拦截应用的顶层窗口的请求、获取它们的某些通知,而应用的顶层窗口已经成为Frame的子窗口了,所以winman需要接收Frame的子窗口的事件,这就是选择Frame窗口的事件掩码SubstructureRedirectMask和SubstructureNotifyMask的目的。Frame窗口也需要绘制,所以选择了ExposureMask。事件掩码中最后选择接收的就是几个鼠标事件,winman按照下面的逻辑处理Frame及作为其子窗口的各个装饰窗口间的鼠标事件。

winman仅接收Frame的鼠标事件,而不接受其他装饰窗口的鼠标事件。所以,当鼠标事件发生在任何装饰窗口上时,按照X的事件传播机制,鼠标事件最终都会在窗口树中向上传播到Frame。也就是说,winman最终接收到的是来自Frame窗口的鼠标事件,鼠标事件中的参数window是Frame,但是subwindow则是鼠标事件发生时,鼠标指针所在的真实的装饰窗口。后面,当处理鼠标事件时,winman就是利用鼠标事件的参数subwindow判断发生在哪个窗口装饰上了,从而判读出用户的意图,比如是关闭窗口,最小化窗口,抑或是移动窗口等。

(3)设置Frame窗口鼠标指针形状

Xlib提供了函数XDefineCursor为窗口设置鼠标指针的形状,winman将Frame窗口的指针设置为XC_arrow,即常用的箭头形状。

(4)创建标题栏、最大化、最小化及关闭按钮

winman为这几个窗口装饰分别创建了窗口,基本与创建Frame窗口相同。它们的父窗口是Frame窗口。winman只接收这几个窗口的Expose事件,毕竟还需要在按钮上绘制图标。winman也无需为这几个窗口定义鼠标指针,它们使用与父窗口Frame相同的鼠标指针。

(5)创建“改变窗口尺寸”指示区域

接下来winman还要创建几个幕后英雄,即前面提到的以rsz_开头的几个窗口。这几个窗口的目的非常单纯,就是为了当鼠标进入这个区域时,显示特殊的鼠标指针形状,提示用户可以在这个区域改变窗口尺寸了。

这几个窗口不需要显示任何内容,因此窗口的类型设置为InputOnly;而且也不需要接收任何事件,所以无须设置任何事件掩码;它们也不需要接受窗口管理器管理,所以窗口属性override_redirect设置为True。

为了给用户一个直观的提示,这几个窗口的鼠标指针分别设置为不同的形状,比如窗口正上方的鼠标指针设置为XC_top_side,左上角设置为XC_ul_angle,等等。

对于位于四个角的窗口,winman还调用了Xlib的函数XLowerWindow,其目的是什么呢?图7-9展示了放大了的窗口右上角的区域,窗口rsz_ur_angle是正方形形状的窗口,但是我们希望鼠标指针只有落在图中使用灰色标出的区域时才允许用户改变窗口尺寸。并且窗口rsz_ur_angle不应该遮挡关闭按钮,导致其失效,因此winman使用XLowerWindow将rsz_ur_angle置于窗口栈的底部,以免遮挡其他兄弟窗口。

7.1.6 构建窗口装饰 - 图4

图 7-9 放大的窗口右上角区域

(6)将应用顶层窗口加入save-set

如前面讨论的,我们不希望winman异常退出时,因为销毁Frame窗口而导致作为Frame子窗口的应用的顶层窗口也受牵连,因此winman调用Xlib的函数XAddToSaveSet将应用的顶层窗口加入到save-set中。

如果读者注释掉函数normal_client_reparent中的XAddToSaveSet,然后尝试终止窗口管理器,就会发现应用的窗口也随之被销毁了。

当save-set中的窗口被销毁时,X服务器负责从save-set中将它们移除。因此,当应用的顶层窗口“扬长而去”时,winman无需调用XRemoveFromSaveSet从save-set中移除它们。

(7)将应用的顶层窗口作为Frame的子窗口

Frame窗口以及作为其子窗口的其他装饰,全部准备完毕。最后,函数normal_client_reparent调用Xlib的XReparentWindow函数将Frame作为应用的顶层窗口的父窗口,完成最后一击。