Loading

Linux X11获取屏幕截图和程序窗口截图,并通过QPixmap显示

最近接手一个项目,需要UOS系统上实现屏幕和窗口捕获。

由于QT只提供了屏幕捕获功能,没有提供窗口捕获,于是就找到了老朋友——OBS(毕竟MacOS的屏幕捕获也是扒的OBS代码)。

幸运的是,UOS系统商店自带OBS,直接省去了编译环节。

  

从上图可以看出,OBS自带屏幕捕获和窗口捕获,窗口捕获也可以获取当前桌面上打开的页面。

试用了一下,窗口捕获时最小化窗口,会导致程序卡死,也就是说最小化时获取不到图像。问题不大,能用就行。

看了一下obs源码,相关代码在plugins->linux-capture里。

用到的主要是X11库。

整理了一下obs和网上的代码,实现屏幕捕获+窗口捕获。

包含头文件

#include <X11/Xlib.h>
#include <X11/Xatom.h>
#include <X11/Xutil.h>

调用库,在xx.pro文件里添加

LIBS += -lX11

窗口列举、获取窗口名称

//获取窗口图片后,在QListWidget中显示,图片缩放大小为210*100
const
QSize IMAGE_SIZE(210, 100); const QSize ITEM_SIZE(210, 120); static Display *xdisplay = 0; Display *disp() { if (!xdisplay) xdisplay = XOpenDisplay(NULL); return xdisplay; } void cleanupDisplay() { if (!xdisplay) return; XCloseDisplay(xdisplay); xdisplay = 0; } bool ewmhIsSupported() { Display *display = disp(); Atom netSupportingWmCheck = XInternAtom(display, "_NET_SUPPORTING_WM_CHECK", true); Atom actualType; int format = 0; unsigned long num = 0, bytes = 0; unsigned char *data = NULL; Window ewmh_window = 0; int status = XGetWindowProperty(display, DefaultRootWindow(display), netSupportingWmCheck, 0L, 1L, false, XA_WINDOW, &actualType, &format, &num, &bytes, &data); if (status == Success) { if (num > 0) { ewmh_window = ((Window *)data)[0]; } if (data) { XFree(data); data = NULL; } } if (ewmh_window) { status = XGetWindowProperty(display, ewmh_window, netSupportingWmCheck, 0L, 1L, false, XA_WINDOW, &actualType, &format, &num, &bytes, &data); if (status != Success || num == 0 || ewmh_window != ((Window *)data)[0]) { ewmh_window = 0; } if (status == Success && data) { XFree(data); } } return ewmh_window != 0; } //枚举所有窗口 std::list<Window> getTopLevelWindows() { std::list<Window> res; if (!ewmhIsSupported()) { qDebug("Unable to query window list " "because window manager " "does not support extended " "window manager Hints"); return res; } Atom netClList = XInternAtom(disp(), "_NET_CLIENT_LIST", true); Atom actualType; int format; unsigned long num, bytes; Window *data = 0; for (int i = 0; i < ScreenCount(disp()); ++i) { Window rootWin = RootWindow(disp(), i); int status = XGetWindowProperty(disp(), rootWin, netClList, 0L, ~0L, false, AnyPropertyType, &actualType, &format, &num, &bytes, (uint8_t **)&data); if (status != Success) { qDebug("Failed getting root " "window properties"); continue; } for (unsigned long i = 0; i < num; ++i) res.push_back(data[i]); XFree(data); } return res; } std::string getWindowAtom(Window win, const char *atom) { Atom netWmName = XInternAtom(disp(), atom, false); int n; char **list = 0; XTextProperty tp; std::string res = "unknown"; XGetTextProperty(disp(), win, &tp, netWmName); if (!tp.nitems) XGetWMName(disp(), win, &tp); if (!tp.nitems) return "error"; if (tp.encoding == XA_STRING) { res = (char *)tp.value; } else { int ret = XmbTextPropertyToTextList(disp(), &tp, &list, &n); if (ret >= Success && n > 0 && *list) { res = *list; XFreeStringList(list); } } XFree(tp.value); return res; } inline std::string getWindowName(Window win) { return getWindowAtom(win, "_NET_WM_NAME"); }

将窗口显示到QListWidget中

int index = 0;
for (Window win : getTopLevelWindows()) {
//获取窗口属性 XWindowAttributes attrs; XGetWindowAttributes(disp(), win,
&attrs);

//XGetImage获取XImage,并通过转换得到QPixmap XImage
* pImage = XGetImage(disp(), win, 0, 0, attrs.width, attrs.height, AllPlanes, ZPixmap); QImage image = QImage((const uchar *)(pImage->data), pImage->width, pImage->height, pImage->bytes_per_line, QImage::Format_RGB32); QPixmap pixmap = QPixmap::fromImage(image);
   //将QPixmap和窗口名添加到QListWidgetItem中,并将item添加到QListWidget中 QListWidgetItem
