如何解决Cairo/X11窗口调整大小时的闪烁问题?
解决Cairo+Xlib窗口缩小背景闪烁问题
闪烁的核心原因是:窗口向内缩小时,X服务器会先用窗口背景色填充新尺寸外的区域,而Cairo重新绘制新区域的动作存在延迟,导致背景色短暂可见。之前仅修改背景色的方案只是掩盖问题,并未从根源解决。
以下是具体解决方案:
1. 禁用X服务器自动背景填充
创建窗口时,不要设置固定背景色,而是将窗口的背景像素图设为None,让X服务器不再自动填充窗口背景,完全由Cairo负责整个窗口的绘制。
修改窗口创建的属性部分:
&(XSetWindowAttributes){ .background_pixmap = None, // 替换原background_pixel设置 .event_mask = StructureNotifyMask | ExposureMask, // 新增ExposureMask处理窗口暴露事件 }
2. 使用离屏双缓冲
直接在窗口表面绘制会导致中间绘制步骤被显示,改用离屏表面先完成所有绘制,再一次性复制到窗口表面,消除绘制过程中的闪烁。
需要新增一个离屏表面变量,并在窗口尺寸变化时同步更新其大小:
_Atomic cairo_surface_t *offscreen_surface; // 新增离屏表面
在主函数中初始化离屏表面:
offscreen_surface = cairo_surface_create_similar(main_surface, CAIRO_CONTENT_COLOR, start_width, start_height);
在GUI线程中,先绘制到离屏表面,再复制到窗口表面:
int guiThread(void *const data) { cairo_t *const ctx = data; cairo_t *offscreen_ctx = cairo_create(offscreen_surface); while (!have_been_killed) { // 先清空离屏表面 cairo_set_source_rgb(offscreen_ctx, 70.0/255, 130.0/255, 180.0/255); cairo_paint(offscreen_ctx); // 这里可以添加其他GUI绘制逻辑,都在offscreen_ctx上操作 // 将离屏表面内容复制到窗口表面 cairo_set_source_surface(ctx, offscreen_surface, 0, 0); cairo_paint(ctx); cairo_surface_flush(main_surface); XFlush(display); thrd_sleep(&(struct timespec){.tv_nsec = 10'000'000}, nullptr); } cairo_destroy(offscreen_ctx); return 0; }
3. 同步尺寸变化与绘制逻辑
在处理ConfigureNotify事件时,同时更新主表面和离屏表面的尺寸,并且加锁避免线程冲突(因为GUI线程可能正在绘制):
新增互斥锁变量:
mtx_t surface_mutex;
主函数初始化锁:
mtx_init(&surface_mutex, mtx_plain);
修改ConfigureNotify事件处理:
case ConfigureNotify: { XConfigureEvent e = event.xconfigure; if (e.window != main_window) { break; } mtx_lock(&surface_mutex); cairo_xlib_surface_set_size(main_surface, e.width, e.height); // 重新创建离屏表面以匹配新尺寸 cairo_surface_destroy(offscreen_surface); offscreen_surface = cairo_surface_create_similar(main_surface, CAIRO_CONTENT_COLOR, e.width, e.height); mtx_unlock(&surface_mutex); // 发送Expose事件触发重绘(可选,确保窗口内容更新) XExposeEvent expose = { .type = Expose, .window = main_window, .x = 0, .y = 0, .width = e.width, .height = e.height, .count = 0 }; XSendEvent(display, main_window, False, ExposureMask, (XEvent*)&expose); break; }
同时在GUI线程绘制前加锁,避免尺寸修改时的冲突:
while (!have_been_killed) { mtx_lock(&surface_mutex); // 绘制逻辑... mtx_unlock(&surface_mutex); // flush和sleep... }
4. 处理Expose事件
新增Expose事件处理,确保窗口被遮挡后恢复时能正确重绘:
case Expose: { XExposeEvent e = event.xexpose; if (e.window == main_window && e.count == 0) { // 触发一次重绘,这里可以直接调用绘制逻辑或者标记需要重绘 // 因为GUI线程在循环绘制,也可以不用额外处理,但添加更稳妥 } break; }
完整修改后代码示例
#include <threads.h> #include <X11/Xlib.h> #include <cairo/cairo.h> #include <cairo/cairo-xlib.h> constexpr int start_width = 400; constexpr int start_height = 400; _Atomic bool have_been_killed = false; Display *_Atomic display; cairo_surface_t *_Atomic main_surface; cairo_surface_t *_Atomic offscreen_surface; mtx_t surface_mutex; int guiThread(void *const data); int main(int, char *[]) { display = XOpenDisplay(nullptr); if (!display) { return 1; } const Window root_window = XDefaultRootWindow(display); if (!root_window) { return 1; } mtx_init(&surface_mutex, mtx_plain); const Window main_window = XCreateWindow( display, root_window, 100, 100, start_width, start_height, 0, CopyFromParent, CopyFromParent, CopyFromParent, CWBackPixmap | CWEventMask, &(XSetWindowAttributes){ .background_pixmap = None, .event_mask = StructureNotifyMask | ExposureMask, } ); XWindowAttributes main_window_attributes; if (!XGetWindowAttributes(display, main_window, &main_window_attributes)) { return 1; } Atom WM_DELETE_WINDOW = XInternAtom(display, "WM_DELETE_WINDOW", true); if(!XSetWMProtocols(display, main_window, &WM_DELETE_WINDOW, 1)) { return 1; } main_surface = cairo_xlib_surface_create(display, main_window, main_window_attributes.visual, start_width, start_height); offscreen_surface = cairo_surface_create_similar(main_surface, CAIRO_CONTENT_COLOR, start_width, start_height); cairo_t *cairo = cairo_create(main_surface); if (!cairo) { return 1; } thrd_t gui_thread; if (thrd_create(&gui_thread, guiThread, cairo) != thrd_success) { return 1; } XMapWindow(display, main_window); XFlush(display); while (!have_been_killed) { XEvent event; XNextEvent(display, &event); switch (event.type) { case ConfigureNotify: { XConfigureEvent e = event.xconfigure; if (e.window != main_window) { break; } mtx_lock(&surface_mutex); cairo_xlib_surface_set_size(main_surface, e.width, e.height); cairo_surface_destroy(offscreen_surface); offscreen_surface = cairo_surface_create_similar(main_surface, CAIRO_CONTENT_COLOR, e.width, e.height); mtx_unlock(&surface_mutex); XExposeEvent expose = { .type = Expose, .window = main_window, .x = 0, .y = 0, .width = e.width, .height = e.height, .count = 0 }; XSendEvent(display, main_window, False, ExposureMask, (XEvent*)&expose); break; } case ClientMessage: { XClientMessageEvent e = event.xclient; if (e.window == main_window && (Atom)e.data.l[0] == WM_DELETE_WINDOW) { have_been_killed = true; } break; } case Expose: { // 无需额外操作,GUI线程会循环重绘 break; } default: break; } } thrd_join(gui_thread, nullptr); mtx_destroy(&surface_mutex); cairo_surface_destroy(offscreen_surface); cairo_destroy(cairo); cairo_surface_destroy(main_surface); XCloseDisplay(display); return 0; } int guiThread(void *const data) { cairo_t *const ctx = data; cairo_t *offscreen_ctx = cairo_create(offscreen_surface); while (!have_been_killed) { mtx_lock(&surface_mutex); if (!cairo_surface_status(offscreen_surface)) { // 绘制背景 cairo_set_source_rgb(offscreen_ctx, 70.0/255, 130.0/255, 180.0/255); cairo_paint(offscreen_ctx); // 这里添加其他GUI元素绘制逻辑,例如: // cairo_set_source_rgb(offscreen_ctx, 1, 0, 0); // cairo_rectangle(offscreen_ctx, 50, 50, 100, 100); // cairo_fill(offscreen_ctx); // 将离屏内容复制到窗口 cairo_set_source_surface(ctx, offscreen_surface, 0, 0); cairo_paint(ctx); } mtx_unlock(&surface_mutex); cairo_surface_flush(main_surface); XFlush(display); thrd_sleep(&(struct timespec){.tv_nsec = 10'000'000}, nullptr); } cairo_destroy(offscreen_ctx); return 0; }
关键修改说明
- 禁用X自动背景填充:让Cairo完全控制窗口内容,避免X服务器插入的背景绘制步骤。
- 离屏双缓冲:所有绘制操作先在后台完成,再一次性提交到窗口,消除中间绘制状态的可见性。
- 线程同步:用互斥锁保护表面尺寸修改和绘制操作,避免多线程冲突导致的异常。
- 处理Expose事件:确保窗口在被遮挡后恢复时能正确重绘,保持内容一致性。
内容的提问来源于stack exchange,提问作者Coarse Rosinflower
相关产品推荐
相关产品推荐

