张赐荣 | Windows 消息处理机制 入门

张赐荣 | Windows 消息处理机制 入门

【文 / 张赐荣】

事件驱动和消息循环

消息概述
消息,就是指Windows发出的一个通知,告诉应用程序某个事情发生了。例如,单击鼠标、改变窗口尺寸、按下键盘上的一个键都会使Windows发送一个消息给应用程序。
消息本身是作为一个记录传递给应用程序的,这个记录(一般在 C/C++/汇编 中称为“结构体”)中包含了消息的类型以及其他信息。例如,对单击鼠标所产生的消息来说,这个记录(结构体)中包含了单击鼠标的消息号(WM_LBUTTONDOWN)、单击鼠标时的坐标(由X,Y值连接而成的一个32位整数)。这个记录类型叫做TMsg。
Windows的消息系统是由3个部分组成的:
1.消息队列。Windows能够为所有的应用程序维护一个消息队列。应用程序必须从消息队列中获取消息,然后分派给某个窗口。
2.消息循环。通过这个循环机制应用程序从消息队列中检索消息,再把它分派给适当的窗口,然后继续从消息队列中检索下一条消息,再分派给适当的窗口,依次进行。
3.窗口过程。每个窗口都有一个窗口过程来接收传递给窗口的消息,它的任务就是获取消息然后响应它。窗口过程是一个回调函数;处理了一个消息后,它通常要返回一个值给Windows。注意回调函数是程序中的一种函数,它是由Windows或外部模块调用的。一个消息从产生到被一个窗口响应,其中有5个步骤:1) 系统中发生了某个事件。2) Windows把这个事件翻译为消息,然后把它放到消息队列中。3)应用程序从消息队列中接收到这个消息,把它存放在TMsg记录中。4)应用程序把消息传递给一个适当的窗口的窗口过程。5) 窗口过程响应这个消息并进行处理。步骤3和4构成了应用程序的消息循环。消息循环往往是Windows应用程序的核心,因为消息循环使一个应用程序能够响应外部的事件。消息循环的任务就是从消息队列中检索消息,然后把消息传递给适当的窗口。如果消息队列中没有消息,Windows就允许其他应用程序处理它们的消息。Windows操作系统最大的特点就是其图形化的操作界面,其图形化界面是建立在其消息处理机制这个基础之上的。如果不理解Windows消息处理机制,肯定无法深入的理解Windows编程。
消息循环是应用程序能够持续存在的根本原因。如果循环退出,则应用程序就结束了。

事件驱动概述
微软视窗操作系统是以事件驱动做为程序设计的基础。程序的线程会从操作系统获取消息。应用程序会不断循环调用GetMessage函数(或是PeekMessage函数)来接收这些消息,这个循环称之为“事件循环”。基本上事件循环的代码如下所示(C语言 / C++编程语言):
MSG msg; //用于存储一条消息
BOOL bRet;
//从UI线程消息队列中取出一条消息
while( (bRet = GetMessage( &msg, NULL, 0, 0 )) != 0)
{
if (bRet == -1)
{
//错误处理代码,通常是直接退出程序
}
else
{
TranslateMessage(&msg); //按键消息转换为字符消息
DispatchMessage(&msg); //分发消息给相应的窗体
}
}
虽然在程序上并没有很严格的规定与要求,但是一般来说,它的事件循环通常会调用TranslateMessage函数与DispatchMessage函数,这两个函数会传递消息给回调函数,以及调用相应视窗的消息处理函数。
现在的绘图接口架构程序设计,例如Visual Basic与Qt基本上是不会要求应用程序直接拥有视窗程序的消息循环,但是会以键盘与鼠标的按键动作来作为事件的处理机制。在这些架构底下,消息循环的痕迹还是可以被找到的。
注意:在上述的源代码里,尤其在while循环大于零的条件。即使GetMessage函数的传回值类型是英文字大写的BOOL,但是在Win32视窗程序里,它是被定义成int整数类型,它有两个值,TRUE是整数的1,FALSE是整数的0。整数 -1代表error(例如第二个参数为输出的窗口句柄但取不到值的时候),整数0值当GetMessage获取到WM_QUIT消息。假如有其他消息,那么非零值会当成传回值(有消息的传回值通常是正值,但是有些程序设计的帮助文档不一定会说明的很详细)。
事件本质上是对消息的封装,是IDE编程环境为了简化编程而提供的有用的工具。这个封装是在窗体过程中实现的。每种IDE封装了许多Windows的消息。
16位Windows系统为非抢先单线程模式,应用程序没有发送消息队列,向窗口发送一个消息总是按同步方式执行,也即发送程序要在接受消息的窗口完全处理完消息之后才能继续运行。这通常是一个所期望的特性。但是,如果接收消息的窗口花很长的时间来处理消息或者出现挂起,则发送程序就不能再执行。这意味着系统是不强壮的。如果应用程序消息队列(只用于存放投寄的消息)为空,由于没有虚拟输入消息队列,SendMessage或PeekMessage函数访问系统事件队列查取可用的鼠标或键盘输入消息。如果系统队列中没有需要处理的事件,SendMessage或PeekMessage函数扫描所有窗口以处理需要修改重绘的区域。如果没有需要重绘的区域,则交出CPU控制权。恢复CPU控制权时,查看是否有定时器过期。至此如果没有消息可返回,SendMessage进入睡眠,直至被输入事件唤醒;PeekMessage如果没有设置PM_NOYIELD标记,则会让出CPU控制权,但不会让线程休眠,重新获得CPU后PeekMessage将控制权返回到线程,并返回一个空值指出这个线程没有要处理的消息了。
本文主要关注Win32系统的消息处理机制。

