在Windows操作系统中,窗口是一种User Object ,隶属于创建它的线程。如果创建窗口的线程结束,则操作系统会自动删除窗口。建立窗口的线程,必须是为窗口处理所有消息的线程,如果你创建了一个后台线程,希望更新界面,那么只能通过消息的形式去通知窗口,并由拥有窗口的线程在窗口的消息处理函数中做出处理。

1、Windows的消息队列与消息循环

所有创建了窗口的Windows程序,都需要运行一个消息循环,我们在无数的Windows编程书籍中都可以看到这样的经典代码:

1:  
while 
(GetMessage
(&msg
, hWnd
, 0, 0) > 0)
2:  
{
3:  
    TranslateMessage
(&msg
);
4:  
    DispatchMessage
(&msg
);
5:  
}

这里的hWnd就是创建的窗口句柄,上述循环会不断的把该窗口(hWnd)相关的消息取出来,并分发到消息处理函数当中。

GetMessage函数是用来获取当前线程消息队列 当中的消息的,其中的第二个参数如果传递一个窗口句柄,那么就会获取该窗口相关的消息,如果传NULL,那么会将线程消息队列中所有的消息都取出来。如果创建了多个窗口,而只对其中一个窗口句柄调用GetMessage形成消息循环,那么别的窗口都会毫无响应。

这里需要补充说明一个概念:消息队列是操作系统为每个需要处理消息的线程创建的 ,任何线程只要调用过与消息有关 的函数(如,GetMessage,PeekMessage),操作系统就会为该线程创建消息队列。可能有人会问,那没有窗口的线程,操作系统也有必要为 其创建消息队列么?这是有可能的,因为我们可以通过诸如PostThreadMessage的Api向别的线程或者本线程发送消息,如果目标线程没有消息 队列,会导致这个函数返回失败。

做一个验证上述想法实验:

我们撰写类似于如下代码(所有的示例代码我都已经在Windows7系统上编写程序做过实验,这里忽略与要说明内容无关的细节并不影响对原理的理解,就不再将完整代码附上,读者感兴趣的话可以自行实验)

1:  
HWND hWnd1 
= CreateWindow
(...);
2:  
HWND hWnd2 
= CreateWindow
(...);
3:  
while 
(GetMessage
(&msg
, hWnd1
, 0, 0) > 0)
4:  
{
5:  
    TranslateMessage
(&msg
);
6:  
    DispatchMessage
(&msg
);
7:  
}

上述程序创建了两个窗口,但是消息循环只传递了其中一个窗口的句柄,那么我们看到的两个窗口中,hWnd2所属的窗口会毫无响应——无法移动也无法关闭,就好象死了一样,另一个hWnd1则可以正常拖动,关闭(这是由于我们将hWnd1的消息取出来并分派处理了)。

如果我们将消息循环稍作改动,GetMessage的参数不再传递某一个窗口的句柄,而是传递NULL:

1:  
while 
(GetMessage
(&msg
, NULL
, 0, 0) > 0)
2:  
{
3:  
    TranslateMessage
(&msg
);
4:  
    DispatchMessage
(&msg
);
5:  
}

则两个窗口都可以正常响应消息了。

更进一步,我们是否可以GetMessage,而不调用DispatchMessage将其分发到窗口处理函数,而是直接把Get出来的Message自行处理呢?答案是可行,但需要区分使用的场合:

1:  
while 
(GetMessage
(&msg
, NULL
, 0, 0) > 0)
2:  
{
3:  
    TranslateMessage
(&msg
);
4:  
    WNDPROC fWndProc 
= (WNDPROC
)GetWindowLong
(msg
.hwnd
, GWL_WNDPROC
);
5:  
    fWndProc
(msg
.hwnd
, msg
.message
, msg
.wParam
, msg
.lParam
);
6:  
}

