移动鼠标会阻塞WM_TIMER和WM_PAINT消息

4
在我正在开发的应用程序中,有些情况下应用程序运行非常缓慢,在这些情况下,我发现我的鼠标移动、定时器/绘制消息没有被处理。如果我以缓慢的圆形移动鼠标,我可以无限期地防止窗口被重新绘制!我发现这是预期行为

除了 WM_PAINT 消息、WM_TIMER 消息和 WM_QUIT 消息之外,系统总是在消息队列的末尾发布消息。这确保窗口按照先进先出(FIFO)顺序接收其输入消息。然而,WM_PAINT 消息、WM_TIMER 消息和 WM_QUIT 消息会保留在队列中,只有当队列中不包含其他消息时,它们才会转发到窗口过程。此外,同一窗口的多个 WM_PAINT 消息被合并成一个 WM_PAINT 消息,将客户区的所有无效部分合并为一个区域。合并 WM_PAINT 消息减少了窗口必须重新绘制其客户区内容的次数。

然而,我该怎么办呢?有时候需要在鼠标移动时立即重新绘制。
我通过类似于以下的方法在我的CWnd派生类中捕获消息:
virtual LRESULT WindowProc(UINT message, WPARAM wParam, LPARAM lParam);

3
如果需要立即重绘,请调用 InvalidateRect 后跟 UpdateWindow,或使用带有 RDW_UPDATENOW 标志的 RedrawWindow - Jonathan Potter
我在想当接收到 WM_MOUSEMOVE 时,是否可以像这样轮询消息队列 while(msg == WM_MOUSEMOVE) 并且只处理最后一个 WM_MOUSEMOVE 消息,避免类似消息的堆积。 - Mr. Boy
简单的解决方案是让你处理 WM_MOUSEMOVE 的速度更快。 - David Heffernan
很遗憾,那不是一个简单的解决方案。 - Mr. Boy
您可以在循环中调用 PeekMessage() 以检索队列中的任何 WM_MOUSEMOVE 消息。如果您忽略 WM_MOUSEMOVE 中的坐标并使用 GetCursorPos() 自己读取光标位置,可能会获得更好的结果。 - Jonathan Potter
2个回答

4
WM_PAINTWM_TIMER,如文档中所述,与其他消息不同。
实际上,它们是最低优先级的消息。也就是说,如果队列中有其他消息,它们将不会被处理。
话虽如此,您正在移动鼠标,因此会发布很多消息,但是这些消息的频率通常相当低(每秒几十个),因此您的程序应该大多处于空闲状态。但是不是,可能是因为其中一些消息比预期的时间长,导致了队列阻塞。您只需要检测哪个消息及其原因即可。
我能想到的一些与鼠标相关的消息:
- WM_MOUSEMOVE - WM_NCMOUSEMOVE - WM_SETCURSOR - WM_NCHITTEST 从网络搜索中找到的:
- WM_MOUSEOVER - WM_MOUSELEAVE - WM_NCMOUSEOVER - WM_NCMOUSELEAVE 无论如何,如果您想立即重绘而不等待WM_PAINT,则应调用UpdateWindow()。此函数强制立即处理WM_PAINT(如果有任何无效内容的话),并在完成之前阻塞,绕过该消息的低优先级问题。
根据您在评论中提到的情况,我认为您最好的解决方案可能是以下这些步骤:
- 在WM_MOUSEMOVE中将光标位置保存在成员变量中,并设置表示鼠标已移动的标志。 - 实现一个OnIdle()处理程序,检查鼠标移动标志。如果未移动,则不执行任何操作。如果已移动,则进行昂贵的计算。 - 您可以尝试使用或不使用从OnIdle()调用UpdateWindow(),并查看哪个更好。 是的,在慢速计算机上仍会感觉不太流畅,但由于OnIdle()的优先级甚至低于WM_TIMERWM_PAINT,因此这些消息不会被无限地排除。更重要的是,您将不会排队多次调用昂贵的函数。

