Manipulate system/visible clipping region in Windows 1809
Asked Answered
T

4

95

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 the 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!

Tortosa answered 7/2, 2019 at 11:37 Comment(26)
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.Twittery
To be honest I am not entirely sure how what's under the hood works. But it doesn't seem to be that the clipping region of the program itself is the problem, but rather Windows is clipping everything to the desktop bounds (for every window on the desktop). This wasn't the case prior to the update.Tortosa
It's one of those things that varies from time to time. IIRC, Vista's Aero theme also changed this behavior.Twittery
Do you have a reproducing code?Skelton
@SimonMourier I added a minimal code example and an executable to the initial question.Tortosa
I can reproduce (also using the official sample learn.microsoft.com/en-us/windows/desktop/gdi/…), 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.Skelton
You're right, you can reproduce it like that. I'm not sure if it's a bug or intended optimization, though. I hope there's some way around it..Tortosa
IMHO you should report it to Microsoft as even their sample code demonstrates the issue.Skelton
Good idea, do you know where the best place is to do that? I googled a bit, but only found dated answers or to use "Windows Feedback App". Is that really the way to go?Tortosa
@SimonMourier I have the same Windows version as you, but I can't reproduce the described bug with Alt-Tab method. Alt-Tab probably uses DwmRegisterThumbnail and should work fine. The BitBlt method, or related GDI methods will surely have problems.Gorgon
@BarmakShemirani Did you make any adjustments to performance/visual settings? Is the thumbnail displaying a live preview?Tortosa
Johannes, actually, I can reproduce the bug now. The thumbnail looks fine if I simply move notepad out of view. Next, click the notepad icon to minimize it, click it again to restore, now the thumbnail is chopped, as described earlier by @SimonMourierGorgon
Note it doesn't do that for all apps. I can reproduce on Visual Studio itself, but I can't reproduce for example for Word, or WinRT/UWP apps...Skelton
I created a bug report for this on the Windows Feedback Hub. Here's the link aka.ms/AA4c5ycTortosa
Did you find a solution?Humour
Unfortunately, not one that is truly satisfying. Here #53710635 is a suggested solution, but it doesn't work for all windows. I've created a new post here #55837046, but haven't found an answer, yet.Tortosa
Unfortunately the problem exist in Windows 1903 and 1909 as well.Humour
Yes, doesn't seem like this is something Microsoft is going to fix.Tortosa
I edited the initial post with some additional insights I gained. Maybe someone has more ideas about this?Tortosa
Thank you for the suggestion, but I thought it's not possible to ask specific users for their help. I looked at his profile and there seems to be no information about how to contact him, so I assume he doesn't want to be contacted personally.Tortosa
Thank you, I did that already when I initially opened the question, though. I might consider adding another bounty.Tortosa
It seems like this has been fixed (on Windows 10 Build 18362). The taskbar thumbnail still isn't updated, but I can take screenshots of offscreen windows now and they properly update. Not sure with which Windows update this was fixed, though. Edit: only seems to work with the PrintWindow api.Tortosa
I (again!) have the problem on Windows 10 Build 19041.264, after updating a few days ago...Peanut
It should be another method, maybe not involving this, since sharing in "discord" is able to capture a window offscreen and seeing it being repainted. Tested today on windows 10.0.19041. In the MDSN documentation, it calls to CreateCompatibleBitmap() before BitBlt... learn.microsoft.com/en-us/windows/win32/gdi/capturing-an-imageRadicalism
Test program works for me in Windows 10 2004 (19041.630)Cymogene
Unfortunately the problem exist in Windows 2020H4 and 2021H1 as wellHumour
B
2

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.

Brisk answered 16/2, 2021 at 23:58 Comment(0)
N
1

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

Newhall answered 1/12, 2020 at 11:38 Comment(1)
Does weird things with tooltips as well. No fix in 1909 for it yet, so gotta upgrade. :)Genuine
L
1

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

Leanneleanor answered 9/2, 2021 at 19:55 Comment(1)
Does not work for Win 1909 & 2020H4 & 2021H1.Humour
S
0

GDI is not the best way for doing screenshots, often it can't get even completely visible window.

Few months ago I found Youtube video with DWM hacking, which allows you to take screenshot of any window.

Here are sources. Personally I, didn't try to compile and run it.

enter image description here

Scarification answered 20/9, 2022 at 8:57 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.