Apparently, Microsoft has changed the way clipping works with Windows update 1809, released in late 2018. Before that update, GetClipBox()
returned the full client rectangle of a window, even when it was (partially) offscreen.
After the update, the same function returns a clipped rectangle, only containing the parts that are still onscreen. This leads to the Device Context contents not being updated for the offscreen area, which prevents me from taking screenshots from these windows.
The question is: can I somehow manipulate the clipping region?
I have researched a bit and it seems that the final clipping region is influenced by the window region, the update rectangle and the system region - as far as I understand the "global clipping region". I've checked the window region with GetWindowRgn()
and GetRgnBox()
, both return the same values for Windows 1809 and older versions. GetUpdateRect()
also returns the full client rectangle, so that cannot be the issue either. I've also tried to hook the BeginPaint()
method and see if changing the PAINTSTRUCT.rcPaint
does anything, without success.
So what I am left with is trying to adjust the system region, or sometimes called visible region. However, I have no idea if and how that is possible. MSDN suggests that it's not, but I thought maybe someone does have an idea for a solution!?
EDIT: To make this more clear, I don't think the clipping is done by the application itself, because offscreen screenshots of the same application version work prior to Windows 1809 and don't work with the updated Windows version. Instead, Windows itself seems to clip any offscreen surfaces.
EDIT2: Here's a minimal working code example for taking the screenshot.
// Get the client size.
RECT crect;
GetClientRect(hwnd, &crect);
int width = crect.right - crect.left;
int height = crect.bottom - crect.top;
// Create DC and Bitmap.
HDC windowDC = GetDC(hwnd);
HDC memoryDC = CreateCompatibleDC(windowDC);
BITMAPINFO bitmapInfo;
ZeroMemory(&bitmapInfo, sizeof(BITMAPINFO));
bitmapInfo.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
bitmapInfo.bmiHeader.biWidth = width;
bitmapInfo.bmiHeader.biHeight = -height;
bitmapInfo.bmiHeader.biPlanes = 1;
bitmapInfo.bmiHeader.biBitCount = 32;
bitmapInfo.bmiHeader.biCompression = BI_RGB;
bitmapInfo.bmiHeader.biSizeImage = width * height * 4;
char* pixels;
HBITMAP bitmap = CreateDIBSection(windowDC, &bitmapInfo, DIB_RGB_COLORS, (void**)&pixels, 0, 0);
HGDIOBJ previousObject = SelectObject(memoryDC, bitmap);
// Take the screenshot. Neither BitBlt nor PrintWindow work.
BitBlt(memoryDC, 0, 0, width, height, windowDC, 0, 0, SRCCOPY);
// ..or..
// PrintWindow(hwnd, memoryDC, PW_CLIENTONLY);
// Save the image.
BITMAPFILEHEADER bitmapFileHeader;
bitmapFileHeader.bfType = 0x4D42;
bitmapFileHeader.bfOffBits = sizeof(BITMAPFILEHEADER) + sizeof(BITMAPINFOHEADER);
std::fstream hFile("./screenshot.bmp", std::ios::out | std::ios::binary);
if(hFile.is_open())
{
hFile.write((char*)&bitmapFileHeader, sizeof(bitmapFileHeader));
hFile.write((char*)&bitmapInfo.bmiHeader, sizeof(bitmapInfo.bmiHeader));
hFile.write(pixels, (((32 * width + 31) & ~31) / 8) * height);
hFile.close();
}
// Free Resources
ReleaseDC(hwnd, windowDC);
SelectObject(memoryDC, previousObject);
DeleteDC(memoryDC);
DeleteObject(bitmap);
You can download a compiled executable from Google Drive here. Usage is Screenshot.exe <HWND>
, where HWND is the hex address of the window handle as it is shown in Spy++ for example. It will save a screenshot of the target window in the working directory as screenshot.bmp
(make sure you're allowed to write to the directory). The screenshot will work for almost all windows (even if they are hidden behind other windows), but as soon as you partially move the window offscreen, the screenshot will continue to show the old window contents for the offscreen part of the window (resize it while it's offscreen for example, to see the effect). This only happens on Windows 1809, it still shows up to date contents on earlier Windows versions.
EDIT3: I did some more research on this. Regarding the AdobeAir application for which the WS_EX_LAYERED
style did not work: I found that it uses BitBlt
internally do render the back buffer to the window dc. The rendering steps are:
GetDC(hwnd)
on the window to obtainhdcWin
CreateCompatibleDC(hdcWin)
to create ahdcMem
- Call
SelectObject(hdcMem, bmp)
to select anHBITMAP
intohdcMem
BitBlt
fromhdcMem
tohdcWin
. During theBitBlt
call, thehdcMem
contains valid pixel data even in the offscreen regions, but that data is never copied to thehdcWin
.
I looked at the system regions during the BitBlt
call. For hdcMem
the system region is a NULLREGION
, but for the hdcWin
the region is always clipped at the screen edges. I also tried to adjust the system region, by replacing all calls to GetDC
with GetDCEx(hwnd, hrgn, DCX_CACHE | DCX_INTERSECTRGN)
(as mentioned in this article), but that doesn't work and doesn't seem to provide options for extending the region. I really think the secret to solving the problem lies in manipulating the system region for the window dc, but I have no idea how to do that..
If found that the CreateDC
function takes a pointer to a DEVMODE
struct as the last argument (msdn). That in turn has fields dmPelsWidth
, dmPelsHeight
and dmPosition
. I believe that these make up the system region and maybe, if I could manipulate them, the DC would no longer get clipped, but I wasn't able to hook the CreateDC
function, yet.
If you have any new ideas based on my new insights, please share them. I'd appreciate any help!