【写在前面】
最近在尝试做一个简单的屏幕控制,本以为截屏应该是最简单的部分,没想到在 windows 下有很大的问题。
其中主要的问题在于截屏效率,查了很多资料以后发现巨多坑。
本篇只讲几种截屏的方式,不讲屏幕控制( 因为比起截屏反而是很简单的部分 )。
注意:后面我专门实现了一个采集库集成了本篇的技术,并且简单易于使用,地址:
【正文开始】
1、首先,因为使用的 Qt,立即能想到的截屏方式是:
QPixmap pixmap = QGuiApplication::primaryScreen()->grabWindow(0);
这种方法在我的电脑上平均花费30ms的时间截取一帧,当然我的电脑很辣鸡,所以一般来说这个性能已经足够了,但是一个屏幕控制,也不仅仅是截屏这一部分,还包括其他诸如比对、压缩、分片、传输等等,所以实际上花费时间远不止30ms。
但是截屏的时间仍是大头。
2、使用 GDI 进行截屏:
头文件:windows.h
pro文件需要加入:LIBS += -lGdi32
这种方法在我的机器上面平均花费30ms。
int width = GetSystemMetrics(SM_CXSCREEN);
int height = GetSystemMetrics(SM_CYSCREEN);
HWND hwnd = GetDesktopWindow();
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);
// copy data
HDC window_dc = GetDC(hwnd);
BitBlt(bitmap_dc, 0, 0, width, height, window_dc, 0, 0, SRCCOPY | CAPTUREBLT);
// clean up all but bitmap
ReleaseDC(hwnd, window_dc);
SelectObject(bitmap_dc, null_bitmap);
DeleteDC(bitmap_dc);
screen = QtWin::fromHBITMAP(bitmap);
DeleteObject(bitmap);
ReleaseDC(nullptr, display_dc);
其中:
QtWin::fromHBITMAP(),Qt 提供的 windows 下特有的便利函数,可以将 HBITMAP 转换成 QPixmap。
头文件 #include <QtWin>
pro文件: QT += winextras
这段代码是从 Qt 源码中截取下来的,也就是说,Qt 在 windows 下使用 GDI 方式进行截图,性能确实不错了,但听说使用 DirectX 可能更快,然后我就去找了一些方法尝试,结果。。
3、使用 D3D 进行截屏:
头文件:d3d9.h d3dx9tex.h
pro文件需要加入:LIBS += -ld3d9 -lD3dx9
这种方法在我的渣机上面平均花费60ms。
LPDIRECT3D9 lpD3D = nullptr;
LPDIRECT3DDEVICE9 lpDevice = nullptr;
D3DDISPLAYMODE ddm;
lpD3D = Direct3DCreate9(D3D_SDK_VERSION);
lpD3D->GetAdapterDisplayMode(D3DADAPTER_DEFAULT, &ddm);
D3DPRESENT_PARAMETERS d3dpp;
ZeroMemory(&d3dpp, sizeof(D3DPRESENT_PARAMETERS));
d3dpp.Windowed = true;
d3dpp.Flags = D3DPRESENTFLAG_LOCKABLE_BACKBUFFER;
d3dpp.BackBufferFormat = ddm.Format;
d3dpp.BackBufferHeight = ddm.Height;
d3dpp.BackBufferWidth = ddm.Width;
d3dpp.MultiSampleType = D3DMULTISAMPLE_NONE;
d3dpp.SwapEffect = D3DSWAPEFFECT_DISCARD;
d3dpp.hDeviceWindow = GetDesktopWindow();
d3dpp.PresentationInterval = D3DPRESENT_INTERVAL_DEFAULT;
d3dpp.FullScreen_RefreshRateInHz = D3DPRESENT_RATE_DEFAULT;
lpD3D->CreateDevice(D3DADAPTER_DEFAULT, D3DDEVTYPE_HAL, GetDesktopWindow(),
D3DCREATE_SOFTWARE_VERTEXPROCESSING, &d3dpp, &lpDevice);
if (lpDevice)
{
LPDIRECT3DSURFACE9 surface;
lpDevice->CreateOffscreenPlainSurface(ddm.Width, ddm.Height, D3DFMT_A8R8G8B8, D3DPOOL_SCRATCH, &surface, nullptr);
lpDevice->GetFrontBufferData(0, surface);
LPD3DXBUFFER bufferedImage = nullptr;
D3DXSaveSurfaceToFileInMemory(&bufferedImage, D3DXIFF_JPG, surface, nullptr, nullptr);
screen.loadFromData((uchar *)bufferedImage->GetBufferPointer(), bufferedImage->GetBufferSize(), "JPG");
surface->Release();
}
一开始我使用的是后备缓冲,GetBackBufferData(),但这个获取到的一直都是黑屏,查了查发现说是 windows 桌面不使用后备缓冲,只能使用前置缓冲,但是性能简直不能看(我电脑太辣鸡也是很大的原因,用法上应该是没问题的)。
D3DXSaveSurfaceToFileInMemory() 将表面保存至文件存储在内存,D3DXSaveSurfaceToFile() 则存储于本地。
4、使用 DXGI 进行截屏:
注意:DXGI 在 Windows 8 及以上提供。
头文件:windows.h dxgi1_6.h d3d11.h
pro 文件需要加入:LIBS += -lD3D11 -lDXGI
这种方法在我的机器上面平均花费10ms。
DXGI 使用起来比较麻烦,分三个步骤:
1、初始化各种接口,设备:
bool DxgiManager::init()
{
ID3D11Device *d3dDevice = nullptr;
ID3D11DeviceContext *d3dContext = nullptr;
D3D_FEATURE_LEVEL feat = D3D_FEATURE_LEVEL_11_0;
HRESULT hr = D3D11CreateDevice(nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, 0, nullptr, 0, D3D11_SDK_VERSION, &d3dDevice, &feat, &d3dContext);
if (FAILED(hr)) {
m_lastError = "Failed to D3D11CreateDevice ErrorCode = " + QString::number(uint(hr), 16);
return false;
}
IDXGIDevice *dxgiDevice = nullptr;
hr = d3dDevice->QueryInterface(__uuidof(IDXGIDevice), reinterpret_cast<void**>(&dxgiDevice));
if(FAILED(hr)) {
m_lastError = "Failed to QueryInterface IDXGIOutput6 ErrorCode = " + QString::number(uint(hr), 16);
return false;
}
IDXGIAdapter *dxgiAdapter = nullptr;
hr = dxgiDevice->GetParent(__uuidof(IDXGIAdapter), reinterpret_cast<void**>(&dxgiAdapter));
dxgiDevice->Release();
if (FAILED(hr)) {
m_lastError = "Failed to Get IDXGIAdapter ErrorCode = " + QString::number(uint(hr), 16);
return false;
}
IDXGIOutput *dxgiOutput = nullptr;
QVector<IDXGIOutput *> outputs;
for(uint i = 0; dxgiAdapter->EnumOutputs(i, &dxgiOutput) != DXGI_ERROR_NOT_FOUND; ++i) {
outputs.push_back(dxgiOutput);
}
dxgiAdapter->Release();
if (outputs.size() > 0) dxgiOutput = outputs.at(0);
else {
m_lastError = "Failed to IDXGIOutput is Empty!";
return false;
}
IDXGIOutput6 *dxgiOutput6 = nullptr;
hr = dxgiOutput->QueryInterface(__uuidof(IDXGIOutput6), reinterpret_cast<void**>(&dxgiOutput6));
dxgiOutput->Release();
if (FAILED(hr)) {
m_lastError = "Failed to QueryInterface IDXGIOutput6 ErrorCode = " + QString::number(uint(hr), 16);
return false;
}
hr = dxgiOutput6->DuplicateOutput(d3dDevice, &m_duplication);
dxgiOutput6->Release();
if (FAILED(hr)) {
m_lastError = "Failed to DuplicateOutput ErrorCode = " + QString::number(uint(hr), 16);
return false;
}
DXGI_OUTDUPL_DESC desc;
m_duplication->GetDesc(&desc);
m_texture = new DxgiTextureStaging(d3dDevice, d3dContext);
if (desc.DesktopImageInSystemMemory) {
qDebug() << "Desc: CPU shared with GPU";
} else {
qDebug() << "Desc: CPU not shared with GPU";
}
return true;
}
2、释放 & 请求下一帧:
QPixmap DxgiManager::grabScreen()
{
IDXGIResource *desktopRes;
DXGI_OUTDUPL_FRAME_INFO frameInfo;
m_duplication->ReleaseFrame();
HRESULT hr = m_duplication->AcquireNextFrame(100, &frameInfo, &desktopRes);
if (FAILED(hr)) {
m_lastError = "Failed to AcquireNextFrame ErrorCode = " + QString::number(uint(hr), 16);
return QPixmap();
}
return m_texture->copyToImage(desktopRes);
}
3、复制纹理 & 像素映射:
QPixmap DxgiTextureStaging::copyToImage(IDXGIResource *res)
{
D3D11_TEXTURE2D_DESC desc;
ID3D11Texture2D *textrueRes = nullptr;
HRESULT hr = res->QueryInterface(__uuidof(ID3D11Texture2D), reinterpret_cast<void **>(&textrueRes));
if (FAILED(hr)) {
qDebug() << "Failed to ID3D11Texture2D result =" << hex << uint(hr);
return QPixmap();
}
textrueRes->GetDesc(&desc);
D3D11_TEXTURE2D_DESC texDesc;
ZeroMemory(&texDesc, sizeof(texDesc));
texDesc.Width = desc.Width;
texDesc.Height = desc.Height;
texDesc.MipLevels = 1;
texDesc.ArraySize = 1;
texDesc.SampleDesc.Count = 1;
texDesc.SampleDesc.Quality = 0;
texDesc.Usage = D3D11_USAGE_STAGING;
texDesc.Format = desc.Format;
texDesc.BindFlags = 0;
texDesc.CPUAccessFlags = D3D11_CPU_ACCESS_READ;
texDesc.MiscFlags = 0;
m_device->CreateTexture2D(&texDesc, nullptr, &m_texture);
m_context->CopyResource(m_texture, textrueRes);
IDXGISurface1 *surface = nullptr;
hr = m_texture->QueryInterface(__uuidof(IDXGISurface1), reinterpret_cast<void **>(&surface));
if (FAILED(hr)) {
qDebug() << "Failed to QueryInterface IDXGISurface1 ErrorCode =" << hex << uint(hr);
return QPixmap();
}
DXGI_MAPPED_RECT map;
surface->Map(&map, DXGI_MAP_READ);
QPixmap pixmap = QPixmap::fromImage(QImage(static_cast<uchar *>(map.pBits),
int(desc.Width), int(desc.Height), QImage::Format_ARGB32));
surface->Unmap();
surface->Release();
m_texture->Release();
return pixmap;
}
【结语】
好了,四个方法都讲完了,其中关键部分的代码量已经是最少的了。
其实说起来,只要不是特别追求性能的 ,Qt 提供的方式已经足够了。
最后,附上完整项目链接(多多star呀..⭐_⭐):