0

I'm trying to take a screenshot of a particular window (HWND) on Windows using C++. The following code works on Notepad but not on another specific process. Instead, the code returns a completely different screenshot for the other process:

#include <Windows.h>

HBITMAP dump_client_window(const HWND window_handle)
{
    RECT window_handle_rectangle;
    GetClientRect(window_handle, &window_handle_rectangle);

    const HDC hdc_screen = GetDC(nullptr);
    const HDC hdc = CreateCompatibleDC(hdc_screen);
    const auto cx = window_handle_rectangle.right - window_handle_rectangle.left;
    const auto cy = window_handle_rectangle.bottom - window_handle_rectangle.top;
    const HBITMAP bitmap = CreateCompatibleBitmap(hdc_screen, cx, cy);
    SelectObject(hdc, bitmap);
    const auto old_bitmap = SelectObject(hdc, bitmap);

    PrintWindow(window_handle, hdc, PW_CLIENTONLY);

    // Cleanup
    SelectObject(hdc, old_bitmap);
    DeleteDC(hdc);
    ReleaseDC(nullptr, hdc_screen);

    return bitmap;
}

What could be the reason for it? If I use DirectX11 for taking the screenshot of the window, it works correctly for both processes:

#include <dxgi.h>
#include <inspectable.h>
#include <dxgi1_2.h>
#include <d3d11.h>
#include <winrt/Windows.System.h>
#include <winrt/Windows.Graphics.Capture.h>
#include <Windows.Graphics.Capture.Interop.h>
#include <windows.graphics.directx.direct3d11.interop.h>
#include <roerrorapi.h>
#include <ShlObj_core.h>
#include <dwmapi.h>
#include <filesystem>

#include "ImageFormatConversion.hpp"

#pragma comment(lib, "Dwmapi.lib")
#pragma comment(lib, "windowsapp.lib")

void capture_window(HWND window_handle, const std::wstring& output_file_path)
{
    // Init COM
    init_apartment(winrt::apartment_type::multi_threaded);

    // Create Direct 3D Device
    winrt::com_ptr<ID3D11Device> d3d_device;

    winrt::check_hresult(D3D11CreateDevice(nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, D3D11_CREATE_DEVICE_BGRA_SUPPORT,
        nullptr, 0, D3D11_SDK_VERSION, d3d_device.put(), nullptr, nullptr));

    winrt::Windows::Graphics::DirectX::Direct3D11::IDirect3DDevice device;
    const auto dxgiDevice = d3d_device.as<IDXGIDevice>();
    {
        winrt::com_ptr<IInspectable> inspectable;
        winrt::check_hresult(CreateDirect3D11DeviceFromDXGIDevice(dxgiDevice.get(), inspectable.put()));
        device = inspectable.as<winrt::Windows::Graphics::DirectX::Direct3D11::IDirect3DDevice>();
    }

    auto idxgi_device2 = dxgiDevice.as<IDXGIDevice2>();
    winrt::com_ptr<IDXGIAdapter> adapter;
    winrt::check_hresult(idxgi_device2->GetParent(winrt::guid_of<IDXGIAdapter>(), adapter.put_void()));
    winrt::com_ptr<IDXGIFactory2> factory;
    winrt::check_hresult(adapter->GetParent(winrt::guid_of<IDXGIFactory2>(), factory.put_void()));

    ID3D11DeviceContext* d3d_context = nullptr;
    d3d_device->GetImmediateContext(&d3d_context);

    RECT rect{};
    DwmGetWindowAttribute(window_handle, DWMWA_EXTENDED_FRAME_BOUNDS, &rect, sizeof(RECT));
    const auto size = winrt::Windows::Graphics::SizeInt32{ rect.right - rect.left, rect.bottom - rect.top };

    winrt::Windows::Graphics::Capture::Direct3D11CaptureFramePool m_frame_pool =
        winrt::Windows::Graphics::Capture::Direct3D11CaptureFramePool::Create(
            device,
            winrt::Windows::Graphics::DirectX::DirectXPixelFormat::B8G8R8A8UIntNormalized,
            2,
            size);

    const auto activation_factory = winrt::get_activation_factory<
        winrt::Windows::Graphics::Capture::GraphicsCaptureItem>();
    auto interop_factory = activation_factory.as<IGraphicsCaptureItemInterop>();
    winrt::Windows::Graphics::Capture::GraphicsCaptureItem capture_item = { nullptr };
    interop_factory->CreateForWindow(window_handle, winrt::guid_of<ABI::Windows::Graphics::Capture::IGraphicsCaptureItem>(),
        winrt::put_abi(capture_item));

    auto is_frame_arrived = false;
    winrt::com_ptr<ID3D11Texture2D> texture;
    const auto session = m_frame_pool.CreateCaptureSession(capture_item);
    m_frame_pool.FrameArrived([&](auto& frame_pool, auto&)
        {
            if (is_frame_arrived)
            {
                return;
            }
            auto frame = frame_pool.TryGetNextFrame();

            struct __declspec(uuid("A9B3D012-3DF2-4EE3-B8D1-8695F457D3C1"))
                IDirect3DDxgiInterfaceAccess : ::IUnknown
            {
                virtual HRESULT __stdcall GetInterface(GUID const& id, void** object) = 0;
            };

            auto access = frame.Surface().as<IDirect3DDxgiInterfaceAccess>();
            access->GetInterface(winrt::guid_of<ID3D11Texture2D>(), texture.put_void());
            is_frame_arrived = true;
            return;
        });

    session.StartCapture();

    // Message pump
    MSG message;
    while (!is_frame_arrived)
    {
        if (PeekMessage(&message, nullptr, 0, 0, PM_REMOVE) > 0)
        {
            DispatchMessage(&message);
        }
    }

    session.Close();

    D3D11_TEXTURE2D_DESC captured_texture_desc;
    texture->GetDesc(&captured_texture_desc);

    captured_texture_desc.Usage = D3D11_USAGE_STAGING;
    captured_texture_desc.BindFlags = 0;
    captured_texture_desc.CPUAccessFlags = D3D11_CPU_ACCESS_READ;
    captured_texture_desc.MiscFlags = 0;

    winrt::com_ptr<ID3D11Texture2D> user_texture = nullptr;
    winrt::check_hresult(d3d_device->CreateTexture2D(&captured_texture_desc, nullptr, user_texture.put()));

    d3d_context->CopyResource(user_texture.get(), texture.get());

    D3D11_MAPPED_SUBRESOURCE resource;
    winrt::check_hresult(d3d_context->Map(user_texture.get(), NULL, D3D11_MAP_READ, 0, &resource));

    BITMAPINFO l_bmp_info;

    // BMP 32 bpp
    ZeroMemory(&l_bmp_info, sizeof(BITMAPINFO));
    l_bmp_info.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
    l_bmp_info.bmiHeader.biBitCount = 32;
    l_bmp_info.bmiHeader.biCompression = BI_RGB;
    l_bmp_info.bmiHeader.biWidth = captured_texture_desc.Width;
    l_bmp_info.bmiHeader.biHeight = captured_texture_desc.Height;
    l_bmp_info.bmiHeader.biPlanes = 1;
    l_bmp_info.bmiHeader.biSizeImage = captured_texture_desc.Width * captured_texture_desc.Height * 4;

    std::unique_ptr<BYTE> p_buf(new BYTE[l_bmp_info.bmiHeader.biSizeImage]);
    UINT l_bmp_row_pitch = captured_texture_desc.Width * 4;
    auto sptr = static_cast<BYTE*>(resource.pData);
    auto dptr = p_buf.get() + l_bmp_info.bmiHeader.biSizeImage - l_bmp_row_pitch;

    UINT l_row_pitch = std::min<UINT>(l_bmp_row_pitch, resource.RowPitch);

    for (size_t h = 0; h < captured_texture_desc.Height; ++h)
    {
        memcpy_s(dptr, l_bmp_row_pitch, sptr, l_row_pitch);
        sptr += resource.RowPitch;
        dptr -= l_bmp_row_pitch;
    }

    // Save bitmap buffer into the file
    WCHAR l_my_doc_path[MAX_PATH];

    winrt::check_hresult(SHGetFolderPathW(nullptr, CSIDL_PERSONAL, nullptr, SHGFP_TYPE_CURRENT, l_my_doc_path));

    FILE* lfile = nullptr;

    if (auto lerr = _wfopen_s(&lfile, output_file_path.c_str(), L"wb"); lerr != 0)
    {
        return;
    }

    if (lfile != nullptr)
    {
        BITMAPFILEHEADER bmp_file_header;

        bmp_file_header.bfReserved1 = 0;
        bmp_file_header.bfReserved2 = 0;
        bmp_file_header.bfSize = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER) + l_bmp_info.bmiHeader.biSizeImage;
        bmp_file_header.bfType = 'MB';
        bmp_file_header.bfOffBits = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER);

        fwrite(&bmp_file_header, sizeof(BITMAPFILEHEADER), 1, lfile);
        fwrite(&l_bmp_info.bmiHeader, sizeof(BITMAPINFOHEADER), 1, lfile);
        fwrite(p_buf.get(), l_bmp_info.bmiHeader.biSizeImage, 1, lfile);

        fclose(lfile);

        convert_image_encoding(output_file_path, L"png");
    }
}

