PAYATHON 2026

使用 libxcb 和 Cairo 时如何正确处理 XCB_RESIZE_REQUEST?

数据小许

结论

对于这个普通的 X11 应用窗口,应将 XCB_EVENT_MASK_RESIZE_REDIRECT 改为 XCB_EVENT_MASK_STRUCTURE_NOTIFY。收到 XCB_CONFIGURE_NOTIFY 后,用事件中的 width、height 调用 cairo_xcb_surface_set_size(),再重绘窗口。

当前代码选择了 ResizeRedirect,接管了其他客户端发起的尺寸修改请求,却没有实际调整 X11 窗口的大小。单独调用 cairo_xcb_surface_set_size() 只会更新 Cairo 对 drawable 尺寸的记录。Cairo 的 XCB surface 文档也指出,顶层窗口应通过 ConfigureNotify 跟踪尺寸变化。

为什么外框变化了,客户端区域却没有变化?

XCB_RESIZE_REQUEST 表示其他客户端请求修改窗口尺寸。选择 XCB_EVENT_MASK_RESIZE_REDIRECT 后,如果 ConfigureWindow 请求符合协议条件并尝试改变窗口宽高,X server 会发送 ResizeRequest,同时保留窗口原来的宽高。应用需要自行决定是否执行尺寸修改。X11 协议说明

示例中的过程是:

  1. 窗口管理器尝试调整客户端窗口尺寸。
  2. 应用收到 XCB_RESIZE_REQUEST,客户端窗口仍保持原来的尺寸。
  3. 应用只修改了 Cairo surface 的尺寸记录,没有修改 X11 窗口本身。

这可以解释为什么外框变化了,客户端绘制区域和鼠标响应范围却没有变化。Cairo 不负责决定 X11 窗口的输入区域。

推荐修改:监听 ConfigureNotify 并在同一线程重绘

1. 修改窗口的事件掩码

保留原来的 xcb_create_window() 调用,将最后一个参数改为:

(uint32_t []){
    screen->black_pixel,
    XCB_EVENT_MASK_STRUCTURE_NOTIFY | XCB_EVENT_MASK_EXPOSURE
}

XCB_EVENT_MASK_STRUCTURE_NOTIFY 用于接收 ConfigureNotify 等结构变化通知,XCB_EVENT_MASK_EXPOSURE 用于在窗口内容需要恢复时触发重绘。其他已有的输入事件掩码仍可通过按位或合并。XCB 事件教程

需要移除 XCB_EVENT_MASK_RESIZE_REDIRECT;如果只是添加新掩码,原来的尺寸拦截行为仍会保留。

2. 替换绘制线程和事件循环

对于示例中的纯色窗口,可以删除 guiThread() 和 thrd_create(),在主线程中完成事件处理、surface 尺寸更新和绘制。

创建 main_surface 后,用下面的代码替换原来的线程创建、事件循环以及末尾的断开连接操作。该片段基于已有的初始化代码:

cairo_t *const ctx = cairo_create(main_surface);
cairo_set_source_rgb(ctx, 0.5, 0, 0.5);

int surface_width = start_width;
int surface_height = start_height;

while (!have_been_killed) {
    xcb_generic_event_t *event = xcb_wait_for_event(connection);
    if (!event) {
        break;
    }

    int repaint = 0;

    switch (event->response_type & 0x7f) {
    case XCB_CONFIGURE_NOTIFY: {
        const xcb_configure_notify_event_t *e =
            (const xcb_configure_notify_event_t *)event;

        if (e->window == main_window &&
            (e->width != surface_width ||
             e->height != surface_height)) {
            cairo_surface_flush(main_surface);
            cairo_xcb_surface_set_size(main_surface,
                                       e->width, e->height);
            surface_width = e->width;
            surface_height = e->height;
            repaint = 1;
        }
        break;
    }

    case XCB_EXPOSE: {
        const xcb_expose_event_t *e =
            (const xcb_expose_event_t *)event;
        if (e->window == main_window && e->count == 0) {
            repaint = 1;
        }
        break;
    }

    case XCB_DESTROY_NOTIFY: {
        const xcb_destroy_notify_event_t *e =
            (const xcb_destroy_notify_event_t *)event;
        if (e->window == main_window) {
            have_been_killed = true;
        }
        break;
    }

    default:
        break;
    }

    free(event);

    if (repaint && !have_been_killed) {
        cairo_paint(ctx);
        cairo_surface_flush(main_surface);
        xcb_flush(connection);
    }
}

cairo_destroy(ctx);
cairo_surface_destroy(main_surface);
xcb_disconnect(connection);
return 0;

移动窗口等操作也可能触发 ConfigureNotify,因此代码会先比较宽高,只在尺寸变化时更新 surface。response_type & 0x7f 用于去掉事件类型中的发送标志位,让事件分派也能匹配通过 SendEvent 发送的通知。X11 事件格式

更新尺寸前调用 cairo_surface_flush(),可以提交尚未完成的绘制操作。Cairo 文档提示,直接修改尺寸可能丢弃部分待处理操作。接口说明

如果确实需要处理 XCB_RESIZE_REQUEST

如果应用有意接管尺寸请求,就需要在事件处理函数中调用 xcb_configure_window(),实际提交窗口尺寸修改。例如,可以接受请求中的宽高:

case XCB_RESIZE_REQUEST: {
    const xcb_resize_request_event_t *e =
        (const xcb_resize_request_event_t *)event;

    if (e->window == main_window) {
        const uint32_t values[] = {e->width, e->height};
        xcb_configure_window(
            connection,
            main_window,
            XCB_CONFIG_WINDOW_WIDTH | XCB_CONFIG_WINDOW_HEIGHT,
            values
        );
        xcb_flush(connection);
    }
    break;
}

这种方案还应选择 XCB_EVENT_MASK_STRUCTURE_NOTIFY,在后续的 ConfigureNotify 中同步 Cairo。请求中的尺寸是期望值,收到请求并不代表窗口已经调整成功;父窗口上的 SubstructureRedirect 也可能影响请求处理。X11 ConfigureWindow 规则

原示例还需要注意线程同步

原代码在绘制线程调用 cairo_paint(),同时在主线程修改同一个 surface。将 connection 指针声明为 _Atomic,不能同步这些 Cairo 操作。

如果保留绘制线程,需要同步对共享 Cairo 对象的访问,或者把尺寸更新通知交给绘制线程处理。退出时还应先停止并等待绘制线程,再销毁 Cairo 对象、断开 XCB 连接,避免线程继续使用已经释放的资源。

上面的单线程替换片段解决了这处并发访问问题,但没有补充完整的窗口关闭协议和错误处理。正式应用仍应按需要处理 WM_DELETE_WINDOW,并检查 XCB、Cairo 的错误状态。

备注:内容仅供参考。