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);
这行代码背后发生了什么?我习惯把它分成三个阶段来看:
- 客户端组装请求:Xlib 把参数打包成 X11 协议请求
- 发送到 X Server:通过 Unix socket 或 TCP 发送
- 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 收到请求后,会做以下几件事:
- 校验参数:检查父窗口是否存在、坐标是否合法
- 分配窗口 ID:从空闲 ID 池中取一个
- 创建窗口对象:初始化窗口的属性(大小、背景色、事件掩码等)
- 插入窗口树:把新窗口挂到父窗口的子窗口链表里
- 返回窗口 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 调用背后又藏着什么玄机。