如何不重绘整个Surface擦除图形,实现Win11式动态截图区域选择
解决Cairo动态截图区域选择的闪烁与绘制异常问题
当前实现的功能是在屏幕上绘制半透明遮罩矩形,并从遮罩中抠出不透明的选择区域。目前的做法是循环重绘整个Surface和半透明矩形,但低配电脑上会出现闪烁;如果停止全量重绘,闪烁消失,但会出现绘制异常(由代码中注释的行导致)。需要实现不重绘整个Surface,仅擦除半透明矩形,完成类似Windows 11的动态截图区域选择功能。
原实现代码:
void OcctGtkViewer::buttonclick(int *x, int *y, int *width, int *height) { cairo_surface_t *surface = cairo_xlib_surface_create(xdisplay, xroot, DefaultVisual(xdisplay, scr), DisplayWidth(xdisplay, scr), DisplayHeight(xdisplay, scr)); cairo_t *cr = cairo_create(surface); const char *backgroundImagePath = "/tmp/test.png"; cairo_surface_write_to_png( surface, backgroundImagePath); cairo_surface_t *surfaceTmp = cairo_image_surface_create_from_png(backgroundImagePath); cairo_set_source_rgba(cr, 0, 0, 0, 0.2); cairo_rectangle(cr, 0, 0, DisplayWidth(xdisplay, scr), DisplayHeight(xdisplay, scr)); cairo_fill(cr); cairo_surface_t *surfaceTmpp = cairo_image_surface_create_from_png(backgroundImagePath); printf("select an area\n"); while (1) { event.type == MotionNotify && isPressed == true) { int tmp_x = xevent.xmotion.x_root; int tmp_y = xevent.xmotion.y_root; cairo_set_source_surface(cr, surfaceTmp, 1, 1); // cairo_paint(cr); cairo_set_fill_rule(cr, CAIRO_FILL_RULE_EVEN_ODD); // The default is CAIRO_FILL_RULE_WINDING. // cairo_set_source_rgba(cr, 0, 0, 0, 0.2); // cairo_rectangle(cr, 0, 0, DisplayWidth(xdisplay, scr), DisplayHeight(xdisplay, scr)); cairo_rectangle(cr, init_x, init_y, tmp_x - init_x, tmp_y - init_y); // set rectangle cairo_fill(cr); } } }
核心解决思路
要兼顾无闪烁和绘制正确,关键是只更新上一次选择区域与当前选择区域的差异部分,而非每次重绘全屏。具体步骤如下:
- 只捕获一次初始屏幕背景,绘制一次全屏半透明遮罩并保存状态,后续不再重复绘制全屏内容
- 记录上一次的选择区域坐标,每次鼠标移动时,先恢复上一次区域的背景+遮罩状态,再绘制新的选择区域
- 利用
CAIRO_FILL_RULE_EVEN_ODD实现遮罩抠图,但仅针对局部区域操作
修改后的完整代码
void OcctGtkViewer::buttonclick(int *x, int *y, int *width, int *height) { // 创建关联屏幕的Surface cairo_surface_t *surface = cairo_xlib_surface_create(xdisplay, xroot, DefaultVisual(xdisplay, scr), DisplayWidth(xdisplay, scr), DisplayHeight(xdisplay, scr)); cairo_t *cr = cairo_create(surface); const char *backgroundImagePath = "/tmp/test.png"; // 捕获初始屏幕背景并保存 cairo_surface_write_to_png(surface, backgroundImagePath); cairo_surface_t *bgSurface = cairo_image_surface_create_from_png(backgroundImagePath); // 创建临时Surface保存全屏半透明遮罩的状态,只绘制一次 cairo_surface_t *maskSurface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, DisplayWidth(xdisplay, scr), DisplayHeight(xdisplay, scr)); cairo_t *maskCr = cairo_create(maskSurface); cairo_set_source_rgba(maskCr, 0, 0, 0, 0.2); cairo_rectangle(maskCr, 0, 0, DisplayWidth(xdisplay, scr), DisplayHeight(xdisplay, scr)); cairo_fill(maskCr); cairo_destroy(maskCr); // 记录上一次选择区域的坐标,用于局部恢复 int last_x = 0, last_y = 0, last_w = 0, last_h = 0; bool hasLastRegion = false; printf("select an area\n"); while (1) { // 修正原代码的语法错误,处理鼠标移动事件 if (event.type == MotionNotify && isPressed == true) { int tmp_x = xevent.xmotion.x_root; int tmp_y = xevent.xmotion.y_root; int curr_w = tmp_x - init_x; int curr_h = tmp_y - init_y; // 先恢复上一次选择区域的原始状态(背景+遮罩) if (hasLastRegion) { // 绘制背景到上一次区域 cairo_set_source_surface(cr, bgSurface, 0, 0); cairo_rectangle(cr, last_x, last_y, last_w, last_h); cairo_fill(cr); // 绘制遮罩到上一次区域 cairo_set_source_surface(cr, maskSurface, 0, 0); cairo_rectangle(cr, last_x, last_y, last_w, last_h); cairo_fill(cr); } // 使用奇偶填充规则,绘制当前选择的抠图区域 cairo_set_fill_rule(cr, CAIRO_FILL_RULE_EVEN_ODD); // 先绘制当前区域的遮罩 cairo_set_source_surface(cr, maskSurface, 0, 0); cairo_rectangle(cr, init_x, init_y, curr_w, curr_h); // 叠加当前区域的背景,实现抠出透明效果 cairo_set_source_surface(cr, bgSurface, 0, 0); cairo_rectangle(cr, init_x, init_y, curr_w, curr_h); cairo_fill(cr); // 更新上一次区域的记录 last_x = init_x; last_y = init_y; last_w = curr_w; last_h = curr_h; hasLastRegion = true; // 局部刷新,避免全量屏幕刷新 XSync(xdisplay, False); } // 补充鼠标松开的退出逻辑 else if (event.type == ButtonRelease && isPressed == true) { isPressed = false; break; } // 等待X11事件 XNextEvent(xdisplay, &event); } // 清理资源,删除临时文件 cairo_surface_destroy(bgSurface); cairo_surface_destroy(maskSurface); cairo_destroy(cr); cairo_surface_destroy(surface); remove(backgroundImagePath); }
关键改进点
- 消除全量重绘:每次仅更新上一次和当前选择的区域,大幅减少绘制操作,解决低配电脑闪烁问题
- 复用遮罩状态:仅绘制一次全屏遮罩并保存,后续直接复用,避免重复绘制全屏
- 修复语法问题:补全原代码中缺失的
if判断,完善事件循环逻辑 - 局部刷新机制:使用
XSync只刷新修改的区域,进一步降低性能消耗
内容的提问来源于stack exchange,提问作者XoDefender
相关产品推荐
相关产品推荐

