Windows消息机制详解:窗口过程处理链路与重入问题剖析
1. 从窗口过程到消息循环Windows消息机制的核心骨架很多人第一次接触Windows消息机制都是从那个经典的GetMessage/TranslateMessage/DispatchMessage三件套开始的。但真正在项目里踩过坑的人都知道光会写这个循环远远不够。消息从哪来、到哪去、中间经过了哪些处理、为什么有时候消息会丢失、为什么PostMessage和SendMessage行为差异那么大——这些问题不搞清楚写出来的界面程序迟早要出问题。这篇文章是Windows消息机制详解系列的第六篇前面几篇分别聊了消息队列的基本结构、窗口类的注册、窗口过程的编写、消息的分类以及消息的同步与异步投递。这一篇我们把焦点收窄专门讲窗口过程WndProc内部的完整处理链路以及消息在到达窗口过程之前和之后系统到底做了哪些事情。换句话说前面几篇是消息怎么来这一篇是消息来了之后怎么处理。适合阅读的人群已经能写出基本Win32窗口程序、但对消息处理细节一知半解的开发者从MFC或WinForms转过来、想搞清楚底层到底发生了什么的工程师以及在做界面性能优化、需要精确控制消息处理时机的老手。文章会涉及具体的代码片段、参数含义、返回值约定以及我在实际项目中遇到过的几个典型问题。2. 窗口过程到底在什么时候被调用2.1 消息循环与窗口过程的关系先把这个链路理清楚。一个标准的消息循环长这样MSG msg; while (GetMessage(msg, nullptr, 0, 0)) { TranslateMessage(msg); DispatchMessage(msg); }GetMessage从当前线程的消息队列里取一条消息填到MSG结构里。TranslateMessage负责把虚拟键消息转成字符消息比如WM_KEYDOWN转成WM_CHAR这个后面会细说。DispatchMessage才是真正把消息送到对应窗口过程的那个调用。这里有个关键点很多人没注意DispatchMessage并不是直接调用你写的WndProc函数。它内部会根据MSG结构里的hwnd找到对应的窗口对象然后调用该窗口注册时指定的窗口过程。如果这个窗口是某个父窗口的子窗口消息不会自动传给父窗口——除非你在子窗口的WndProc里显式调用DefWindowProc或者手动转发。我在一个多窗口嵌套的项目里就吃过这个亏。当时做了一个自定义的控件容器子控件的鼠标消息死活传不到父容器。后来才明白Windows的消息分发是精确投递的DispatchMessage只认hwnd对应的那个窗口过程不会做冒泡。想要冒泡效果得自己在子窗口过程里处理完后调用父窗口的过程或者用SendMessage转发。2.2 窗口过程的调用时机与重入问题窗口过程不是只在DispatchMessage的时候被调用。以下几种情况都会触发WndProcDispatchMessage分发队列消息时其他线程调用SendMessage发送非队列消息时此时目标窗口过程会在调用线程的上下文中执行系统在某些操作中直接调用窗口过程比如窗口创建时的WM_CREATE、WM_NCCREATEDefWindowProc内部在某些情况下会回调窗口过程这就引出了重入问题。窗口过程是可重入的也就是说在WndProc执行到一半的时候可能因为调用了某个API比如MessageBox、SendMessage导致WndProc被再次进入。如果你在WndProc里用了静态变量或者全局变量来保存临时状态重入就会导致状态错乱。我遇到过一个典型案例在一个WM_PAINT处理里调用了一个会触发SendMessage的函数结果WM_PAINT被重入绘图上下文被破坏界面出现花屏。解决办法是把状态保存在栈上或者用窗口的GWLP_USERDATA来存每个窗口独立的数据避免跨调用共享可变状态。注意窗口过程的重入是正常行为不是bug。设计时要假设任何一次API调用都可能导致重入尤其是SendMessage、MessageBox、DialogBox这类会跑消息循环的调用。2.3 返回值约定与消息处理结果窗口过程的返回值含义取决于具体的消息。对于WM_CREATE返回0表示继续创建返回-1表示创建失败、窗口会被销毁。对于WM_CLOSE返回0表示已处理DefWindowProc会调用DestroyWindow。对于大多数消息返回0表示我处理了返回非零的具体含义要看消息定义。这里有个容易混淆的地方WM_ERASEBKGND返回非零表示我已经擦除背景了返回0表示还没擦系统你帮我擦。如果你在处理这个消