为何X11程序关闭背景绘制后仍始终收到全窗口Expose事件
问题描述
我在进行X11窗口事件相关开发时遇到了重绘效率问题,具体情况如下:
测试程序代码
/* * Study for Xwindow events. */ #include <X11/Xlib.h> #include <stdio.h> #include <stdlib.h> #include <string.h> //#define BACKGROUND_PAINT void prtxevtt(int type) { switch (type) { case 2: fprintf(stderr, "KeyPress"); break; case 3: fprintf(stderr, "KeyRelease"); break; case 4: fprintf(stderr, "ButtonPress"); break; case 5: fprintf(stderr, "ButtonRelease"); break; case 6: fprintf(stderr, "MotionNotify"); break; case 7: fprintf(stderr, "EnterNotify"); break; case 8: fprintf(stderr, "LeaveNotify"); break; case 9: fprintf(stderr, "FocusIn"); break; case 10: fprintf(stderr, "FocusOut"); break; case 11: fprintf(stderr, "KeymapNotify"); break; case 12: fprintf(stderr, "Expose"); break; case 13: fprintf(stderr, "GraphicsExpose"); break; case 14: fprintf(stderr, "NoExpose"); break; case 15: fprintf(stderr, "VisibilityNotify"); break; case 16: fprintf(stderr, "CreateNotify"); break; case 17: fprintf(stderr, "DestroyNotify"); break; case 18: fprintf(stderr, "UnmapNotify"); break; case 19: fprintf(stderr, "MapNotify"); break; case 20: fprintf(stderr, "MapRequest"); break; case 21: fprintf(stderr, "ReparentNotify"); break; case 22: fprintf(stderr, "ConfigureNotify"); break; case 23: fprintf(stderr, "ConfigureRequest"); break; case 24: fprintf(stderr, "GravityNotify"); break; case 25: fprintf(stderr, "ResizeRequest"); break; case 26: fprintf(stderr, "CirculateNotify"); break; case 27: fprintf(stderr, "CirculateRequest"); break; case 28: fprintf(stderr, "PropertyNotify"); break; case 29: fprintf(stderr, "SelectionClear"); break; case 30: fprintf(stderr, "SelectionRequest"); break; case 31: fprintf(stderr, "SelectionNotify"); break; case 32: fprintf(stderr, "ColormapNotify"); break; case 33: fprintf(stderr, "ClientMessage"); break; case 34: fprintf(stderr, "MappingNotify"); break; case 35: fprintf(stderr, "GenericEvent"); break; default: fprintf(stderr, "???"); break; } } void prtxevt(Display* d, XEvent* e) { fprintf(stderr, "X Event: %5ld Window: %lx ", e->xany.serial, e->xany.window); prtxevtt(e->type); switch (e->type) { case Expose: fprintf(stderr, ": x: %d y: %d w: %d h: %d", e->xexpose.x, e->xexpose.y, e->xexpose.width, e->xexpose.height); break; case ConfigureNotify: fprintf(stderr, ": x: %d y: %d w: %d h: %d", e->xconfigure.x, e->xconfigure.y, e->xconfigure.width, e->xconfigure.height); break; case MotionNotify: fprintf(stderr, ": x: %d y: %d", e->xmotion.x, e->xmotion.y); break; case PropertyNotify: fprintf(stderr, ": atom: %s", XGetAtomName(d, e->xproperty.atom)); } fprintf(stderr, "\n"); fflush(stderr); } int main(void) { Window w; GC gracxt; XEvent e; const char* msg = "Hello, window"; int s; Display* d; XFontStruct* font; d = XOpenDisplay(NULL); if (d == NULL) { fprintf(stderr, "Cannot open display\n"); exit(1); } s = DefaultScreen(d); font = XLoadQueryFont(d, "-bitstream-courier 10 pitch-bold-r-normal--0-0-200-200-m-0-iso8859-1"); if (!font) { fprintf(stderr, "*** No font ***\n"); exit(1); } gracxt = XDefaultGC(d, s); XSetFont(d, gracxt, font->fid); #ifdef BACKGROUND_PAINT w = XCreateSimpleWindow(d, RootWindow(d, s), 10, 10, 1000, 1000, 5, BlackPixel(d, s), WhitePixel(d, s)); #else // no background draw w = XCreateWindow(d, RootWindow(d, s), 0, 0, 1000, 1000, 0, CopyFromParent, InputOutput, CopyFromParent, 0, NULL); #endif XSelectInput(d, w, ExposureMask|KeyPressMask|/*PointerMotionMask|*/ StructureNotifyMask/*|PropertyChangeMask*/); XMapWindow(d, w); while (1) { XNextEvent(d, &e); prtxevt(d, &e); if (e.type == Expose) { XSetForeground(d, gracxt, WhitePixel(d, s)); XFillRectangle(d, e.xany.window, gracxt, e.xexpose.x, e.xexpose.y, e.xexpose.width, e.xexpose.height); XSetForeground(d, gracxt, BlackPixel(d, s)); XDrawString(d, e.xany.window, gracxt, 10, 50, msg, strlen(msg)); } } XCloseDisplay(d); return 0; }
测试现象
上述程序会生成一个绘制有hello, window文本的普通窗口(因使用4K显示器,文本做了放大处理),我已关闭了默认背景绘制功能,改为自行处理窗口绘制逻辑。
当我拖拽窗口右下角缓慢向x、y两个方向放大窗口时,得到如下事件日志:
miam@samiam-h-pc-2:~/projects/petit_ami_tools/linux$ ./xmltwin X Event: 13 Window: 4e00002 ReparentNotify X Event: 13 Window: 4e00002 ConfigureNotify: x: 20 y: 90 w: 1000 h: 1000 X Event: 13 Window: 4e00002 MapNotify X Event: 13 Window: 4e00002 Expose: x: 0 y: 0 w: 1000 h: 1000 X Event: 17 Window: 4e00002 ConfigureNotify: x: 20 y: 90 w: 1001 h: 1000 X Event: 17 Window: 4e00002 Expose: x: 0 y: 0 w: 1001 h: 1000 X Event: 21 Window: 4e00002 ConfigureNotify: x: 20 y: 90 w: 1005 h: 1002 X Event: 21 Window: 4e00002 Expose: x: 0 y: 0 w: 1005 h: 1002 X Event: 25 Window: 4e00002 ConfigureNotify: x: 20 y: 90 w: 1007 h: 1003 X Event: 25 Window: 4e00002 Expose: x: 0 y: 0 w: 1007 h: 1003 X Event: 29 Window: 4e00002 ConfigureNotify: x: 20 y: 90 w: 1008 h: 1004 X Event: 29 Window: 4e00002 Expose: x: 0 y: 0 w: 1008 h: 1004 X Event: 33 Window: 4e00002 ConfigureNotify: x: 20 y: 90 w: 1009 h: 1004 X Event: 33 Window: 4e00002 Expose: x: 0 y: 0 w: 1009 h: 1004 X Event: 37 Window: 4e00002 ConfigureNotify: x: 20 y: 90 w: 1010 h: 1005 X Event: 37 Window: 4e00002 Expose: x: 0 y: 0 w: 1010 h: 1005 X Event: 41 Window: 4e00002 ConfigureNotify: x: 20 y: 90 w: 1012 h: 1005 X Event: 41 Window: 4e00002 Expose: x: 0 y: 0 w: 1012 h: 1005 X Event: 45 Window: 4e00002 ConfigureNotify: x: 20 y: 90 w: 1013 h: 1006 X Event: 45 Window: 4e00002 Expose: x: 0 y: 0 w: 1013 h: 1006 X Event: 49 Window: 4e00002 ConfigureNotify: x: 20 y: 90 w: 1015 h: 1009 X Event: 49 Window: 4e00002 Expose: x: 0 y: 0 w: 1015 h: 1009 X Event: 53 Window: 4e00002 ConfigureNotify: x: 20 y: 90 w: 1016 h: 1010 X Event: 53 Window: 4e00002 Expose: x: 0 y: 0 w: 1016 h: 1010 X Event: 57 Window: 4e00002 ConfigureNotify: x: 20 y: 90 w: 1016 h: 1011 X Event: 57 Window: 4e00002 Expose: x: 0 y: 0 w: 1016 h: 1011 X Event: 61 Window: 4e00002 ConfigureNotify: x: 20 y: 90 w: 1017 h: 1011 X Event: 61 Window: 4e00002 Expose: x: 0 y: 0 w: 1017 h: 1011 X Event: 65 Window: 4e00002 ConfigureNotify: x: 20 y: 90 w: 1017 h: 1012 X Event: 65 Window: 4e00002 Expose: x: 0 y: 0 w: 1017 h: 1012 X Event: 69 Window: 4e00002 ConfigureNotify: x: 20 y: 90 w: 1018 h: 1013 X Event: 69 Window: 4e00002 Expose: x: 0 y: 0 w: 1018 h: 1013 X Event: 73 Window: 4e00002 ConfigureNotify: x: 20 y: 90 w: 1019 h: 1014 X Event: 73 Window: 4e00002 Expose: x: 0 y: 0 w: 1019 h: 1014
可以看到每次窗口调整大小后,我收到的都是全窗口范围的Expose事件,要求重绘整个窗口。该行为逻辑上正确但效率极低,按预期仅新扩展出的右侧、底部单像素条区域需要重绘即可。
疑问点
- 我已知如果指定了背景像素值,Xwindow会因为需要自动擦除背景而触发全窗口Expose事件,但我已经关闭了背景绘制逻辑,还注释掉了Expose处理逻辑中的XFillRectangle调用,确认窗口不会绘制背景,原有下层窗口内容会保留。请问我关闭背景绘制的配置是否存在错误?
- 我注意到Xwindows底层基于XCB实现,窗口被其他窗口遮挡再移开时不会触发对应区域的重绘事件,这说明显示服务会缓存窗口绘制内容并自动恢复。我猜测是否当前XLIB/XCB已经不再对Expose事件做区域优化?
运行环境:Ubuntu 20.04 LTS + GDM3
问题解答
1. 关闭背景绘制的配置不存在错误
你使用XCreateWindow时未指定背景像素/pixmap,背景属性设为CopyFromParent且未绑定默认背景绘制逻辑,这个配置是正确的,确实关闭了X服务自动擦除背景的行为。
2. 全窗口Expose事件是合成窗口管理器的行为,和Xlib/XCB无关
你当前使用的GNOME桌面默认开启了Mutter合成器,合成窗口管理器为了实现窗口动画、阴影、透明等效果,会默认缓存所有窗口的完整表面纹理。当你调整窗口大小时,合成器会直接作废原有窗口的整个缓存纹理,要求应用重绘全窗口,而不是仅计算新增区域。
你可以切换到非合成窗口管理器(如默认无合成的Openbox、i3)验证,此时就能收到仅包含新增区域的Expose事件。
3. 遮挡不触发Expose也是合成器的特性
你观察到的窗口被遮挡再移开不需要重绘,同样是合成器缓存窗口纹理的结果,和Expose事件的区域优化逻辑无关。传统非合成的X环境下,遮挡移开仍然会触发对应损坏区域的Expose事件。
优化方案
如果要在当前GNOME环境下提升重绘效率,可以采用以下方案:
- 使用双缓冲机制:预先在离屏Pixmap绘制完整窗口内容,Expose事件触发时仅拷贝对应损坏区域到窗口,避免每次重绘都执行复杂绘制逻辑
- 给窗口设置
_NET_WM_BYPASS_COMPOSITOR属性,提示窗口管理器对当前窗口绕过合成,即可恢复传统X11的区域损坏通知逻辑 - 迁移到GTK/Qt等成熟UI框架,这类框架已经内置了合成环境下的重绘优化逻辑,不需要手动处理X11底层事件
内容的提问来源于stack exchange,提问作者Scott Franco

