如何在 WM_PAINT 处理程序中捕获 assert()?

How to catch assert() in WM_PAINT handler?

我有 MSVC 2019,vc142 x64,SDK 10.0.18362.0,WINAPI 游戏项目,启用了 JIT 调试,定义了 _DEBUG。我使用标准库 #include <cassert> 中的 assert()assert(expr) 呼叫扩展到 _wassert。如果测试代码 assert(false) 放置在除 WM_PAINT 处理程序之外的任何位置,则会显示带有 Abort/Retry/Ignore 选项的 window,这是预期的行为。

但如果我在 case WM_PAINT 中有 assert(false),则断言 window 未显示。程序只是中止并写入 stderr。问题是很多游戏逻辑是从 WM_PAINT 的处理程序(例如 Core::Update(dt))调用的,我无法捕获我的代码生成的任何断言。

WndProc 代码:

LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    switch (message)
    {
    case WM_PAINT:
    {
        assert(false);
    }
    break;
    case WM_DESTROY:
        PostQuitMessage(0);
        break;
    default:
        return DefWindowProc(hWnd, message, wParam, lParam);
    }
    return 0;
}

程序发送垃圾邮件并且不会停止:

Program: ...t\source\repos\Test3\x64\Debug\WindowsProject1.exe
File: C:\Users\b2soft\source\repos\Test3\...\Windows...ct1.cpp
Line: 149

Expression: false

For information on how your program can cause an assertion
failure, see the Visual C++ documentation on asserts

(Press Retry to debug the application - JIT must be enabled)Assertion failed!

即使 assert() 是从 WM_PAINT

触发的,我也希望使用 Abort/Retry/Ignore 选项进行相同的调试 window

闲置时如果不使用BeginPaint and EndPaint functions, the system will continuously send WM_PAINT消息,会出现一些异常情况。

所以只需要修改代码如下:

LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
        HDC hdc;
        PAINTSTRUCT ps;
        RECT rect;
    switch (message)
    {
    case WM_PAINT:
        hdc = BeginPaint(hWnd, &ps);
        assert(false);
        EndPaint(hWnd, &ps);
        return 0;
    case WM_DESTROY:
        PostQuitMessage(0);
        break;
    default:
        return DefWindowProc(hWnd, message, wParam, lParam);
    }
    return 0;
}

输出:

我们在处理 WM_PAINT 消息时必须小心。 Windows 管理器不断监视屏幕的哪些部分需要重绘并将此信息添加到 window 的更新区域。当调用GetMessagePeekMessage并且没有更高优先级的消息时,为windows生成具有非空更新区域的WM_PAINT消息。

BeginPaint 准备 window 的设备上下文使用更新区域来限制绘画区域。清除此更新区域后,window 甚至可以在开始绘制当前区域之前收集新区域进行绘制。

如果我们省略 BeginPaint 调用,更新区域不会被清除(除非我们使用替代解决方案,例如 ValidateRect)并且 WM_PAINT 始终准备好被调度。这会导致无休止的 WM_PAINT 消息流,处理器使用率很高。请记住,DefWindowProc 在内部处理 WM_PAINT,因此这些问题仅在您明确拦截 WM_PAINT 消息时才会出现。

WM_PAINT 处理程序中使用 asert,而不是 BeginPaint 调用或在 BeginPaint 调用之前,会导致消息框和 WM_PAINT 消息之间出现不必要的交互。根据设置 assert 可以显示启动新(嵌套)消息循环的消息框。当 assert 构建的消息框要显示时 WM_PAINT 将再次生成原始 window。这导致下一个嵌套循环一次又一次。 32 次嵌套调用 MessageBox 失败后程序中止(通过调用 abort 函数)。

assert 停止递归循环嵌套之前放置 BeginPaint(或清除更新区域的任何替代方法)(至少直到调度下一个 WM_PAINT)和 assert可以正确显示消息框。

因此,如果您想在 WM_PAINT 处理程序中使用 assert,请将其放在 BeginPaintValidateRect(hWnd, NULL) 之后。

LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
    // assert here could lead to abort

    switch (message)
    {
    case WM_PAINT:
    {
        // assert here could lead to abort

        PAINTSTRUCT ps;
        HDC hdc = BeginPaint(hWnd, &ps);
        // Update region cleared, no WM_PAINT will be generated
        // until some event creates new dirty area

        // assert here have chance to be handled properly
        assert(false);

        EndPaint(hWnd, &ps);

        // assert here have chance to be handled properly
    }
    break;
    case WM_DESTROY:
        PostQuitMessage(0);
        break;
    default:
        return DefWindowProc(hWnd, message, wParam, lParam);
    }

    // assert here have chance to be handled properly

    return 0;
}

请记住,这是基于观察到的行为而不是基于任何特定文档,因此您可能会根据设置、运行时间版本甚至是否附加调试器获得不同的结果。

使用 VC2017 x86 Debug build ucrt 10.0.17763.0

在 Windows 10 上完成的测试

我个人并不知道这个限制,它看起来有点糟糕。特别是对于放置在 window 过程顶部的最终断言。幸运的是附加的调试器至少在输出中显示错误 window 并且我从来没有 运行 在没有附加调试器的情况下调试构建。