Win32消息循环:从入门到精通

先把结论说前面:
只要你是在 Windows 上写原生程序(包括很多游戏引擎、工具、编辑器),Win32 消息循环几乎是绕不开的底层机制。
它听起来玄乎,其实本质就一句话:

Windows 不停给你“塞消息”(按键了、鼠标动了、窗口要重绘了……),
你的程序里要有个“循环”,一条一条把这些消息拿出来处理——
这玩意儿就叫“消息循环”。

下面我会用大白话,把 Win32 消息循环从“是什么、为什么、怎么玩”到“各种细节坑”一口气掰开讲。
你看完应该能做到两件事:

  1. 看懂绝大部分 Win32 示例代码里的消息循环到底在干嘛。
  2. 自己能写出一个基本正确的消息循环,知道哪里能动、哪里不能乱动。

目录大纲

  1. 为什么 Windows 程序一定要有“消息循环”?
  2. Windows 消息模型:消息从哪来、去哪儿
  3. 最简单的消息循环长啥样?(GetMessage 版本)
  4. GetMessage / PeekMessage / TranslateMessage / DispatchMessage 逐个讲清
  5. 窗口过程函数(WindowProc):消息去哪儿“落地处理”?
  6. 各类常见消息:键盘、鼠标、重绘、关闭……
  7. 非阻塞消息循环 & 游戏循环:PeekMessage 的正确用法
  8. 多窗口、多线程、多消息队列:一个程序里不止一个窗口怎么办?
  9. 计时器、PostMessage、SendMessage:主动“发消息”的几种方式
  10. 常见坑和边角问题:空转、高 CPU、死循环、重入……
  11. 综合小例子:一个“能动的窗口程序”的全流程
  12. 最后再总结一遍:你真的搞懂消息循环了吗?

一、为什么 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 系统做的事情包括:

  1. 监听键盘、鼠标、窗口行为等;
  2. 当某个操作发生,比如你在窗口上点击鼠标:
    • 操作系统决定:“这应该给窗口发一个 WM_LBUTTONDOWN”;
    • 然后把这条消息放进这个线程的消息队列。

有些消息是由系统自动投递(比如 WM_PAINT),
有些消息可以由你自己用 PostMessageSendMessage 主动发。

2.3 消息队列的消费:谁来处理它?

程序里最终要有一个消费消息的人——
那就是你定义的窗口过程函数(Window Procedure / WndProc)

整个链路大概是这样的:

  1. 系统 → 消息队列
    • OS 把 MSG 放到队列里。
  2. 消息循环 → 取出消息
    • GetMessage / PeekMessage 从队列里拿消息出来。
  3. 消息循环 → 分发消息
    • DispatchMessage 把消息分派给对应窗口的 WndProc
  4. 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_QUITGetMessage 返回 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.messagewParamlParam 传过去。

伪代码就好像这样:

WndProc = 找到这个 hwnd 对应的窗口过程函数;
WndProc(msg.hwnd, msg.message, msg.wParam, msg.lParam);

所以整个循环串起来就是:

  1. 从队列拿消息(GetMessage);
  2. 把键盘消息翻译为字符消息(TranslateMessage);
  3. 把消息交给窗口过程函数处理(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,每次都“拿走一条消息”);
  • 处理完消息后,即使现在没有新消息了,你不会被挂起,而是继续下面的 GameUpdateGameRender

所以 PeekMessage 的典型场景:

你既想处理用户输入,又想让程序自己持续运行(比如渲染、更新物理),
不能只在有消息的时候才动。

这就是实时渲染程序、游戏程序最常见的写法。

4.3 TranslateMessage:只对键盘消息“翻译成字符”

再强调一下它到底干啥:

  • 输入法 / 键盘布局下,同一个按键可能对应不同“字符”;
  • WM_KEYDOWN 只告诉你哪个虚拟键(VK)被按了,比如 VK_A;
  • TranslateMessage 会根据当前键盘状态、输入法,把它变成“字符消息”(WM_CHAR)。

