# XGetImage
XGetImage 是最简单的获取窗口图像的方法。
return XGetImage(
display,
window,
0,
0,
width,
height,
AllPlanes,
ZPixmap
);
# DISPLAY
display 是 X11 中显示器的抽象概念,不一定指一个物理显示器,也可能是一个由 Xvfb 创建的虚拟显示器。它可以由以下 API 获取:
return XOpenDisplay(display.c_str());
其中,display
是一个屏幕的标识符,它可以是类似 :99
的形式(Xvfb 创建的默认 DISPLAY),或是 NULL,表示当前 DISPLAY。
# Window
Window 即窗口,标识着一个应用程序窗口。因为 X11 是 C/S 架构,所以窗口的数据也分别存放在服务端和客户端两部分。在客户端这边的图像叫做 XImage,在服务端那边则叫做 Pixmap。X11 中 Window 对象只是一个标识,它可以使用以下方式获得:
RootWindow(display, DefaultScreen(display));
上面是获取当前显示器的根窗口,也可以通过 XGetInputFocus
获取当前焦点所在的窗口:
XGetInputFocus(display, &focused, &unknown);
还可以通过 XGetWindowAttributes
获取窗口的一些信息:
XWindowAttributes window_attributes;
XGetWindowAttributes(display, window, &window_attributes);
# XImage
从 XGetImage
可以获取 XImage 对象,这个对象中包含着窗口/屏幕(根窗口)的像素、尺寸、对齐格式等信息。
对于 XImage,我们可以调用 XGetPixel
函数来获取某个像素的信息。但是这种方式需要进行额外拷贝,因此 CPU 占用率较高。比较常见的方式还是直接读取它的 data
属性。这是 char*
类型数据,包含着全部的图片像素信息,对齐格式为 BGRA,即使用四个 u8 存储一个像素信息。尺寸可以使用如下方式计算获得:
im.height * im.width * 4
另外,它的顺序是从左上到右下,与 OpenGL 纹理 Y 轴相反,因此在使用 OpenGL 时需要提前处理图像反转 Y 轴,或在创建纹理时修改纹理坐标。
# XShm
正是因为 XGetImage 每次从服务端申请图像资源,因此需要经过多次分配和拷贝,性能较低,因此很多程序会选择使用 XShmGetImage 的方法进行替代。XShm 是一个 X11 扩展,提供了通过共享内存获取图像的方法。它可以做到一次分配多次使用,从而减少了内存分配和拷贝次数。
void XwrapWindow::begin_get_image() {
int screen = DefaultScreen(display);
auto attr = this->get_attributes();
auto image = XShmCreateImage(
display,
attr.visual,
attr.depth,
ZPixmap,
NULL,
&shminfo,
attr.width,
attr.height
);
shminfo.shmid = shmget(
IPC_PRIVATE,
image->bytes_per_line * image->height,
IPC_CREAT | 0777
);
shminfo.shmaddr = image->data = (char*)shmat(shminfo.shmid, 0, 0);
shminfo.readOnly = False;
XShmAttach(display, &shminfo);
this->image = image;
}
使用 XShm 需要先进行初始化,这包括共享内存区的申请和图像内存分配。
void XwrapWindow::get_xshm_image() {
XShmGetImage(display, window, image, 0, 0, AllPlanes);
}
获取图像方式与 XGetImage
基本一致,只是无需指定要获取的图片尺寸,这些信息包括在了已经分配好了的 image 对象中。这个函数可以在程序中多次调用。
void XwrapWindow::end_get_image() {
XShmDetach(display, &shminfo);
shmdt(shminfo.shmaddr);
shmctl(shminfo.shmid, IPC_RMID, 0);
XDestroyImage(image);
}
在析构阶段,首先释放共享内存区,然后销毁图片。
# 后话
除了这两种方式,还可以利用 XComposite
扩展。具体思路大概是重定向窗口,然后获取窗口的 Pixmap,从中获取图像。可惜几经尝试无法成功。
另外,通过 X11 API 无法获取最小化或在其他虚拟桌面的窗口。当然,具体还要看窗口管理器策略。有些窗口管理器会创建一个相当大的屏幕,然后将不需要的窗口移出当前视图,这样窗口仍然被认为在当前屏幕上,是可以进行捕获的。但类似 i3 这种使用 MAP 和 UNMAP 管理窗口的就不行了,因为窗口被 UNMAP 之后就不会进行更新,也无法获取 Pixmap,此时调用 XGetImage 大概率会获得一个全黑图像。