【发布时间】:2019-07-01 12:29:08
【问题描述】:
显然,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 版本上的最新内容。
EDIT3:我对此做了更多研究。关于 WS_EX_LAYERED 样式不起作用的 AdobeAir 应用程序:我发现它在内部使用 BitBlt 将后台缓冲区渲染到窗口 dc。渲染步骤为:
-
GetDC(hwnd)在窗口上获取hdcWin -
CreateCompatibleDC(hdcWin)创建一个hdcMem - 致电
SelectObject(hdcMem, bmp)选择HBITMAP到hdcMem -
BitBlt从hdcMem到hdcWin。 在BitBlt调用期间,hdcMem即使在屏幕外区域也包含有效的像素数据,但该数据永远不会复制到hdcWin。
我在BitBlt 调用期间查看了系统区域。对于hdcMem,系统区域是NULLREGION,但对于hdcWin,系统区域总是在屏幕边缘被剪裁。我还尝试调整系统区域,将所有对GetDC 的调用替换为GetDCEx(hwnd, hrgn, DCX_CACHE | DCX_INTERSECTRGN)(如in this article 所述),但这不起作用并且似乎没有提供扩展区域的选项。我真的认为解决问题的秘诀在于操纵窗口 dc 的系统区域,但我不知道该怎么做..
如果发现CreateDC 函数将指向DEVMODE 结构的指针作为最后一个参数(msdn)。这又具有字段dmPelsWidth、dmPelsHeight 和dmPosition。我相信这些构成了系统区域,也许,如果我可以操纵它们,DC 将不再被剪辑,但我还不能挂钩 CreateDC 函数。
如果您对我的新见解有任何新想法,请分享。如有任何帮助,我将不胜感激!
【问题讨论】:
-
据我了解,您想更改其他程序的剪辑区域吗?我要在这里引导 Raymond Chen 并问:如果两个程序同时尝试这样做会怎样?操作系统可以做到这一点,因为根据定义只有一个。
-
我可以复现(同样使用官方示例docs.microsoft.com/en-us/windows/desktop/gdi/capturing-an-image),其实我什至不需要编写任何代码。启动 Windows(我的是 Windows 10,64 位,6.3.17763),打开记事本,将它移到屏幕之外的一半,然后将一大块文本粘贴到其中。运行 ALT-TAB 会显示居中的缩略图(或将鼠标移动到任务栏),你会看到只有一半的记事本被涂上了文字……闻起来像个 bug。
-
恕我直言,您应该将其报告给 Microsoft,因为即使他们的示例代码也说明了该问题。
-
我在 Windows 反馈中心为此创建了一个错误报告。这是链接aka.ms/AA4c5yc
-
不幸的是,Windows 1903 和 1909 也存在这个问题。
标签: c++ windows winapi clipping