X.Org 项目提供了 X 窗口系统的开源实现。开发工作是在 freedesktop.org 社区的通力合作下完成。X.Org 组织是非盈利教育机构,其董事会为这项工作服务,其成员领导这项工作。Xorg (通常简称为 X)在 Linux 用户中非常流行,已经成为图形用户程序的必备条件,所以大部分发行版都提供了它。详情参见 Xorg 维基文章或访问Xorg 网站。客户端-服务器分离是X11架构的基石。所有你看到的图形界面,都由运行在远端的“客户端”程序生成,通过协议发送给本地的“服务器”来呈现。- X Server:唯一直接与硬件(显卡、显示器、键盘、鼠标、触摸板)对话的组件。
- 在屏幕上绘制基本的图形原语(点、线、矩形、文本,而非现代GPU的复杂渲染)。
- 不负责窗口装饰、布局、切换——这些是窗口管理器(一个特殊客户端)的工作。
- X Client:即应用程序(如
xterm, firefox)。
- 决定“画什么”(内容),但不知道“具体怎么画到像素点上”。
- 通过向X Server发送协议请求来实现绘图和接收输入。
- 窗口管理器/合成器 (特权 X Client) - 桌面外观和行为的总控。
- 合成效果 (现代):作为合成器时,接收各个窗口的缓冲图像,混合后输出,实现阴影、动画、透明等效果。
APP、Xorg 和窗管的整体架构如下:
Xorg 内部的组件架构如下:
X11 窗口的创建和显示是一个典型的客户端-服务器交互过程,涉及多个关键步骤和函数。其核心是:在 X 服务器上创建资源(窗口),设置其属性,然后请求将其“映射”到屏幕上。下图清晰地展示了这一核心流程及主要阶段:下面是详细的步骤:
1. 客户端 → 服务器: CreateWindow2. 服务器 → 客户端: Success Reply3. 服务器 → 窗管: ConfigureRequest (事件)4. 窗管 → 服务器: ConfigureWindow (调整大小位置)5. 客户端 → 服务器: MapWindow (显示窗口)6. 服务器 → 客户端: XNextEvent (事件循环)
函数 | 作用 |
XOpenDisplay
| 连接到 X 服务器 |
XCloseDisplay
| 关闭与 X 服务器的连接 |
XDefaultScreen
| 获取默认屏幕编号 |
XDisplayName
| 获取显示名称 |
函数 | 作用 |
XCreateWindow
XCreateSimpleWindow
| 创建新窗口 |
XDestroyWindow
| 销毁窗口 |
XMapWindow
| 映射窗口(显示) |
XUnmapWindow
| 取消映射窗口(隐藏) |
XRaiseWindow
| 将窗口提升到堆叠顶部 |
XLowerWindow
| 将窗口降低到堆叠底部 |
XMoveWindow
| 移动窗口位置 |
XResizeWindow
| 调整窗口大小 |
XReparentWindow
| 改变窗口的父窗口 |
XGetWindowAttributes
| 获取窗口属性 |
函数 | 作用 |
XSelectInput
| 选择要接收的事件类型 |
XNextEvent
| 获取下一个事件 |
XCheckWindowEvent
| 检查指定窗口的事件 |
XCheckTypedEvent
| 检查特定类型的事件 |
XSendEvent
| 发送事件到窗口 |
XPending
| 检查待处理事件数量 |
函数 | 作用 |
XCreateGC
| 创建图形上下文 |
XFreeGC
| 释放图形上下文 |
XDrawLine
| 绘制直线 |
XDrawRectangle
| 绘制矩形边框 |
XFillRectangle
| 填充矩形 |
XDrawString
| 绘制文本 |
XClearWindow
| 清除窗口内容 |
函数 | 作用 |
XChangeProperty
| 修改窗口属性 |
XGetWindowProperty
| 获取窗口属性 |
XDeleteProperty
| 删除窗口属性 |
XSetWMProperties
| 设置窗口管理器属性 |
函数 | 作用 |
XGrabKey
| 捕获键盘按键 |
XGrabButton
| 捕获鼠标按钮 |
XSetInputFocus
| 设置输入焦点 |
XGetInputFocus
| 获取当前焦点窗口 |
在X11中,事件是服务器向客户端发送的通知,表示某些事情已经发生,比如键盘按键、鼠标移动、窗口暴露等。客户端需要处理这些事件以响应用户输入和系统状态变化。由于 XEvent 是一个联合体,所以每次事件只有一个结构体是有效的,有效结构体与XEvent中的 type 相关。常用 type与结构体对应关系如下。事件类型 | 事件结构体 | 描述 |
KeyPress | XKeyEvent | 键盘按键被按下事件,XKeyEvent结构体记录被按下的键盘。 |
KeyRelease | XKeyEvent | 键盘按键被按下松开事件 |
ButtonPress | XButtonEvent | 鼠标键被按下事件 |
ButtonRelease | XButtonEvent | 鼠标键被按下松开事件 |
MotionNotify | XMotionEvent | 鼠标在窗口内移动事件 |
EnterNotify | XCrossingEvent | 鼠标进入窗口事件 |
LeaveNotify | XCrossingEvent | 鼠标离开窗口事件 |
FocusIn | XFocusChangeEvent | 输入焦点进入当前窗口 |
FocusOut | XFocusChangeEvent | 输入焦点离开当前窗口 |
Expose | XExposeEvent | 当前窗口被显示或者区域需要重绘时产生的事件 |
CreateNotify | XCreateWindowEvent | 窗口被创建时事件 |
DestroyNotify | XDestroyWindowEvent | 窗口被销毁时事件 |
MapNotify | XMapEvent | 窗口显示时事件 |
ConfigureNotify | XConfigureEvent | 窗口位置、大小改变时事件 |
ClientMessage | XClientMessageEvent | 用于窗口之间的通信,如WM和应用 |
XSelectInput() 是 X11 事件系统的核心“订阅”机制。它的作用是:告诉 X 服务器,你对特定窗口的哪些类型的事件感兴趣。如果不使用它,你的程序将收不到任何事件,成为“植物人”程序。
如下事件比较特殊:
- 窗口显示、大小、位置改变:即使没有订阅
StructureNotifyMask,窗口管理器仍能改变窗口大小,但是窗口收不到对应的事件。 - ClientMessage接收:不需要
XSelectInput也能收到ClientMessage事件。如 WM_DELETE_WINDOW 窗口管理器发送的关闭请求能收到。
函数的原型如下:
intXSelectInput(Display *display, Window window, long event_mask);
参数:
掩码常量 | 对应事件 | 典型用途 |
ExposureMask
| Expose, GraphicsExpose
| 窗口需要重绘时 |
KeyPressMask
| KeyPress
| 键盘输入处理 |
KeyReleaseMask
| KeyRelease
| 按键释放(游戏、输入法) |
ButtonPressMask
| ButtonPress
| 鼠标点击 |
ButtonReleaseMask
| ButtonRelease
| 鼠标释放 |
PointerMotionMask
| MotionNotify
| 鼠标移动跟踪 |
ButtonMotionMask
| MotionNotify(按下时)
| 拖拽操作 |
EnterWindowMask
| EnterNotify
| 鼠标进入窗口 |
LeaveWindowMask
| LeaveNotify
| 鼠标离开窗口 |
FocusChangeMask
| FocusIn, FocusOut
| 焦点管理 |
StructureNotifyMask
| ConfigureNotify, MapNotify, UnmapNotify等
| 窗口大小/位置/映射状态变化 |
PropertyChangeMask
| PropertyNotify
| 监控窗口属性变化(如标题) |
VisibilityChangeMask
| VisibilityNotify
| 窗口可见性变化 |
SubstructureNotifyMask
| 子窗口相关事件 | 管理子窗口 |
SubstructureRedirectMask
| 子窗口请求事件 | 窗口管理器使用 |
// 创建窗口后立即设置事件订阅Window create_app_window(Display *dpy, int x, int y, int w, int h) { Window win = XCreateSimpleWindow(dpy, DefaultRootWindow(dpy), x, y, w, h, 1, 0, 0); // 必须订阅的事件(最基本交互) XSelectInput(dpy, win, ExposureMask | // 窗口需要重绘 KeyPressMask | // 按键按下 KeyReleaseMask | // 按键释放 ButtonPressMask | // 鼠标按钮按下 ButtonReleaseMask | // 鼠标按钮释放 PointerMotionMask | // 鼠标移动 StructureNotifyMask | // 窗口结构变化(大小/位置等) FocusChangeMask | // 焦点变化 PropertyChangeMask | // 属性变化 EnterWindowMask | // 鼠标进入窗口 LeaveWindowMask); // 鼠标离开窗口 return win;}
// 根据功能需求选择性订阅voidsetup_window_events(Display *dpy, Window win, int window_type) { long base_mask = ExposureMask | KeyPressMask | StructureNotifyMask; switch (window_type) { case WINDOW_TYPE_MAIN: XSelectInput(dpy, win, base_mask | PropertyChangeMask | // 监控属性变化 FocusChangeMask | // 焦点变化 SubstructureNotifyMask); // 子窗口事件 break; case WINDOW_TYPE_DIALOG: XSelectInput(dpy, win, base_mask | FocusChangeMask | // 对话框需要焦点 EnterWindowMask | // 鼠标进入/离开 LeaveWindowMask); break; case WINDOW_TYPE_POPUP: // 弹出窗口可能不需要所有事件 XSelectInput(dpy, win, ExposureMask | ButtonPressMask | KeyPressMask); break; }}
#include<X11/Xlib.h>#include<X11/Xatom.h>#include<stdio.h>#include<stdlib.h>#include<unistd.h>intmain(){ // 1. 连接X服务器 Display *display = XOpenDisplay(NULL); if (!display) { fprintf(stderr, "无法打开显示\n"); return 1; } int screen = DefaultScreen(display); Window root = RootWindow(display, screen); // 2. 创建窗口 Window window = XCreateSimpleWindow(display, root, 100, 100, 400, 300, 1, BlackPixel(display, screen), WhitePixel(display, screen)); // 3. 关键:完全不调用 XSelectInput! XSelectInput(display, window, 0); // 或者完全注释掉这行 // 4. 设置窗口标题 XStoreName(display, window, "测试窗口 - 无XSelectInput"); // 5. 设置WM_DELETE_WINDOW协议(这是ClientMessage) Atom wm_delete_window = XInternAtom(display, "WM_DELETE_WINDOW", False); XSetWMProtocols(display, window, &wm_delete_window, 1); // 6. 映射窗口 XMapWindow(display, window); XFlush(display); printf("窗口已创建,ID: 0x%lx\n", window); printf("程序没有调用 XSelectInput()!\n"); printf("请尝试:\n"); printf("1. 用鼠标拖动改变窗口大小\n"); printf("2. 点击窗口标题栏的关闭按钮\n"); printf("3. 在终端按Ctrl+C退出程序\n\n"); // 7. 非阻塞事件检查 int count = 0; while (1) { // 使用非阻塞方式检查事件 if (XPending(display) > 0) { XEvent event; XNextEvent(display, &event); printf("收到事件!类型: %d", event.type); // 检查事件类型 switch (event.type) { case ClientMessage: { XClientMessageEvent *cme = &event.xclient; printf(" (ClientMessage)\n"); printf(" 消息类型: %s\n", XGetAtomName(display, cme->message_type)); // 检查是否是关闭请求 if (cme->data.l[0] == wm_delete_window) { printf(" 检测到WM_DELETE_WINDOW协议\n"); printf(" 窗口管理器请求关闭窗口\n"); } break; } case ConfigureNotify: { XConfigureEvent *ce = &event.xconfigure; printf(" (ConfigureNotify)\n"); printf(" 窗口新大小: %dx%d\n", ce->width, ce->height); printf(" 窗口新位置: (%d, %d)\n", ce->x, ce->y); break; } case MapNotify: printf(" (MapNotify) 窗口已映射\n"); break; case Expose: printf(" (Expose) 窗口需要重绘\n"); break; default: printf(" (未知类型)\n"); break; } } else { // 没有事件时等待 printf("等待事件... (循环 %d)\n", ++count); sleep(1); } } // 8. 清理 XDestroyWindow(display, window); XCloseDisplay(display); return 0;}
编译运行
$ gcc demo.c -lX11$ ./a.out
Atom 是X11中用来表示字符串的整数标识符。由于字符串在网络上传输效率低,X11使用原子来代表常用的字符串,从而减少数据传输量。Atom 在连接期间被缓存,一旦定义,所有客户端都可以使用这个整数 ID 来引用该字符串。原子的用途:
- 窗口属性(Properties):窗口属性由原子命名,例如
WM_NAME、WM_CLASS等。 - 选择(Selections):用于剪贴板和数据传输,例如
PRIMARY、CLIPBOARD。 - 协议(Protocols):如
WM_DELETE_WINDOW,用于窗口管理器与应用程序的通信。
常用原子:
- 预定义原子:X11有一些预定义的原子,如
PRIMARY、SECONDARY、CLIPBOARD等。 - 窗口属性原子:如
WM_NAME(窗口标题)、WM_CLASS(应用程序类)、WM_PROTOCOLS(协议列表)等。 - EWMH原子:用于扩展窗口管理器提示,如
_NET_WM_NAME、_NET_WM_STATE、_NET_WM_WINDOW_TYPE等。
原子的操作:
# 获取原子:将字符串转换为原子。cAtom atom = XInternAtom(display, "WM_NAME", False);#获取原子名:将原子转换为字符串。cchar *name = XGetAtomName(display, atom);
// 在 Xatom.h 中定义XA_PRIMARY // 主要选择(剪贴板)XA_SECONDARY // 次要选择XA_ARC // 窗口弧形XA_ATOM // 原子类型XA_BITMAP // 位图XA_CARDINAL // 32位整数XA_COLORMAP // 颜色映射XA_CURSOR // 光标XA_DRAWABLE // 可绘制对象XA_FONT // 字体XA_INTEGER // 整数XA_PIXMAP // 像素图XA_POINT // 点XA_RECTANGLE // 矩形XA_STRING // 字符串XA_VISUALID // 视觉IDXA_WINDOW // 窗口
// ICCCM (Inter-Client Communication Conventions Manual)WM_NAME // 窗口标题WM_ICON_NAME // 图标名称WM_CLASS // 应用程序类/实例名WM_TRANSIENT_FOR // 临时窗口的父窗口WM_PROTOCOLS // 支持的协议列表WM_COLORMAP_WINDOWS // 颜色映射窗口列表WM_HINTS // 窗口提示WM_NORMAL_HINTS // 窗口大小提示WM_CLIENT_MACHINE // 客户端机器名// 协议原子WM_DELETE_WINDOW // 关闭窗口请求WM_TAKE_FOCUS // 焦点请求WM_SAVE_YOURSELF // 保存状态请求
4.3. EWMH (Extended WM Hints)
现代桌面扩展,以 _NET_ 开头:
// 窗口类型_NET_WM_WINDOW_TYPE_NET_WM_WINDOW_TYPE_DESKTOP_NET_WM_WINDOW_TYPE_DOCK_NET_WM_WINDOW_TYPE_TOOLBAR_NET_WM_WINDOW_TYPE_MENU_NET_WM_WINDOW_TYPE_UTILITY_NET_WM_WINDOW_TYPE_SPLASH_NET_WM_WINDOW_TYPE_DIALOG_NET_WM_WINDOW_TYPE_NORMAL// 窗口状态_NET_WM_STATE_NET_WM_STATE_MODAL_NET_WM_STATE_STICKY_NET_WM_STATE_MAXIMIZED_VERT_NET_WM_STATE_MAXIMIZED_HORZ_NET_WM_STATE_SHADED_NET_WM_STATE_SKIP_TASKBAR_NET_WM_STATE_SKIP_PAGER_NET_WM_STATE_HIDDEN_NET_WM_STATE_FULLSCREEN_NET_WM_STATE_ABOVE_NET_WM_STATE_BELOW// 其他重要 EWMH 原子_NET_SUPPORTED // 支持的 EWMH 特性列表_NET_CLIENT_LIST // 客户端窗口列表_NET_CLIENT_LIST_STACKING // 堆叠顺序的客户端列表_NET_NUMBER_OF_DESKTOPS // 虚拟桌面数量_NET_CURRENT_DESKTOP // 当前虚拟桌面_NET_DESKTOP_NAMES // 桌面名称_NET_ACTIVE_WINDOW // 活动窗口_NET_CLOSE_WINDOW // 关闭窗口(窗管发送)_NET_WM_NAME // UTF-8 窗口标题_NET_WM_VISIBLE_NAME // 可见名称_NET_WM_ICON // 窗口图标_NET_WM_PID // 进程ID_NET_WM_USER_TIME // 用户最后活动时间
// 剪贴板相关CLIPBOARD // 剪贴板选择TARGETS // 可用目标格式列表UTF8_STRING // UTF-8 字符串COMPOUND_TEXT // 复合文本TIMESTAMP // 时间戳// 拖放 (XDND)XdndAware // 支持拖放的窗口XdndSelection // 拖放选择XdndDrop // 拖放事件XdndPosition // 拖放位置XdndStatus // 拖放状态XdndFinished // 拖放完成// 应用程序状态SM_CLIENT_ID // 会话管理客户端ID_NET_WM_STATE_HIDDEN // 窗口隐藏状态_GTK_SHOW_WINDOW_MENU // GTK 显示窗口菜单
void setup_window_properties(Display *dpy, Window w) { // 设置窗口类 XClassHint class_hint; class_hint.res_name = "myapp"; class_hint.res_class = "MyApplication"; XSetClassHint(dpy, w, &class_hint); // 设置窗口标题(传统方式) XStoreName(dpy, w, "My Application"); // 设置窗口标题(EWMH 方式) Atom utf8_string = XInternAtom(dpy, "UTF8_STRING", False); Atom net_wm_name = XInternAtom(dpy, "_NET_WM_NAME", False); const char *title = "My Application"; XChangeProperty(dpy, w, net_wm_name, utf8_string, 8, PropModeReplace, (unsigned char*)title, strlen(title)); // 设置窗口协议 Atom wm_protocols = XInternAtom(dpy, "WM_PROTOCOLS", False); Atom wm_delete = XInternAtom(dpy, "WM_DELETE_WINDOW", False); Atom protocols[] = {wm_delete}; XSetWMProtocols(dpy, w, protocols, 1); // 设置窗口类型 Atom net_wm_window_type = XInternAtom(dpy, "_NET_WM_WINDOW_TYPE", False); Atom net_wm_window_type_normal = XInternAtom(dpy, "_NET_WM_WINDOW_TYPE_NORMAL", False); XChangeProperty(dpy, w, net_wm_window_type, XA_ATOM, 32, PropModeReplace, (unsigned char*)&net_wm_window_type_normal, 1);}// 应用程序请求全屏的完整过程void request_fullscreen(Display *dpy, Window w) { Atom net_wm_state = XInternAtom(dpy, "_NET_WM_STATE", False); Atom net_wm_state_fullscreen = XInternAtom(dpy, "_NET_WM_STATE_FULLSCREEN", False); XEvent ev; memset(&ev, 0, sizeof(ev)); ev.xclient.type = ClientMessage; ev.xclient.window = w; ev.xclient.message_type = net_wm_state; ev.xclient.format = 32; ev.xclient.data.l[0] = 1; // _NET_WM_STATE_ADD ev.xclient.data.l[1] = net_wm_state_fullscreen; ev.xclient.data.l[2] = 0; // 第二个属性,0 表示没有 ev.xclient.data.l[3] = 1; // 源指示器:1=应用程序 ev.xclient.data.l[4] = 0; // 发送到根窗口(窗管监听根窗口) XSendEvent(dpy, DefaultRootWindow(dpy), False, SubstructureRedirectMask | SubstructureNotifyMask, &ev); XFlush(dpy);}
窗口调试相关的包有xdotool、wmctrl、x11-utils。其中 x11-utils 是 X11 的项目的调试包,包含了很多调试工具,下面是主要工具:
工具名称 | 主要功能描述 |
xdpyinfo
| 查询并显示X服务器的详细信息,如支持的扩展、屏幕参数等。 |
xlsclients
| 列出当前连接到X服务器的所有客户端应用程序。 |
xwininfo
| 查询窗口的详细信息,如 ID、几何尺寸、位置等。常用 xwininfo -tree -root 查看窗口树结构。 |
xprop
| 查看和修改窗口的X属性,是调试窗口管理器协议的关键工具。 |
xev
| 创建一个窗口并实时打印所有X事件,是学习X11事件模型的绝佳工具。 |
xlsfonts
| 列出X服务器上可用的字体。 |
xfontsel
| 提供一个图形界面来浏览和选择字体。 |
xfd
| 打开一个窗口,显示指定字体的所有字符。 |
appres, editres, listres, viewres
| 用于查询和编辑X应用程序的资源(Resource)数据库。 |
xkill
| 强制终止行为异常的X客户端。执行后会变成骷髅光标,点击要关闭的窗口即可。 |
xlsatoms
| 列出X服务器上定义的所有原子(Atom),即属性的唯一标识符。 |
xdriinfo
| 查询Direct Rendering Infrastructure驱动配置。 |
xvinfo
| 打印X-Video扩展的功能信息。 |
xmessage
| 弹出一个简单的消息对话框,常用于脚本中给用户提示。 |
luit
| 在非UTF-8终端与UTF-8应用之间进行编码转换的过滤器。 |
# 启动事件查看器xev -event keyboard,mouse,expose,property,clientmessage# 查看特定窗口的事件xev -id $(xwininfo | grep "Window id" | awk '{print $4}')
# 查看窗口所有属性xprop -notype -id <window_id># 查看激活窗口xprop -root _NET_ACTIVE_WINDOW# 查看特定属性xprop -id <window_id> WM_NAME _NET_WM_STATE _NET_WM_WINDOW_TYPE# 监控属性变化xprop -spy -id <window_id> _NET_WM_STATE
# 使用xwininfo查看层级关系xwininfo -root -tree# 使用wmctrl查看窗口列表和状态wmctrl -l -G -x# 查看特定窗口的属性xprop -id $(xwininfo | grep "Window id" | awk '{print $4}')
# 选择窗口将它至顶wmctrl -r :SELECT: -b add,sticky# 使当前窗口全屏wmctrl -r :ACTIVE: -b add,fullscreen# 移除全屏wmctrl -r :ACTIVE: -b remove,fullscreen# 最大化wmctrl -r :ACTIVE: -b add,maximized_vert,maximized_horz# 选择窗口将它的标题修改为 Selected Windowwmctrl -r :SELECT: -T "Selected Window"# 选择窗口将它设为粘贴状态(所有虚拟桌面都显示)wmctrl -r :SELECT: -b add,sticky
5.5.1. 使用socat将Unix域套接字转换为TCP套接字# 1. 创建 TCP 到 UNIX socket 的桥接socat TCP-LISTEN:6001,bind=localhost,fork \ UNIX-CONNECT:/tmp/.X11-unix/X0 &# 2. 启动 Wireshark 捕获wireshark -k -i lo -f "tcp port 6001" &# 3. 启动应用DISPLAY=localhost:1 xedit
# 安装sudo apt-get install xtrace# 捕获 xterm 的所有 X11 通信xtrace -o x11.log xterm
X服务器本身有扩展可以记录协议数据。启动Xorg时启用记录:
# 启动带日志的 XorgXorg :1 -listen tcp -verbose 6 2>&1 | tee x11_protocol.log# 然后分析日志文件grep "Request" x11_protocol.log | head -20
# 跟踪所有 X11 相关的系统调用strace -e trace=network,ipc -f xterm 2>&1 | grep -E "(connect|send|recv)"