Windows 消息循环 && 消息循环在 WPF 中的应用

使用 EN5 课件获得更好的阅读体验:

【希沃白板5】课件分享 : 《Windows培训 - 消息循环》
https://r302.cc/q2d1jB
点击链接直接预览课件

1 程序是怎么跑起来的?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello Cvte.");
Console.ReadLine();
}
}
```
这是一段 C# Main 函数,如果不写 `Console.ReadLine();` ,则程序会“一闪而过”,写了 `Console.ReadLine();` 程序会阻塞,可以查看结果。
下面看一段复杂一点点的:
``` csharp
Console.WriteLine("Starting, Input Something:");
while (true)
{
string input = Console.ReadLine();
if (input == "exit")
{
break;
}
else
{
Console.WriteLine(
!string.IsNullOrWhiteSpace(input)
? $"Your Input to lower is:{input.ToLower()}"
: "You Inputted Nothing");
}
}
```
这里有一个 `while` 循环,这样程序就可以一直运行了,我们可以说:这个程序由这个 while 循环驱动。
那,Windows 程序是由什么驱动的,答案呼之欲出:“消息循环”。
![](images/2018-11-16-10-49-38.png)
# 2 消息循环的数据结构
``` c
typedef struct {
HWND hwnd; // 消息的目标窗口句柄
UINT message; // 消息标识
WPARAM wParam; // 16位的参数
LPARAM lParam; // 32位的参数
DWORD time; // 消息发生的时间
POINT pt; // 消息发生时,鼠标的屏幕坐标
} MSG, *PMSG;
```
## 2.1 消息的分类
消息的取值范围是 0x0000 - 0xFFFF
0x00000x03FF,为系统定义的消息,常见的 WM_PAINT、WM_CREATE 等均在其中;
0x04000x7FFF,专用于用户自定义的消息,可以使用 WM_USER + x 的形式自行定义,其中WM_USER 的值就是 0x0400,x 取一个整数;
0x80000xBFFF,从 Windows 95 开始,也用作用户自定义的消息范围,可以使用 WM_APP + x 的形式自行定义。
根据微软的建议,WM_APP类消息用于程序之间的消息通信,而 WM_USER 类消息则最好用于某个特定的窗口类。
微软自己遵循这一惯例,所以,公用控件的消息,如 TVM_DELETEITEM,基本都是 WM_USER 类属。
0xC0000xFFFF,这个区段的消息值保留给 RegisterWindowMessage 这个 API,此 API 可以接受一个字符串,把它变换成一个唯一的消息值。
# 3 消息的处理流程
消息产生 => 消息队列 => 消息循环 => 消息处理
## 3.1 消息产生
消息产生的源头
* 系统
一部分由输入设备(键盘鼠标等)产生,如 WM_MOUSEMOVE 。
一部分由系统User库自己产生,User部分(或者是系统内的其他部分通过User部分)为了实现自身的正常行为或者管理功能而主动生成的。如 WM_WINDOWPOSCHANGED。
* 应用程序自定义的消息
消息产生的方式
这里说主要的两个消息产生函数
* SendMessage
等待消息处理完成后,SendMessage才返回。
深入一点的表达式:等待窗口处理函数返回后,SendMessage才返回。
* PostMessage
不等待消息处理完成,立刻返回。
PostMessage只管发送消息,消息有没有被送到则并不关心,只要发送了消息,便立刻返回。
两个问题:
问:消息产生之后到了哪里?
答:消息队列。
问:SendMessage 产生的消息,会进入消息队列吗?
答:在同一个线程内,SendMessage 会直接调用目标窗口的窗口过程函数处理消息,并等待其返回。
跨线程的情况,SendMessage 会将消息发送到目标线程的消息队列(高优先级,排序在前)。然后等待目标线程的返回值。
## 3.2 消息队列
* 系统消息队列
接收输入设备的消息,分配给线程消息队列。
输入设备(键盘、鼠标或者其他)的驱动程序会把用户的操作输入转化成消息放置于系统队列中,然后系统会把此消息转到目标窗口所在线程的消息队列中等待处理。
* 线程(UI)消息队列
当前UI线程中的消息。
每一个GUI线程都会维护这样一个线程消息队列。(这个队列只有在线程调用 User 或者 GDI 函数时才会创建,默认并不创建)。然后线程消息队列中的消息会被本线程的消息循环(有时也被称为消息泵)派送到相应的窗口过程(也叫窗口回调函数)处理。
两个问题:
问:消息队列属于谁?
答:属于UI线程(不属于窗口)。
问:非UI线程有消息队列吗?
没有。
## 3.3 消息循环
``` c
while(GetMessage(&msg, NULL,0, 0))
{
TranslateMessage(&msg);
DispatchMessage (&msg);
}
```
如上,消息循环就是一个 while 循环,与文章最开始提到 while 向呼应。
其中 `GetMessage` 取出消息,`TranslateMessage` 翻译消息,`DispatchMessage` 调度消息。
问:消息循环属于谁?
答:每一个UI线程有一个消息循环(不是每一个窗口。)
消息循环的另一个样子:
``` c
while (!done)
{
if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE))
{
if (msg.message == WM_QUIT)
{
done = TRUE;
}
else
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
else
{
// 还可以驱动点别的事情,如 openGL 绘图。
}
}
```
分别来看:
* 取出消息
GetMessage
GetMessage会阻塞等待,直到取到一个消息。
PeekMessage
PeekMessage则不阻塞,立即返回。
PeekMessage有一个标志参数,这个标志参数指定了如果队列中如果有消息的话,PeekMessage 的行为。
如果该标志中含有 PM_REMOVE,则 PeekMessage 会把新消息返回到 MSG 结构中,正如 GetMessage 的行为那样。
如果标志中指定了 PM_NOREMOVE,则不会从消息队列中移除任何消息。
* 翻译消息
望文生义地看,翻译消息是对消息数据结构进行某种转换吗?
不是的,TranslateMessage不修改原有消息,只在特定情况下产生新的消息。
TranslateMessage函数不修改由参数lpMsg指向的消息结构。
仅为那些由键盘驱动器映射为ASCII字符的键产生WM_CHAR消息。
如:
消息WM_KEYDOWN和WM_KEYUP组合产生一个WM_CHAR或WM_DEADCHAR消息。
消息WM_SYSKEYDOWN和WM_SYSKEYUP组合产生一个WM_SYSCHAR或 WM_SYSDEADCHAR 消息。
所以,如果程序中没有字符处理的需要,这句是可以不要的。
* 分派消息
将消息分配给 hwnd 指定的窗口函数,让其处理。
如果没有找到对应的窗口,则丢弃。
## 3.4 消息处理
消息在消息循环中被分配到指定的窗口过程函数,由其处理。
``` cpp
// 有删减的窗口过程函数
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
switch (message)
{
case WM_COMMAND:
case WM_PAINT:
case WM_CREATE:
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
return 0;
}
```
回顾两个问题:
问:WndProc 函数由谁调用?
答:DispatchMessage or SendMessage。
从上文中可以看到,窗口过程函数不是有程序员自己调用的,而是系统在恰当的时机调用,这个时机就是 DispatchMessage or SendMessage。
问:未处理的消息交给谁?
答:DefWindowProc。
DefWindowProc只处理关闭等感兴趣的消息,其它的消息则忽略。
## 回顾
消息队列和消息循环属于UI线程,窗口没有,其它普通线程没有。
窗口有自己的窗口过程函数,消息在这里被处理。
消息循环驱动整个程序跑起来。
想一睹消息循环究竟是如何跑起来的?
原始 win32 窗口是如何被创建的?
在 VS 中,新建一个win32的窗口程序,即可看到。
![](images/2018-11-16-11-23-49.png)
---
win32/MFC/WinForm/WPF 都依靠消息循环驱动,让程序跑起来。
![](images/2018-11-16-11-25-02.png)
# 4 消息循环在 WPF 中的应用
# 4.1 引入
只听说过 Dispatcher ,哪里来的消息循环?
先瞧一眼 WPF 启动运行堆栈:
![](images/2018-11-16-11-28-05.png)
可以发现 `PushFrameImpl` 这个方法。
去看其源码,就发现了熟悉的消息循环 :
![](images/2018-11-16-11-29-23.png)
可以理解为:Dispatcher 对消息循环的操作进行了“封装” 。
那,Dispatcher 是谁?
# 4.2 Dispatchcer
Provides services for managing the queue of work items for a thread.
提供用于管理线程工作项队列的服务。
大部分WPF对象,都是 DispatcherObject。这意味着,可以在 DispatcherObject 中(如 Window 中),
使用 this.Dispatchcer 获取到 Dispatchcer 。
![](images/2018-11-16-12-03-22.png)
一般我们会通过三种方式获取 Dispatchcer :
``` csharp
// App.Current.Dispatcher;(Application.Current.Dispatcher)
var dispatcher1 = App.Current.Dispatcher;
// CurrentDispatcher;
var dispatcher2 = System.Windows.Threading.Dispatcher.CurrentDispatcher;
// System.Windows.Threading.DispatcherObject.Dispatcher;
var dispatcher3 = this.Dispatcher;
```
可分为两类:
* 当前线程的 Dispatcher:
System.Windows.Threading.Dispatcher.CurrentDispatcher;
* 创建对应对象的 Dispatcher:
App.Current.Dispatcher;
DispatcherObject.Dispatcher;
可参见:
[Why not Dispather.CurrentDispatcher - haungtengxiao](https://huangtengxiao.gitee.io/post/Why-not-Dispather.CurrentDispatcher.html)
Dispatcher 和线程是什么关系?
* Dispatcher 属于线程(与线程一一对应)。
* WPF的对象在获取this.Dispatcher属性时,不同对象取的都是同一个Dispatcher实例。(因为都是同一个UI线程创建的。)
* 在默认的 WPF UI线程中:
App.Current.Dispatcher = DispatcherObject.Dispatcher
所有的线程(UI线程,普通线程)都有 Dispatcher 吗?
是的。
在所有线程中,调用 System.Windows.Threading.Dispatcher.CurrentDispatcher
都会得到一个属于这个线程的 Dispatcher 对象。(不用的时候不会创建)
所以:如果你想在一个后台线程中,使用 Dispatcher.CurrentDispatcher.Invoke
将操作封送到 UI 线程,是做不到的。因为这时候获取到的 Dispatcher 不是UI线程的 Dispatcher , 而是当前线程自己的 Dispatcher。
## 4.3 Dispatcher 如何实现跨线程的调用。
最常使用 Dispatcher 的创建就是,在后台线程更新 UI ,那 Dispatcher 是如何做到的呢。
当你调用
``` csharp
Application.Current.Dispatcher.Invoke(() =>
{
SendMessageBtn.Content = "更新按钮";
});