比如:

  • 切换到中文输入法,你按下键盘“a”:
    • 系统先给你一个 WM_KEYDOWN(VK_A);
    • TranslateMessage 会生成一个 WM_CHARwParam = '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 当例子,当你在窗口上点击左键时:

  1. 系统把 WM_LBUTTONDOWN 放入线程消息队列;
  2. 消息循环 GetMessage 取出这条消息;
  3. DispatchMessage 调用你的 WndProc(hWnd, WM_LBUTTONDOWN, wParam, lParam)
  4. 你在 case WM_LBUTTONDOWN: 分支里获取鼠标坐标、做一些处理;
  5. 若没返回特殊值,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();
}

核心点:

  1. PeekMessage 不阻塞,让你“无消息时也能干活”;
  2. 一帧可能收到好多消息,我们用内层 while 把队列清空;
  3. 如果收到 WM_QUIT,我们把 running 设为 false,跳出外层循环;
  4. 每一帧都做自己的渲染更新逻辑。

这样你就有了一个**“消息驱动 + 自主更新”混合模式**的循环,适合游戏、实时仿真、动画等等。


八、多窗口、多线程、多消息队列:稍微高级一点的情况

很多人刚开始以为“一进程一个消息队列”,
实际上,准确说法是:“每个 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;
  • 但你没有任何判断,照样 TranslateMessageDispatchMessage
  • 这会错用一堆垃圾数据,甚至直接出错;
  • 即便你加判断,仍然是个死循环,没有任何等待,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;
}

整个程序的关注点:

  1. RegisterClass 时把 WndProc 传进去;
  2. CreateWindowEx 返回一个 HWND
  3. ShowWindow 之后,就进入消息循环;
  4. 窗口过程 WndProc 里根据不同消息做不同处理;
  5. WM_DESTROYPostQuitMessage,消息循环收到 WM_QUITGetMessage 返回 0,退出。

这就是最基础的 Win32 消息循环框架。


十二、最后总结:你应该掌握哪些关键点?

整理一下这篇长文里最重要的知识点,如果你都能用自己的话复述出来,那说明 Win32 消息循环你已经“入门并理解”了:

  1. 为什么要有消息循环?

    • Windows 为每个 GUI 线程维护一个消息队列;
    • 系统和你自己都可以往里放消息;
    • 你的程序必须循环把消息拿出来处理,不然窗口就“不响应”。
  2. 标准消息循环长啥样?

    • while (GetMessage(&msg, NULL, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg); }
    • GetMessage 阻塞等待消息;
    • TranslateMessage 负责键盘转字符;
    • DispatchMessage 调用相应窗口的 WndProc。
  3. WndProc 是谁?

    • 每个窗口都有一个窗口过程函数;
    • 所有消息最终都在这里被处理;
    • 对于不关心的消息,必须调用 DefWindowProc
  4. PeekMessage 用于非阻塞循环,适合游戏等实时程序

    • while (PeekMessage(..., PM_REMOVE)) { ... } 处理所有当前消息;
    • 没有消息也会继续往下执行游戏逻辑。
  5. 退出机制:WM_DESTROY + PostQuitMessage + WM_QUIT + GetMessage 返回 0

    • WndProc 收到 WM_DESTROY 时通常 PostQuitMessage(0)
    • 消息循环里的 GetMessage 收到 WM_QUIT 后返回 0,循环退出。
  6. 你可以主动发消息:PostMessage(异步)、SendMessage(同步)

    • 计时器用 WM_TIMER
    • 工作线程可以用 PostMessage 通知 UI 线程。
  7. 一个线程可以有多个窗口,但一个线程只有一个消息队列

    • 多线程 + 多窗口时,每个线程要有自己的消息循环。
  8. 慎重在 WndProc 写耗时逻辑,避免“未响应”

    • 把重计算、IO 等放到工作线程;
    • UI 线程尽量只干消息处理。

如果把消息循环比喻成“前台”,那:

  • 操作系统是“门口保安 + 邮递员”;
  • 消息队列是“前台桌上的收件篮”;
  • 消息循环是“前台每隔一小会儿翻翻篮子,拿出新信件”;
  • DispatchMessage 是“前台根据收件人把信件送到对应办公室”;
  • WndProc 就是办公室里的那个人,看到信件后决定怎么办。

理解了这个流程,Win32 消息循环就不再是“黑魔法”,而是一个非常朴素、可预期的事件分发模型。
以后再看任何一个 Windows 桌面程序的源码,只要看到那几行 GetMessage/PeekMessage,你就知道:
哦,这就是那套“消息队列 + 消息循环”的体系在工作。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

你一身傲骨怎能输

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值