在Windows 1809中操作系统/可见剪切区域

时间:2019-02-07 11:37:35

标签: c++ windows winapi clipping

显然,Microsoft已更改了2018年末发布的Windows更新1809的裁剪方式。在该更新之前,GetClipBox()返回了窗口的整个客户端矩形,即使该窗口(部分)处于屏幕外。 更新后,相同的函数将返回一个裁剪的矩形,仅包含仍在屏幕上的部分。 这导致不在屏幕外区域更新设备上下文内容,这使我无法从这些窗口中截取屏幕截图。

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

我进行了一些研究,似乎最终的裁剪区域受窗口区域,更新矩形和系统区域的影响-就我所了解的“全局裁剪区域”而言。我已经用GetWindowRgn()GetRgnBox()检查了窗口区域,对于Windows 1809和更早的版本,它们都返回相同的值。 GetUpdateRect()还返回完整的客户端矩形,因此也不是问题。我还尝试钩住BeginPaint()方法,看看更改PAINTSTRUCT.rcPaint是否有任何效果,但没有成功。

所以我剩下的就是尝试调整系统区域,有时也称为可见区域。但是,我不知道是否可行以及如何实现。 MSDN suggests that it's not,但我想也许有人确实有解决方案的想法!?

编辑:为了使这一点更加清楚,我不认为剪辑是由应用程序本身完成的,因为同一应用程序版本的屏幕外屏幕截图在Windows 1809之前有效,并且不起作用使用更新的Windows版本。相反,Windows本身似乎会修剪屏幕外的任何表面。

EDIT2 :这是获取屏幕截图的最小工作代码示例。

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

您可以下载compiled executable from Google Drive here。用法是Screenshot.exe <HWND>,其中HWND是窗口句柄的十六进制地址,例如在Spy ++中所示。它将目标窗口的屏幕快照保存为工作目录screenshot.bmp(确保允许您写入该目录)。屏幕截图几乎适用于所有窗口(即使它们隐藏在其他窗口后面),但是一旦您将窗口部分移至屏幕外,屏幕截图将继续显示该窗口屏幕外部分的旧窗口内容(调整其大小)例如在屏幕外查看效果)。这仅在Windows 1809上发生,在Windows早期版本中仍会显示最新内容。

3 个答案:

答案 0 :(得分:1)

对于浏览无数谷歌页面、博客文章、SO 答案的任何人......致电:

SetWindowLong(hwnd, GWL_EXSTYLE, WS_EX_LAYERED);

似乎可以解决问题。这并没有回答如何在受窗口限制的区域上扩展剪切区域,但允许正确捕获屏幕,这几乎是目标。这也是许多其他问题的解决方案,例如任务栏缩略图未更新、拖动时窗口部分变黑或捕获到视频文件时出现错误。 MSDN 没有在任何地方具体解释。

还有一点值得指出的是,Discord 能够在不修改任何窗口属性的情况下流式传输部分屏幕外的窗口,因此可能还有更多......

答案 1 :(得分:1)

以 ReactOS 为例,剪切区域在 dc->dclevel.prgnClip,系统区域在 dc->prgnVis。当您在窗口上调用 BeginPaint 时,它调用 NtUserBeginPaint 存根,该存根通过 win32k SSDT 捕获到其内核对应项,后者调用 IntBeginPaint,后者传递窗口的更新区域 ({{1} }) 到Window->hrgnUpdate,将其复制到UserGetDCEx 并调用Dce->hrgnClip,然后调用DceUpdateVisRgn 获取可见区域,DceGetVisRgn 使用VIS_ComputeVisibleRegion 计算可见区域,它通过遍历所有子窗口、所有父窗口和每个级别的所有兄弟窗口(顶级窗口有一个父窗口作为桌面(((PCLIENTINFO)(NtCurrentTeb()->Win32ClientInfo))->pDeskInfo->spwnd)并且所有顶级窗口都是兄弟窗口;桌面的父窗口)来开发一个复杂的区域是 NULL 并删除它们覆盖的部分 - 这似乎不会对桌面窗口执行任何特殊处理,例如剪切到客户区,并且像 z 顺序中的任何其他窗口一样对待它,只有它覆盖的东西被移除)。 DceGetVisRgn 然后将返回的这个可见区域组合起来,并将其组合到剪切区域 Dce->hrgnClip 中,并使用 RgnVisible 将它们组合成 IntGdiCombineRgn(RgnVisible, RgnVisible, RgnClip, RGN_AND),然后使用 dc->prgnVis 将其复制到 GdiSelectVisRgn(Dce->hDC, RgnVisible)DCDCE 是设备上下文,IntBeginPaint 是 DC 缓存中 DC 的设备上下文条目。因此,DC 的系统区域现在是窗口的可见区域和更新区域的交集。 GdiGetClipBox(Ps->hdc, &Ps->rcPaint) 还调用 REGION_GetRgnBox(pdc->prgnVis, prc),它调用 pdc->prgnVis 将区域 pdc->prgnVis->rdh.rcBound (Ps->rcPaint) 的边界复制到 GdiGetClipBox,然后 {{1 }} 调用 IntDPtoLP(pdc, (LPPOINT)prc, 2) 将边界从物理坐标转换为逻辑坐标,DPI-unaware 应用程序使用逻辑坐标。 Paintstruct 现在包含最小的逻辑矩形,其中包含更新区域和可见区域的复杂交集。

GetClipRgn 调用 NtGdiGetRandomRgn,当使用 pdc->dclevel.prgnClip 调用时返回 CLIPRGN,这是使用 SetClipRgn

定义的应用程序 <块引用>

应用程序定义的剪辑区域是由 SelectClipRgn 函数标识的剪辑区域。它不是应用程序调用 BeginPaint 函数时创建的剪切区域。

有 2 个剪辑区域。一个是应用程序定义的应用程序,由应用程序使用SelectClipRgn创建,指针存储在pdc->dclevel.prgnClip中,另一个裁剪区域是系统区域,更新到系统区域和系统区域的交集后通过 BeginPaint 调用更新区域,在其中它作为 PAINTSTRUCT 中的逻辑剪切矩形呈现给应用程序。

GetClipBox调用NtGdiGetAppClipBox,它调用GdiGetClipBox,当然返回当前系统区域的最小逻辑矩形边界,如果GetDC可能是可见区域使用过,也可能是系统区域与带有GetDCEx的自定义裁剪区域相交,也可能是使用BeginPaint时与窗口更新区域相交的系统区域。您的问题意味着系统区域在计算时现在正在对 VIS_ComputeVisibleRegion

中的桌面窗口执行特殊处理

要真正直接访问 DC,进而访问系统区域,您必须启动驱动程序并与之交互才能从应用程序执行此操作。

答案 2 :(得分:0)

这似乎是Windows相关版本中的错误,显然已经在较新的版本中对其进行了修复。