上述代码(源自winprog.org ) 是可以工作的。但是对于WM_TIMER类型的消息,需要回调到Timer的回调函数中,就需要另作处理了。当你创建的窗口很多的时候,或者有Timer 类型消息的时候,这种做法就可能带来麻烦,但是通过尝试,可以弄明白实际上是DispatchMessage替我们回调了窗口处理函数,并没有什么神秘之 处,我们也完全可以绕开DispatchMessage。

另外在某些特殊场合,这种写法则有特别的用处:比如想在一个已存在的控件(例如系统的TreeCtrl)中添加框选一系列Item的功能,则可以在 收到控件中鼠标点下消息的时候,开启一个局部的GetMessage循环,在自己的局部消息循环中等候鼠标抬起的消息,并在鼠标抬起消息中检查选框的范 围,并选中相应元素。

延伸:模态对话框与非模态对话框的本质区别在哪?为什么前者会将界面控制权完全接管,而后者则可以让用户继续操作其他窗口?

在MFC中调用CDialog::DoModal() 或者 在.Net中调用继承自CommonDialog的ShowDialog() 都会在该函数当中创建一个局部的消息循环,从而将上一级消息循环阻断。直到该函数返回,我们才得以继续处理上一级消息。这便是模态对话框。

而调用CWnd::Create创建出来的对话框实际上则共用了同一级的消息循环,成功创建窗口之后GetMessage(&msg, NULL, 0, 0)也会得到针对新窗口的消息,从而让新创建的窗口与原来的窗口保持并行。

对比两者,我们可以发现模态与非模态对话框的本质区别实际上就在于是否存在一个局部的消息循环 ,以及该消息循环是否阻断了上一级消息循环的运行。

2、线程与消息

网上经常可以搜到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线程之一说。

 1:  
#include 
"stdafx.h"
 2:  
 
#include 
<Windows.h>
 3:  
#include 
<process.h>
 4:  
#include 
<stdio.h>
 5:  
 6:  
#define 
MSG_SEND_OVERTHREAD WM_USER 
+ 100
 7:  
HANDLE hStartEvent
; // thread start event 
 8:  
 9:  
unsigned _stdcall 
ThreadProc
(PVOID param
)
10:  
{
11:  
    MSG msg
;
12:  
    PeekMessage
(&msg
, NULL
, WM_USER
, WM_USER
, PM_NOREMOVE
);
13:  
    if 
(!SetEvent
(hStartEvent
)) //set thread start event 
14:  
        
return 
1;
15:  
16:  
    while
(true
)
17:  
    {
18:  
        if 
(GetMessage
(&msg
,0,0,0)) //get msg from message queue
19:  
        
{
20:  
            switch
(msg
.message
)
21:  
            {
22:  
            case 
MSG_SEND_OVERTHREAD
:
23:  
                char 
* pInfo 
= (char 
*)msg
.wParam
;
24:  
                printf
("%s/n"
, pInfo
);
25:  
                delete 
pInfo
;
26:  
                break
;
27:  
            }
28:  
        }
29:  
    }
30:  
    return 
1;
31:  
}
32:  
33:  
int 
main
()
34:  
{
35:  
    HANDLE hThread
;
36:  
    unsigned 
nThreadID
;
37:  
    char 
szBuf
[1024];
38:  
    //create thread start event
39:  
    
hStartEvent 
= ::CreateEvent
(0,FALSE
,FALSE
,0);
40:  
    if
(hStartEvent 
== 0)
41:  
    {
42:  
        printf
("create start event failed,errno:%d/n"
,GetLastError
());
43:  
        return 
1;
44:  
    }
45:  
    //start thread
46:  
    
hThread 
= (HANDLE
)_beginthreadex
( NULL
47:  
        
, 0
48:  
        , &ThreadProc
49:  
        
, NULL
50:  
        
, 0
51:  
        , &nThreadID 
);
52:  
53:  
    if
(hThread 
== 0)
54:  
    {
55:  
        printf
("start thread failed,errno:%d/n"
,GetLastError
());
56:  
        CloseHandle
(hStartEvent
);
57:  
        return 
1;
58:  
    }
59:  
60:  
    //wait thread start event to avoid PostThreadMessage return errno:1444
61:  
    
::WaitForSingleObject
(hStartEvent
,INFINITE
);
62:  
    CloseHandle
(hStartEvent
);
63:  
    int 
count 
= 0;
64:  
    while
(true
)
65:  
    {
66:  
        char
* pInfo 
= new char
[100];
67:  
        //create dynamic msg
68:  
        
sprintf
(pInfo
,"msg_%d"
,++count
);
69:  
        //post thread msg
70:  
        
if
( !PostThreadMessage
(nThreadID
71:  
            
, MSG_SEND_OVERTHREAD
72:  
            
, (WPARAM
)pInfo
73:  
            
, 0))
74:  
        {
75:  
            printf
("post message failed,errno:%d/n"
,GetLastError
());
76:  
            delete
[] pInfo
;
77:  
        }
78:  
        ::Sleep
(1000);
79:  
    }
80:  
    CloseHandle
(hThread
);
81:  
    return 
1;
82:  
}

 

