为什么我的应用程序冻结?——消息循环和线程简介
这种现象并非孤立于任何特定消息。这是 Windows 消息循环的一个基本属性:在处理一条消息时,不能同时处理其他消息。它并不完全以这种方式实现,但您可以将其视为一个队列,您的应用程序将消息从队列中拉出,以与插入消息相反的顺序进行处理。
因此,花费太长时间处理任何消息将暂停其他消息的处理,从而有效地冻结您的应用程序(因为它无法处理任何输入)。解决这个问题的唯一方法是显而易见的:不要花太长时间处理任何一条消息。
通常这意味着将处理委托给后台线程。您仍然需要在主线程上处理所有消息,并且后台工作线程需要在完成后向主方法报告。与 GUI 的所有交互都需要在单个线程上进行,这几乎总是应用程序中的主线程(这就是为什么它通常被称为 UI 线程)。
(为了回答您的问题中提出的反对意见,是的,您可以在单处理器机器上运行多个线程。您不一定会看到任何性能改进,但它会使 UI 更具响应性。这里的逻辑是线程可以一次只做一件事,但处理器可以非常快速地在线程之间切换,有效地模拟一次做不止一件事。)
此 MSDN 文章中提供了更多有用的信息:防止 Windows 应用程序中的挂起
特殊情况:模态事件处理循环
Windows 上的某些窗口操作是模态操作。模态是计算中的一个常用词,基本上是指将用户锁定在一个特定的模式中,在这种模式下他们不能做任何其他事情,直到他们改变(即退出)模式。每当开始模式操作时,都会启动一个单独的新消息处理循环,并且在该模式期间发生消息处理(而不是主消息循环)。这些模式操作的常见示例是拖放、窗口大小调整和消息框。
考虑这里的窗口大小调整示例,您的窗口会收到一条WM_NCLBUTTONDOWN
消息,您将其传递给DefWindowProc
默认处理。DefWindowProc
发现用户打算开始移动或调整大小操作,并进入了位于 Windows 自己代码内部深处某处的移动/调整大小消息循环。因此,您的应用程序的消息循环不再运行,因为您已进入新的移动/调整大小模式。
只要用户以交互方式移动/调整窗口大小,Windows 就会运行此移动/调整大小循环。它这样做是为了拦截鼠标消息并相应地处理它们。当移动/调整大小操作完成时(例如,当用户释放鼠标按钮或按下Esc键时),控制将返回到您的应用程序代码。
值得指出的是,通过消息通知您已发生此模式更改;相应的消息表明模式事件处理循环已退出。这允许您创建一个计时器,该计时器将继续生成您的应用程序可以处理的消息。如何实现的实际细节相对不重要,但快速解释是继续在其自己的模式事件处理循环内向您的应用程序发送消息。使用该函数创建一个计时器以响应消息,并使用该函数销毁它以响应消息。WM_ENTERSIZEMOVE
WM_EXITSIZEMOVE
WM_TIMER
DefWindowProc
WM_TIMER
SetTimer
WM_ENTERSIZEMOVE
KillTimer
WM_EXITSIZEMOVE
不过,我只是为了完整性而指出这一点。在我编写的大多数 Windows 应用程序中,我从来不需要这样做。
那么,我的代码有什么问题?
除此之外,您在问题中描述的行为是不寻常的。如果您使用 Visual Studio 模板创建一个新的空白 Win32 应用程序,我怀疑您是否能够复制此行为。在没有看到您的窗口过程的其余部分的情况下,我无法判断您是否阻止了任何消息(如上所述),但我在问题中看到的部分是错误的。您必须始终调用DefWindowProc
您自己未明确处理的消息。
在这种情况下,您可能会误以为您正在这样做,但WM_SYSCOMMAND
它的wParam
. 你只处理其中一个,SC_CLOSE
. 所有其他人都只是因为你而被忽略return 0
。这包括所有的窗口移动和调整大小的功能(例如SC_MOVE
, SC_SIZE
, SC_MINIMIZE
, SC_RESTORE
,SC_MAXIMIZE
等)。
而且真的没有很好的理由来处理WM_SYSCOMMAND
自己;只是让DefWindowProc
你照顾它。唯一需要处理WM_SYSCOMMAND
的是当您将自定义项添加到窗口菜单时,即使这样,您也应该将您无法识别的每个命令传递给DefWindowProc
.
一个基本的窗口过程应该是这样的:
LRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
switch(uMsg)
{
case WM_CLOSE:
DestroyWindow(hWnd);
return 0;
case WM_DESTROY:
PostQuitMessage(0);
return 0;
}
return DefWindowProc(hWnd, uMsg, wParam, lParam);
}
您的消息循环也可能是错误的。惯用的 Win32 消息循环(位于WinMain
函数底部附近)如下所示:
BOOL ret;
MSG msg;
while ((ret = GetMessage(&msg, nullptr, 0, 0)) != 0)
{
if (ret != -1)
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
else
{
// An error occurred! Handle it and bail out.
MessageBox(nullptr, L"Unexpected Error", nullptr, MB_OK | MB_ICONERROR);
return 1;
}
}
你不需要任何类型的钩子。有关这些的 MSDN 文档非常好,但您是对的:它们很复杂。在您对 Win32 编程模型有更好的理解之前,请远离。在极少数情况下,您需要钩子提供的功能。