【问题标题】:Manipulate system/visible clipping region in Windows 1809在 Windows 1809 中操作系统/可见剪辑区域
【发布时间】: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 样式不起作用的 Adob​​eAir 应用程序:我发现它在内部使用 BitBlt 将后台缓冲区渲染到窗口 dc。渲染步骤为:

  • GetDC(hwnd)在窗口上获取hdcWin
  • CreateCompatibleDC(hdcWin) 创建一个hdcMem
  • 致电SelectObject(hdcMem, bmp) 选择HBITMAPhdcMem
  • BitBlthdcMemhdcWin。 在BitBlt 调用期间,hdcMem 即使在屏幕外区域也包含有效的像素数据,但该数据永远不会复制到hdcWin

我在BitBlt 调用期间查看了系统区域。对于hdcMem,系统区域是NULLREGION,但对于hdcWin,系统区域总是在屏幕边缘被剪裁。我还尝试调整系统区域,将所有对GetDC 的调用替换为GetDCEx(hwnd, hrgn, DCX_CACHE | DCX_INTERSECTRGN)(如in this article 所述),但这不起作用并且似乎没有提供扩展区域的选项。我真的认为解决问题的秘诀在于操纵窗口 dc 的系统区域,但我不知道该怎么做..

如果发现CreateDC 函数将指向DEVMODE 结构的指针作为最后一个参数(msdn)。这又具有字段dmPelsWidthdmPelsHeightdmPosition。我相信这些构成了系统区域,也许,如果我可以操纵它们,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


【解决方案1】:

这似乎是 Windows 相关版本中的一个错误,显然已在更新的版本中得到修复。

【讨论】:

  • weird things 也带有工具提示。 1909年还没有修复它,所以必须升级。 :)
【解决方案2】:

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

GetClipRgn 调用NtGdiGetRandomRgn,当使用CLIPRGN 调用时返回pdc->dclevel.prgnClip,这是使用SetClipRgn 定义的应用程序

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

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

GetClipBox调用NtGdiGetAppClipBox,它调用GdiGetClipBox,当然返回当前系统区域的最小逻辑矩形边界,如果使用GetDC可能是可见区域,也可能是系统区域与GetDCEx 的自定义剪切区域相交,或者使用BeginPaint 时可能是与窗口更新区域相交的系统区域。您的问题意味着系统区域在计算时正在对VIS_ComputeVisibleRegion中的桌面窗口执行特殊处理

要实际直接访问 DC 并因此访问系统区域,您必须启动并与驱动程序交互才能从应用程序中执行此操作。

【讨论】:

    【解决方案3】:

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

    SetWindowLong(hwnd, GWL_EXSTYLE, WS_EX_LAYERED);
    

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

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

    【讨论】:

    • 不适用于 Win 1909 & 2020H4 & 2021H1。
    猜你喜欢
    • 2017-08-08
    • 1970-01-01
    • 1970-01-01
    • 2019-07-30
    • 1970-01-01
    • 2014-08-02
    • 2011-11-10
    • 1970-01-01
    • 2021-05-27
    相关资源
    最近更新 更多