UI线程
Windows系统规定,窗口和钩子(hook)这两种User对象分别由创建窗口和安装钩子的线程所拥有,一旦该线程结束,操作系统会自动删除窗口或卸载钩子。而其他的User对象(图标icon、光标cursor、窗口类WndClass、菜单、加速键表等)则归进程所有,进程结束时操作系统会自动删除这些对象。
创建窗口的线程必须就是处理窗口所有消息的线程,即UI线程(User Interface Thread)创建了窗体及窗体上的各种控件,系统为UI线程分配一个消息队列用于窗口消息的派送(dispatch)。为了使窗口处置这些消息,线程必须有它自己的“消息循环”。只有当一个线程调用Windows API中的GDI(Graphics Device Interface)和User函数时,操作系统才会将其看成是一个UI线程,并为它分配一些另外的资源,创建一套线程消息队列;否则,操作系统把非UI线程视作普通工作线程(Workhorse),不会为它创建消息队列。因此,调用PostThreadMessage前,这个线程必须是UI线程从而有投寄消息的队列,通常可在该线程中调用一次PeekMessage函数以达到这个目的。
如果一个UI线程结束运行,操作系统会自动回收它所创建的所有窗体。

窗体过程
窗体过程(Window Procedure)是一个函数,每个窗体有一个窗体过程,负责处理该窗体的所有消息。
UI控件也是独立的“Window”,拥有自己的“窗体过程”。