*listWidgetItemScreen = new QListWidgetItem(QIcon(pixmap.scaled(IMAGE_SIZE)), getWindowName(win).c_str()); listWidgetItemScreen->setSizeHint(ITEM_SIZE); ui->listWidget->insertItem(index++, listWidgetItemScreen);
}

获取整个屏幕图像并显示到QListWidget中

//获取屏幕Window属性使用RootWindow函数获取
Window rootwin = RootWindow(disp(),0);
XWindowAttributes attrs; XGetWindowAttributes(disp(), rootwin,
&attrs); XImage * pImage = XGetImage(disp(), rootwin, 0, 0, attrs.width, attrs.height, AllPlanes, ZPixmap); QImage image = QImage((const uchar *)(pImage->data), pImage->width, pImage->height, pImage->bytes_per_line, QImage::Format_RGB32); QPixmap pixmap = QPixmap::fromImage(image); QListWidgetItem *listWidgetItemScreen = new QListWidgetItem(QIcon(pixmap.scaled(IMAGE_SIZE)), "Screen"); listWidgetItemScreen->setSizeHint(ITEM_SIZE); ui->listWidget->insertItem(index++, listWidgetItemScreen);

可以看出屏幕截图跟窗口截图的区别就是获取Window参数方法不同。使用XGetImage获取图像完全相同。

再进一步观察可以发现,在获取窗口Window时,先调用 RootWindow 获取当前屏幕的Window,然后使用 XGetWindowProperty 获取当前屏幕下的所有窗口。

最终实现效果:

捕获到了Desktop和QT这两个程序的窗口图像,以及整个屏幕的图像。

但是Desktop为什么是黑的?

原来XGetImage获取到的是窗口的可见部分,运行程序时,QT处于最大化,把整个Desktop都挡住了,获取到的图像就是全黑的,把QT缩小以后在运行一遍试试。

DDE Dock是dock栏的图像,被拉伸以后就这么奇怪。

可以看出Desktop页面也显示出来了,并且Dock栏和QT遮挡住的部分是黑色。

程序目标可以说达成一半了。

顺便说一个程序运行时出现的崩溃错误

Debug发现程序崩溃在XImage转QImage这里。传来的XImage是一个空指针,导致程序崩溃。

 QImage image = QImage((const uchar *)(pImage->data), pImage->width, pImage->height, pImage->bytes_per_line, QImage::Format_RGB32); 

崩溃在QT Creator窗口,当时的情况是

当我把QT最大化或者移动到桌面最中间时,程序又能正常运行了。

经过反复试验得出了一个结论,如果窗口有一部分位于屏幕外,XGetImage就会返回空指针。

修改一下代码,只截屏显示在桌面的部分,截去在屏幕外的部分

    int width = DisplayWidth(disp(),0),height = DisplayHeight(disp(),0);
    int index = 0;
    for (Window win : getTopLevelWindows()) {
        XWindowAttributes attrs;
        XGetWindowAttributes(disp(), win, &attrs);int x1 = attrs.x,y1 = attrs.y;
        int x2=attrs.width+x1,y2=attrs.height+y1;
        if (x1<0)x1 = 0;
        if (y1<0)y1=0;
        if (x2>width)x2=width;
        if (y2>height)y2=height;

        XImage * pImage = XGetImage(disp(), win, 0, 0, x2-x1, y2-y1, AllPlanes, ZPixmap);
        QImage image = QImage((const uchar *)(pImage->data), pImage->width, pImage->height, pImage->bytes_per_line, QImage::Format_RGB32);
        QPixmap pixmap = QPixmap::fromImage(image);

        QListWidgetItem *listWidgetItemScreen = new QListWidgetItem(QIcon(pixmap.scaled(IMAGE_SIZE)), getWindowName(win).c_str());
        listWidgetItemScreen->setSizeHint(ITEM_SIZE);
        ui->listWidget->insertItem(index++, listWidgetItemScreen);
    }

发现还是会崩溃,调试发现attrs.x和attrs.y永远都是0,并不是窗口的实际坐标。

谷歌一下才知道,要想获取当前窗口在屏幕上的坐标,需要用 XTranslateCoordinates 转换一下

 Window child;
 int x, y;
 XTranslateCoordinates(disp(), win, attrs.root, 0, 0, &x, &y,&child);

x和y就是窗口的实际坐标。

改一下代码,运行成功

大功告成(一半)。

对比OBS,OBS可以获取窗口的完整图像,哪怕窗口有一部分在屏幕外。还需要继续研究obs代码。。。

未完待续。

posted @ 2021-11-21 22:19  柴承训  阅读(2782)  评论(0编辑  收藏  举报