Why is the DirectX11 code so complex/long and slow (about 800ms - 1s per call including cold start initialization)? Also, the latter version causes blinking borders around the captured window which I might want to get rid of. I also seem to have to take the more inefficient route of storing the BMP image to the disk and then loading it back in order to convert it to PNG and then storing it again to produce the final result on the disk which I like to have.

Any suggestions or help with any of these things are welcome, especially why the first screenshot capture code can yield unexpected images depending on the window being captured. Other than that, I like the first version for its speed, brevity and simplicity.

BullyWiiPlaza
  • 17,329
  • 10
  • 113
  • 185
  • "*Instead, the code returns a completely different screenshot for the other process*" - meaning what exactly? Also, you are not saving the original `HBITMAP` returned by `SelectObject(hdc, bitmap)` and restoring it before calling `DeleteDC(hdc)` – Remy Lebeau Aug 12 '21 at 00:10
  • @RemyLebeau: Yeah, instead of the specified window I'm getting a screenshot of Visual Studio and a CMD window on top of it. Totally strange. I'm going to try your suggestion and see if it helps. – BullyWiiPlaza Aug 12 '21 at 06:09
  • my suggestion won't fix your issue. I was merely pointing out a leak in your code. However, I do have a suggestion for you - rather than use `PrintWindow()`, use `BitBlt()` instead to copy the window rect from the screen `HDC` to your bitmap `HDC`. – Remy Lebeau Aug 12 '21 at 06:19
  • The second method is meant for real time capture, so it should be much faster than the first one (capture FPS can match monitor resolution by construction), but only in the capture loop (TryGetNextFrame). Initialization, cleanup etc. are relatively costly, and it's not exactly a "screenshot" API, it's a frame capture API (ie: you get a screenshot when the graphic card has generated a frame to be presented) BTW you can use WIC to save the bitmap, for ex like in here: https://stackoverflow.com/a/30138664/403671 – Simon Mourier Aug 12 '21 at 11:25

0 Answers0