87
votes

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 obtain hdcWin
  • CreateCompatibleDC(hdcWin) to create a hdcMem
  • Call SelectObject(hdcMem, bmp) to select an HBITMAP into hdcMem
  • BitBlt from hdcMem to hdcWin. During the BitBlt call, the hdcMem contains valid pixel data even in the offscreen regions, but that data is never copied to the hdcWin.

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!

3
As I understand it, you want to change the clipping region of other programs? I'm going to channel Raymond Chen here and ask: what if two programs would try to do that simultaneously? The OS can do that because by definition there's only one.MSalters
I can reproduce (also using the official sample docs.microsoft.com/en-us/windows/desktop/gdi/capturing-an-image), and in fact, I don't even need to code anything. Start Windows (mine is Windows 10, 64-bit, 6.3.17763), open Notepad, move it half beyond the screen, and paste a big chunk of text into it. Run ALT-TAB do show centered thumbnails (or move the mouse to the taskbar) and you'll see only half of notepad is painted with text... smells like a bug.Simon Mourier
IMHO you should report it to Microsoft as even their sample code demonstrates the issue.Simon Mourier
I created a bug report for this on the Windows Feedback Hub. Here's the link aka.ms/AA4c5ycJohannes Stricker
Unfortunately the problem exist in Windows 1903 and 1909 as well.Maria

3 Answers

2
votes

This seems to be a bug in the relevant versions of Windows which has apparently been fixed in more recent versions.

1
votes

For anybody sailing through countless google pages, blog posts, SO answers... calling:

SetWindowLong(hwnd, GWL_EXSTYLE, WS_EX_LAYERED);

seems to do the trick. This doesn't answer how to extend the clipping region over the area restricted by windows, but allows to capture screen correctly which is pretty much the goal anyway. This also is a solution to many other issues, like the taskbar thumbnail not updating, parts of window filckering with black when dragging or bugs when capturing to video file. MSDN doesn't specifically explain that anywhere.

One more thing worth pointing out is that Discord is able to stream a partially offscreen window WITHOUT modifying any of window's attributes, so there's probably more to that...

1
votes

If we take ReactOS as an example, the clipping region is at dc->dclevel.prgnClip and the system region is at dc->prgnVis. When you call BeginPaint on a window, it calls NtUserBeginPaint stub which traps to its kernel counterpart through the win32k SSDT, which calls IntBeginPaint, which passes the window's update region (Window->hrgnUpdate) to UserGetDCEx, which copies this to Dce->hrgnClip and calls DceUpdateVisRgn, which then gets the visible region by calling DceGetVisRgn which calculates the visible region using VIS_ComputeVisibleRegion, which develops a complex region by traversing all child windows, all parent windows and all siblings at each level (a top level window has a parent as the desktop (((PCLIENTINFO)(NtCurrentTeb()->Win32ClientInfo))->pDeskInfo->spwnd) and all top level windows are siblings; the desktop's parent is NULL and removing the parts they cover up – this does not appear to perform any special handling for the desktop window when it gets to it like clipping to the client area, and is treated like any other window in the z order, where only what it is covering is removed). DceGetVisRgn then combines this returned visible region and combines it wil the clipping region Dce->hrgnClip and combines them into RgnVisible using IntGdiCombineRgn(RgnVisible, RgnVisible, RgnClip, RGN_AND), which is then copied into dc->prgnVis using GdiSelectVisRgn(Dce->hDC, RgnVisible). DC is the device context and DCE is the device context entry for the DC in the DC cache. Therefore, the system region of the DC is now the intersection of the visible region and the update region of the window. IntBeginPaint also calls GdiGetClipBox(Ps->hdc, &Ps->rcPaint), which calls REGION_GetRgnBox(pdc->prgnVis, prc) to copy the bound of the region pdc->prgnVis (pdc->prgnVis->rdh.rcBound) to Ps->rcPaint and then GdiGetClipBox calls IntDPtoLP(pdc, (LPPOINT)prc, 2) to convert the bound from physical coordinates to logical coordinates, which DPI-unaware apps use. The paintstruct now contains the smallest logical rectangle that contains the complex intersection of the update region and the visible region.

GetClipRgn calls NtGdiGetRandomRgn, which returns pdc->dclevel.prgnClip when called with CLIPRGN, which is application defined using SetClipRgn

An application-defined clipping region is a clipping region identified by the SelectClipRgn function. It is not a clipping region created when the application calls the BeginPaint function.

There are 2 clipping regions. One is an application defined one created by the application using SelectClipRgn and the pointer is stored in pdc->dclevel.prgnClip, and the other clipping region is system region, after it has been updated to the intersection of the system region and the update region by a BeginPaint call, where it is presented to the application as a logical clipping rectangle in the PAINTSTRUCT.

GetClipBox calls NtGdiGetAppClipBox, which calls GdiGetClipBox, which of course returns the smallest logical rect boundary of the current system region, which may be the visible region if GetDC was used, or it may be the system region intersected with a custom clipping region with GetDCEx, or it may be the system region intersected with the window update region when using BeginPaint. Your issue would imply that the system region, when calculated, is now performing special handling for the desktop window in VIS_ComputeVisibleRegion

To actually access the DC directly, and hence the System region, you'd have to start and interact with a driver to do it from the application.