消息队列
Windows操作系统的内核空间中有一个系统消息队列(system message queue),在内核空间中还为每个UI线程分配各自的线程消息队列(Thread message queue)。在发生输入事件之后,Windows操作系统的输入设备驱动程序将输入事件转换为一个“消息”投寄到系统消息队列;操作系统的一个专门线程从系统消息队列取出消息,分发到各个UI线程的输入消息队列中。
每个UI线程的线程信息块TIB分配一个THREADINFO的结构,该结构包含一族成员变量,包括:[4]
发送消息队列(send-message queue)指针:其他发起线程通过SendMessage、SendMessageTimeout、SendMessageCallback、SendNotifyMessage、ReplyMessage等函数产生的消息放入该队列,发起的线程阻塞(挂起)在该队列上(对于SendMessageCallback、SendNotifyMessage不被阻塞)直至消息处理完或者超时返回。
投寄消息队列(posted-message queue)指针:其他线程通过PostMessage函数或PostThreadMessage函数投寄的消息;
虚拟输入消息队列(virtualized-input queue)指针:键盘与鼠标事件。该队列最多只保存一个键盘消息,仅当应用程序处理完这个键盘消息,Windows才会从操作系统消息队列取出下一个键盘消息放入线程的虚拟输入消息队列。这种方式至少有两点用途:一是如果用户的键盘输入速度快于应用程序处理键盘消息的速度,并且特定按键会使输入焦点从一个窗口切换到另一个窗口,随后的按键就应该是另一个窗口的输入;二是Windows API函数TranslateMessage把按键消息转化为字符消息,如WM_KEYDOWN转化为WM_CHAR,然后放入线程的虚拟输入消息队列中,成为下一个待处理的键盘消息。
回复消息队列(reply-message queue)指针:调用SendMessage函数的线程在这个函数上阻塞后,实际上仍可能被系统使用该线程执行其他处理,因此SendMessage函数的目标线程把窗口函数的返回值登记到这个队列作为SendMessage的返回值,以便SendMessage函数从阻塞状态恢复时能取到该返回值(16位Windows系统是单线程的,因此不可能存在这种需求)。另外一种使用情形是SendMessageCallback函数(给所有重叠(overlapped)窗口广播)时,总是调用后立即返回并继续执行,因此接收了此消息的线程把窗口函数执行结果登记到发起线程的回复消息队列,在发起线程下一次调用GetMessage、PeekMessage、WaitMessage或某个SendMessage挂起时从回复消息队列中取出该msg并执行登记的ResultCallBack函数。
nExitCode:由PostQuitMessage函数设置该成员,作为线程的退出码。
唤醒标志(wake flage)
局部输入状态变量
QS_POSTMESSAGE位:投寄消息队列是否为空;
QS_QUIT位:由PostQuitMessage函数给该标志置位。
QS_SENDMESSAGE位:发送消息队列是否为空;
QS_KEY:有按键消息
QS_MOUSE:有鼠标消息
QS_PAINT:有WM_PAINT
QS_TIMER:有WM_TIMER
应用程序的每个UI线程中有一段称之为“消息循环”的代码,通过GetMessage系统调用(或是PeekMessage系统调用)访问系统空间中的对应的UI线程的消息队列,并依照下述次序处理:
QS_SENDMESSAGE置位:则对发送消息队列中的每个消息,依次调用各个发送消息的窗口函数直接处理,GetMessage不返回;直至所有发送消息队列中的消息处理完毕。
QS_POSTMESSAGE置位:则填充GetMessage函数参数的MSG结构为相应的投寄消息,GetMessage返回为真。该消息通过DispatchMessage系统调用把消息分发给相应窗口的消息处理函数。
QS_QUIT置位:则填充GetMessage函数参数的MSG结构为WM_QUIT,QS_QUIT复位,GetMessage返回为假。
QS_INPUT置位:则填充GetMessage函数参数的MSG结构为相应的输入消息,GetMessage返回为真。该消息通过DispatchMessage系统调用把消息分发给相应窗口的消息处理函数。
再一次检查QS_SENDMESSAGE置位,如是则处理发送消息队列中的每个消息。
QS_PAINT置位:则填充GetMessage函数参数的MSG结构为WM_PAINT,GetMessage返回为真。GetMessage不从队列中删除WM_PAINT消息(即不对QS_PAINT复位)。
QS_TIMER置位:则填充GetMessage函数参数的MSG结构为WM_TIMER,QS_TIMER复位,GetMessage返回为真。如果QS_TIMER复位状态,则当前线程挂起(hung)。
需要注意的是,GetMessage如果在应用程序消息队列未获取消息,则GetMessage调用不返回,该线程挂起,CPU使用权交给操作系统。即GetMessage为阻塞调用。
由此可见,Windows的事件驱动模式,并不是操作系统把消息主动分发给应用程序;而是由应用程序的每个UI线程通过“消息循环”代码从UI线程消息队列获取消息。

Windows消息类别
Windows操作系统中包括以下几种消息:
1、标准Windows消息:
这种消息以WM_打头。
2、通知消息
通知消息是针对标准Windows控件的消息。这些控件包括:按钮(Button)、组合框 (ComboBox)、编辑框(TextBox)、列表框(ListBox)、
ListView控件、Treeview控件、工具条(Toolbar)、菜单(Menu)等。每种消息以不同的字符串打头。
3、自定义消息
编程人员还可以自定义消息。

