I need to capture the image of a specific window and save it as an array. I initially used BitBlt, which worked well except for windows with hardware acceleration (it seems to work for DirectX-rendered windows but not for OpenGL and Vulkan).
I am trying winrt/Windows.Graphics.Capture, but it does not seem to work for non-top-level windows. However, if I use SetParent to promote the target window to a top-level window, it can capture successfully.
Please tell me a direct way to use GraphicsCapture APIs to capture child windows or explain why it cannot be done.
Here are some imperfect solutions, each with its own drawbacks. I would like to avoid using them if possible.
Capture the top-level window and calculate the position of the child window's image.
Drawback: This requires converting the child window's image position to that of the parent window, and since the capture range is larger, it consumes more computational resources.
Run the target program in full-screen mode so that the top-level window contains only the image of the child window I want to capture.
Drawback: This forces me to use full-screen mode, and although it does not require coordinate conversion, the capture range is even larger, consuming more computational resources.
Use SetParent to promote the target window to a top-level window.
Drawback: This may have unexpected effects on the target program, and I need to keep capturing for a period of time. Currently, I know that doing so makes the mouse unusable, though the keyboard still works unexpectedly on BlueStacks.
Here is the code snippet that will cause the problem. If hwndTarget isn't a top-level window, CreateForWindow will fail.
void CaptureWindow(HWND hwndTarget) {
winrt::init_apartment(winrt::apartment_type::single_threaded);
// Create Direct3D device
winrt::com_ptr<ID3D11Device> d3dDevice;
winrt::check_hresult(D3D11CreateDevice(nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, D3D11_CREATE_DEVICE_BGRA_SUPPORT,
nullptr, 0, D3D11_SDK_VERSION, d3dDevice.put(), nullptr, nullptr));
auto dxgiDevice = d3dDevice.as<IDXGIDevice>();
winrt::com_ptr<::IInspectable> inspectable;
winrt::check_hresult(CreateDirect3D11DeviceFromDXGIDevice(dxgiDevice.get(), inspectable.put()));
auto device = inspectable.as<winrt::Windows::Graphics::DirectX::Direct3D11::IDirect3DDevice>();
RECT rect{};
HRESULT hr = DwmGetWindowAttribute(hwndTarget, DWMWA_EXTENDED_FRAME_BOUNDS, &rect, sizeof(RECT));
if (FAILED(hr)) {
// If hwndTarget isn't top-level windows, DwmGetWindowAttribute will fail.
std::cerr << "DwmGetWindowAttribute Failed! Changed to use GetWindowRect.\n";
GetWindowRect(hwndTarget, &rect);
}
auto size = winrt::Windows::Graphics::SizeInt32{ rect.right - rect.left, rect.bottom - rect.top };
auto framePool = winrt::Windows::Graphics::Capture::Direct3D11CaptureFramePool::Create(
device,
winrt::Windows::Graphics::DirectX::DirectXPixelFormat::B8G8R8A8UIntNormalized,
2,
size
);
auto activationFactory = winrt::get_activation_factory<winrt::Windows::Graphics::Capture::GraphicsCaptureItem>();
auto interopFactory = activationFactory.as<IGraphicsCaptureItemInterop>();
winrt::Windows::Graphics::Capture::GraphicsCaptureItem captureItem = nullptr;
// Try to create capture item for the window
// If hwndTarget isn't top-level windows, CreateForWindow will fail.
hr = interopFactory->CreateForWindow(hwndTarget, winrt::guid_of<ABI::Windows::Graphics::Capture::IGraphicsCaptureItem>(),
reinterpret_cast<void**>(winrt::put_abi(captureItem)));
if (FAILED(hr)) {
std::cerr << "CreateForWindow failed. HRESULT: " << std::hex << hr << std::endl;
throw std::runtime_error("Failed to create GraphicsCaptureItem for window.");
}
}
Below is the complete code, modified from this answer, that does not use the Direct3D11CaptureFramePool.FrameArrived event to simplify the logic.
#include <iostream>
#include <vector>
#define WINRT_LEAN_AND_MEAN
#include <winrt/Windows.Foundation.h>
#include <winrt/Windows.Graphics.Capture.h>
#include <d3d11.h>
#include <windows.graphics.capture.interop.h>
#include <windows.graphics.directx.direct3d11.interop.h>
#include <dwmapi.h>
#pragma comment(lib, "dwmapi.lib")
#pragma comment(lib, "d3d11.lib")
void CaptureWindow(HWND hwndTarget);
int main() {
HWND bluestacks_hWnd = FindWindow(nullptr, TEXT("BlueStacks App Player"));
HWND bluestacks_hWnd2 = ::FindWindowEx(bluestacks_hWnd, nullptr, nullptr, nullptr);
if (bluestacks_hWnd) {
winrt::init_apartment(winrt::apartment_type::single_threaded);
CaptureWindow(bluestacks_hWnd); // Success
CaptureWindow(bluestacks_hWnd2); // Failure
SetParent(bluestacks_hWnd2, NULL);
CaptureWindow(bluestacks_hWnd2); // Success
SetParent(bluestacks_hWnd2, bluestacks_hWnd);
}
else {
std::cout << "Window not found.";
return -1;
}
winrt::uninit_apartment();
return 0;
}
void CaptureWindow(HWND hwndTarget) {
// Create Direct3D device
winrt::com_ptr<ID3D11Device> d3dDevice;
winrt::check_hresult(D3D11CreateDevice(nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr, D3D11_CREATE_DEVICE_BGRA_SUPPORT,
nullptr, 0, D3D11_SDK_VERSION, d3dDevice.put(), nullptr, nullptr));
const auto dxgiDevice = d3dDevice.as<IDXGIDevice>();
winrt::Windows::Graphics::DirectX::Direct3D11::IDirect3DDevice device;
{
winrt::com_ptr<::IInspectable> inspectable;
winrt::check_hresult(CreateDirect3D11DeviceFromDXGIDevice(dxgiDevice.get(), inspectable.put()));
device = inspectable.as<winrt::Windows::Graphics::DirectX::Direct3D11::IDirect3DDevice>();
}
RECT rect{};
HRESULT hr = DwmGetWindowAttribute(hwndTarget, DWMWA_EXTENDED_FRAME_BOUNDS, &rect, sizeof(RECT));
if (FAILED(hr)) {
// If hwndTarget isn't top-level windows, DwmGetWindowAttribute will fail.
std::cerr << "DwmGetWindowAttribute Failed! Changed to use GetWindowRect.\n";
GetWindowRect(hwndTarget, &rect);
}
const auto size = winrt::Windows::Graphics::SizeInt32{ rect.right - rect.left, rect.bottom - rect.top };
auto framePool = winrt::Windows::Graphics::Capture::Direct3D11CaptureFramePool::Create(
device,
winrt::Windows::Graphics::DirectX::DirectXPixelFormat::B8G8R8A8UIntNormalized,
2,
size
);
auto activationFactory = winrt::get_activation_factory<winrt::Windows::Graphics::Capture::GraphicsCaptureItem>();
auto interopFactory = activationFactory.as<IGraphicsCaptureItemInterop>();
winrt::Windows::Graphics::Capture::GraphicsCaptureItem captureItem = nullptr;
// Try to create capture item for the window
// If hwndTarget isn't top-level windows, CreateForWindow will fail.
hr = interopFactory->CreateForWindow(hwndTarget, winrt::guid_of<ABI::Windows::Graphics::Capture::IGraphicsCaptureItem>(),
reinterpret_cast<void**>(winrt::put_abi(captureItem)));
if (FAILED(hr)) {
std::cerr << "CreateForWindow failed. HRESULT: " << std::hex << hr << std::endl;
throw std::runtime_error("Failed to create GraphicsCaptureItem for window.");
}
winrt::com_ptr<ID3D11Texture2D> texture;
auto session = framePool.CreateCaptureSession(captureItem);
session.IsCursorCaptureEnabled(false);
session.StartCapture();
winrt::Windows::Graphics::Capture::Direct3D11CaptureFrame frame = framePool.TryGetNextFrame();
for (int i = 0; !frame; ++i) {
if (i > 1000) {
// ERROR: Can't get Direct3D11CaptureFrame
// TODO: Add better error handling
throw std::runtime_error("Failed to GetNextFrame");
}
Sleep(1);
frame = framePool.TryGetNextFrame();
}
session.Close();
framePool.Close();
struct __declspec(uuid("A9B3D012-3DF2-4EE3-B8D1-8695F457D3C1"))
IDirect3DDxgiInterfaceAccess : ::IUnknown {
virtual HRESULT __stdcall GetInterface(GUID const& id, void** object) = 0;
};
winrt::com_ptr<IDirect3DDxgiInterfaceAccess> access = frame.Surface().as<IDirect3DDxgiInterfaceAccess>();
winrt::check_hresult(access->GetInterface(winrt::guid_of<ID3D11Texture2D>(), texture.put_void()));
D3D11_TEXTURE2D_DESC capturedTextureDesc;
texture->GetDesc(&capturedTextureDesc);
capturedTextureDesc.Usage = D3D11_USAGE_STAGING;
capturedTextureDesc.BindFlags = 0;
capturedTextureDesc.CPUAccessFlags = D3D11_CPU_ACCESS_READ;
capturedTextureDesc.MiscFlags = 0;
winrt::com_ptr<ID3D11Texture2D> stagingTexture;
winrt::check_hresult(d3dDevice->CreateTexture2D(&capturedTextureDesc, NULL, stagingTexture.put()));
winrt::com_ptr<ID3D11DeviceContext> d3dContext;
d3dDevice->GetImmediateContext(d3dContext.put());
d3dContext->CopyResource(stagingTexture.get(), texture.get());
D3D11_MAPPED_SUBRESOURCE resource;
winrt::check_hresult(d3dContext->Map(stagingTexture.get(), 0, D3D11_MAP_READ, 0, &resource));
BITMAPINFO bmpInfo = {};
bmpInfo.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
bmpInfo.bmiHeader.biWidth = capturedTextureDesc.Width;
bmpInfo.bmiHeader.biHeight = -static_cast<LONG>(capturedTextureDesc.Height); // Negative height to indicate top-down bitmap
bmpInfo.bmiHeader.biPlanes = 1;
bmpInfo.bmiHeader.biBitCount = 32;
bmpInfo.bmiHeader.biCompression = BI_RGB;
auto bufferSize = capturedTextureDesc.Width * capturedTextureDesc.Height * 4;
std::vector<BYTE> buffer(bufferSize);
auto srcPtr = static_cast<BYTE*>(resource.pData);
for (UINT row = 0; row < capturedTextureDesc.Height; ++row) {
memcpy_s(buffer.data() + row * capturedTextureDesc.Width * 4, buffer.size() - row * capturedTextureDesc.Width * 4,
srcPtr + row * resource.RowPitch, capturedTextureDesc.Width * 4);
}
d3dContext->Unmap(stagingTexture.get(), 0);
// Save BMP to file
auto filePath = std::wstring(L"ScreenShot.bmp");
FILE* file = nullptr;
if (_wfopen_s(&file, filePath.c_str(), L"wb") == 0 && file) {
BITMAPFILEHEADER fileHeader = {};
fileHeader.bfType = 0x4d42; // "BM"
fileHeader.bfSize = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER) + buffer.size();
fileHeader.bfOffBits = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER);
fwrite(&fileHeader, sizeof(BITMAPFILEHEADER), 1, file);
fwrite(&bmpInfo.bmiHeader, sizeof(BITMAPINFOHEADER), 1, file);
fwrite(buffer.data(), buffer.size(), 1, file);
fclose(file);
}
}
Windows.Graphics.Captureoperates on resources the desktop compositor uses. It maintains video surfaces for top-level windows, but not for child windows. That's why you can capture top-level windows, but not child windows. I have not ever seen even a single game that would actively prevent having its screen captured, as @TimRoberts suggests.