基本上是鼠标移动功能 - 我们检测鼠标下方的内容(第三方库也是如此)。这是一个地图应用程序,如果地图的某个区域非常繁忙,速度较慢的计算机将花费所有时间来执行此操作。遗憾的是,没有简单的解决方法可以加快速度,我们只需要在所有情况下停止它优先执行即可。 - Mr. Boy
如果您在 WM_MOUSEMOVE 中花费了太多时间并导致消息队列阻塞,那么就应该考虑使用工作线程。您的 GUI 不应该做这样的重活。 - tenfour
@John:假设你每10毫秒收到一次WM_MOUSEMOVE,但处理它需要25毫秒:你会遇到问题。如果你使用UpdateWindow()修复它,那么更新将滞后于鼠标。如果你不使用UpdateWindow(),则重绘将不会发生,直到鼠标停止移动...一个工作线程并且只在每几个点之一执行任务可能是最好的解决方案。 - rodrigo
我认为在目前情况下重新设计的程度是不可行的,特别是因为我们将鼠标移动事件输入到了一个闭源的第三方库中。如果在25ms内收到任何WM_MOUSEMOVE事件就简单地丢弃掉那将是可以接受的,但我不知道CWnd::WindowProc()是否能够从队列中"吮吸"消息?在纯Win32游戏编程中,通常会这样做——每帧您会弹出消息,直到队列为空,然后再进行渲染。无论这是否是良好实践,都欢迎提供演示的答案 - Mr. Boy
@John:请看一下我的更新答案,如果它适用于你的问题。 - rodrigo
@rodrigo 您对 UpdateWindow() 的担忧是不正确的。如果鼠标每 10 毫秒移动一次,但您需要 25 毫秒来处理消息,则 WM_MOUSEMOVE 消息将每 25 毫秒传递一次。WM_MOUSEMOVE 消息不会排队。如果调用 UpdateWindow() 导致鼠标处理时间更长(50 毫秒?),则 WM_MOUSEMOVE 消息的传入速率将下降以匹配。使用 UpdateWindow(),窗口将每秒重绘 20 次。没有它 - 零。UpdateWindow() 是解决方案。请参见 http://blogs.msdn.com/b/oldnewthing/archive/2003/10/01/55108.aspx - Bruce Dawson

2
几乎所有情况下的正确解决方案是,在响应WM_MOUSEMOVE时更新应用程序状态以需要重绘的方式,应调用UpdateWindow()。就是这样。不应使用PeekMessage()、GetCursorPos、OnIdle()等。
如果调用UpdateWindow(),则将强制发送WM_PAINT消息并更新窗口,用户将看到他们的鼠标移动反应,并且应用程序将感觉灵敏。这很好。事实上,这是理想的。
如果不调用UpdateWindow(),则可能会在传递WM_PAINT消息之前出现另一个鼠标消息。大多数鼠标可以以每秒128次的速度传递WM_MOUSEMOVE消息,触摸屏似乎以200/s的速度运行,游戏鼠标可能会更快。因此,如果用户快速移动鼠标,则如果不强制进行绘画,则可能永远无法传递WM_PAINT消息。这使您在处理每秒100多个WM_MOUSEMOVE消息但从未绘制窗口的情况下处于困境。这意味着您的内部帧速率为100 fps,而可见帧速率为0 fps。破碎了。
因此,再次,答案是在需要绘制的WM_MOUSEMOVE处理之后调用UpdateWindow()。您的内部帧速率可能较低(因为现在每条消息都要做更多的工作),但您的可见帧速率现在将与您的内部帧速率相匹配,而正是您的可见帧速率很重要。
额外的WM_MOUSEMOVE消息将静默丢弃,如http://blogs.msdn.com/b/oldnewthing/archive/2003/10/01/55108.aspx所述。
我已经对许多应用程序进行了此修复,结果总是非常显著。这个一行代码的修正通常会使应用程序看起来更加灵敏。

如果您正在使用WM_TIMER执行类似于改变颜色的动画效果,则此方法无法正常工作。 - Robinson
好的,假设你正在WM_TIMER中进行动画处理。很好,那就这样做吧。 然后另一个WM_TIMER出现了,所以你再次进行动画处理。 如此循环。你的程序不断更新动画,但由于屏幕从未更新,所有这些更新工作都是浪费的。理论上?几乎不可能。我曾在多个商业软件程序中遇到过这个问题。解决方法是UpdateWindow。如果这意味着WM_TIMER只被调用一次,比如每秒钟一次,那么适当地增加动画参数。你的评论没有承认这种风险。 - Bruce Dawson
并不是真的。WM_MOUSEMOVE会阻塞WM_TIMER,因此您永远不会到达UpdateWindow,因为您永远不会检查状态是否已更改。这会发生在进程中的组件(在该消息泵上)上,这些组件与接收鼠标移动的窗口无关。解决这个问题需要增加相当多的复杂性(例如使用空闲处理程序而不是WM_TIMER等)。 - Robinson
1
这解决了你的疑虑。WM_TIMER消息被记录为低优先级,但文档似乎没有提到其他消息的优先级 - 这是一个错失的机会。 你可以有一个线程,只需在循环中调用Sleep(whatever),然后发送适当的消息。可惜这一切都太复杂了。 - Bruce Dawson

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接