键盘消息:
按键消息:WM_SYSKEYDOWN、WM_SYSKEYUP、WM_KEYDOWN、WM_KEYUP等消息。
wParam包含虚拟键码(virtual-key code),表示按下或释放的键
lParam包含按键6个字段信息:
重复按键次数(Repeat Count,0~15 位):通常设为1。大于1说明按键速度大于程序处理能力。可以根据实际需要忽略或处理。
OEM扫描码(scan code,16~23位):硬件产生的代码。
扩充键标志(extended key,24位):如果为扩充键(如右侧的Alt键或Ctrl键)按下时为1,否则为0。
保留位(25~28位):保留位是系统缺省保留的,一般不用。
上下文代码(context code,29位):如同时按下ALT,标志为1;否则为0。WM_SYSKEYUP或WM_SYSKEYDOWN常为1。WM_KEYUP或WM_KEYDOWN常为0。当所有程序都最小化时,没有窗口具有输入焦点,Windows仍将发送键盘消息给活动窗口;所有的按键都会产生WM_SYSKEYUP与WM_SYSKEYDOWN消息,此情况下如果没按下ALT,该字段为0,这样使最小化的活动窗口不处理这些按键。对于一些非英文键盘,有些字符是shift等组合键产生的,这时内容代码为1,但是其是非系统按键。
键先前状态(previous key state,位30):键此前是释放的,则为0,还则为1。很明显UP为1,DOWN可以为1或0,为1表示该键自动重复。
转换状态(transition state,31位):键被按下为0,键被松开时为1。如UP为1,DOWN为零。
SYSKEY:按下F10(将激活菜单条)或者按下Alt后再按下别的键,或者没有窗口具有键盘输入焦点(WM_SETFOCUS指示窗口得到输入焦点,WM_KILLFOCUS指示窗口失去输入焦点)时的按键,为SYSKEY。lParam的第29位为context code,如果为1表示Alt被按下,如果为0表示WM_SYSKEYDOWN发出时没有窗口具有键盘输入焦点。无论用户处理与否,必须传送给Windows默认窗口过程处理此类按键消息。
其他情形为普通按键。除Print键之外都有“按下”消息;所有键都存在“弹起”消息。
字符消息:按键消息WM_SYSKEYDOWN、WM_KEYDOWN被Windows API函数TranslateMessage处理后该函数在线程消息队列投寄(post)相应的字符消息,wParam参数是ASCII或Unicode的character code;这取决于RegisterClass函数是A或W版;IsWindowUnicode函数判断窗口过程会接受哪种编码。产生字符消息的按键有:任何字符键、回退键(BACKSPACE)、回车键(carriage return)、ESC、SHIFT + ENTER(linefeed换行)、TAB。因为TranslateMessage函数从WM_KEYDOWN和WM_SYSKEYDOWN消息产生了字符消息,所以字符消息是夹在按键消息之间传递给窗口消息处理程序的。如果用户按住一个键不放,会自动重复产生一系列的WM_KEYDOWN消息;对每条WM_KEYDOWN消息,都会得到一条字符消息。如果某些WM_KEYDOWN消息的重复计数大于1,那么相应的WM_CHAR消息将具有同样的重复计数。
WM_SYSCHAR:按下Alt后再按下别的键的WM_SYSKEYDOWN消息被翻译,
WM_CHAR:WM_KEYDOWN消息被翻译为WM_CHAR消息。
WM_DEADCHAR:TranslateMessage函数处理“死键”(dead key)的WM_KEYUP消息,向具有输入焦点的窗口投寄(post)出WM_DEADCHAR消息。死键是产生附加符号的按键。例如在德语键盘,锐音符被按下、释放后,再按下A,将获得字母á的WM_CHAR。如果在死键之后跟有不能带此附件符号的字母(例如锐音符后跟“s”),那么将接收到两条WM_CHAR消息:前一个消息的wParam等于附加符号本身的ASCII码(与传递到WM_DEADCHAR消息的wParam值相同),第二个消息的wParam等于字母的ASCII代码。
WM_SYSDEADCHAR:按下Alt时又按下了“死键”的WM_SYSKEYUP消息。

