c++windowsscreenshotdirect2d

How to Create a Screenshot-Invisible Overlay Using DirectComposition and Direct2D in C++?


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!


Solution

  • 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.