I am trying to create an overlay in C++ that does not appear in screenshots. The overlay should be visible to the user but not captured by a screenshot (which will also be done by the code). I've read that this is possible with DirectComposition and Direct2D, but I am not able to achieve the desired effect.
Current Behavior: The overlay is displayed on the screen, but it is also captured in screenshots.
Desired Behavior: I want the overlay to be visible on the screen but not captured in screenshots.
Here's the code I'm working with:
#include <windows.h>
#include <dcomp.h>
#include <d2d1.h>
#include <thread>
#include <iostream>
#include <fstream>
#pragma comment(lib, "dcomp.lib")
#pragma comment(lib, "d2d1.lib")
void OverlayThreadFunction() {
const wchar_t CLASS_NAME[] = L"DirectCompWindowClass";
WNDCLASSW wc = {};
wc.lpfnWndProc = DefWindowProc;
wc.hInstance = GetModuleHandle(NULL);
wc.lpszClassName = CLASS_NAME;
wc.hbrBackground = (HBRUSH) (COLOR_BACKGROUND);
RegisterClassW(&wc);
HWND hwnd = CreateWindowExW(
WS_EX_TOPMOST | WS_EX_LAYERED | WS_EX_TRANSPARENT,
CLASS_NAME,
L"DirectComposition Overlay",
WS_POPUP | WS_VISIBLE,
0, 0, 100, 100,
// 0, 0, GetSystemMetrics(SM_CXSCREEN), GetSystemMetrics(SM_CYSCREEN), (for fullscreen)
nullptr, nullptr, GetModuleHandle(NULL), nullptr
);
if (!hwnd) {
return;
}
// Set window transparency
SetLayeredWindowAttributes(hwnd, RGB(0, 0, 0), 0, LWA_COLORKEY);
// Initialize DirectComposition
IDCompositionDevice *pDCompDevice = nullptr;
HRESULT hr = DCompositionCreateDevice(nullptr, __uuidof(IDCompositionDevice),
reinterpret_cast<void **>(&pDCompDevice));
if (FAILED(hr)) {
return;
}
IDCompositionTarget *pDCompTarget = nullptr;
hr = pDCompDevice->CreateTargetForHwnd(hwnd, TRUE, &pDCompTarget);
if (FAILED(hr)) {
return;
}
IDCompositionVisual *pDCompVisual = nullptr;
hr = pDCompDevice->CreateVisual(&pDCompVisual);
if (FAILED(hr)) {
return;
}
// Initialize Direct2D
ID2D1Factory *pD2DFactory = nullptr;
hr = D2D1CreateFactory(D2D1_FACTORY_TYPE_SINGLE_THREADED, &pD2DFactory);
if (FAILED(hr)) {
return;
}
ID2D1HwndRenderTarget *pRT = nullptr;
D2D1_RENDER_TARGET_PROPERTIES props = D2D1::RenderTargetProperties();
D2D1_HWND_RENDER_TARGET_PROPERTIES hwndProps = D2D1::HwndRenderTargetProperties(hwnd, D2D1::SizeU(
GetSystemMetrics(SM_CXSCREEN), GetSystemMetrics(SM_CYSCREEN)));
hr = pD2DFactory->CreateHwndRenderTarget(props, hwndProps, &pRT);
if (FAILED(hr)) {
return;
}
ID2D1SolidColorBrush *pBrush = nullptr;
hr = pRT->CreateSolidColorBrush(D2D1::ColorF(D2D1::ColorF::Red, 1.0f), &pBrush);
if (FAILED(hr)) {
return;
}
pDCompVisual->SetContent(pRT);
// Set the visual as the root of the composition target
hr = pDCompTarget->SetRoot(pDCompVisual);
if (FAILED(hr)) {
return;
}
// Commit the composition
hr = pDCompDevice->Commit();
if (FAILED(hr)) {
return;
}
// Draw overlay
pRT->BeginDraw();
pRT->Clear(D2D1::ColorF(D2D1::ColorF::Black, 0.0f)); // Fully transparent background
pRT->FillRectangle(D2D1::RectF(100, 100, 400, 300), pBrush); // Solid red rectangle
pRT->EndDraw();
MSG msg = {};
while (GetMessage(&msg, nullptr, 0, 0)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
// Clean up
pBrush->Release();
pRT->Release();
pD2DFactory->Release();
pDCompVisual->Release();
pDCompTarget->Release();
pDCompDevice->Release();
}
bool SaveBitmap(HBITMAP hBitmap, const char *filename) {
BITMAP bmp;
GetObject(hBitmap, sizeof(BITMAP), &bmp);
LONG height = (bmp.bmHeight > 0) ? -bmp.bmHeight : bmp.bmHeight;
BITMAPFILEHEADER bmfHeader;
BITMAPINFOHEADER bi;
bi.biSize = sizeof(BITMAPINFOHEADER);
bi.biWidth = bmp.bmWidth;
bi.biHeight = height;
bi.biPlanes = 1;
bi.biBitCount = 32; // Change this to 24 if your bitmap doesn't have alpha channel
bi.biCompression = BI_RGB;
bi.biSizeImage = 0;
bi.biXPelsPerMeter = 0;
bi.biYPelsPerMeter = 0;
bi.biClrUsed = 0;
bi.biClrImportant = 0;
DWORD dwBmpSize = ((bmp.bmWidth * bi.biBitCount + 31) / 32) * 4 * abs(height);
// Add the size of the headers to the size of the bitmap to get the total file size
DWORD dwSizeofDIB = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER) + dwBmpSize;
// Offset to where the actual bitmap bits start.
bmfHeader.bfOffBits = (DWORD) sizeof(BITMAPFILEHEADER) + (DWORD) sizeof(BITMAPINFOHEADER);
// Size of the file
bmfHeader.bfSize = dwSizeofDIB;
// bfType must always be BM for Bitmaps
bmfHeader.bfType = 0x4D42; //BM
// Create the file
std::ofstream file(filename, std::ios::out | std::ios::binary);
if (!file) {
std::cerr << "Error: Unable to create file: " << filename << std::endl;
return false;
}
// Write the Bitmap file header
file.write(reinterpret_cast<const char *>(&bmfHeader), sizeof(BITMAPFILEHEADER));
// Write the Bitmap info header
file.write(reinterpret_cast<const char *>(&bi), sizeof(BITMAPINFOHEADER));
// Get the bitmap bits
BYTE *pBits = new BYTE[dwBmpSize];
GetBitmapBits(hBitmap, dwBmpSize, pBits);
// Write the Bitmap bits
file.write(reinterpret_cast<const char *>(pBits), dwBmpSize);
// Clean up
delete[] pBits;
file.close();
return true;
}
int main() {
std::thread overlayThread(OverlayThreadFunction);
Sleep(2000); //Sleep for 2secs. to ensure that overlay is rendered
// Make screenshot
HWND hDesktopWnd = GetDesktopWindow();
HDC hDesktopDC = GetDC(hDesktopWnd);
HDC hCaptureDC = CreateCompatibleDC(hDesktopDC);
HBITMAP hCaptureBitmap = CreateCompatibleBitmap(hDesktopDC, GetSystemMetrics(SM_CXSCREEN),
GetSystemMetrics(SM_CYSCREEN));
SelectObject(hCaptureDC, hCaptureBitmap);
BitBlt(hCaptureDC, 0, 0, GetSystemMetrics(SM_CXSCREEN), GetSystemMetrics(SM_CYSCREEN), hDesktopDC, 0, 0, SRCCOPY);
SaveBitmap(hCaptureBitmap, "desktop_screenshot.bmp");
DeleteObject(hCaptureBitmap);
DeleteDC(hCaptureDC);
ReleaseDC(hDesktopWnd, hDesktopDC);
std::cout << "Screenshot captured and saved\n";
overlayThread.join();
return 0;
}
By "screenshot," I do not necessarily mean the standard screenshot feature provided by Windows. I am also open to solutions that involve custom screenshot code. If there is a way to achieve this invisibility using a self-programmed screenshot method, that would also be acceptable.
Any suggestions on how to make the overlay invisible in screenshots would be greatly appreciated!
You can use SetWindowDisplayAffinity()
with the WDA_EXCLUDEFROMCAPTURE
flag:
The window is displayed only on a monitor. Everywhere else, the window does not appear at all.
One use for this affinity is for windows that show video recording controls, so that the controls are not included in the capture.
Introduced in Windows 10 Version 2004. See remarks about compatibility regarding previous versions of Windows.