从最初开始学写Windows应用程序以来,都免不了和Windows消息打交道,但是事实上很长时间都没能把Windows的消息机制彻底弄清楚。本文 记叙了我对Windows消息机制以及线程与消息关系的理解,因为水平所限,不免会有些错漏,希望对此有了解的同学指正。
Windows的消息队列与消息循环
所有创建了窗口的Windows程序,都需要运行一个消息循环<即下面这段程序循环>,我们在无数的Windows编程书籍中都可以看到这样的经典代码:
while (GetMessage(&msg, hWnd, 0, 0) > 0)
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
这里的hWnd就是创建的窗口句柄,上述循环会不断的把该窗口(hWnd)相关的消息取出来,并分发到消息处理函数当中。
GetMessage函数是用来获取当前线程消息队列当中的消息的,其中的第二个参数如果传递一个窗口句柄,那么就会获取该窗口相关的消息,如果传NULL,那么会将线程消息队列中所有的消息都取出来。
如果创建了多个窗口,而只对其中一个窗口句柄调用GetMessage形成消息循环,那么别的窗口都会毫无响应。
这里需要补充说明一个概念:
消息队列是操作系统为每个需要处理消息的线程创建的,任何线程只要调用过与消息有关的函数(如GetMessage,PeekMessage),操作系统就会为该线程创建消息队列。 可能有人会问,那没有窗口的线程,操作系统也有必要为其创建消息队列么?这是有可能的,因为我们可以通过诸如PostThreadMessage的Api向别的线程或者本线程发送消息,如果目标线程没有消息队列,会导致这个函数返回失败。
做一个验证上述想法实验:
我们撰写类似于如下代码(所有的示例代码我都已经在Windows7系统上编写程序做过实验,这里忽略与要说明内容无关的细节并不影响对原理的理解,就不再将完整代码附上,读者感兴趣的话可以自行实验)
HWND hWnd1 = CreateWindow(...);
HWND hWnd2 = CreateWindow(...);
while (GetMessage(&msg, hWnd1, 0, 0) > 0)
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
上 述程序创建了两个窗口,但是消息循环只传递了其中一个窗口的句柄,那么我们看到的两个窗口中,hWnd2所属的窗口会毫无响应——无法移动也无法关闭,就 好象死了一样,另一个hWnd1则可以正常拖动,关闭(这是由于我们将hWnd1的消息取出来并分派处理了)。
如果我们将消息循环稍作改动,GetMessage的参数不再传递某一个窗口的句柄,而是传递NULL:
while (GetMessage(&msg, NULL, 0, 0) > 0)
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
则两个窗口都可以正常响应消息了。
更进一步,我们是否可以GetMessage,而不调用DispatchMessage将其分发到窗口处理函数,而是直接把Get出来的Message自行处理呢?答案是可行,但需要区分使用的场合:
while (GetMessage(&msg, NULL, 0, 0) > 0)
{
TranslateMessage(&msg);
WNDPROC fWndProc = (WNDPROC)GetWindowLong(msg.hwnd, GWL_WNDPROC);
fWndProc(msg.hwnd, msg.message, msg.wParam, msg.lParam);
}
上 述代码(源自winprog.org)是可以工作的。但是对于WM_TIMER类型的消息,需要回调到Timer的回调函数中,就需要另作处理了。当你创 建的窗口很多的时候,或者有Timer类型消息的时候,这种做法就可能带来麻烦,但是通过尝试,可以弄明白实际上是
DispatchMessage替我们回调了窗口处理函数 ,并没有什么神秘之处,我们也完全可以绕开DispatchMessage。
另外在某些特殊场合,这种写法则有特别的用处:比如想在一个已存在的控件(例如系统的TreeCtrl)中添加框选一系列Item的功能,则可以在收到控件中鼠标点下消息的时候,
开启一个局部的GetMessage循环,在自己的局部消息循环中等候鼠标抬起的消息,并在鼠标抬起消息中检查选框的范围,并选中相应元素。
模态对话框与非模态对话框的本质区别
模态对话框与非模态对话框的本质区别在哪?为什么前者会将界面控制权完全接管,而后者则可以让用户继续操作其他窗口?
在MFC中调用CDialog::DoModal() 或者在.Net中调用继承自CommonDialog的ShowDialog() 都会在该函数当中创建一个
局部的消息循环 ,从而
将上一级消息循环阻断。 直到该函数返回,我们才得以继续处理上一级消息。这便是模态对话框。
而调用CWnd::Create创建出来的对话框实际上则共用了同一级的消息循环,成功创建窗口之后GetMessage(&msg, NULL, 0, 0)也会得到针对新窗口的消息,从而让新创建的窗口与原来的窗口保持并行。
对比两者,我们可以发现模态与非模态对话框的本质区别实际上就在于是否存在一个局部的消息循环,以及该消息循环是否阻断了上一级消息循环的运行。
线程与消息
网 上经常可以搜到UI线程(User Interface Thread)和工作线程(Working Thread)的说法,认为UI线程就是创建了窗体的线程,从而操作系统会提供消息队列,而工作线程则无消息队列——我认为这种说法有欠妥当,为了查找 UI线程这种说法的起源,我在MSDN当中搜索了User Interface Thread,发现这个UI线程的概念是MFC给出来的,默认情况下派生自CWinApp的类都会默认工作在User Interface Thread中,我并没有发现Windows自身提供UI线程这个概念的证据,而且也没有什么理由让我相信操作系统会针对线程是否创建了窗体而对当前线程 提供什么特别对待。我倾向于认为是这个来自于MFC的概念被多数人误解了。
操作系统可能为任何线程创建消息队列,只要该线程调用了消息获取函数,甚至都不需要该线程创建任何窗口。
为了验证上述想法,我们可以做下面这样一个实验(代码出自使用PostThreadMessage在Win32线程之间传递消息一文):
其 中ThreadProc是程序运行期间创建的线程,该线程默认是没有消息队列的,因此如果主线程直接以它的线程id调用 PostThreadMessage,会返回FALSE,并在GetLastError中得到1444号错误——MSDN中的解释是 ERROR_INVALID_THREAD_ID 1444 Invalid thread identifier。而经过调用了PeekMessage之后,ThreadProc运行的Thread已经拥有了消息队列了,之后主线程或其他线程再 调用PostThreadMessage就可以正常运行了。(这里的HANDLE hStartEvent是用来保证上述顺序的同步事件。)
此外,该程序是Console版的程序,从头至尾没有创建过window,与MFC更无瓜葛,可以证实我们的想法——
任何线程都可以有消息队列,Windows并没有提供什么特殊的UI线程之一说。
#include "stdafx.h"
#include
#include 4: #include
#define MSG_SEND_OVERTHREAD WM_USER + 100
HANDLE hStartEvent; // thread start event
unsigned _stdcall ThreadProc(PVOID param)
{
MSG msg;
PeekMessage(&msg, NULL, WM_USER, WM_USER, PM_NOREMOVE);
if (!SetEvent(hStartEvent)) //set thread start event
return 1;
while(true){
if (GetMessage(&msg,0,0,0)) //get msg from message queue
{
switch(msg.message)
{
case MSG_SEND_OVERTHREAD:
char * pInfo = (char *)msg.wParam;
printf("%s/n", pInfo);
delete pInfo;
break;
}
}
}
return 1;
}
int main()
{
HANDLE hThread;
unsigned nThreadID;
char szBuf[1024];
//create thread start event
hStartEvent = ::CreateEvent(0,FALSE,FALSE,0);
if(hStartEvent == 0){
printf("create start event failed,errno:%d/n",GetLastError());
return 1;
}
//start thread
hThread = (HANDLE)_beginthreadex( NULL, 0 , &ThreadProc, NULL, 0, &nThreadID );
if(hThread == 0) {
printf("start thread failed,errno:%d/n",GetLastError());
CloseHandle(hStartEvent);
return 1;
}
//wait thread start event to avoid PostThreadMessage return errno:1444
::WaitForSingleObject(hStartEvent,INFINITE);
CloseHandle(hStartEvent);
int count = 0;
while(true){
char* pInfo = new char[100];
//create dynamic msg
sprintf(pInfo,"msg_%d",++count);
//post thread msg
if( !PostThreadMessage(nThreadID , MSG_SEND_OVERTHREAD, (WPARAM)pInfo, 0)){
printf("post message failed,errno:%d/n",GetLastError());
delete[] pInfo;
}
::Sleep(1000);
}
CloseHandle(hThread);
return 1;
}
小结
对上述的诸多概念做一番小节如下:线程可能拥有消息队列,也可能没有,如果线程调用过消息获取函数,操作系统会为线程准备一个消息队列,之后,其他线程便可以向此线程发送消息。
线程中可以有消息循环,消息循环将线程中的消息取出来并且进行处理——可以自行根据消息的类型进行处理,也可以交给DispatchMessage处理, 该API会回调窗口类中的窗口处理函数(依据该窗口所属窗口类别WNDCLASS的不同分别回调不同的消息处理函数)。如果线程创建了窗口,那么窗口的各种响应事件全部是由消息循环以及相关处理完成的,一个消息循环可以处理很多个窗口的消息。
消息循环可以有多个,可以在上一级消息循环的某个消息的处理过程中,局部创建一个消息循环,模态对话框就是采用这种机制创建出来的。