PrintWindow 发送消息 WM_PAINT 还是 WM_PRINT?

Posted

技术标签:

【中文标题】PrintWindow 发送消息 WM_PAINT 还是 WM_PRINT?【英文标题】:PrintWindow send message WM_PAINT or WM_PRINT? 【发布时间】:2017-10-06 16:09:25 【问题描述】:

根据 msdn PrintWindow(检索日期 2017 年 5 月 5 日)

拥有 hWnd 引用的窗口的应用程序处理 PrintWindow 调用并在 hdcBlt 引用的设备上下文中呈现图像。应用程序接收 WM_PRINT 消息,或者,如果指定了 PW_PRINTCLIENT 标志,则接收 WM_PRINTCLIENT 消息。有关详细信息,请参阅 WM_PRINT 和 WM_PRINTCLIENT。

MSDN 从未声称有关消息 WM_PAINT。 但是我的测试证明了上面关于 WM_PRINT 消息的说法是错误的。

应用 A:

LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)

    switch (message)
    
    case WM_PAINT:
        DefWindowProc(hWnd, message, wParam, lParam);
        break;
    case WM_PRINT:
        OutputDebugStringA("WM_PRINT");
        break;
    case WM_PRINTCLIENT:
        OutputDebugStringA("WM_PRINTCLIENT");
        break;
    //other cases ...
    
    return 0;

App B(更多关于App B的细节)

HWND hwnd = FindWindow(NULL, lpString);
//...
//PrintWindow(hwnd, hdc, PW_CLIENTONLY);
PrintWindow(hwnd, hdc, 0);
 

当我调用 App B 来捕获 App A 时。根据 msdn PrintWindow,应该命中大小写 WM_PRINT,但命中大小写 WM_PAINT。

根据this article

如果是这样,则无法捕获未实现 WM_PAINT 的分层窗口,因为 UpdateWindow 只发送 WM_PAINT

所以最后,我只想知道是msdn错了还是我的代码错了? 如果它确实发送了消息 WM_PRINT,那么消息 WM_PRINT 是如何工作的?

【问题讨论】:

This MSDN forum entry 似乎证实了这个问题。无论如何,我不会依赖PrintWindow()WM_PRINT,因为它们需要来自目标窗口的合作。相反,只需从屏幕 DC 中BitBlt() 请注意,SetWindowDisplayAffinity 让这变得更难实现:How do I make it more difficult for somebody to take a screenshot of my window? 如果用户 Ðаn 停下来:停止删除适当的语言标签 (c++),并添加不适当的编译器标签 (visual-c++)。谢谢。 也许在过去,PrintWindow 支持发送 WM_PRINT,但它改变了并且微软忘记更新文章。也许这就是它改变的原因:***.com/a/830375/4608491 【参考方案1】:

简单回答:是的,我在 Windows 10 和 Windows XP 上都重现了您所描述的行为。当我调用PrintWindow 时,目标窗口收到WM_PAINT 消息,不是 WM_PRINT 消息。

我不仅可以使用断点和跟踪输出来重现它,而且我还可以通过使用调试器逐步执行PrintWindow(埋在Windows操作系统本身内部)来确认它。与几乎所有的用户和 GDI 函数一样,它是一个客户端存根,它转发到服务器端系统函数NtUserPrintWindow。从这一点开始,执行通过更多的系统函数和错误检查,并最终将值 15(对应于WM_PAINT 消息)加载到EDX 寄存器中,然后通过一个名为@987654334 的内部函数调度此消息@。

这基本上就是PrintWindow 所做的一切——向指定的窗口发送WM_PAINT 消息,要求它打印到指定的设备上下文中。所以是的,MSDN 文档提出了错误的声明。 PrintWindow 的实现发送WM_PRINT 消息。

查看 ReactOS 源代码(Windows 操作系统的开源克隆,旨在与二进制 API 兼容),您可以看到它实现 PrintWindow 有点不同,但仍然是道德上等价的。 (或者,更准确地说,它开始以道德上等价的方式实现PrintWindow。它的实现似乎不完整,只是返回FALSE。)在its NtUserPrintWindow function中,参数是验证,然后它调用一个内部函数IntPrintWindow,它根据PW_CLIENTONLY标志的规范设置坐标,然后——如果它没有提前返回——将强制更新窗口并简单地从窗口的 DC 到指定的 DC。

Wine 项目(Windows API 的另一个开源克隆)的做法有所不同。在那里,the PrintWindow function(完全在用户端实现)只是通过SendMessage 向指定窗口发送WM_PRINT 消息。这是implemented by Luke Benstead in December of 2009。我的猜测是,Luke 只是阅读了 MSDN 文档并编写了遵循其规范的代码,而不是复制 Microsoft 操作系统的实际行为。

