You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何解决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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.01 15:24:54