4. 窗口创建与映射:CreateWindow、MapWindow的底层调用链与X11协议交互

窗口管理器的核心工作,说白了就是两件事:创建窗口显示窗口。很多初学者以为调个 CreateWindow 就完事了,其实背后藏着一条很深的调用链。我当年第一次读 X11 协议时,也被这层抽象搞得有点懵。

今天我们就把它拆开来看。从应用层的 API 一路追到 X11 协议报文,看看窗口到底是怎么「生」出来的。

4.1 CreateWindow:不只是分配内存

先看一个最简单的例子:

// 创建一个 800x600 的顶层窗口
Window win = XCreateWindow(display, root_window,
                           0, 0, 800, 600, 0,
                           CopyFromParent, InputOutput,
                           CopyFromParent, 0, NULL);

这行代码背后发生了什么?我习惯把它分成三个阶段来看:

  1. 客户端组装请求:Xlib 把参数打包成 X11 协议请求
  2. 发送到 X Server:通过 Unix socket 或 TCP 发送
  3. Server 端处理:X Server 分配资源,返回窗口 ID

嗯,这里要注意:XCreateWindow 返回的 Window 其实只是一个 32 位整数。它本质上是个句柄,不是内存指针。X Server 那边才真正持有窗口的数据结构。

核心概念:窗口 ID 是 X Server 分配的资源标识符。客户端拿着这个 ID 去发后续请求,Server 根据 ID 找到对应的窗口对象。

4.2 底层调用链:从 Xlib 到 X11 协议

我们来看看 XCreateWindow 在 Xlib 内部是怎么走的。我简化了关键路径:

// Xlib 内部实现(伪代码)
Status XCreateWindow(Display *dpy, Window parent, ...) {
    xCreateWindowReq *req;
    
    // 1. 从显示连接的缓冲区获取一个请求槽
    req = (xCreateWindowReq *) XAllocReq(dpy);
    
    // 2. 填充请求结构体
    req->reqType = X_CreateWindow;
    req->parent = parent;
    req->width = width;
    req->height = height;
    // ... 其他属性
    
    // 3. 发送请求到 X Server
    XSendRequest(dpy);
    
    // 4. 等待 Server 返回窗口 ID
    return XReply(dpy);
}

你看,本质上就是拼了一个 xCreateWindowReq 结构体,然后通过 XSendRequest 发出去。X11 协议规定,所有请求都以 操作码(opcode) 开头。X_CreateWindow 的操作码是 1。

小技巧:你可以用 XSynchronize(dpy, True) 让 Xlib 变成同步模式。这样每个请求都会等待 Server 回复,方便调试。我早期排查窗口创建失败问题时,就靠这个定位到是属性参数传错了。

4.3 X Server 端:窗口对象的诞生

X Server 收到请求后,会做以下几件事:

  1. 校验参数:检查父窗口是否存在、坐标是否合法
  2. 分配窗口 ID:从空闲 ID 池中取一个
  3. 创建窗口对象:初始化窗口的属性(大小、背景色、事件掩码等)
  4. 插入窗口树:把新窗口挂到父窗口的子窗口链表里
  5. 返回窗口 ID:通过回复报文告诉客户端

这里有个细节:窗口创建后默认是不可见的。你想想看,如果创建完立刻显示,那用户会看到一堆窗口「啪」地冒出来,体验很差。所以 X11 把创建和显示分成了两步。

4.4 MapWindow:让窗口真正可见

创建完窗口,下一步就是 XMapWindow

XMapWindow(display, window);

这个函数做的事情比名字看起来要多。它的底层调用链是这样的:

// XMapWindow 内部
XMapWindow(Display *dpy, Window w) {
    xMapWindowReq *req;
    
    req = (xMapWindowReq *) XAllocReq(dpy);
    req->reqType = X_MapWindow;
    req->window = w;
    
    XSendRequest(dpy);
    // 注意:MapWindow 不需要等待回复!
}

为什么不需要回复?因为 MapWindow 是个 异步请求。X Server 收到后,会触发一系列事件:

  • 生成 MapNotify 事件通知客户端
  • 如果窗口有父窗口,还会向父窗口发送 SubstructureNotify 事件
  • 窗口管理器会收到 MapRequest 事件(如果设置了相应的事件掩码)

我曾经踩过的坑:在窗口管理器开发中,如果忘记处理 MapRequest 事件,子窗口永远显示不出来。因为窗口管理器需要先给窗口加装饰(标题栏、边框),然后才能让它映射。我当年调试了一整天,才发现是事件掩码没设对。

4.5 窗口映射的完整流程

把创建和映射串起来,完整的流程是这样的:

步骤 客户端操作 X Server 响应 协议报文
1 XCreateWindow 分配窗口 ID,创建对象 CreateWindow 请求 + Reply
2 XMapWindow 标记窗口为已映射 MapWindow 请求
3 生成 MapNotify 事件 Event 报文
4 窗口管理器介入 ConfigureWindow 等
5 窗口真正显示在屏幕上

你看,从第 3 步开始,窗口管理器就登场了。它会拦截 MapRequest,然后根据自己的策略决定窗口的位置、大小、装饰风格。

4.6 实战:用 xdpyinfo 和 xev 观察

我建议你亲自验证一下。打开两个终端:

终端 1:监听窗口事件

xev -event window

终端 2:创建一个窗口并映射

xclock &

你会看到 xev 输出类似这样的内容:

MapNotify event, serial 17, synthetic NO, window 0x1e00001
    event serial: 17
    override_redirect: No

这个 MapNotify 就是 X Server 在告诉你:「窗口已经映射好了」。注意看 serial 字段,它对应的是请求序列号,可以用来追踪调用链。

个人经验:我习惯用 xtrace 工具来抓 X11 协议报文。它能显示每个请求的 opcode、参数和耗时。有一次我发现 XCreateWindow 返回特别慢,一看报文,原来是传了一个无效的 visual 参数,Server 在那边做 fallback 处理。

4.7 避坑指南

最后分享几个我实际工作中遇到的坑:

  • 窗口创建后不要立刻 Map:如果你需要设置窗口属性(比如标题、图标),最好在 Create 和 Map 之间完成。因为 Map 之后窗口管理器可能已经介入,再改属性会有闪烁。
  • 注意 override_redirect 标志:如果你不想让窗口管理器管理某个窗口(比如工具提示、下拉菜单),记得在 CreateWindow 时设置 override_redirect = True。否则窗口管理器会强行给它加边框。
  • MapWindow 不是立即生效的:由于 X11 是异步的,MapWindow 调用返回后,窗口可能还没显示。如果你需要等待窗口真正可见,可以监听 VisibilityNotify 事件。

嗯,窗口创建和映射这块,说白了就是客户端和 Server 之间的一次握手。理解了底层协议交互,你写出来的代码才会更健壮。下一章我们聊聊窗口属性管理,看看那些 XChangeProperty 调用背后又藏着什么玄机。