先把结论说前面:
只要你是在 Windows 上写原生程序(包括很多游戏引擎、工具、编辑器),Win32 消息循环几乎是绕不开的底层机制。
它听起来玄乎,其实本质就一句话:
Windows 不停给你“塞消息”(按键了、鼠标动了、窗口要重绘了……),
你的程序里要有个“循环”,一条一条把这些消息拿出来处理——
这玩意儿就叫“消息循环”。
下面我会用大白话,把 Win32 消息循环从“是什么、为什么、怎么玩”到“各种细节坑”一口气掰开讲。
你看完应该能做到两件事:
- 看懂绝大部分 Win32 示例代码里的消息循环到底在干嘛。
- 自己能写出一个基本正确的消息循环,知道哪里能动、哪里不能乱动。
目录大纲
- 为什么 Windows 程序一定要有“消息循环”?
- Windows 消息模型:消息从哪来、去哪儿
- 最简单的消息循环长啥样?(
GetMessage版本) GetMessage/PeekMessage/TranslateMessage/DispatchMessage逐个讲清- 窗口过程函数(WindowProc):消息去哪儿“落地处理”?
- 各类常见消息:键盘、鼠标、重绘、关闭……
- 非阻塞消息循环 & 游戏循环:
PeekMessage的正确用法 - 多窗口、多线程、多消息队列:一个程序里不止一个窗口怎么办?
- 计时器、PostMessage、SendMessage:主动“发消息”的几种方式
- 常见坑和边角问题:空转、高 CPU、死循环、重入……
- 综合小例子:一个“能动的窗口程序”的全流程
- 最后再总结一遍:你真的搞懂消息循环了吗?
一、为什么 Windows 程序一定要有“消息循环”?
先别管 API 细节,想象一个场景:
- 你写了一个 Windows 窗口程序;
- 用户用鼠标点了你的窗口一下;
- 用户按了键盘上的一个键;
- 用户拖动窗口边框调整大小;
- 用户点了右上角的 X 关闭。
问题来了:
你程序怎么知道用户干了这些事?
你没法每隔 1ms 去问操作系统:
“有人按键了吗?有人点我了吗?有人拖窗口了吗?”
那样会浪费大量 CPU,还可能错过瞬时事件。
Windows 的做法是:
“我来帮你监听键盘、鼠标、窗口事件,
一旦发生,就给你这儿塞一条消息,
你自己到消息队列里取出来处理就行。”
这个“塞消息”的地方就叫消息队列(Message Queue)。
每个 GUI 线程都有一个自己的消息队列。
你的程序要干的,就是:
- 在一个循环里,不停从队列里取消息;
- 然后根据消息类型,做对应的处理;
- 没消息的时候,可以休息 / 做自己的逻辑。
这整个过程,就叫:消息循环(Message Loop)。
如果你不写这个循环,会发生什么?
- 程序启动了一闪而过就结束;
- 或者窗口一创建就“假死”(没响应),被系统判定为“无响应”;
- 用户点窗口、拖动、关闭,全都没反应。
所以可以很简单地记住一句话:
在 Windows 里,只要你想写一个响应用户操作的窗口程序,
就一定要跑一个消息循环,不停地“听系统说话”。
二、Windows 消息模型:消息从哪来、去哪儿?
来点稍微严肃一点的架构视角。
2.1 消息都是什么鬼?
Windows 定义了一大堆“消息常量”,比如:
WM_PAINT:窗口需要重绘;WM_KEYDOWN/WM_KEYUP:键盘按下/抬起;WM_MOUSEMOVE/WM_LBUTTONDOWN/WM_RBUTTONUP:鼠标移动/按键;WM_CLOSE:用户希望关闭窗口(点了 X);WM_SIZE:窗口尺寸变化;WM_DESTROY:窗口被销毁;- ……还有一大堆。
每条消息都是一个 MSG 结构体:
typedef struct tagMSG {
HWND hwnd; // 发给哪个窗口
UINT message;// 消息类型,比如 WM_PAINT
WPARAM wParam; // 附加信息(无符号整型/指针)
LPARAM lParam; // 附加信息(长整型)
DWORD time; // 消息发出时间
POINT pt; // 当时的鼠标坐标等
} MSG;
你可以粗暴理解为:
“某个时间点,在某个窗口上,发生了某件事(message),
附带一些参数(wParam/lParam)。”
2.2 消息是怎么进消息队列的?
Windows 系统做的事情包括:
- 监听键盘、鼠标、窗口行为等;
- 当某个操作发生,比如你在窗口上点击鼠标:
- 操作系统决定:“这应该给窗口发一个 WM_LBUTTONDOWN”;
- 然后把这条消息放进这个线程的消息队列。
有些消息是由系统自动投递(比如 WM_PAINT),
有些消息可以由你自己用 PostMessage、SendMessage 主动发。
2.3 消息队列的消费:谁来处理它?
程序里最终要有一个消费消息的人——
那就是你定义的窗口过程函数(Window Procedure / WndProc)。
整个链路大概是这样的:
- 系统 → 消息队列:
- OS 把
MSG放到队列里。
- OS 把
- 消息循环 → 取出消息:
GetMessage/PeekMessage从队列里拿消息出来。
- 消息循环 → 分发消息:
DispatchMessage把消息分派给对应窗口的WndProc。
- WndProc → 真正处理:
- 比如在 WM_PAINT 里画东西,
- 在 WM_KEYDOWN 里处理按键逻辑,
- 在 WM_CLOSE 里结束程序。
这就是 Win32 GUI 程序的“基本反应链”。
三、最简单的消息循环长啥样?
给你看几乎所有 Win32 教程都会出现的一段标准代码:
MSG msg;
while (GetMessage(&msg, NULL, 0, 0)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
你第一眼看可能只记住了“背下来”,但其实每一行都很有讲头。
先从最外层说起:
3.1 GetMessage:有消息就拿,没有就等
函数声明长这样:
BOOL GetMessage(
LPMSG lpMsg,
HWND hWnd,
UINT wMsgFilterMin,
UINT wMsgFilterMax
);
简单解释:
lpMsg:存放取出的消息;hWnd:想要接收哪个窗口的消息,通常传 NULL 表示“当前线程所有窗口”;wMsgFilterMin/wMsgFilterMax:消息过滤范围,一般写 0、0 表示不过滤。
关键点:
- 如果消息队列里有消息:
GetMessage取出一条放到msg里,然后返回非 0; - 如果没有消息:它会阻塞等待(线程挂起),直到有消息进来;
- 如果取到的是
WM_QUIT,GetMessage返回 0,循环就结束了。
所以那段代码的意思其实是:
只要还能取到消息(没收到 WM_QUIT),就一直在这儿循环拿消息;
没消息就睡觉,有消息就醒来处理。
这就是一个最典型、最省 CPU 的 GUI 线程写法。
3.2 TranslateMessage:处理键盘“字符翻译”
这一行经常让人迷糊:
TranslateMessage(&msg);
很多人以为它“不重要”,甚至有的教程直接删掉它。
其实它做了一件事:
把某些键盘消息(比如 WM_KEYDOWN)根据当前输入法,
转成对应的字符消息(WM_CHAR)。
也就是说,如果你想在 WM_CHAR 里获取用户输入的“字符”(比如文本框),
TranslateMessage 就是帮你从按键转换成字符的关键一步。
- 如果你只是写游戏,只看
WM_KEYDOWN/UP,不在乎具体字符,可以省略; - 但如果你将来写编辑器、文本输入框,不调用它就收不到
WM_CHAR,用户打字没效果。
3.3 DispatchMessage:把消息交给窗口处理函数
DispatchMessage(&msg);
这一行的作用就是:
根据
msg.hwnd里的窗口句柄,找到那个窗口对应的WndProc函数,
调用它,把msg.message、wParam、lParam传过去。
伪代码就好像这样:
WndProc = 找到这个 hwnd 对应的窗口过程函数;
WndProc(msg.hwnd, msg.message, msg.wParam, msg.lParam);
所以整个循环串起来就是:
- 从队列拿消息(
GetMessage); - 把键盘消息翻译为字符消息(
TranslateMessage); - 把消息交给窗口过程函数处理(
DispatchMessage)。
你可以脑补成:
**“操作系统 → 队列 → 消息循环(Get/Translate/Dispatch) → 窗口回调”**这一整条链路。
四、把四大金刚拆开讲:GetMessage / PeekMessage / TranslateMessage / DispatchMessage
刚才只是粗略过一遍,现在单独把常用的几个函数再细讲一下实现思路和适用场景。
4.1 GetMessage:阻塞式消息获取
特点:
- 有消息就拿,没有就“睡(挂起)”;
- 帮你省 CPU;
- 一般用在普通 GUI 程序,不需要每帧做大量自定义逻辑。
什么时候用:
- 写工具、编辑器、普通窗口程序,UI 驱动逻辑占主导;
- 比如记事本、弹窗程序、配置工具等。
伪代码:
while (GetMessage(&msg, NULL, 0, 0)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
注意:
- 当别人调用
PostQuitMessage发出WM_QUIT时,
下一次GetMessage返回 0,循环结束,程序退出。
4.2 PeekMessage:非阻塞式消息获取
函数原型:
BOOL PeekMessage(
LPMSG lpMsg,
HWND hWnd,
UINT wMsgFilterMin,
UINT wMsgFilterMax,
UINT wRemoveMsg
);
与 GetMessage 最大区别:不会阻塞。
- 如果队列里有消息:把消息取出来(或只偷看不取,看你传的 flag),返回 TRUE;
- 如果队列里根本没有消息:立刻返回 FALSE,不阻塞。
典型用法(游戏循环里很常见):
MSG msg;
while (running) {
while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {
if (msg.message == WM_QUIT) {
running = false;
break;
}
TranslateMessage(&msg);
DispatchMessage(&msg);
}
// 没消息的时候你也会到这里
// 这里做游戏自己的循环
GameUpdate();
GameRender();
}
解释一下逻辑:
- 内层
while (PeekMessage(...)):
把当前所有在队列里的消息都处理完(因为用了PM_REMOVE,每次都“拿走一条消息”); - 处理完消息后,即使现在没有新消息了,你不会被挂起,而是继续下面的
GameUpdate和GameRender。
所以 PeekMessage 的典型场景:
你既想处理用户输入,又想让程序自己持续运行(比如渲染、更新物理),
不能只在有消息的时候才动。
这就是实时渲染程序、游戏程序最常见的写法。
4.3 TranslateMessage:只对键盘消息“翻译成字符”
再强调一下它到底干啥:
- 输入法 / 键盘布局下,同一个按键可能对应不同“字符”;
WM_KEYDOWN只告诉你哪个虚拟键(VK)被按了,比如 VK_A;TranslateMessage会根据当前键盘状态、输入法,把它变成“字符消息”(WM_CHAR)。
比如:
- 切换到中文输入法,你按下键盘“a”:
- 系统先给你一个
WM_KEYDOWN(VK_A); TranslateMessage会生成一个WM_CHAR,wParam = 'a'或更复杂的;
- 系统先给你一个
- 文本编辑器会在
WM_CHAR里做处理,把字符插入文本。
如果你做的事情完全不用输入文本,只需要知道“哪个键被按”,
TranslateMessage 可用可不用。
4.4 DispatchMessage:消息分发器
它会根据消息里的 hwnd,找到该窗口关联的窗口过程函数,然后调用它。
这一步为什么要由 DispatchMessage 做,而不是你自己直接调用 WndProc 呢?
- 因为一个线程可以创建多个窗口,每个窗口有自己的
WndProc; DispatchMessage会帮你根据hwnd找到正确的 WndProc;- 同时,还会做一些系统级管理(内部细节你不用管)。
所以正常情况别自己绕过它,自己直接调用 WndProc,那属于“高级骚操作”,容易搞出问题。
五、窗口过程函数(WindowProc):消息最后在哪里被“消化”?
说了半天消息怎么进队列、怎么被拿出来,现在要讲:
“那最后是谁真正干活?”
答案就是:窗口过程函数,也就是我们常说的 WndProc。
5.1 它长啥样?
经典写法:
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) {
switch (message) {
case WM_PAINT:
// 绘制窗口界面
break;
case WM_DESTROY:
PostQuitMessage(0); // 发一个 WM_QUIT,退出消息循环
break;
case WM_KEYDOWN:
// 键盘按下处理
break;
case WM_LBUTTONDOWN:
// 鼠标左键按下
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
return 0;
}
解释一下:
HWND hWnd:哪个窗口收到的消息;UINT message:消息类型(比如 WM_PAINT 等);WPARAM wParam/LPARAM lParam:具体消息的附加信息;- 返回值
LRESULT:一般系统会根据返回值做一些后续判断,用文档说的为准。
5.2 必须记住的一条铁律:DefWindowProc
WndProc 里的 default 分支会调用:
return DefWindowProc(hWnd, message, wParam, lParam);
这句话的意思是:
对于我不关心、不处理的消息,交给系统默认处理逻辑。
比如:
- 窗口被移动;
- 窗口被最小化 / 最大化;
- 一些你根本没意识到的系统消息。
如果你不调用 DefWindowProc,很多行为会出大问题,比如:
- 标题栏没法拖动;
- 不能最小化、不能关闭;
- 窗口边框不能调整大小;
- 有各种稀奇古怪的 bug。
除非你真的非常清楚自己在干嘛,一般都要在 default 分支调用 it。
5.3 一个典型的处理流程示意
拿一个 WM_LBUTTONDOWN 当例子,当你在窗口上点击左键时:
- 系统把
WM_LBUTTONDOWN放入线程消息队列; - 消息循环
GetMessage取出这条消息; DispatchMessage调用你的WndProc(hWnd, WM_LBUTTONDOWN, wParam, lParam);- 你在
case WM_LBUTTONDOWN:分支里获取鼠标坐标、做一些处理; - 若没返回特殊值,Windows 可能还会做一些系统默认处理(根据消息类型而定)。
六、常见消息类型快速过一遍(你至少要听过的那些)
这里不打算做字典式枚举,而是挑几个你日常肯定遇得到的讲一下“大致含义”。
6.1 窗口生命周期相关
WM_CREATE:窗口创建时发一次,你可以在这里做一些初始化。WM_DESTROY:窗口被销毁时发。很多程序在这儿调用PostQuitMessage(0)。WM_CLOSE:用户发出“希望关闭窗口”的意图(比如点 X),你可以在里边弹“是否保存”的提示框,然后决定要不要真的关。
常见写法:
case WM_CLOSE:
if (确认要退出吗?) {
DestroyWindow(hWnd); // 会触发 WM_DESTROY
}
return 0;
case WM_DESTROY:
PostQuitMessage(0); // 通知消息循环退出
return 0;
6.2 绘制相关
WM_PAINT:窗口某区域失效,需要重绘时发。- 你要用
BeginPaint/EndPaint来开始/结束绘制; - 这是最传统的 GDI 绘制模型。
- 你要用
典型写法:
case WM_PAINT: {
PAINTSTRUCT ps;
HDC hdc = BeginPaint(hWnd, &ps);
// 在 hdc 上画东西
EndPaint(hWnd, &ps);
return 0;
}
很多游戏使用 “自己控制渲染(例如 DirectX/OpenGL)”,
会在自己的循环中绘制,而不是完全依赖 WM_PAINT。
但 UI/工具类程序通常还是展示在 WM_PAINT 里画。
6.3 键盘消息
WM_KEYDOWN/WM_KEYUP:某个虚拟键被按下 / 抬起;WM_CHAR:字符输入(基于键盘操作 + 输入法,由TranslateMessage生成)。
在 WM_KEYDOWN 里,wParam 是虚拟键码:
case WM_KEYDOWN:
switch (wParam) {
case VK_ESCAPE:
// 处理 ESC
break;
case VK_SPACE:
// 空格
break;
}
return 0;
6.4 鼠标消息
WM_MOUSEMOVE:鼠标移动;WM_LBUTTONDOWN/UP:左键按下 / 抬起;WM_RBUTTONDOWN/UP:右键按下 / 抬起;WM_MBUTTONDOWN/UP:中键;WM_MOUSEWHEEL:滚轮。
坐标怎么从 lParam 里取?
int x = GET_X_LPARAM(lParam);
int y = GET_Y_LPARAM(lParam);
滚轮的 delta 在 GET_WHEEL_DELTA_WPARAM(wParam):
short delta = GET_WHEEL_DELTA_WPARAM(wParam);
七、实时程序(比如游戏)常用的:PeekMessage 消息循环写法
前面提过,游戏需要每帧渲染、更新逻辑,不能光靠操作系统“有消息时才醒”。
最典型写法就是这样:
MSG msg;
BOOL running = TRUE;
while (running) {
// 处理所有当前积累的消息
while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {
if (msg.message == WM_QUIT) {
running = FALSE;
break;
}
TranslateMessage(&msg);
DispatchMessage(&msg);
}
if (!running) break;
// 这里是你自己的游戏主循环内容
// 每一帧都要执行:物理、逻辑、渲染等
GameUpdate();
GameRender();
}
核心点:
PeekMessage不阻塞,让你“无消息时也能干活”;- 一帧可能收到好多消息,我们用内层
while把队列清空; - 如果收到
WM_QUIT,我们把running设为 false,跳出外层循环; - 每一帧都做自己的渲染更新逻辑。
这样你就有了一个**“消息驱动 + 自主更新”混合模式**的循环,适合游戏、实时仿真、动画等等。
八、多窗口、多线程、多消息队列:稍微高级一点的情况
很多人刚开始以为“一进程一个消息队列”,
实际上,准确说法是:“每个 GUI 线程一个消息队列”。
8.1 同一个线程可以有多个窗口
比如,你的程序有:
- 主窗口;
- 一个工具子窗口;
- 几个子控件也各自是窗口。
这些窗口全挂在同一个线程上,那么它们:
- 共用同一个消息队列;
- 但每条消息里的
hwnd不同。
GetMessage / PeekMessage 从队列里拿出的 MSG,
DispatchMessage 会根据 hwnd 把它送到对应的 WndProc。
你不用为每个窗口写一个消息循环,同一个线程只有一个主循环就行。
8.2 多线程 + 多消息队列
如果你开启一个新 GUI 线程,在那个线程里创建窗口,那么:
- 那个线程有自己的消息队列;
- 需要自己跑一套消息循环;
- 不同线程的窗口彼此消息隔离。
通常来说:
- 主 UI 线程负责所有窗口、控件,跑消息循环,
- 其他工作线程做计算、加载资源,不直接创建窗口(除非有特别需求)。
多线程 GUI 编程会复杂很多,比如跨线程 SendMessage/PostMessage 的问题,这里不展开,只要你知道:
一个线程一个消息队列,一个线程需要自己有消息循环。
九、计时器、PostMessage、SendMessage:你也可以“主动发消息”
截至目前我们讲的,都是系统把消息塞给你。
其实你也可以“反过来”自己发消息给窗口(包括自己)。
9.1 PostMessage:异步发消息,放进队列
BOOL PostMessage(
HWND hWnd,
UINT Msg,
WPARAM wParam,
LPARAM lParam
);
特点:
- 把消息放进指定线程的消息队列;
- 不会立刻执行 WndProc,等消息循环取出来再处理;
- 调用方立即返回,异步的。
使用场景:
- 跨线程通知:工作线程给 UI 线程发消息,让 UI 刷新;
- 在某个时机想让某窗口“假装收到了某个消息”。
9.2 SendMessage:同步发消息,立刻调用 WndProc
LRESULT SendMessage(
HWND hWnd,
UINT Msg,
WPARAM wParam,
LPARAM lParam
);
特点:
- 直接调用目标窗口的 WndProc,
- 不经过消息队列;
- 调用方会一直等到 WndProc 返回之后才继续执行。
使用场景:
- 想要立刻获得返回结果,比如问:“你的文本内容是什么?”
- 调用系统控件的一些行为(比如让 Edit 控件设置/获取内容)。
注意:跨线程使用 SendMessage 要极度小心,容易造成死锁或 UI 卡死。
9.3 计时器:SetTimer + WM_TIMER
Windows 有简单的定时器机制:
UINT_PTR SetTimer(
HWND hWnd,
UINT_PTR nIDEvent,
UINT uElapse, // 毫秒
TIMERPROC lpTimerFunc
);
你可以:
- 给某个窗口设定一个定时器,每隔 uElapse 毫秒触发一次;
- 触发的方式是向这个窗口投递
WM_TIMER消息; - 你在 WndProc 里处理
WM_TIMER就行。
或者也可以指定一个回调函数 lpTimerFunc,不过一般直接在消息里处理更直观。
十、常见坑和边角问题:你可能会遇到的“诡异行为”
10.1 消息循环写错导致 CPU 100% 空转
典型错误写法:
while (true) {
PeekMessage(&msg, NULL, 0, 0, PM_REMOVE);
TranslateMessage(&msg);
DispatchMessage(&msg);
// 更新、渲染
}
问题在于:
- 当消息队列里没有消息时,
PeekMessage返回 FALSE; - 但你没有任何判断,照样
TranslateMessage和DispatchMessage; - 这会错用一堆垃圾数据,甚至直接出错;
- 即便你加判断,仍然是个死循环,没有任何等待,CPU 会被你占满。
正确做法:
while (running) {
while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {
// ...处理消息
}
// 这里可以考虑用 Sleep(0) or Sleep(1) 降低空转时的 CPU 占用
// 或者用定时器/高精度计时控制帧率
GameUpdate();
GameRender();
}
10.2 忘了在 WM_DESTROY / WM_CLOSE 时发送 WM_QUIT
你如果不在某个地方调用 PostQuitMessage(0),
消息循环里的 GetMessage 永远拿不到 “返回 0” 的情况,就会一直死循环。
典型正确写法:
case WM_DESTROY:
PostQuitMessage(0);
return 0;
然后外面的:
while (GetMessage(&msg, NULL, 0, 0)) {
...
}
当收到 WM_QUIT 后,这个 GetMessage 返回 0,循环结束。
10.3 在 WndProc 里写耗时逻辑导致“未响应”
如果你在 WndProc 里干了一件非常耗时的事情,比如:
- 大量 I/O;
- 复杂运算;
那么在这段时间里:
- 你的线程不能处理其他消息;
- OS 看到你几秒不响应,会标记窗口为“未响应”(灰屏 + 标题栏写“未响应”)。
解决方法:
- 耗时操作放到工作线程去做;
- UI 线程(消息循环线程)尽量只处理消息和轻逻辑;
- 必要时用
PostMessage把工作完成的结果通知回来。
十一、综合小例子:一个完整、能跑的 Win32 消息循环小程序
给你一个从 WinMain 到消息循环到 WndProc 的简化示例,帮你把所有概念串起来。
#include <windows.h>
const wchar_t CLASS_NAME[] = L"SampleWindowClass";
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) {
switch (message) {
case WM_PAINT: {
PAINTSTRUCT ps;
HDC hdc = BeginPaint(hWnd, &ps);
TextOutW(hdc, 10, 10, L"Hello, Win32 Message Loop!", 28);
EndPaint(hWnd, &ps);
return 0;
}
case WM_KEYDOWN:
if (wParam == VK_ESCAPE) {
DestroyWindow(hWnd);
}
return 0;
case WM_LBUTTONDOWN: {
int x = GET_X_LPARAM(lParam);
int y = GET_Y_LPARAM(lParam);
wchar_t buf[64];
wsprintf(buf, L"Mouse clicked at (%d, %d)", x, y);
MessageBoxW(hWnd, buf, L"Info", MB_OK);
return 0;
}
case WM_DESTROY:
PostQuitMessage(0); // 通知消息循环:可以退了
return 0;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
}
int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, PWSTR pCmdLine, int nCmdShow) {
// 注册窗口类
WNDCLASS wc = {};
wc.lpfnWndProc = WndProc; // 指定窗口过程函数
wc.hInstance = hInstance;
wc.lpszClassName = CLASS_NAME;
RegisterClass(&wc);
// 创建窗口
HWND hWnd = CreateWindowEx(
0,
CLASS_NAME,
L"Win32 Message Loop Demo",
WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, CW_USEDEFAULT, 640, 480,
NULL,
NULL,
hInstance,
NULL
);
if (hWnd == NULL) return 0;
ShowWindow(hWnd, nCmdShow);
// 消息循环
MSG msg = {};
while (GetMessage(&msg, NULL, 0, 0)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return 0;
}
整个程序的关注点:
RegisterClass时把WndProc传进去;CreateWindowEx返回一个HWND;ShowWindow之后,就进入消息循环;- 窗口过程
WndProc里根据不同消息做不同处理; - 在
WM_DESTROY里PostQuitMessage,消息循环收到WM_QUIT,GetMessage返回 0,退出。
这就是最基础的 Win32 消息循环框架。
十二、最后总结:你应该掌握哪些关键点?
整理一下这篇长文里最重要的知识点,如果你都能用自己的话复述出来,那说明 Win32 消息循环你已经“入门并理解”了:
-
为什么要有消息循环?
- Windows 为每个 GUI 线程维护一个消息队列;
- 系统和你自己都可以往里放消息;
- 你的程序必须循环把消息拿出来处理,不然窗口就“不响应”。
-
标准消息循环长啥样?
while (GetMessage(&msg, NULL, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg); }GetMessage阻塞等待消息;TranslateMessage负责键盘转字符;DispatchMessage调用相应窗口的 WndProc。
-
WndProc 是谁?
- 每个窗口都有一个窗口过程函数;
- 所有消息最终都在这里被处理;
- 对于不关心的消息,必须调用
DefWindowProc。
-
PeekMessage用于非阻塞循环,适合游戏等实时程序while (PeekMessage(..., PM_REMOVE)) { ... }处理所有当前消息;- 没有消息也会继续往下执行游戏逻辑。
-
退出机制:WM_DESTROY + PostQuitMessage + WM_QUIT + GetMessage 返回 0
- WndProc 收到
WM_DESTROY时通常PostQuitMessage(0); - 消息循环里的
GetMessage收到WM_QUIT后返回 0,循环退出。
- WndProc 收到
-
你可以主动发消息:PostMessage(异步)、SendMessage(同步)
- 计时器用
WM_TIMER; - 工作线程可以用
PostMessage通知 UI 线程。
- 计时器用
-
一个线程可以有多个窗口,但一个线程只有一个消息队列
- 多线程 + 多窗口时,每个线程要有自己的消息循环。
-
慎重在 WndProc 写耗时逻辑,避免“未响应”
- 把重计算、IO 等放到工作线程;
- UI 线程尽量只干消息处理。
如果把消息循环比喻成“前台”,那:
- 操作系统是“门口保安 + 邮递员”;
- 消息队列是“前台桌上的收件篮”;
- 消息循环是“前台每隔一小会儿翻翻篮子,拿出新信件”;
DispatchMessage是“前台根据收件人把信件送到对应办公室”;- WndProc 就是办公室里的那个人,看到信件后决定怎么办。
理解了这个流程,Win32 消息循环就不再是“黑魔法”,而是一个非常朴素、可预期的事件分发模型。
以后再看任何一个 Windows 桌面程序的源码,只要看到那几行 GetMessage/PeekMessage,你就知道:
哦,这就是那套“消息队列 + 消息循环”的体系在工作。
23万+

被折叠的 条评论
为什么被折叠?



