点击蓝字 关注我们
本文介绍了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的区别:
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效率高。
后话
往期回顾
致力于互联网教育技术
的创新和推广
微信公众号@学而思网校技术团队