最近接手一個項目,需要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代碼。。。
未完待續。