在 Windows1809中操作系统/可见剪辑区域

显然,微软已经改变了在2018年底发布的 Windows 更新1809中使用剪辑的方式。在此更新之前,GetClipBox()返回窗口的完整客户端矩形,即使它(部分)在屏幕之外。 在更新之后,同一个函数返回一个剪裁过的矩形,只包含屏幕上仍然存在的部分。< strong > 这会导致设备上下文内容没有在离屏区域更新,这会阻止我从这些窗口截屏。

问题是: 我能以某种方式操纵剪切区域吗?

我做了一些研究,似乎最终的裁剪区域受到窗口区域,更新矩形和系统区域的影响-就我所理解的“全局裁剪区域”。我用 GetWindowRgn()GetRgnBox()检查了窗口区域,它们返回的值对于 Windows1809和旧版本都是相同的。GetUpdateRect()还返回完整的客户端矩形,因此这也不会成为问题。我还试图钩住 BeginPaint()方法,看看改变 PAINTSTRUCT.rcPaint是否有用,但没有成功。

所以我剩下的工作就是调整系统区域,或者有时候称为可见区域。然而,我不知道这是否可能以及如何可能。但我想也许有人确实有一个解决方案的想法! ?

编辑: 为了更清楚地说明这一点,我不认为剪辑是由应用程序本身完成的,因为同一个应用程序版本的屏幕外截图在 Windows 1809之前就可以工作了,而且不适用于升级后的 Windows 版本。相反,Windows 本身似乎会剪切屏幕外的任何表面。

编辑2: 这里有一个截屏的最小工作代码示例。

// 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);

你可以下载 编译的可执行文件从谷歌驱动器在这里。使用方法是 Screenshot.exe <HWND>,其中 HWND 是窗口句柄的十六进制地址,如 Spy + + 中所示。它会在工作目录中保存目标窗口的截图为 screenshot.bmp(确保你可以写入目录)。这个屏幕快照几乎适用于所有窗口(即使它们隐藏在其他窗口后面) ,但是只要你将窗口部分移出屏幕,这个屏幕快照就会继续显示窗口离开屏幕部分的旧窗口内容(例如,在离开屏幕时调整它的大小,以查看效果)。这只发生在 Windows 1809上,它仍然显示早期 Windows 版本的最新内容。

编辑3: 我对此做了更多的研究。关于使用 WS_EX_LAYERED样式不起作用的 AdobeAir 应用程序: 我发现它在内部使用 BitBlt,并将后台缓冲区呈现给窗口 dc。渲染步骤如下:

  • 在窗口上获取 hdcWin
  • 创建一个 hdcMem
  • 调用 SelectObject(hdcMem, bmp)选择一个 HBITMAPhdcMem
  • hdcMemhdcWin。 在 BitBlt调用期间,即使在离屏区域,hdcMem也包含有效的像素数据,但是这些数据永远不会复制到 hdcWin

我在 BitBlt呼叫期间查看了系统区域。对于 hdcMem,系统区域是 NULLREGION,但对于 hdcWin,该区域总是在屏幕边缘被剪切。我还尝试调整系统区域,将所有对 GetDC的调用替换为 GetDCEx(hwnd, hrgn, DCX_CACHE | DCX_INTERSECTRGN)(正如前面提到的 在这篇文章中) ,但是这不起作用,而且似乎也没有提供扩展该区域的选项。我真的认为解决这个问题的秘诀在于操作窗口直流电的系统区域,但我不知道如何做到这一点。

如果发现 CreateDC函数接受一个指向 DEVMODE结构的指针作为最后一个参数(我不知道)。它依次包含字段 dmPelsWidthdmPelsHeightdmPosition。我相信这些构成了系统区域,也许如果我能操纵它们,直流将不再被剪切,但我还不能钩住 CreateDC函数。

如果你有任何新的想法基于我的新见解,请分享他们。我会很感激任何帮助!

2239 次浏览

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

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

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 dc->prgnVis0, 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 (dc->prgnVis1) and all top level windows are siblings; the desktop's parent is dc->prgnVis2 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 dc->prgnVis5 using dc->prgnVis6, which is then copied into dc->prgnVis using dc->prgnVis8. dc->prgnVis9 is the device context and BeginPaint0 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 BeginPaint2, which calls BeginPaint3 to copy the bound of the region BeginPaint4 (BeginPaint5) to BeginPaint6 and then BeginPaint7 calls BeginPaint8 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.

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