天天看點

使用Qt在Windows下進行截屏的一些嘗試

【寫在前面】

最近在嘗試做一個簡單的螢幕控制,本以為截屏應該是最簡單的部分,沒想到在 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。

繼續閱讀