现在,我最初认为 MSDN 已经过时了,而不是完全错误。随 Windows Vista 引入的 DWM 促使各种绘图 API 的实现方式发生了许多变化,我认为 PrintWindow 的文档仍然指的是旧绘图模型中的工作方式。 (记录实现细节而不是行为的结果。)事实上,在 Windows XP 上进行的测试证明了这一假设。 XP 的行为方式与 Windows 10 完全相同,PrintWindow 发送 WM_PAINT 消息,而不是 WM_PRINT 消息。更改可能是在更早的时间进行的,并且 MSDN 文档甚至更加过时了。例如,也许 Windows 9x 以这种方式实现了PrintWindow,但 NT 从未这样做过。我目前无法使用编译器访问这样的系统,因此无法验证。如果我记得,我稍后会更新这个答案。

让我感到奇怪的是Raymond Chen described in passingPrintWindow 函数的行为方式与 MSDN 文档一致:

Print­Window 函数将自定义设备上下文作为参数传递给WM_PRINT 消息...

这是在 2012 年左右写的,他当然可以访问 Windows 源代码,所以要么我在分析中遗漏了一些东西,要么 Raymond 将他的文章基于官方文档实际查看实现的功能,因为它不会真正影响文章的重点。

说到这一点,我对你的问题不太了解的是为什么这很重要。当然,研究操作系统的实际工作方式很有趣,但你不应该根据逆向工程时发现的内容编写代码。我无法想象为什么PrintWindow 是通过发送WM_PAINT 消息还是WM_PRINT 消息在内部实现的。在任何一个的正常版本中,效果都是一样的:您会将指定窗口的请求部分绘制到指定的设备上下文中。就那么简单。换句话说,App B 既不需要知道也不需要关心PrintWindow 是如何实现的。

一个正确编写的应用程序 A(换句话说,所有 Windows GUI 应用程序)将具有WM_PAINTWM_PRINTCLIENT 消息的处理程序。 WM_PAINT 应该以显而易见的方式处理,WM_PRINTCLIENT 应该简单地背负这个实现——例如

case WM_PAINT:

    PAINTSTRUCT ps;
    BeginPaint(hWnd, &ps);

    OnPaintContent(ps);

    EndPaint(hWnd, &ps);
    return 0;

case WM_PRINTCLIENT:

    PAINTSTRUCT ps;
    ps.hdc = reinterpret_cast<HDC>(wParam);
    GetClientRect(hWnd, &ps.rcPaint);

    OnPaintContent(ps);        

    return 0;

 ...

void PaintContent(const PAINTSTRUCT& ps)

    // Paint the window's content here.

您根本没有理由处理WM_PRINT,因为这是由默认窗口过程处理的。不仅必须如此(逻辑上),因为此消息的实现必须处理非客户区的绘制,窗口通常不会自行绘制,但the MSDN documentation 在“备注”部分,DefWindowProc 处理此消息,根据指定的标志向窗口发送适当的子消息,包括WM_ERASEBKGNDWM_PRINTCLIENT

【讨论】:

感谢您的回答,我也想回答您的问题:关于您的问题,我不太了解的是为什么这很重要:这是我对知识的渴望。如果有孩子问我这件事,我终于可以说他在 MSDN 上发现的内容是错误的 :)【参考方案2】:

windows API PrintWindow 使用 WM_PAINT 是有原因的:它可以跨不同的进程工作。像其他 GDI 句柄一样的 DC 句柄仅在创建它的同一进程中有效。因此,从另一个进程向窗口发送 WM_PRINT 或 WM_PRINTCLIENT 会失败。

但是... PrintWindow 成功地抓取了另一个进程的内容,因为它首先通过 BeginPaint 在被调用进程的上下文中创建一个 DC,然后将内容复制回调用进程的 DC。当然,他们也可以用另一个句柄存根 WM_PRINT(CLIENT),但我认为当前的解决方案更简单。

顺便说一句,在 XP 中引入了 PrintWindow API,但在 W2K 中尚不存在。

【讨论】:

以上是关于PrintWindow 发送消息 WM_PAINT 还是 WM_PRINT?的主要内容,如果未能解决你的问题,请参考以下文章

诊断神秘 WM_PAINT 消息

WM_PAINT 与 WM_ERASEBKGND(三种情况,比较清楚)

WM_PAINT与WM_ERASEBKGND

窗口绘制

Invalidate() InvalidateRect() 与 UpdateWindow()

WM_PAINT消息在窗口重绘的时候产生,那什么时候窗口会重绘(异步工作方式,效率更高,灵活性更强)