时,Dispatcher 究竟做了什么,把操作转移到 UI 线程上去了。

关于 Invoke,InvokeSync,BeginInvoke 的区别,参见:
深入了解 WPF Dispatcher 的工作原理(Invoke/InvokeAsync 部分) - walterlv

  1. 将调用的Delegate和优先级包装成一个DispatcherOperation放入Dispatcher维护的优先级队列当中,这个Queue是按DispatcherPriority排序的,总是高优先级的DispatcherOperation先被处理。

  2. 往当前线程的消息队列当中Post一个名为MsgProcessQueue的Message。(这个消息是WPF自己定义的。)这个消息被Post到消息队列之前,还要设置MSG.Handle,这个Handle就是Window 1#的Handle。指定Handle是为了在消息循环Dispatch消息的时候,指定哪个窗口的 WndProc 处理这个消息。

  3. 消息循环读取消息。

  4. 系统根据获取消息的Handle,发现跟Window1#的Handle相同,那么这个消息派发到Window1#的窗口过程,让其处理。

  5. 在窗口过程中,优先级队列当中取一个DispatcherOperation。

  6. 执行DispatcherOperation.Invoke方法,Invoke方法的核心就是调用DispatcherOperation构造时传入的Delegate,也就是Dispatcher.BeginInvoke传入的Delegate。最终这个Foo()方法就被执行了。

4.4 回顾

  • WPF 底层仍然靠信息循环来驱动。
  • Dispatcher 使用消息循环来实现跨进程的委托调用。
  • Dispatcher 属于线程,需要理解当前拿到的 Dispatcher 到底是哪个 Dispatcher 。

参考资料:

Windows 消息机制浅析 - bitbit - 博客园

SendMessage、PostMessage原理-大白菜-51CTO博客

WPF的消息机制(一)- 让应用程序动起来 - 葡萄城技术团队 - 博客园

WPF的消息机制(二)- WPF内部的5个窗口之隐藏消息窗口 - 葡萄城技术团队 - 博客园

WPF的消息机制(三)- WPF内部的5个窗口之处理激活和关闭的消息窗口以及系统资源通知窗口 - 葡萄城技术团队 - 博客园

Why not Dispather.CurrentDispatcher - haungtengxiao

深入了解 WPF Dispatcher 的工作原理(Invoke/InvokeAsync 部分) - walterlv

Tutorial: Getting Started