【问题标题】:WinAPI timer callback not called while child window is in focus子窗口处于焦点时未调用 WinAPI 计时器回调
【发布时间】:2019-11-04 15:48:59
【问题描述】:

我正在开发一个使用 Direct3D 和 WinAPI 的 3D 编辑器应用程序。我创建了一个主窗口,它有一个占据主窗口客户区一部分的子窗口。这被 D3D 用作渲染目标。然后,我还使用CreateWindow 创建了一个单独的窗口,它的构建方式与主窗口相同(即“主窗口”和用作渲染目标的内部子窗口),我将此窗口设为主窗口的子窗口应用程序窗口(以确保它们一起最小化/恢复/关闭)。

D3D 渲染由处理其WM_PAINT 消息的渲染目标子窗口执行。为了减少不必要的开销,我将窗口过程设置为仅在 GetForegroundWindowGetFocus 匹配各自的窗口句柄时在 WM_PAINT 上呈现。换句话说,我只希望一个窗口的渲染在它位于顶部并获得焦点时被刷新。

这是我的主要消息循环:

HWND mainWnd;
HWND mainRenderWnd;
HWND childWnd;
HWND childRenderWnd;

// ...

MSG msg = {};
while (WM_QUIT != msg.message)
{
    if (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE))
    {
        if (!TranslateAccelerator(mainWnd, accel, &msg))
        {
            TranslateMessage(&msg);
            DispatchMessage(&msg);
        }
    }
    else
    {
        // Run non-UI code...
    }
}

当主窗口获得WM_SETFOCUS 时,我让它将焦点设置到其渲染目标子窗口,因为我想在那里处理输入(例如相机控件):

// ...
LRESULT CALLBACK MainWndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    switch(uMsg)
    {
        //...
        case WM_SETFOCUS:
        {
            SetFocus(mainRenderWnd);
            return 0;
        }
        //...
    }
    return DefWindowProc(hWnd, uMsg, wParam, lParam);
}

我在childWnd 的窗口过程中做同样的事情,将焦点设置为childRenderWnd。此窗口由用户打开和关闭,即在任何时候它可能存在也可能不存在,但是当它存在且未最小化时,它需要是具有焦点的前景窗口。另外,为了控制子窗口的帧率,我使用 Timer 来刷新它:

static constexpr UINT_PTR RENDER_TIMER_ID = (UINT_PTR)0x200;
void TimerCallback(HWND Arg1, UINT Arg2, UINT_PTR Arg3, DWORD Arg4)
{
    if (IsIconic(childWnd) || !(GetFocus() == childRenderWnd))
        return;

    // Invalidate the render area to make sure it gets redrawn
    InvalidateRect(childRenderWnd, nullptr, false);
}

// ...

SetTimer(childWnd, RENDER_TIMER_ID, 16, (TIMERPROC)TimerCallback);

完成所有这些设置后,mainWndmainRenderWnd 似乎工作得很好。然而,childRenderWnd 拒绝在它处于前景和焦点时对其进行渲染。在调试时,我发现虽然是这种情况,但定时器回调永远不会被执行,WM_TIMER 消息也不会被分派到子窗口。

另一方面,当我故意将焦点从子窗口移到主窗口上时(同时保持两者都打开),定时器消息被发送,回调被执行。另一个问题是,当我在两个窗口都打开时最小化应用程序,然后恢复它们,两个窗口的渲染目标都没有刷新。相反,似乎焦点被“翻转”了,因为我必须先点击我的子窗口,然后点击我的主窗口,这使它正确刷新(而孩子仍然拒绝渲染任何东西)。

我错过了什么?我搜索了其他有问题的人,例如不正确的消息泵设置阻止 WM_TIMER,但似乎没有什么可以解释这里发生了什么。

提前感谢您的帮助!

【问题讨论】:

  • (请注意,您的TimerCallback 调用约定不匹配;删除(TIMERPROC) 演员以显示它。)计时器消息的优先级较低。如果队列中始终存在非定时器消息,则将处理该非定时器消息,并且定时器消息将永远不会出现。
  • @RaymondChen 那么如何解决呢?拥有这样一个功能并没有多大意义,只是在 UI 发生其他任何事情时它会被渲染为不可用。
  • 我们需要看到minimal reproducible example。您发布的那部分代码没有表现出您描述的行为。
  • UI 计时器的优先级较低,因为该应用现在可能正忙于做更重要的事情。当应用程序最终准备好做其他事情时,计时器将运行。 UI 线程通常大部分时间都处于空闲状态,因此计时器延迟通常很短。如果您设计了一个通常持续忙碌的 UI 线程,那么您可以使用不同的计时器机制,例如线程池计时器。
  • 我找出了问题所在,并使用相关细节编辑了我的问题。谢谢你们的cmets!

标签: c++ windows winapi timer rendering


【解决方案1】:

IInspectableRaymond Chen 的 cmets 帮助我找到了答案。我试图在一个最小的应用程序中重现该错误,最终找到了我的问题的根源。最初,如果没有要发送的消息,主窗口只会在消息循环中调用InvalidateRect,从而有效地在每次有机会时重新绘制自己。这最初似乎没有造成任何伤害,场景渲染得很好,窗口响应输入。然而,一旦我引入了第二个窗口,它肯定已经用绘制消息淹没了消息循环,使得任何计时器消息都无法通过。

解决方案很简单,就是为两个窗口设置一个计时器,以设置它们刷新 D3D 渲染目标的速率。再加上焦点检查,我现在可以轻松地在两个窗口之间切换,在任何给定时间只有一个会自行刷新。

问题仍然是 WinAPI 计时器系统是否是最佳选择。除了我的糟糕代码之外,人们还提到计时器的消息是低优先级的,从长远来看,这可能使其成为帧率控制的糟糕选择。

编辑:我最终接受了IInspectable 的建议,即在主消息循环中使用while 循环来处理调度所有消息,一旦处理完这些消息,我就会执行帧更新。这使我能够持续更新所有窗口并控制帧速率,而不会冒窗口消息被卡住的风险。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-10-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多