【寫在前面】
最近在嘗試做一個簡單的螢幕控制,本以為截屏應該是最簡單的部分,沒想到在 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呀..⭐_⭐):
Github的:https://github.com/mengps/RemoteControl。