本文将以实际的典型场景来解释发生的消息,从而能够将窗口的消息具体化,并能连贯起来。文中的内容绝大部分为VC提供的Spy++工具获得,小部分为通过程序获得。文中显示的片段多数为一个窗口中的消息,对于发往父窗口的消息,将会附带说明。
Windows的标准消息定义位于winuser.h中(%Program Files%/Microsoft Visual Studio .NET 2003/Vc7/PlatformSDK/include/winuser.h).可是,使用Spy++能够捕获所有窗口消息,经常会看到一些MSDN中未提及的消息.这一少部分消息的定义位于afxpriv.h中(%Program Files%/Microsoft Visual Studio .NET 2003/Vc7/atlmfc/include/afxpriv.h),这部分为MFC的内部消息(0x0360 ~ 0x037f),因此在MSDN中通常也找不到它们的踪迹.不过,MSDN中的"TN024: MFC-Defined Messages and Resources"解释了这些消息.
下面的片段是Mouse在Dialog的Caption上移动时产生的片段:
<00001> 00050592
S WM_NCHITTEST
xPos:254 yPos:170
<00002> 00050592 R WM_NCHITTEST nHittest:HTCAPTION
<00003> 00050592
S WM_SETCURSOR
hwnd:00050592 nHittest:HTCAPTION wMouseMsg:WM_MOUSEMOVE
<00004> 00050592 R WM_SETCURSOR fHaltProcessing:False
<00005> 00050592 P WM_NCMOUSEMOVE nHittest:HTCAPTION xPos:254 yPos:170
<00006> 00050592 S WM_KICKIDLE
其中,
1. WM_NCHITTEST:当鼠标在窗体移动时,系统会产生该消息。对于窗口的不同的区域,会返回不同的代码,。对于上述场景,为HTCAPTION,即title bar。如果是客户区,则都为HTCLIENT。其它代码都是用来区分非客户区的不同部分。
2. WM_SETCURSOR可以用来设置光标的形状。其nHittest同WM_NCHITTEST的代码,wMouseMsg参数表明了此时鼠标的动作
3. WM_NCMOUSEMOVE为post消息,而且只有当窗口拥有光标时,系统才会发送该消息。如果一个窗口capture了光标,那么该消息不会被发送。
4. WM_KICKIDLE为消息队列空闲时,系统发送的消息,以便程序可以在此时处理一些事情,比如:同步状态等。应用可以控制系统是否发送该消息。对对话框而言,就是设置DS_NOIDLEMSG风格。如果是主消息循环,那么CWinApp提供了OnIdle虚方法(实际为CWinThread的方法),其中lIdleCount可以用来度量消息队列空闲的时间长短。对于模态对话框,则需要响应WM_KICKIDLE消息。
在鼠标进入客户区前,通常要经过非客户区,比如:窗口的Border。因此,在该场景中的消息序列中,前面的部分表明了这个过程,它重复了上面的场景,只是由于是从Border进入的,WM_NCHITTEST代码为HTBORDER。值得注意的是,如果是Resize窗口(WS_THICKFRAME或者WS_SIZEBOX),那么Border代码将具化成精确的含义的代码(HTSIZEFIRST到HTSIZELAST)。
接下来的消息序列相当简单,
<00035>
000C
0BDC
S WM_SETCURSOR
hwnd:
000C
0BDC nHittest:HTCLIENT wMouseMsg:WM_MOUSEMOVE
<00036>
000C
0BDC R WM_SETCURSOR fHaltProcessing:False
<00037>
000C
0BDC P WM_MOUSEMOVE fwKeys:0000 xPos:376 yPos:149
注意,WM_MOUSEMOVE为post消息。对于几乎所有的鼠标消息,如果mouse没有被捕获(capture),那么将被发送到包含光标的窗口,否则发送到捕获鼠标的窗口。只有前景窗口可以Capture鼠标。使用SetCapture捕获鼠标,且一个时刻只能有一个窗口Capture鼠标。
鼠标的其它行为包括左、右、中键的按下(ButtonDown)和抬起(ButtonUp)。同时,又分为客户区和非客户区(NC开头)鼠标消息。这里暂且就不详细描述了(有时间,我将用专门的篇幅来解释这些行为)。补充一点,在任何鼠标动作时,都会产生WM_SETCURSOR消息,其参数之一会指明此时的鼠标动作。
以对话框为例:鼠标左键在对话框的Close按钮上点击。消息序列如下:
<00115> 001D
0A
94
S WM_SETCURSOR
hwnd:001D
0A
94 nHittest:HTCLOSE wMouseMsg:WM_LBUTTONDOWN
<00116> 001D
0A
94 R WM_SETCURSOR fHaltProcessing:False
<00117> 001D
0A
94 P WM_NCLBUTTONDOWN nHittest:HTCLOSE xPos:439 yPos:238
<00118> 001D
0A
94 P WM_MOUSEMOVE fwKeys:MK_LBUTTON xPos:396 yPos:-12
<00119> 001D
0A
94 P WM_MOUSEMOVE fwKeys:MK_LBUTTON xPos:396 yPos:-12
<00120> 001D
0A
94 P WM_LBUTTONUP fwKeys:0000 xPos:396 yPos:-12
<00121> 001D
0A
94
S WM_CAPTURECHANGED
hwndNewCapture:00000000
<00122> 001D
0A
94 R WM_CAPTURECHANGED
<00123> 001D
0A
94
S WM_SYSCOMMAND
uCmdType:SC_CLOSE xPos:439 yPos:238
<00124> 001D
0A
94
S WM_CLOSE
<00125> 001D
0A
94 R WM_CLOSE
<00126> 001D
0A
94 R WM_SYSCOMMAND
<00127> 001D
0A
94 P WM_NCMOUSELEAVE
<00128> 001D
0A
94 P WM_COMMAND wNotifyCode:BN_CLICKED wID:IDCANCEL hwndCtl:00030AA0
<00129> 001D
0A
94
S WM_CTLCOLORBTN
hdcButton:
38010C
99 hwndButton:
00040A
9E
<00130> 001D
0A
94 R WM_CTLCOLORBTN hBrush:
0110005A
<00133> 001D
0A
94
S WM_SETFOCUS
hwndLoseFocus:
00040A
9E
<00134> 001D
0A
94 R WM_SETFOCUS
<00135> 001D
0A
94
S WM_WINDOWPOSCHANGING
lpwp:
0012F
7F
0
<00136> 001D
0A
94 R WM_WINDOWPOSCHANGING
<00137> 001D
0A
94
S WM_WINDOWPOSCHANGED
lpwp:
0012F
7F
0
<00138> 001D
0A
94 R WM_WINDOWPOSCHANGED
<00139> 001D
0A
94
S WM_WINDOWPOSCHANGING
lpwp:0012FC88
<00140> 001D
0A
94 R WM_WINDOWPOSCHANGING
<00141> 001D
0A
94
S WM_DESTROY
<00142> 001D
0A
94 R WM_DESTROY
<00143> 001D
0A
94
S WM_NCDESTROY
<00144> 001D
0A
94 R WM_NCDESTROY
其中,我们看到在按下TitleBar右上角的Close按钮后,系统还产生了WM_MOUSEMOVE和WM_LBUTTONUP两个原本在客户区的鼠标消息。
接下来的消息中,<00127>比较有意思,WM_NCMOUSELEAVE,或许是因为窗口位置的改变造成的。<00128>消息实际为对对话框上的Cancel按钮的点击(00030AA0为该按钮的句柄)。<00129>为针对对话框上有焦点控件的CTLCOLOR消息,此时对话框上焦点在OK按钮上。接着,就是对话框得到焦点WM_SETFOCUS消息,可以看出前一个焦点是就是CTLCOLOR消息的控件。
然后是窗口位置改变消息-->为客户区被销毁-->非客户区被销毁。WM_DESTROY发生时,表示窗口从屏幕消失,将要被销毁,此时,系统还会发送该消息到各个子窗口,此时子窗口还未被销毁。然而,WM_NCDESTROY发生时,子窗口已经被销毁,同时表示非客户区正在被销毁。该消息的响应函数中,可以释放任何已经分配的与该窗口相关的内存。
以对话框为例:对话框没有被激活(当前活动应用为另一个程序),鼠标左键在客户区空白区域点击。消息序列如下:
<00112> 002209E0
S WM_NCHITTEST
xPos:345 yPos:253
<00113> 002209E0 R WM_NCHITTEST nHittest:HTCLIENT
<00114> 002209E0
S WM_MOUSEACTIVATE
hwndTopLevel:002209E0 nHittest:HTCLIENT uMsg:WM_LBUTTONDOWN
<00115> 002209E0 R WM_MOUSEACTIVATE fuActivate:MA_ACTIVATE
<00116> 002209E0
S WM_WINDOWPOSCHANGING
lpwp:0012FC34
<00117> 002209E0 R WM_WINDOWPOSCHANGING
<00118> 002209E0
S WM_WINDOWPOSCHANGED
lpwp:0012FC34
<00119> 002209E0 R WM_WINDOWPOSCHANGED
<00120> 002209E0
S WM_ACTIVATEAPP
fActive:True dwThreadID:00000000
<00121> 002209E0 R WM_ACTIVATEAPP
<00122> 002209E0
S WM_NCACTIVATE
fActive:True
<00123> 002209E0 R WM_NCACTIVATE
<00124> 002209E0
S WM_ACTIVATE
fActive:WA_CLICKACTIVE fMinimized:False hwndPrevious:(null)
<00125> 002209E0
S WM_ACTIVATETOPLEVEL
fActive:True dwThreadID:
0012F
920
<00126> 002209E0 R WM_ACTIVATETOPLEVEL
<00127> 002209E0
S WM_CTLCOLORBTN
hdcButton:
5C
010C
54 hwndButton:001E
0A
80
<00128> 002209E0 R WM_CTLCOLORBTN hBrush:
0110005A
<00129> 002209E0 R WM_ACTIVATE
<00130> 002209E0
S WM_SETCURSOR
hwnd:002209E0 nHittest:HTCLIENT wMouseMsg:WM_LBUTTONDOWN
<00131> 002209E0 R WM_SETCURSOR fHaltProcessing:False
<00132> 002209E0 P WM_LBUTTONDOWN fwKeys:MK_LBUTTON xPos:289 yPos:111
<00133> 002209E0
S WM_NCHITTEST
xPos:345 yPos:253
<00134> 002209E0 R WM_NCHITTEST nHittest:HTCLIENT
<00135> 002209E0
S WM_SETCURSOR
hwnd:002209E0 nHittest:HTCLIENT wMouseMsg:WM_MOUSEMOVE
<00136> 002209E0 R WM_SETCURSOR fHaltProcessing:False
<00137> 002209E0 P WM_MOUSEMOVE fwKeys:MK_LBUTTON xPos:289 yPos:111
<00138> 002209E0
S WM_NCHITTEST
xPos:345 yPos:253
<00139> 002209E0 R WM_NCHITTEST nHittest:HTCLIENT
<00140> 002209E0
S WM_SETCURSOR
hwnd:002209E0 nHittest:HTCLIENT wMouseMsg:WM_MOUSEMOVE
<00141> 002209E0 R WM_SETCURSOR fHaltProcessing:False
<00142> 002209E0 P WM_MOUSEMOVE fwKeys:MK_LBUTTON xPos:289 yPos:111
<00143> 002209E0
S WM_CTLCOLORBTN
hdcButton:
5C
010C
54 hwndButton:001E
0A
80
<00144> 002209E0 R WM_CTLCOLORBTN hBrush:
0110005A
<00145> 002209E0
S WM_NCHITTEST
xPos:345 yPos:253
<00146> 002209E0 R WM_NCHITTEST nHittest:HTCLIENT
<00147> 002209E0
S WM_NCHITTEST
xPos:345 yPos:253
<00148> 002209E0 R WM_NCHITTEST nHittest:HTCLIENT
<00149> 002209E0
S WM_NCHITTEST
xPos:345 yPos:253
<00150> 002209E0 R WM_NCHITTEST nHittest:HTCLIENT
<00151> 002209E0
S WM_NCHITTEST
xPos:345 yPos:253
<00152> 002209E0 R WM_NCHITTEST nHittest:HTCLIENT
<00153> 002209E0
S WM_SETCURSOR
hwnd:002209E0 nHittest:HTCLIENT wMouseMsg:WM_LBUTTONUP
<00154> 002209E0 R WM_SETCURSOR fHaltProcessing:False
<00155> 002209E0 P WM_LBUTTONUP fwKeys:0000 xPos:289 yPos:111
其中,
1. 窗口因为鼠标点击由Inactive变为active,会产生WM_MOUSEACTIVATE。紧接着,窗口位置消息被发送。
2. 窗口激活消息组:
WM_ACTIVATEAPP:分别发送给被激活的窗口和另一个应用中失去激活状态的窗口。
WM_NCACTIVATE:指示非客户区需要响应窗口的激活。
WM_ACTIVATE:分别发送给应用中被激活和失去激活状态的窗口。激活的方式分为鼠标激活和其它方式激活,包括:键盘,该参数在该消息的wParam的低字节中。上述场景为鼠标激活。
WM_ACTIVATETOPLEVEL:激活top-level窗口,但却在另一个非主线程中。(MFC内部消息, 类似WM_ACTIVATEAPP,但适用于属于不同进程的窗口混合在一个单一的窗口层次中,在OLE应用中较为普遍)
3. 接着,OK按钮为默认焦点控件,需要绘制。因此,系统会发送一个WM_CTLCOLORBTN消息。如果为别的焦点控件,将会是别的CTLCOLOR消息。
4. 接着是我们熟悉的鼠标系列消息
(WM_SETCURSOR,WM_LBUTTONDOWN,WM_NCHITTEST,WM_LBUTTONUP)
其间,系统还会发送一个WM_CTLCOLORBTN消息。
当窗口为对话框时,消息序列如下:
<00004>
000C
0BDC
S WM_SYNCPAINT
<00005>
000C
0BDC
S WM_NCPAINT
hrgn:E
804073A
<00006>
000C
0BDC R WM_NCPAINT
<00007>
000C
0BDC
S WM_ERASEBKGND
hdc:18010CB0
<00008>
000C
0BDC
S WM_CTLCOLORDLG
hdcDlg:18010CB0 hwndDlg:
000C
0BDC
<00009>
000C
0BDC R WM_CTLCOLORDLG hBrush:
0110005A
<00010>
000C
0BDC R WM_ERASEBKGND fErased:True
<00011>
000C
0BDC R WM_SYNCPAINT
<00012>
000C
0BDC P WM_PAINT hdc:00000000
<00013>
000C
0BDC
S WM_CTLCOLORBTN
hdcButton:
2A
010CCB hwndButton:000E0BDA
<00014>
000C
0BDC R WM_CTLCOLORBTN hBrush:
0110005A
<00015>
000C
0BDC
S WM_CTLCOLORBTN
hdcButton:
2A
010CCB hwndButton:
001C
0BBA
<00016>
000C
0BDC R WM_CTLCOLORBTN hBrush:
0110005A
其中,
1.WM_SYNCPAINT为操作系统同步不同线程的top-level窗口的绘制而发送的。应用不需要处理该消息,根据当前窗口的非客户区是否需要被绘制和背景是否必须被擦除,系统会将该消息转换成WM_NCPAINT和WM_ERASEBKGND,应用可以处理这两个消息。
2. WM_CTLCOLORDLG:在系统决定绘制对话框客户区前(从消息序列可以看出,该消息在WM_ERASEBKGND之后发出),会发送该消息,应用可以使用传入的dc,设置文本的前景和背景色。在WM_CTLCOLOR开头的消息中,唯独该消息被发送到窗口本身,其它消息均发送到控件的Owner窗口,通常也就是所在的对话框。在本消息序列最后,能看到WM_CTLCOLORBTN消息,共有两个按钮。
3.WM_PAINT:客户区绘制消息,为post消息。系统将在消息队列空闲,且存在无效区域时,发送该消息。既然是post消息,那么意味着是一种异步绘画。关于绘图,还存在一种直接(同步)绘图,请参见《DC和绘图》
(未完,待续......)