鼠标消息:
客户区鼠标消息:WM_MOUSEMOVE及鼠标按键的DOWN、UP、DBLCLK消息。双击事件的处理只有窗口类定义接收(CS_DBLCLKS)时,才起作用,这时接收到的鼠标消息顺序为:DOWN、UP、DBLCLK、UP。鼠标消息发送给被单击的窗口或鼠标经过的窗口,即使该窗口处于非活动或不带输入焦点;例外情况有“捕获鼠标”时或模式对话框处于活动状态时。消息参数wParam(指出那个鼠标按钮、Shift键、Ctrl键被按下;lParam的低位表示x坐标,高位表示y坐标的鼠标位置。
非客户区鼠标消息:为WM_NC*形式。
击中测试消息:WM_NCHITTEST。Windows利用此消息来产生其他所有的鼠标消息。
WM_MOUSEWHEEL发送给具有焦点的窗口(注意不一定是鼠标下面的窗口)
定时器消息

控件消息
跨进程发送数据的消息:WM_SETTEXT、WM_GETTEXT、WM_COPYDATA,系统自动分配使用可在进程间共享的内存映射文件来传递数据。
键盘输入时需要明确插入符位置,相关API函数为:CreateCaret、SetCaretPos、ShowCaret、HideCaret、DestroyCaret、GetCaretPos、GetCaretBlinkTime、SetCaretBlinkTime。
3个获得键状态的函数:GetKeyState、GetAsyncKeyState、GetKeyboardState。
对于自定义的控件,当单击子窗口时,父窗口会得到焦点。但对于标准子窗口控件,单击时会自动获得焦点(子窗口过程在WM_LBUTTONDOWN中实现了SetFocus(hwnd))。如果一个子窗口拥有输入焦点,鼠标单击另一个兄弟子窗口,则兄弟子窗口获得输入焦点。

同步与阻塞
Windows API函数SendMessage是个同步调用,即它发出的Windows消息没被处理完之前这个函数就不返回。但这个函数不是阻塞的。分两种情形:[5]
如果被指定的窗口是发起调用SendMessage的线程创建的,则该线程立即执行窗口过程(window procedure);
如果被指定的窗口不是发起调用SendMessage的线程创建的,操作系统切换到该窗口所属的线程执行相应的窗口过程。消息处理完之前,发送线程不从调用SendMessage处返回,但发送线程这期间可以处理非队列消息(nonqueued message)。这是“同步非阻塞”。
异步发送或投寄消息的函数,如PostMessage、SendMessageCallback、SendNotifyMessage,消息参数中不能使用指针,否则函数调用失败。

GetMessage伪代码
BOOL GetMessage(MSG *lpMsg, HWND hWnd , UINT wMsgFilterMin, UINT wMsgFilterMax)
{
//查看QS_SENDMESSAGE标志,如果有的话循环处理,直到没有消息位置
DWORD dwRetVal = 0;
ThreadInfo threadInfo;
FLAG_SENDPROCLOOP:
GetThreadInfo(GetCurrentThreadId(), &threadInfo);
while (threadInfo.QS_SENDMESSAGE == QS_SIGNALSET) {
//从发送消息队列中获取消息
dwReturnVal = GetMsgFromQueue(QUEUE_SEND, lpMsg, hWnd,wMsgFilterMin, wMsgFilterMax);
//判断是否取到消息,有则调用窗口函数,无则复为QS_SENDMESSAGE标志
If (dwReturnVal == GETMESSAGE_HASMESSAGE) {
//调用指定窗口的窗口函数
CallWindowProc(hWnd, &threadInfo, lpMsg);
}
else {
QS_SENDMESSAGE = QS_SIGNALRESET;
break;
}
}
//在继续处理之前再次检查发送消息队列
if (threadInfo.QS_SENDMESSAGE == QS_SIGNALSET) goto FLAG_SENDPROCLOOP;
//检查发送消息队列, 如果有消息则取发送消息
//判断是否还有发送消息,没有了则复位QS_POSTMESSAGE标志
if (threadInfo.QS_POSTMESSAGE == QS_SIGNALSET) {
dwReturnVal = GetMsgFromQueue(QUEUE_POST, lpMsg, hWnd, wMsgFilterMin, wMsgFilterMax);
if (dwReturnVal == GETMESSAGE_LASTMESSAGE)
threadInfo.QS_POSTMESSAGE = QS_SIGNALRESET;
return TRUE;
}
//如果退出标志被置位
if (threadInfo.QS_QUIT == QS_SIGNALSET) {
threadInfo.QS_QUIT = QS_SIGNALRESET;
FillMessage(lpMsg, MESSAGE_QUIT);
return FALSE;
}
//检查输入消息队列
if (threadInfo.QS_INPUT == QS_SIGNALSET) {
DWORD dwRetVal = GetMessageFromQueue(QUEUE_INPUT, lpMsg, hWnd, wMsgFilterMin, wMsgFilterMax);
//检查是否有键盘,鼠标消息
if (Test(dwRetVal, QS_KEY) == QS_LASTMOUSEKEYMESSAGE)
threadInfo.QS_KEY = QS_SIGNALRESET;
if (Test(dwRetVal, QS_MOUSEBUTTON) == QS_LASTMOUSEMESSAGE)
threadInfo.QS_MOUSEBUTTON = QS_SIGNALRESET;
return TRUE;
}
//测试QS_PAINT
if (threadInfo.QS_PAINT == QS_SIGNALSET) {
//填充MSG,如果没有窗口过程确认窗口,则复位QS_PAINT标志
//...
//返回TRUE
threadInfo.QS_PAINT = QS_SIGNALRESET;
return TRUE;
}
if (threadInfo.QS_TIMER == QS_SIGNALSET) {
//填充MSG,如果没有定时器报时,则复位QS_TIMER标志
//...
//返回TRUE
return TRUE;
}
//等待有消息到达
dwRetVal = MsgWaitForMultipleObjectsEx(...);
if (...)
goto FLAG_SENDPROCLOOP;
//等待失败
return FALSE;
}

消息处理机制
消息处理过程
Windows窗体是怎样展现在屏幕上的呢?众所周知,是通过API绘制实现的。Windows操作系统提供了一系列的API函数 来实现界面的绘制功能,例如:
DrawText 绘制文字
DrawEdge 绘制边框
DrawIcon 绘制图标
Bitmap 绘制位图
Rectangle 绘制矩形

再复杂的程序界面都是通过这些函数来实现的。
那什么时候调用这些函数呢?显然我们需要一个控制中心,用来进行“发号施令”,我们还需要一个命令传达机制,将命令即时的传达到目的地。这个控制中心,就是一个动力源,就像一颗心脏,源源不断地将血液送往各处。这个命令传达机制就是Windows
消息机制,Windows消息就好比是身体中的血液,它是命令传达的使者。
Windows消息控制中心一般是三层结构,其顶端就是Windows内核。Windows内核维护着一个消息队列 ,第二级控制中心从这个消息队列中获取属于自己管辖的消息,后做出处理,有些消息直接处理掉,有些还要发送给下一级
窗体 (Window)或控件(Control)。第二级控制中心一般是各Windows应用程序的Application对象。第三级控制中心就是Windows
窗体对象 ,每一个窗体都有一个默认的窗体过程,这个过程负责处理各种接收到的消息。
注:windows指windows操作系统;窗口:即windows窗口;窗体 :包括窗口,以及有句柄的控件;control指控件,控件本身也可能是一个window,也可能不是;Application即应用程序,应用程序也可能不会用到Windows消息机制 ,这里我们专门讨论有消息循环的应用程序。
消息是以固定的结构传送给应用程序的,结构如下:
Public Type MSG
hwnd As Long
message As Long
wParam As Long
lParam As Long
time As Long
pt As POINTAPI
End Type
其中hwnd是窗体的句柄,message是一个消息常量 ,用来表示消息的类型,wParam和lParam都是32位的附加信息,具体表示什么内容,要视消息的类型而定,time是消息发送的时间,pt是消息发送时鼠标所在的位置。

句柄
不是每个控件都能接收消息,转发消息和绘制自身,只有具有句柄(handle)的控件才能做到。有句柄的控件本质上都是一个
窗体 (window),它们可以独立存在,可以作为其它控件的容器,而没有句柄的控件,如Label,是不能独立存在的,只能作为窗口控件的子控件,它不能绘制自身,只能依靠父窗体将它绘制来。
句柄的本质是一个系统自动维护的32位的数值,在整个操作系统的任一时刻,这个数值是唯一的。但该句柄代表的窗体释放后,句柄也会被释放,这个数值又可能被其它窗体使用。也就是说,句柄的数值是动态的,它本身只是一个唯一性标识,操作系统通过句柄来识别和查找它所代表的对象。
然而,并非所有的句柄都是窗体 的句柄,Windows系统中还中很多其它类型的句柄,如画布(hdc)句柄,画笔句柄,画刷句柄,应用程序句柄(hInstance)等。这种句柄是不能接收消息的。但不管是哪种句柄,都是系统中对象的唯一标识。本文只讨论窗体句柄。
那为什么句柄使窗口具有了如此独特的特性呢?实际是都是由于消息的原因。由于有了句柄,窗体能够接收消息,也就知道了该什么时候绘制自己,绘制子控件,知道了鼠标在什么时候点击了窗口的哪个部分,从而作出相应的处理。句柄就好像是一个人的身份证,有了它,你就可以从事各种社会活动;否则的话,你要么是一个社会看不到的黑户,要么跟在别人后面,通过别人来证明你的存在。

消息传送
消息传送 消息传送
1、从消息队列获取消息:
可以通过PeekMessage或GetMessage函数从Windows消息队列中获取消息。Windows保存的消息队列是以线程 (Thread)来分组的,也就是说每个线程都有自己的消息队列。
2、发送消息
发送消息到指定窗体 一般通过以下两个函数完成:SendMessage和PostMessage。两个函数的区别在于:PostMessage函数只是向线程消息队列中添加消息,如果添加成功,则返回True,否则返回False,消息是否被处理,或处理的结果,就不知道了。而SendMessage则有些不同,它并不是把消息加入到队列里,而是直接翻译消息和调用消息处理(线程向自己发送消息才是这样),直到消息处理完成后才返回。所以,如果我们希望发送的消息立即被执行,就应该调用SendMessage。
还有一点,就是SendMessage发送的消息由于不会被加入到消息队列 中(错:线程向其他线程发送消息也是追加到其他线程的发送消息队列的,即使这两个线程在同一个进程也是如此),所以通过PeekMessage或GetMessage是不能获取到由SendMessage发送的消息。
另外,有些消息用PostMessage不会成功,比如wm_settext。所以不是所有的消息都能够用PostMessage的。
还有一些其它的发送消息API函数,如PostThreadMessage,SendMessageCallback,SendMessageTimeout,
SendNotifyMessage等。

消息循环
消息循环是应用程序能够持续存在的根本原因。如果循环退出,则应用程序就结束了。
我们来看一看Delphi中封装的消息循环是怎样的:
第一步:程序开始运行(Run)
Application.Initialize; //初始化
Application.CreateForm(TForm1, Form1); //创建主窗体
Application.Run; //开始运行,准备进行消息循环
如果不创建主窗体,应用程序同样可以存在和运行。
第二步:开始调用消息循环(HandleMessage)
procedure TApplication.Run;
begin
FRunning := True;
try
AddExitProc(DoneApplication);
if FMainForm <> nil then
begin
case CmdShow of
SW_SHOWMINNOACTIVE: FMainForm.FWindowState := wsMinimized;
SW_SHOWMAXIMIZED: MainForm.WindowState := wsMaximized;
end;
if FShowMainForm then
if FMainForm.FWindowState = wsMinimized then
Minimize else
FMainForm.Visible := True;
Repeat //注:循环开始
try
HandleMessage;
except
HandleException(Self);
end;
until Terminated; //循环结束条件
end;
finally
FRunning := False;
end;
end;
第三步:消息循环中对消息的处理。
procedure TApplication.HandleMessage;
var
Msg: TMsg;
begin
if not ProcessMessage(Msg) then Idle(Msg);
end;
function TApplication.ProcessMessage(var Msg: TMsg): Boolean;
var
Handled: Boolean;
begin
Result := False;
if PeekMessage(Msg, 0, 0, 0, PM_REMOVE) then
begin
Result := True;
if Msg.Message <> WM_QUIT then
begin
Handled := False;
if Assigned(FOnMessage) then FOnMessage(Msg, Handled);
if not IsHintMsg(Msg) and not Handled and not IsMDIMsg(Msg) and
not IsKeyMsg(Msg) and not IsDlgMsg(Msg) then
begin
TranslateMessage(Msg);
DispatchMessage(Msg);
end;
end
else
FTerminate := True;
end;
end;
窗体过程实际上是一个回调函数 。所谓的回调函数,实际上就是由Windows操作系统或外部程序调用的函数。回调函数一般都有规定的参数格式,以地址方式传递给调用者。窗口过程中是Windows操作系统调用了,在一个窗口创建的时候,在分配窗体句柄的时候就需要传入回调函数地址。那为什么我们平时编程看不到这个回调函数呢?这是由于我们的编程工具已经为我们生成了默认的窗体过程,这个过程的要做的事情就是判断不同的消息类型,然后做出不同的处理。例如可以为键盘或鼠标输入生成事件等。

消息事件
事件本质上是对消息的封装,是IDE编程环境为了简化编程而提供的有用的工具。这个封装是在窗体 过程中实现的。每种IDE封装了许多Windows的消息,例如:
事件 消息
OnActivate WM_ACTIVATE
OnClick WM_XBUTTONDOWN
OnCreate WM_CREATE
OnDblClick
WM_XBUTTONDBLCLICK
OnKeyDown WM_KEYDOWN
OnKeyPress WM_CHAR
OnKeyUp WIN_KEYUP
OnPaint WM_PAINT
OnResize WM_SIZE OnTimer WM_TIMER

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值