V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
wzzzx
V2EX  ›  程序员

[翻译] X 窗口管理器的原理剖析(一)

  •  
  •   wzzzx · 2021-02-04 13:56:34 +08:00 · 1388 次点击
    这是一个创建于 1148 天前的主题,其中的信息可能已经有所发展或是发生改变。

    原文链接: https://jichu4n.com/posts/how-x-window-managers-work-and-how-to-write-one-part-i/#role-of-a-window-manager

    窗口管理器是现在Linux系统的最为核心的组件之一了。因为窗口管理器的职责是去管理每个独立窗口如何显示 /移动,怎样反馈输入,怎样组织各个窗口,可以不夸张地说,窗口管理器决定了日常的使用体验。因此,即便第一个X窗口管理器已经存在近 30 年了,我们还可以去讨论不同的窗口管理器的优势。而许多不一样的新式窗口管理器正在不断地创造出来并且改变着生活。

    在接下来的章节里,希望能够把窗口管理器的工作原理解释清晰,并且你能够以此制作一个属于自己的窗口管理器。

    在这系列文章中,我会大量地也引用Xlib Programming Manual (3rd Ed, 1994) 的内容。在过去的二十年间,X并没如你所想的那样,发生过太大的变化,所以尽管其年代久远,这依旧是最适合用来了解X内部实现细节的文章。所以我非常推荐你花一杯咖啡的价格去购买这本书。在这本书的第 16 章,基本就覆盖了窗口管理器的内容。

    窗口管理器的定位

    就以窗口管理器在现在Linux/BSD系统中的定位来开始吧

    窗口管理器的权力

    WindowsMac OS X这些操作系统不一样的是,X并没有指定一个专用的窗口管理器,也没有定义窗口管理器的行为。正因为这个决定,今儿的我们才可以看到X下窗口管理器的多样性。

    X不同于其他窗口系统的一个地方在于其没有指定一个窗口管理器。因为X的开发者希望X能够尽可能的摆脱窗口管理器和用户接口策略的影响。

    — Xlib Programming Manual §1.2.3

    事实上,X甚至根本不需要窗口管理器。

    跟人所不同的是,窗口管理器拥有权力,但不需要承担义务。应用程序应该能够适配任何一个窗口管理器,或者在没有窗口管理器的环境下运行。

    — Xlib Programming Manual §1.2.3

    这个定义跟其他GUI系统形成了一个强烈对比。对于Mac OS XUnity而言,如果没有窗口管理器的存在,应用程序基本就没办法实现任何的功能。窗口管理器在这里的作用是渲染像菜单这样的用户接口。

    X窗口管理器的职责

    正如你已经知道的,X以一种服务端-客户端的架构运行。X的服务端能够控制多个物理显示设置和输入设备。应用程序以X客户端的角色来跟这些设备完成交互。当X服务端和客户端运行在同一台设备上的时,他们使用domain socket来进行通信。若处于不同的设备,他们可以使用TCP/IP来完成通信。

    窗口管理器的本质上就是一个常规的X客户端,它并没有超级用户的权限来使用内核后门。对于X服务器而言,窗口管理器就是一个普通的用户进程,并且这个进程能够让X使用一系列的特殊APIs。当一个客户端已经成功连接了这些特定API的时候,若有其他客户端试图连接,X会直接拒绝掉,通过这样的方式能够确保系统在任一时刻,至多只会有一个窗口管理器的存在。而第一个程序总是可以成功的连接上这些特殊的APIs

    窗口管理器通过X所提供的properties events这种通信机制来跟窗口进行通信。对于这个机制,我们会在后文详细展开,但有个地方需要注意,X服务器是窗口管理器和窗口通信的中介,窗口管理器和窗口并不直接进行通信。

    通过下图可以展现这种通信机制。

    so1jXbe2d2Vvx917pbA5Cjw

    窗口管理器是如何管理窗口的

    现在让我们来深入探索窗口管理器管理窗口的细节。

    窗口层级

    在现代GUI中,我们使用术语widgetscontrols来形容UI元素,例如常见的按钮,滚动条,文本框。同时,我们使用windows这个术语来形容那个含有各种widgets的容器,并且这个容器拥有一个自定义的专属名字,并且可以单独的进行移动,关闭,调整大小等等。

    X的设计是尽可能地接近底层。其所提供的基础UI模型,是GTK+Qt这些UI框架的基础,它们都是矩形的层级结构。在X的术语里,所有顶层窗口和UI元素,都被称为windows。换句话说,windows就是一个用户交互空间和图形显示的基本单位。

    窗口以树形的结构组织起来。这个树形结构的根是root window,这是一个虚拟的,不可见的窗口,但是它的大小跟屏幕一样,同时会永远的存在。顶层窗口都是root window的直接孩子节点。而顶层窗口内的UI元素则是这些窗口的直接孩子节点。

    窗口展示

    举个例子,在XFce这个桌面环境下创建一个对话框。整个对话框就是一个X window。对话框内的所有UI元素,比如放大镜图标,文本框,绿色箭头,关闭按钮,加载按钮,所有的元素,都是X window

    这一整个对话框就是root window的孩子,放大镜图标,文本框,开关按钮都是对话框window的孩子节点,而绿色箭头则是文本框的孩子节点。同样的,关闭和启动中的图标都是这些按钮的孩子节点。

    X中,有一个很重要的地方是,孩子节点都会被父节点的边界所裁剪。

    孩子节点可以部分或者全部位于父类窗口的外面,但孩子节点的显示以及输入的接收,仅仅发生在跟父类窗口重叠的地方。

    — Xlib Programming Manual §2.2.2

    简单地说就是,如果将文本框的宽度增大两倍,但是不更改弹窗的尺寸。那么,文本框所超出弹窗的那部分区域将不可见,同时,如果点击文本框所超出的区域,也不会有事件发送到文本框上。

    Substructure Redirection

    没有窗口管理器的时候,若应用需要对窗口进行操作,它的操作请求会被X服务端直接处理,这些窗口操作请求包括移动窗口,调整窗口大小,显示窗口,隐藏窗口等等。但窗口管理器存在的话,它会直接拦截这些操作请求。比如,窗口管理器需要知道顶层窗口何时被创建和显示出来,什么时候进行了窗口大小的调整,这样才能够及时地进行窗口渲染。

    这种允许窗口管理器拦截请求的机制叫做**substructure redirection**。

    下面简要的介绍一下这个机制是如何工作。假如我们有一个窗口W,如果W在程序M上注册了substructure redirection,那么修改W子窗口的任何匹配请求都不会被X服务端直接处理。相反,X服务端会将请求直接转发给程序M。收到这些请求的后,M可以决定如何去处理,包括直接拒绝这些请求,或者允许这些请求对窗口进行变更。

    这里所提到的*structure,便是窗口的位置,大小,堆叠顺序,边框宽度,映射状态等等。substructure指的就是所有子窗口的这些状态量。这些是窗口管理器用于实现其关于屏幕布局策略所需要的完整信息集。Redirection*意味着事件会被重定向到所选择的客户端,通常就是窗口管理器。而原先的事件集并不会被执行。

    — Xlib Programming Manual §16.2

    这里要注意,substructure redirection只影响窗口W的直接孩子节点,其他更深层次的窗口节点并不会被影响。这就导致了root窗口上的substructure redirection会非常有趣。

    当窗口管理器选择使用SubstructureRedirectMask作用于root窗口上时,任何客户端尝试去改变root窗口直接孩子节点配置的操作都会失败。这些带有布局变化改变信息的请求事件会被直接发送到窗口管理器上。由窗口管理器来决定哪些事件可以执行,哪些应该要改变,哪些要直接拒绝。如果窗口管理器决定授权给这些请求,它会调用请求客户端那些相同参数的回调函数。如果窗口管理器选择更改这些请求,它会以不同的参数去调用客户端的回调函数。

    — Xlib Programming Manual §16.2

    换句话说,窗口管理器必须在root窗口上注册substructure redirection,这样才能够将root窗口下的所有顶层窗口纳入窗口管理器的管理范围内,窗口管理器会管理这些窗口的创建,销毁,重配置等等。这个X服务所提供的挂钩,能够让窗口管理器完美地完成它的工作。

    下面是一个示意图。

    窗口管理器管理示意图

    在任一时刻,X保证任意一个窗口都只会一个程序对其注册了substructure redirection。当程序尝试对一个已经被注册了substructure redirection的窗口进行注册的时候,其调用会失败,并且原先的程序不会从X解注册,断连或者崩溃。因为窗口管理器必须对root进行substructure redirection注册,所以这个机制会保障系统在同一时刻只会有一个窗口管理器在运行。

    Reparenting

    在上文的弹窗例子中,可以看到弹窗顶部的工具栏,其中含有最小化按钮,最大化按钮,关闭按钮。这些UI元素并不由应用程序所创建,相反,它们是由窗口管理器通过*reparenting* 或*framing*的方式做创建的。

    窗口管理器可以使用标题栏修饰应用窗口,在标题栏里放置一些简单的按钮,用于移动或调整大小。

    窗口管理器创建了一个root窗口的孩子窗口,该窗口会比应用创建的窗口要更大一些。然后调用 XReparentWindow()将应用窗口设置为刚刚创建的窗口的孩子节点。

    — Xlib Programming Manual §16.3

    换句话说,如果没有窗口管理器的存在,我们所创建的应用顶层窗口会是root的孩子节点。当窗口管理器存在的情况下,它会自己创建一个框架窗口,将该窗口设置为root窗口的孩子节点,再将应用窗口设置为这个框架窗口的孩子节点。窗口管理器可以根据自己的需求,在框架窗口上添加自己所需要的的UI元素。

    所以,前文所示的弹窗,实际上是Xfce的窗口管理器Xfwm所创建的框架窗口的孩子节点。它在应用窗口上创建了一些自己所需要的UI元素。

    窗口示意图

    Reparent这一技术允许不同的窗口管理器绘制不同的窗口修饰,从而实现一致的窗口样式。但是,也有一些窗口管理器并不会使用,它们被称为*non-reparenting*窗口管理器。有两个原因导致这些窗口管理器不使用这一机制:

    1. 对于xmonad, dwm这样的窗口管理器而言,它们并不会去绘制窗口修饰,所以没有必要使用这一机制。
    2. 对于像Compiz这样的窗口合成管理器而言,出于后文会讨论到的原因,它也不需要这一机制。但还是存在一些窗口合成管理器会使用Reparent的,比如GNOME的默认窗口管理器Mutter,就是一个使用Reparent技术的窗口合成管理器。

    现在来了解一下在Reparent这一机制下,**substructure redirection**的行为。当一个顶层窗口W准备显示的时候,窗口管理器会收到一个通知。因为窗口管理器会在root窗口上注册substructure redirection,而这个顶层窗口W又是root窗口的直接孩子节点。但如果我们创建了一个框架窗口F,并且使用ReparentW成为F的孩子节点,此时F才是root窗口的孩子。所以此时因为W不再是root的直接孩子节点了,窗口管理器便不能拦截W的请求了。

    因此,Reparent窗口管理器必须递归地在框架窗口上注册substructure redirection

    Compositing

    窗口合成管理器(compositing window manager)相对而言是一项较新的技术。在 2004 年X服务才支持窗口合成,此时距离最新版本的 Xlib Programming Manual已经过去了十年了。第一个窗口合成管理器XfwmCompiz在 2005 年便开始启动。

    所以,窗口合成管理器到底是做什么的。

    我们在上文讨论substructure redirectionreparenting的时候,谈及到窗口管理器可以对顶层窗口的各种请求做出回应,比如显示窗口,隐藏窗口,调整窗口大小,移动窗口等等。但是我们并没有谈及到顶层窗口的内部是怎样的。

    事实上,从窗口管理器的角度来看,顶层窗口就是一个黑盒。这些顶层窗口可能通过GTK+Qt这样的UI框架来管理内部的子节点,而窗口管理器并没有权利干涉内部的运作。创建了顶层窗口的应用需要自己去管理内部的渲染,以及直接从X获取到的内部层级UI节点的事件。如同在我们上文的弹窗例子中所展示的那样。

    伴随着图形硬件性能的增长,人们对于窗口管理器的要求也日益提高。使用硬件加速技术,可以创建更多的计算敏感型用户界面,比如Compiz的(非)著名桌面立方体

    桌面立方体

    或者Shift Switcher

    Shift Switcher

    现在思考一下,如何实现一个类似于Shift Switcher的用户界面。当用户触发这个界面的时候,我们需要:

    1. 渲染每一个顶层窗口以及它的内部`UI`节点到一个离屏内存缓冲区,而不是直接输出到硬件上
    
    2. 根据我们的设计,使用旋转,扭曲这样的方式来变化每个缓冲区的样式
    3. 将这些经过变化的缓冲区融合到一个最终的缓冲区中,同时还需要将背景和其他需要展示的`UI`也合并进来
    4. 将这个最终缓冲区的内容渲染到屏幕上
    

    这个过程存在非常多的挑战:

    1. 我们需要收集所有得顶层窗口的内容。然而,正如我们上文所提到的那样,顶层窗口的内容是直接通过X进行渲染的,并不通过窗口管理器
    2. 当顶层窗口发生变化的时候,我们必须实时更新界面。然而,当顶层窗口的内容发生变化时,这些顶层窗口并不会通知窗口管理器,因为它们直接通过X进行渲染
    3. 一个顶层窗口A有可能覆盖了另一个顶层窗口B,这意味窗口B的一部分是不显示的。然而这一界面需要获取到窗口AB的完整页面渲染
    4. 这些合成操作是计算敏感型,它要求通过硬件加速来完成这些功能

    很明显,缺乏跟X服务的深度交互,是没办法完成这些事情的。

    许多用户界面的操作都受益于窗口层次结构中忽略了同级和前级剪切的像素内容。另外,将这些对像素内容的控制移动到外部应用的最终窗口图像中,有助于构建一个具有伸缩性的动态程序内容展现系统。

    — X Composite Extension

    这个合成扩展提供了一种机制,让X服务将窗口和它的子类节点内容渲染到内部所维护的缓冲区,而不是硬件上,并且能够忽视正常的裁剪和重复计算。这个缓冲区可以被客户端通过请求读取和使用到。

    这便是窗口合成管理器的内部细节:它要求X渲染每一个顶层窗口到一个离屏内存缓冲区,并将这些结果合并为叠加的窗口本身。而窗口管理器所需要的做的并不仅仅是示例中所展现的switcher interfaces,还可以去实现半透明,动画,软件阴影等效果。

    下面是一个示例图:

    窗口合成器示例图

    最后我们来思考一下窗口合成管理器是否要对顶层窗口使用reparent技术。因为窗口合成管理器是明确知道所有顶层窗口的大小和位置的,所以它可以使用图形接口(OpenGL)在融合到一个窗口的时候绘制窗口修饰,而不需要创建一个真实的框架窗口再进行reparent。一些合成窗口管理器就是这么实现的。

    而另一方面,为了适配一些较老的图形硬件,窗口管理器需要同时支持合成模式和非合成模式。在这种情况下,非合成模式就一定要去实现reparent,尽管这对于合成模式而言,是一种多余的计算。这也是为什么,一些窗口合成器会选择实现reparent

    3 条回复    2021-02-05 14:20:57 +08:00
    secondwtq
        1
    secondwtq  
       2021-02-04 14:11:19 +08:00 via iPhone
    xmonad 其实有做 window decoration 的插件…… 不过貌似是单独创建的窗口
    还有个鼠标拖动交换窗口的插件,再加个 bar 的插件切换 workspace,就可以脱离键盘使用了……
    listenerri
        2
    listenerri  
       2021-02-04 15:30:32 +08:00
    手动点个赞
    zbelial
        3
    zbelial  
       2021-02-05 14:20:57 +08:00
    赞一个
    不过可以继续校正,有些翻译不够准确。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   4971 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 28ms · UTC 01:13 · PVG 09:13 · LAX 18:13 · JFK 21:13
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.