3、对上述的诸多概念做一番小节如下:线程第一次被建立时,系统假定线程不会被用于任何与用户相关的任务,这样可以减少线程对系统资源的要求。但是一旦线程调用了一个与图形用户界面相关的函数,例如检查消息队列或窗口创建,操作系统就会为线程创建一些额外的资源,以便它能执行和用户界面有关的任务 。 特别的,系统会为该线程分配一个THREADINFO结构(参考下图,来源于《Windows核心编程》第四版 26-1),并将该数据结构与线程联系起来,此后该线程就拥有了自己的消息队列集合(意味着我们可以向该线程发送消息PostThreadMessage 等)。

THREADINFO

向一个线程传递消息可以使用PostThreadMessage ,这个函数的接口很明确,要求传递一个线程Id,但是还有PostMessageSendMessage 这样的API,要求传递的是窗口句柄,这可能就是我们此前会迷惑于消息队列是跟窗口相关还是跟线程相关的原因。事实上,在调用PostMessage或SendMessage的时候,系统要根据传入的窗口句柄,确认是哪一个线程创建了该窗口(我们也可以通过GetWindowsThreadProcessId 自行获取是哪个线程创建了一个窗口),然后系统分配一块内存,将消息参数存储在这块内存中,并将这块内存增加到相应线程的登记消息队列中。

SendMessage和PostMessage有一个值得注意的区别:

PostMessage是简单的将消息登记到目标线程(创建窗口的线程)的消息队列中。

而SendMessage是一个阻塞调用,调用者必须要等待消息处理后,函数才会返回。(注意,这并不意味着调用SendMessage的线程就是处理消息线程 ,前文提到过,处理窗口消息的线程必须是创建窗口的线程,如果一个线程调用SendMessage向不是由它创建的窗口发送消息,则会挂起调用线程,直到另外的线程处理完消息,才会唤醒该线程。)

线程中可以有消息循环,消息循环将线程中的消息取出来并且进行处理——可以自行根据消息的类型进行处理,也可以交给DispatchMessage 处理,该API会回调窗口类中的窗口处理函数(依据该窗口所属窗口类别WNDCLASS的不同分别回调不同的消息处理函数)。如果线程创建了窗口,那么窗 口的各种响应事件全部是由消息循环以及相关处理完成的,一个消息循环可以处理很多个窗口的消息。

消息循环可以有多个,可以在上一级消息循环的某个消息的处理过程中,局部创建一个消息循环,模态对话框就是采用这种机制创建出来的。

#1 确认了消息队列是隶属于线程而非窗口,补充了部分说明,本文参考了《Windows核心编程》 第四版 第26章 窗口消息的部分内容,感谢redcoder 同学热心指出