cover_image

截屏、截窗口方法及在Qt中的实现和探索

罗晨涛 学而思网校技术团队
2022年06月02日 10:20

点击蓝字 关注我们

本文介绍了GDI和DXGI这两种截图方法,同时在Qt中实现了这几种截图方法,并进行了性能和效果上的比较。

本人是做教师端开发的,不论是最常用的三分屏教师端还是半身直播教师端,都离不开截图、截屏功能。三分屏中,截窗口、截桌面区域是上课是老师的一种教学工具,可以给学生们展示更加丰富的课程内容和一些课外补充。半身直播中(附图为半身直播间的3D图),老师在播放课件中的动画片时,可以直接通过截取课件截图,来给学生更加沉浸的动画体验。

图片

 教师端开发初,就有了截窗口、截屏幕区域的功能,当时用的是第一代桌面采集(GDI),是一个Windows 图形设备接口。在当时,这个不论从性能还是方案上完全满足三分屏的屏幕抓取功能的。

        后面随着半身直播的开发,屏幕要抓取的范围变大,包括后面AI抠图的开发,需要的抓取频率增加,导致单纯用GDI截屏方案的性能承受不住半身直播间如此大的消耗,在截图的时候笔记会出现卡顿。于是我们又调研和实现了DXGI这种高性能的截图方案。

        再后来,越来越多的软件使用了3D加速,导致GDI中的Bitblt方法会有部分窗口截不出来,这里也再介绍一种PrintWindow的方法。

GDI

先来讲讲GDI: 因为windows的应用程序是不能直接访问图形硬件(GPU)的,所以在windows中实现了一系列交互的接口(Graphics Device Interface),也就是我们所说的GDI。GDI 截图就是通过屏幕的DC(Device context,设备的描述表)获取到当前屏幕的位图数据。这种截图方法的好处是:不受windows版本限制,基本兼容各版本的系统;

