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);
}
}
As @IInspectable said, DWM maintains video surfaces for top-level windows, but not for child windows. Therefore, you can only clip the image of the parent window to the child window by yourself.
The following code captures the image of the child window by capturing the parent window screen and calculating the child window rectangle. The captured image will be displayed in the upper left corner of the screen for immediate viewing.
#include <iostream>
#include <vector>
#include <memory>
#include <Windows.h>
#include <dwmapi.h>
#include <dxgi1_2.h>
#include <d3d11.h>
#include <winrt/Windows.Foundation.h>
#include <winrt/Windows.Graphics.Capture.h>
#include <windows.graphics.capture.interop.h>
#include <windows.graphics.directx.direct3d11.interop.h>
#pragma comment(lib,"Dwmapi.lib")
#pragma comment(lib,"windowsapp.lib")
using namespace winrt;
using namespace winrt::Windows::Graphics::Capture;
using namespace winrt::Windows::Graphics::DirectX;
using namespace winrt::Windows::Graphics::DirectX::Direct3D11;
//Display the captured image(ignore padding) on the screen. Just for Debug
static void ShowImage(const BYTE* pdata, int width, int height, UINT RowPitch)
{
std::cout << width << 'x' << height << '\n';
HDC hdc = GetDC(0);
HDC memDC = CreateCompatibleDC(hdc);
HBITMAP bitmap = CreateCompatibleBitmap(hdc, RowPitch, height);
SelectObject(memDC, bitmap);
SetBitmapBits(bitmap, height * RowPitch * sizeof(RGBQUAD), pdata);
BitBlt(hdc, 0, 0, width, height, memDC, 0, 0, SRCCOPY);
DeleteObject(bitmap);
DeleteDC(memDC);
ReleaseDC(0, hdc);
}
static void ClipToChildWindow(BYTE* pdata, int parentWidth,int parentHeight, UINT RowPitch, HWND parent, HWND child) {
RECT rect;
GetClientRect(child, &rect);
MapWindowPoints(child, parent, reinterpret_cast<LPPOINT>(&rect), 2);
if (rect.left<0 || rect.top<0 || rect.right>parentWidth || rect.bottom>parentHeight) {
//throw("The child window not be located inside the parent window");
if (rect.left < 0) rect.left = 0;
if (rect.top < 0) rect.top = 0;
if (rect.right > parentWidth) rect.right = parentWidth;
if (rect.bottom > parentHeight) rect.bottom = parentHeight;
}
const int width = rect.right - rect.left;
const int height = rect.bottom - rect.top;
std::vector<BYTE> image(width * height * sizeof(RGBQUAD));
for (BYTE* src = pdata + (rect.left + rect.top * RowPitch) * sizeof(RGBQUAD),
*end = src + height * RowPitch * sizeof(RGBQUAD),
*dst= image.data();
src < end;
src += RowPitch * sizeof(RGBQUAD),dst+=width* sizeof(RGBQUAD)) {
memcpy(dst, src, width * sizeof(RGBQUAD));
}
ShowImage(image.data(), width, height, width);
}
void CALLBACK CountdownTimerProc(HWND unnamedParam1, UINT unnamedParam2, UINT_PTR unnamedParam3, DWORD unnamedParam4) {
static int time_left = 10;
--time_left;
printf("\rCountdown:%ds ", time_left);
if (time_left == 0) {
PostQuitMessage(0);
}
}
void CaptureChildWindow(HWND hwndTarget, HWND hwndChild)
{
winrt::init_apartment(apartment_type::multi_threaded);
winrt::com_ptr<ID3D11Device> d3dDevice;
HRESULT hr = D3D11CreateDevice(
nullptr, D3D_DRIVER_TYPE_HARDWARE, nullptr,
D3D11_CREATE_DEVICE_BGRA_SUPPORT,
nullptr, 0, D3D11_SDK_VERSION,
d3dDevice.put(), nullptr, nullptr);
if (FAILED(hr)) { std::cerr << "D3D11CreateDevice failed.\n"; return; }
winrt::com_ptr<ID3D11DeviceContext> d3dContext;
d3dDevice->GetImmediateContext(d3dContext.put());
if (!d3dContext) { std::cerr << "Failed to get D3D context.\n"; return; }
auto dxgiDevice = d3dDevice.as<IDXGIDevice>();
winrt::com_ptr<IInspectable> inspectable;
hr = CreateDirect3D11DeviceFromDXGIDevice(dxgiDevice.get(), inspectable.put());
if (FAILED(hr)) { std::cerr << "CreateDirect3D11DeviceFromDXGIDevice failed.\n"; return; }
IDirect3DDevice device = inspectable.as<IDirect3DDevice>();
RECT rect{};
hr = DwmGetWindowAttribute(hwndTarget, DWMWA_EXTENDED_FRAME_BOUNDS, &rect, sizeof(RECT));
if (FAILED(hr)) { std::cerr << "DwmGetWindowAttribute failed.\n"; return; }
winrt::Windows::Graphics::SizeInt32 frameSize{ rect.right - rect.left, rect.bottom - rect.top };
auto interopFactory = get_activation_factory<GraphicsCaptureItem>().as<IGraphicsCaptureItemInterop>();
GraphicsCaptureItem item = nullptr;
hr = interopFactory->CreateForWindow(
hwndTarget,
__uuidof(ABI::Windows::Graphics::Capture::IGraphicsCaptureItem),
reinterpret_cast<void**>(put_abi(item)));
if (FAILED(hr) || !item) { std::cerr << "CreateForWindow failed.\n"; return; }
auto framePool = Direct3D11CaptureFramePool::Create(
device,
DirectXPixelFormat::B8G8R8A8UIntNormalized,
2,
frameSize);
auto session = framePool.CreateCaptureSession(item);
session.IsCursorCaptureEnabled(false);
winrt::com_ptr<ID3D11Texture2D> reusableStagingTexture;
std::vector<BYTE> imageBuffer;
// FrameArrived callback
framePool.FrameArrived([=, &reusableStagingTexture, &imageBuffer, &frameSize, &framePool](auto& pool, auto&)
{
auto frame = pool.TryGetNextFrame();
if (!frame) return;
auto newSize = frame.ContentSize();
if (newSize.Width != frameSize.Width || newSize.Height != frameSize.Height)
{
std::cout << "Frame size changed: " << newSize.Width << "x" << newSize.Height << "\n";
frameSize = newSize;
framePool.Recreate(
device,
DirectXPixelFormat::B8G8R8A8UIntNormalized,
2,
frameSize);
reusableStagingTexture = nullptr;
return;
}
auto surface = frame.Surface();
struct __declspec(uuid("A9B3D012-3DF2-4EE3-B8D1-8695F457D3C1")) IDirect3DDxgiInterfaceAccess : IUnknown {
virtual HRESULT __stdcall GetInterface(GUID const& id, void** object) = 0;
};
auto access = surface.as<IDirect3DDxgiInterfaceAccess>();
winrt::com_ptr<ID3D11Texture2D> texture;
HRESULT hr = access->GetInterface(__uuidof(ID3D11Texture2D), texture.put_void());
if (FAILED(hr)) { std::cerr << "GetInterface(ID3D11Texture2D) failed.\n"; return; }
// Check if staging texture needs to be rebuilt
D3D11_TEXTURE2D_DESC desc;
texture->GetDesc(&desc);
bool needNewTexture = false;
if (!reusableStagingTexture)
{
needNewTexture = true;
}
else
{
D3D11_TEXTURE2D_DESC existingDesc;
reusableStagingTexture->GetDesc(&existingDesc);
if (existingDesc.Width != desc.Width || existingDesc.Height != desc.Height)
needNewTexture = true;
}
if (needNewTexture)
{
desc.Usage = D3D11_USAGE_STAGING;
desc.BindFlags = 0;
desc.CPUAccessFlags = D3D11_CPU_ACCESS_READ;
desc.MiscFlags = 0;
hr = d3dDevice->CreateTexture2D(&desc, nullptr, reusableStagingTexture.put());
if (FAILED(hr)) { std::cerr << "CreateTexture2D for staging failed.\n"; return; }
}
d3dContext->CopyResource(reusableStagingTexture.get(), texture.get());
D3D11_MAPPED_SUBRESOURCE mapped{};
hr = d3dContext->Map(reusableStagingTexture.get(), 0, D3D11_MAP_READ, 0, &mapped);
if (FAILED(hr)) { std::cerr << "Map failed.\n"; return; }
ClipToChildWindow((BYTE*)mapped.pData, frameSize.Width, frameSize.Height, mapped.RowPitch / 4, hwndTarget, hwndChild);
/*This code is used to capture the full window image, include padding
size_t totalBytes = mapped.RowPitch * desc.Height;
if (imageBuffer.size() != totalBytes)
imageBuffer.resize(totalBytes);
memcpy(imageBuffer.data(), mapped.pData, totalBytes);
ShowImage(imageBuffer.data(), desc.Width, desc.Height, mapped.RowPitch / 4);
*/
d3dContext->Unmap(reusableStagingTexture.get(), 0);
});
session.StartCapture();
MSG msg;
UINT_PTR timerId = SetTimer(nullptr, 1, 1000, CountdownTimerProc);
while (GetMessage(&msg, nullptr, 0, 0))
{
DispatchMessage(&msg);
}
KillTimer(nullptr, timerId);
session.Close();
framePool.Close();
}
int main() {
HWND parent = FindWindowW(L"Notepad",nullptr);
HWND child = FindWindowExW(parent,nullptr,L"NotepadTextBox", nullptr);
if (!parent || !child) {
std::cerr << "FindWindow failed";
return -1;
}
CaptureChildWindow(parent, child);
return 0;
}