Windows消息机制
消息机制是windows程序的基础。windows程序都是消息驱动,也就是说,在接收到消息时,才根据消息进行相应的动作。Windows程序的核心就是那一段经典的消息循环:
MSG msg;
while(GetMessage (&msg, NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage (&msg);
}
这段代码不断地从消息队列中取出消息,然后调用相应的消息处理函数,对消息进行处理。当这个循环退出的时候,整个程序也就退出了。对于有图形界面的程序来说,系统会在界面需要刷新的时候,向消息队列插入WM_DRAW这个消息,GetMessage在获取了这个消息后,就会执行OnDraw来绘制自身。
这三个最重要的函数原型如下:
BOOL GetMessage( LPMSG lpMsg, HWND hWnd, UINT wMsgFilterMin, UINT wMsgFilterMax );
第一个参数lpMsg是一个指向MSG结构体的指针,用以接收消息。MSG结构体暂且不表,只需要知道这个结构体表示消息即可。
hWnd是一个句柄,表示想要接收谁的消息,通常情况下,这个句柄属于某一个窗口。且这个窗口必须要属于当前的线程。如果hWnd为NULL,则函数会接收所有消息。
wMsgFilterMin 和wMsgFilterMax表示要接收的消息的最小值和最大值。实际上在windows中,消息是以一个整型的非负数表示的。如果这两个参数都设置为0,那么表示函数将会接收所有的消息。
GetMessage函数从消息队列中取出一个消息,如果消息队列中没有消息,则GetMessage会阻塞直到有消息被插入到消息队列中。如果从消息队列中取出的消息是WM_QUIT,这个消息代表退出程序,那么GetMessage会返回0,如果函数调用失败,比如说hWnd不是一个有效的句柄,GetMessage返回-1,其他情况下均返回一个非零。
TranslateMessage负责对消息进行转换,主要是对按键消息的转化,其原型如下:
BOOL TranslateMessage( const MSG *lpMsg );
lpMsg是由GetMessage获取到的消息结构的指针。
DispatchMessage函数负责消息分发,即调用处理lpMsg这个消息的相应的函数,其原型为:
BOOL DispatchMessage( const MSG *lpMsg );
我们使用GetMessage获取消息队列中的消息,也可以使用PeekMessage来探查消息队列中的消息。PeekMessage函数原型如下:
BOOL PeekMessage( LPMSG lpMsg, HWND hWnd, UINT wMsgFilterMin, UINT wMsgFilterMax, UINT wRemoveMsg );
PeekMessage最后一个参数表示如何处理这个消息,取值可以为:
PM_NOREMOVE : 不从消息队列中移除这个消息
PM_REMOVE : 从消息队列中移除这个消息
可以不从消息队列中移除消息,这是PeekMessage与GetMessage的不同之处,另外一个不同之处在于PeekMessage不会阻塞等待消息,在消息队列为空时,PeekMessage直接返回0.
在消息机制中,消息结果MSG可谓是最核心的一个数据机构,其声明为:
typedef struct
{
HWND hwnd; // 消息的目的地句柄
UINT message; // 消息值,由非负整数来表示
WPARAM wParam; // 消息参数
LPARAM lParam; // 消息参数
DWORD time; // 消息发送时间
POINT pt; // 消息发送时,鼠标的位置
} MSG, *PMSG;
消息以非负整数表示,在Widnows中定义了很多系统消息,以 WM_打头,我们也可以定义自己的消息,为了不与系统消息重复,windows提供一个宏 WM_USER,我们自己定义的消息必须要大于WM_USER,通常我们会定义为:
#define MY_MSG WM_USER + 1
这种形式。
说完了消息的接收,下面该介绍消息的发送了。对windows编程有一点了解的人,都应该知道SendMessage和PostMessage,这两个函数用于发送消息,其原型和介绍分别为:
LRESULT SendMessage(
HWND hWnd,
UINT Msg,
WPARAM wParam,
LPARAM lParam
);
BOOL PostMessage(
HWND hWnd,
UINT Msg,
WPARAM wParam,
LPARAM lParam
);
这两个函数参数一致,第一个参数表示发送目的地,是一个窗口句柄,我们在前面接收中有过相应介绍,第二个参数不是MSG结构体,而是一个非负整数表示的消息值。后面两个参数则是此消息带的参数,LPARAM和WPARAM都是4个字节,通常对于我们自定义的消息,我们可以将一个指针的值赋予lParam或者wParam,则在消息处理函数中,我们就可以通过lParam或者wParam将这个指针取出来。(还记得吗?MSG结构体中是有这两个字段的,我们发送的lParam和wParam就会保存在接收到的MSG结构的这两个字段中)
SendMessage和PostMessage的不同之处在于:
SendMessage 会调用对应的消息处理函数,且等待函数执行完毕再返回。
PostMessage 则是向创建hWnd窗口的线程投递一个消息后,即返回消息的处理则留待消息循环去完成。
除了SendMessage外,Send系列还有两个函数:
BOOL SendMessageCallback( HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam, SENDASYNCPROC lpCallBack, ULONG_PTR dwData ); BOOL SendNotifyMessage( HWND hWnd, UINT Msg, WPARAM wParam, LPARAM lParam );
SendMessageCallBack 函数调用指定窗口的窗口过程,然后立即返回,目标窗口执行完消息处理函数后,系统会调用lpCallBack函数,传入dwData作为参数。
SendNotifyMessage则是分两种情况,如果目标窗口属于本线程,则SendNotifyMessage会调用消息处理函数,等到处理函数结束后再行返回。如果目标窗口属于其他线程,则SendNotifyMessage将消息传递给目标窗口后立即返回。
可以看出来,Send系列的消息函数,比较关注返回值。Post系列则只是见消息插入消息队列,一切交由消息循环去处理,自身并不关心返回结果。
我们提到了Post系列,但上面只提到了PostMessage一种Post打头的消息相关函数,实际上还有一个函数是 PostThreadMessage,其原型为
BOOL PostThreadMessage( DWORD idThread, UINT Msg, WPARAM wParam, LPARAM lParam );
PostThreadMessage除了第一个参数与PostMessage不同外,其他都相同。PostThreadMessage是以线程作为消息发送的目标,直接将消息发送到线程的消息队列中。
这个地方就要对我们多次提到的消息队列做一个交代了。不交代清楚,就会老觉得神秘。
很多地方都提到过,线程分为两种,工作线程和UI线程。工作线程就是用来干活的,比如说计算,硬盘操作等。UI线程则是负责界面显示的。windows的UI线程会维护一个消息队列,用以接收消息。也就是说,消息队列的所有者是线程。
UI线程实际上就是文章开头提到的那个死循环,不断地接收消息,然后用Dispatch分发消息。PostThreadMessage可以向某个线程的消息队列中发送消息。
除了PostThreadMessage以外的其他API,消息的目标均是窗口,这有些容易让人误会消息队列是属于某个窗口的,实际上不是这样,使用窗口作为目标只是因为窗口比线程这个概念更加直观。
线程会将消息分发给消息的目的窗口进行处理,这个分发实际上就是调用窗口的窗口过程(Window Procedure),每一个窗口都会有一个窗口过程函数用以处理消息。我们可以重载窗口类的窗口过程,以实现自己的功能:
LRESULT WindowProc(UINT message, WPARAM wParam, LPARAM lParam)
我们可以在 WindowProc 里写一个switch-case来处理我们定义的消息,但实际上现在很少有人会这么做,一般都是通过MFC的MESSAGE_MAP宏来做消息映射,将窗口的消息映射到窗口的一个成员函数上。具体操作请参考MFC编程。
windows的消息机制不仅可以用于应用程序内部发送消息,还可以用于不同的应用程序之间,我们可以使用FindWindow这个API函数,找到窗口句柄,然后调用消息发送函数发送消息给这个窗口,下面是一个例子,把你的qq2010给关闭掉:
void CTestMFCDlg::OnBnClickedOk()
{
CWnd* wnd = FindWindow(NULL, "QQ2010");
if (wnd != NULL)
{
HWND hWnd = wnd->m_hWnd;
::SendMessage(hWnd, WM_CLOSE, 0, 0);
}
}
要测试这个例子,可以新建一个MFC工程,名为TestMFC,类型为Dialog Based,双击对话框上的OK,把OK代码改为上面这段代码即可。想要关闭其他程序,只需要将QQ2010改成其他程序左上角显示的名字即可。
除了干这种坏事以外,消息机制还可以用于多个应用程序之间的通信操作,这就需要使用到RegisterWindowMessage函数,其原型如下:
UINT RegisterWindowMessage(LPCTSTR lpString);
这个函数注册一个消息,返回消息值。如果在两个程序中,使用相同的字符串注册消息,则会返回相同的消息值,我们可以用这个消息值进行两个程序之间的通信。

浙公网安备 33010602011771号