QImage GrabWindowHelper::grabWindow(WId winId, bool needMouse){    QSize windowSize;    int x = 0;        int y = 0;        HWND hwnd = reinterpret_cast<HWND>(winId);        if (!hwnd) {                return QImage();        }    //以下部分在获取这个句柄应用的size        RECT r;        GetClientRect(hwnd, &r);        windowSize = QSize(r.right - r.left, r.bottom - r.top);        int width = windowSize.width();        int height = windowSize.height();         HDC display_dc = GetDC(nullptr);        HDC bitmap_dc = CreateCompatibleDC(display_dc);        HBITMAP bitmap = CreateCompatibleBitmap(display_dc, width, height);        HGDIOBJ null_bitmap = SelectObject(bitmap_dc, bitmap);        BOOL imgFlag = FALSE;HDC window_dc = GetDC(hwnd);  //获取句柄DC    imgFlag = BitBlt(bitmap_dc, 0, 0, width, height, window_dc, x, y, SRCCOPY | CAPTUREBLT); //使用Bitblt方法    ReleaseDC(hwnd, window_dc);        if (needMouse) {  //需要截取鼠标的话单独处理                CURSORINFO curinfo;                curinfo.cbSize = sizeof(curinfo);                GetCursorInfo(&curinfo);                POINT screenPt = curinfo.ptScreenPos;                ScreenToClient(hwnd, &screenPt);                DrawIcon(bitmap_dc, screenPt.x, screenPt.y, curinfo.hCursor);        }        SelectObject(bitmap_dc, null_bitmap);        DeleteDC(bitmap_dc);        QImage image;        if (imgFlag) {                image = QtWin::imageFromHBITMAP(bitmap);       }        DeleteObject(bitmap);        ReleaseDC(nullptr, display_dc);        return image;}

上面是用Bitblt方法实现的,有个StretchBlt方法,和Bitblt类似,只是增加了生成图片的缩放。

        其实如果是使用Qt写的话,是有现成的函数的:

QPixmap QScreen::grabWindow(WId window, int x = 0, int y = 0, int width = -1, int height = -1)

这个方法就是使用的Bitblt,当然上面还提到了printWindow,因为PrintWindow不是所有电脑都支持的,所以这里使用了Qt的QLibrary动态加载,来测试是否支持这个函数:

typedef BOOL(__stdcall *PtrPrintWindow )(HWND ,HDC ,UINT );  PtrPrintWindow GrabWindowHelper::printWindow() {      static bool hasTestPrintWindowFunction = false;      static PtrPrintWindow printWindowFunnction = nullptr;      if (hasTestPrintWindowFunction) {          return printWindowFunnction;      }       hasTestPrintWindowFunction = true;      printWindowFunnction = reinterpret_cast<PtrPrintWindow>(QLibrary::resolve("user32.dll", "PrintWindow"));       return printWindowFunnction; }

这样只需要将之前使用bitblt的地方换成printWindow,就可以了:

imgFlag = printWindow()(hwnd, bitmap_dc, PW_CLIENTONLY | PW_RENDERFULLCONTENT);

可以看一下使用printwindow和bitblt的区别:

图片

没有勾选使用printWindow的是使用的Bitblt方法,可以看到使用Bitblt的截图方法ppt只有个外围的框架,没截取到里面内容分,右下角完全黑的是我edge浏览器放的一个当前时间的网页。而使用print后,两个均能截出来。
printwindow其实是给对方发了个WN_PAINTER消息,对方响应了这个消息就能截出来,频率高了的话可能出现闪屏现象。

DXGI

下面讲讲DXGI:

DXGI其实是Microsoft DirectX Graphics Infrastructure的做些,是微软提供的一种可以在win8及以上系统使用的图形设备接口。它负责枚举图形适配器、枚举显示模式、选择缓冲区格式、在进程之间共享资源以及将呈现的帧传给窗口或监视器以供显示。其直接和硬件设备进行交互,具有很高的效率和性能。

DXGI最大的有点就是效率高,CPU占用非常低。

它的缺点是:

1 有版本要求,win8以上才支持。

2 因为采集需要获取设备的adapter,所以无法采集桌面窗口。

不过因为在半身直播间 1 和 2 都不成问题,所以接下来就开干。

BOOL Init(){        int adaptIndex = 0, outputIndex = 0;        QList<ScreenOutput> list;        bool flag = getScreens(list); //获取屏幕列表        if (!flag || list.size() == 0) {                return false;        }        adaptIndex = list.at(0).adaptorIndex;        outputIndex = list.at(0).outputIndex;        HRESULT hr = S_OK;        if (m_bInit) {        return FALSE;        }        // Driver types supported        D3D_DRIVER_TYPE DriverTypes[] =        {                D3D_DRIVER_TYPE_HARDWARE,                D3D_DRIVER_TYPE_WARP,                D3D_DRIVER_TYPE_REFERENCE,        };        UINT NumDriverTypes = ARRAYSIZE(DriverTypes);        // Feature levels supported        D3D_FEATURE_LEVEL FeatureLevels[] =        {                D3D_FEATURE_LEVEL_11_0,                D3D_FEATURE_LEVEL_10_1,                D3D_FEATURE_LEVEL_10_0,                D3D_FEATURE_LEVEL_9_1        };        UINT NumFeatureLevels = ARRAYSIZE(FeatureLevels);        D3D_FEATURE_LEVEL FeatureLevel;        // 创建 D3D device        for (UINT DriverTypeIndex = 0; DriverTypeIndex < NumDriverTypes; ++DriverTypeIndex) {        hr = D3D11CreateDevice(NULL, DriverTypes[DriverTypeIndex], NULL, 0, FeatureLevels, NumFeatureLevels, D3D11_SDK_VERSION, &m_hDevice, &FeatureLevel, &m_hContext);        if (SUCCEEDED(hr)) {            break;                }        }        if (FAILED(hr)) {        return FALSE;        }            // 获取 DXGI device        IDXGIDevice *hDxgiDevice = NULL;        hr = m_hDevice->QueryInterface(__uuidof(IDXGIDevice), reinterpret_cast<void**>(&hDxgiDevice));        if (FAILED(hr)){                return FALSE;        }        // 获取 DXGI adapter        IDXGIFactory* pFactory;        hr = CreateDXGIFactory(__uuidof(IDXGIFactory), (void**)(&pFactory));        IDXGIAdapter* hDxgiAdapter = nullptr;        hr = pFactory->EnumAdapters(0, &hDxgiAdapter);        RESET_OBJECT(hDxgiDevice);        if (FAILED(hr)) {                 return FALSE;         }        int nOutput = outputIndex;         IDXGIOutput *hDxgiOutput = NULL;         hr = hDxgiAdapter->EnumOutputs(nOutput, &hDxgiOutput);         RESET_OBJECT(hDxgiAdapter);         if (FAILED(hr)) {                 return FALSE;         }         hDxgiOutput->GetDesc(&m_dxgiOutDesc);         IDXGIOutput1 *hDxgiOutput1 = NULL;         hr = hDxgiOutput->QueryInterface(__uuidof(hDxgiOutput1), reinterpret_cast<void**>(&hDxgiOutput1));         RESET_OBJECT(hDxgiOutput);         if (FAILED(hr)) {                 return FALSE;        }         hr = hDxgiOutput1->DuplicateOutput(m_hDevice, &m_hDeskDupl);         RESET_OBJECT(hDxgiOutput1);         if (FAILED(hr)) {                 return FALSE;         }         // 初始化成功         m_bInit = TRUE;         return TRUE; }

这个要注意:初始化的线程一定要在一个没有处理过UI的线程,否则会初始化失败

接下来就是截图了:

BOOL QueryFrame(QRect &rect, void *pImgData, INT &nImgSize){        if (!m_bInit || !AttatchToThread()) {            return FALSE;        }       DXGI_OUTDUPL_FRAME_INFO FrameInfo;        IDXGIResource *hDesktopResource = NULL;        HRESULT hr = m_hDeskDupl->AcquireNextFrame(20, &FrameInfo, &hDesktopResource);        if (FAILED(hr)) {               hDesktopResource = nullptr;                return TRUE;         }        // query next frame staging buffer         ID3D11Texture2D *hAcquiredDesktopImage = NULL;         hr = hDesktopResource->QueryInterface(__uuidof(ID3D11Texture2D), reinterpret_cast<void **>(&hAcquiredDesktopImage));         RESET_OBJECT(hDesktopResource);         if (FAILED(hr)) {                 return FALSE;       }         // copy old description         D3D11_TEXTURE2D_DESC frameDescriptor;         hAcquiredDesktopImage->GetDesc(&frameDescriptor);         // create a new staging buffer for fill frame image         ID3D11Texture2D *hNewDesktopImage = NULL;         frameDescriptor.Usage = D3D11_USAGE_STAGING;         frameDescriptor.CPUAccessFlags = D3D11_CPU_ACCESS_READ;         frameDescriptor.BindFlags = 0;         frameDescriptor.MiscFlags = 0;         frameDescriptor.MipLevels = 1;         frameDescriptor.ArraySize = 1;         frameDescriptor.SampleDesc.Count = 1;         hr = m_hDevice->CreateTexture2D(&frameDescriptor, NULL, &hNewDesktopImage);          if (FAILED(hr)) {                  RESET_OBJECT(hAcquiredDesktopImage);                  m_hDeskDupl->ReleaseFrame();                  return FALSE;         }          // copy next staging buffer to new staging buffer         m_hContext->CopyResource(hNewDesktopImage, hAcquiredDesktopImage);          RESET_OBJECT(hAcquiredDesktopImage);          m_hDeskDupl->ReleaseFrame();          // create staging buffer for map bits          static IDXGISurface *hStagingSurf = NULL;          if (hStagingSurf == NULL) {                  hr = hNewDesktopImage->QueryInterface(__uuidof(IDXGISurface), (void **)(&hStagingSurf));                  RESET_OBJECT(hNewDesktopImage);                  if (FAILED(hr)) {                          return FALSE;                  }          }          // copy bits to user space          DXGI_MAPPED_RECT mappedRect;          hr = hStagingSurf->Map(&mappedRect, DXGI_MAP_READ);          if (SUCCEEDED(hr)) {          QRect desttopRect(m_dxgiOutDesc.DesktopCoordinates.left, m_dxgiOutDesc.DesktopCoordinates.top, m_dxgiOutDesc.DesktopCoordinates.right - m_dxgiOutDesc.DesktopCoordinates.left, m_dxgiOutDesc.DesktopCoordinates.bottom - m_dxgiOutDesc.DesktopCoordinates.top);                  copyImageByRect((char*)mappedRect.pBits, desttopRect.size(), (char*)pImgData, nImgSize, rect);                  hStagingSurf->Unmap();          }          RESET_OBJECT(hStagingSurf);          return SUCCEEDED(hr);  }

DXGI GDI 性能比较

最后比较一下在直播间用GDI方法和DXGI方法的CPU和GPU占用情况。

图片

很明显,DXGI使用的GPU效率高。

后话

 为了适应当今高清、高效的互联网时代,出现了很多其他的截图技术,比如Magnification、Window Graphics Capturer等等,要不断学习新的技术,适应新的环境。

往期回顾


· grpc与http的错误传递

· 网校web直播框架之涂鸦套索原理大揭密

· iOS端人脸贴纸实践

· RTMP直播和Nginx-RTMP实践

图片

致力于互联网教育技术

的创新和推广

 微信公众号@学而思网校技术团队

继续滑动看下一个
学而思网校技术团队
向上滑动看下一个