对于很多人来说,VC开发Windows界面程序,一般都基于MFC界面库,当前MFC并非windows下唯一的VC界面库,但由于历史原因,其使用者绝对是最多的。我们不对MFC库进行研究,仅仅是研究下windows中线程、界面、消息直接的关系。
线程是什么?线程就是一个执行流,它可以被CPU调度执行。被CPU调度执行,更准确应该说是被操作系统调度,因为计算机硬件系统只是触发一个中断,而具体中断后如何处理,是操作系统底层的处理,而时间片的中断一般触发了操作系统进程切换,所以应该说是被操作系统调度。
那通过上边对进程调度的简单描述,可以发现,进程、线程这些被操作系统调度的执行流,本质上也是数据,包含有代码数据、数据数据、寄存器数据。如果我们完全清楚Windows的存储机制,内存数据含义,理论上来说,我们可以直接修改内存而让系统产生一个进程,只是实际工作来说太复杂。
当然,上边的叙述中有些模糊的地方,例如多次一起提到进程、线程,但本质上来说,进程进程只是一组线程的一个载体,更像一个集合的含义,感觉上有点类似家与人的关系,一个家至少有一个人,每个人是独立的个体,并且大家有联系,属于一个家庭,可以使用家里的所有资源。
这样来看,进程中实际的执行体是线程,以前说进程堆栈,本质上指线程堆栈,每个线程有自己独立的堆栈。以上内容比较容易理解,操作系统类似课程讲的也比较多了。
那下一个概念,在windows中什么是消息?
我认为这一部分内容很生硬,最主要的原因是很多东西只能看文档说明,而不好用实践来证明,不过我们尽量努力把它弄清楚。
了解这里的前提,先建立一个win32项目,也就是windows应用程序项目,千万别建MFC项目。建立完成后,可以看到里边最为核心的一段代码,
// 主消息循环:
while (GetMessage(&msg, NULL, 0, 0))
{
if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
它用于从消息队列中获取消息,然后处理消息。
这里就涉及三个问题:
1、 消息队列在哪里?
2、 消息从哪里来?
3、 消息对应的事件如何被触发?
这里,最简单的是第三个问题,因为这个问题相当一部分代码是我们自己来完成的,前两个问题都是与系统交互完成的,所以不好分析,那我们先从简单开始。
消息对应的事件如何被触发执行?
一般来说,一个win32程序的核心就是消息处理函数,一般默认都建立了以下这样一个函数。
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
它是一个需要注册的消息处理函数,从代码上来看,它是隶属与某个或某几个线程的,从参数可以看出,该消息处理函数与窗口无关,多个窗口的消息可以被一个消息处理函数处理。
该消息处理内部的处理相对很简单,仅仅是分析消息类型,获取消息参数,然后对对应的窗口执行响应的操作。
比较重要的是,该消息函数如何才能被调用?被谁调用?
我们在WndProc函数入口处加一个断点,如果该函数被调用,那就可以说明消息发送开始工作了,第一次运行程序的时候,会发现该断点停下时的调用堆栈如下:
del_.exe!WndProc(HWND__ * hWnd=0x001b1818, unsigned int message=0x00000024, unsigned int wParam=0x00000000, long lParam=0x0012f734) 行143 C++
user32.dll!77d18734()
[下面的框架可能不正确和/或缺失,没有为 user32.dll 加载符号]
user32.dll!77d18816()
user32.dll!77d28ea0()
user32.dll!77d2d08a()
ntdll.dll!7c92e473()
user32.dll!77d2e389()
user32.dll!77d2e34f()
user32.dll!77d2e442()
user32.dll!77d2d0d6()
del_.exe!InitInstance(HINSTANCE__ * hInstance=0x00400000, int nCmdShow=0x00000001) 行111 + 0x31 字节
这说明该函数第一次被调用时在InitInstance中,该函数创建窗口语句触发该调用。
hWnd = CreateWindow(/*szWindowClass*/_T("xxxx"), szTitle, WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInstance, NULL);
也就是说创建窗体的过程中,系统默认会调用消息处理函数,相当于发送多个处理消息。但以上只是推测,如果成立,那说明就存在一种机制可以跳过消息循环,而直接调用消息处理函数。从文档上来看,windows提供了两个消息发送的函数,SendMessage和PostMessage,那我们来测试这两个函数,如果发送消息写到创建窗体后,当该语句被执行时,而消息处理函数被调用,那么说明该消息发送发生根本就不进入消息队列,而直接调用。
经过测试,SendMessage会直接调用消息处理函数,而PostMessage会等待消息队列循环跑起来才会发送消息,是一种延后的调用。
那现在的问题就是SendMessage是如何工作的?我猜想PostMessage的工作仅仅是将一个消息放入到某个消息队列中,而消息队列循环会提取消息,然后执行消息处理函数的调用,并且消息处理函数的调用与sendMessage消息处理函数的调用底层是相同的,仅仅我们看到的调用函数不一样而已。
为了证明我对sendmessage与DispatchMessage实现是一样的,我将创建窗体后的消息发送方式SendMessage更改为DispatchMessage,结果与我预想的一致,都可以直接进行调用,但这样可以说明底层实现基本一致吗?好像有点问题,但我们看了另外一个证据,这个结论应该就比较肯定了。
首先看DispatchMessage的调用堆栈情况
WndProc(HWND__ * hWnd=0x00161896, unsigned int message=0x00000403, unsigned int wParam=0x00000000, long lParam=0x00000000) 行150 C++
user32.dll!77d18734()
[下面的框架可能不正确和/或缺失,没有为 user32.dll 加载符号]
user32.dll!77d18816()
user32.dll!77d189cd()
user32.dll!77d18a10()
del_.exe!InitInstance(
然后再看看sendmessage的调用堆栈情况
WndProc(HWND__ * hWnd=0x00161896, unsigned int message=0x00000401, unsigned int wParam=0x00000000, long lParam=0x00000000) 行150 C++
user32.dll!77d18734()
[下面的框架可能不正确和/或缺失,没有为 user32.dll 加载符号]
user32.dll!77d18816()
user32.dll!77d2927b()
user32.dll!77d292e3()
del_.exe!InitInstance(
我们发现了什么,堆栈顶上的三个函数指针是一样的,这就说明这两种消息发送方式最后三层函数调用是一样的,从整体上来看,从消息发送调用到消息处理函数被调用整个函数调用堆栈,只有两个函数指针是不一样的,因为消息发送函数本身不一样,所以必然至少一个函数指针不一样,也就是说,sendmessage与DispatchMessage函数体内实现不一样,但其调用的子函数,都调用了共同的消息处理相关的函数。
消息的几种发送方式做了了解后,我们就会想,那到底sendmessage背后做了什么小动作,使得消息处理函数可以被直接调用?如果存储了一个窗口类型到消息处理函数的映射关系,那么做到直接调用也就没啥奇特的了,从代码来看,我们也确实注册过一个东西,并且标记处理窗体类型与消息处理函数指针,如果我们将注册的窗体类型与创建窗体时的窗体类型设置不一样,会发现消息发送失灵了。
那接下来我们新的问题是,这个映射关系存储在那里?是属于线程?还是属于进程?我觉得这块数据是属于进程的,是公共空间中的数据,如何证明呢?如果是线程私有,那么A线程注册一个消息处理函数,B线程理论上可以针对同一个窗口类型再注册一个消息处理函数,但测试是失败的,也就是说这块数据是进程全局的,并且一个窗口类型只可以注册一个消息处理函数。
那现在这样是不是就可以说A线程中注册消息处理函数,并创建窗体,B函数可以直接通过SendMessage来调用A线程中注册的消息处理函数?答案是否定的。我做了以下几个测试,
1、 A线程注册消息处理函数,A线程创建窗体,B线程发送消息;
2、 A线程注册消息处理函数,B线程创建窗体,A线程发送消息;
3、 A线程注册消息处理函数,B线程创建窗体,B线程发送消息;
以上测试没有A线程注册处理函数,然后建窗体,然后发消息,因为这个一定可以,平时都这么干的。针对以上三个测试创建窗体都成功,1、2发现哦那个消息都失败,3成功,这从另一个角度支持了注册消息处理函数是进程全局的,但为何1、2发送消息会失败?我想原因是窗体所属是线程私有数据,例如测试1中,A创建了窗体,那么A就拥有该窗体数据管理权,而B线程中获取不到该窗体类型信息,所以也就无法知道该窗体的消息处理函数地址了。但不能通过窗体句柄而获取窗体类型总是感觉哪里不对,难道真的获取不到吗?还是说B线程发送消息失败是其它原因?
为了验证上边想法的是否正确,我需要做一个测试,利用现有的API看看是否可以在另一个线程中向另一线程的窗体发送消息成功,当前PostMessage是没戏的,最终我的代码如下:
TCHAR className[300];
GetClassName(hWndX,className,300);
WNDCLASSEXW exw;
if(GetClassInfoEx(hInstXX,className,&exw))
{
exw.lpfnWndProc(hWndX,WM_USER+5,0,0);
}
首先通过窗体获取窗体类型名称,然后再通过类型名称获取注册的窗体类型信息,在窗体类型信息中有一个注册的消息处理指针的字段,我直接调用该函数,结果很好,确实调用成功,如果将我写的这部分代码封装到一个函数,例如SendMessageBB,那应该从调用来看看就像是消息发送成功了吧?
这样的结论是什么?从一个线程发送消息到另一个线程在技术上应该是没有问题的,而windows系统不支持这样来发送,必然有某些原因还不知道,这个我就不知道该如何分析了。
理解消息循环?
Windows程序基于消息机制,这个毫无疑问,但消息循环并不一定只能在一个地方循环,可以在任何代码中加入消息循环处理,我们来看看如何利用这样的机制来实现模态与非模态对话框。
非模态对话框就是该对话框弹出的时候,可以继续操作其它对话框;
模态对话框弹出的时候,不能继续操作其它对话框;
很明显,只要消息循环同时处理多个对话框的消息时,就是模态对话框,如果在创建某对话框后,写一个消息循环处理,仅仅处理该窗体相关的消息,那么这个时候就是模态对话框。
还有一种情况,写消息循环比较有价值,当一个密集计算执行时,如何显示进度,获取启动线程,一个进行计算,一个进行界面显示,一个线程可以吗?可以,只要在密集计算中的适当位置加入消息循环处理代码,那么计算的时候,界面也可以做响应了。