消息机制、子窗口和父窗口的消息传递
1. 什么是窗口
MSDN: In a graphical Win32-based application, a window is a rectangular area of the screen where the application displays output and receives input from the user. Therefore, one of the first tasks of a graphical Win32-based application is to create a window.
大意:窗口就是一个矩形区域,应用程序可以用它来显示输出,或者从user来获得输入(键盘鼠标)。在windows中,“一切皆为窗口”。虽然不是很贴切,但是说明了窗口的普遍性和其重要性。比如,你现在看到的QQ聊天窗口,就是由多个窗口组成的!
Windows程序是由一系列的窗口构成的,每个窗口都有自己的窗口过程。
2. 什么是消息
MSDN: A data packet used for communicating information or a request. Messages can be passed between the operating system and an application, different applications, threads within an application, and windows within an application.
大意:消息就是一组数据包(结构体),用于传递信息。消息可以在操作系统和一个进程之间传递,也可以在两个不同的进程间传递,也可以在同一个进程的不同线程间传递或同一个进程的不同窗口间传递。 比如,你在QQ聊天窗口中点一下鼠标,打字等,都会产生消息。在代码中消息是以消息结构体MSG来保存的,里面的成员包括处理该消息的窗口句柄,消息代码,消息产生的时间和光标的位置等。
消息分类:
<1>.队列消息和非队列消息:从消息的发送途径上看,消息分两种:队列消息和非队列消息。 队列消息送到系统消息队列,然后到线程消息队列;非队列消息直接送给目的窗口过程。这里,对消息队列阐述如下:
Windows维护一个系统消息队列(System message queue),每个GUI线程有一个线程消息队列(Thread message queue)。鼠标、键盘事件由鼠标或键盘驱动程序转换成输入消息并把消息放进系统消息队列,例如WM_MOUSEMOVE、WM_LBUTTONUP、WM_KEYDOWN、WM_CHAR等等。Windows每次从系统消息队列移走一个消息,确定它是送给哪个窗口的和这个窗口是由哪个线程创建的,然后,把它放进窗口创建线程的线程消息队列。线程消息队列接收送给该线程所创建窗口的消息。线程从消息队列取出消息,通过Windows把它送给适当的窗口过程来处理。
除了键盘、鼠标消息以外,队列消息还有WM_PAINT、WM_TIMER和WM_QUIT。这些队列消息以外的绝大多数消息是非队列消息。
<2>.系统消息和应用程序消息: 从消息的来源来看,可以分为:系统定义的消息和应用程序定义的消息。(控件改变时自己也会向系统发送消息,如invalidate)
系统消息ID的范围是从0到WM_USER-1,或0X80000到0XBFFFF;应用程序消息从WM_USER(0X0400)到0X7FFF,或0XC000到0XFFFF;WM_USER到0X7FFF范围的消息由应用程序自己使用;0XC000到0XFFFF范围的消息用来和其他应用程序通信,为了ID的唯一性,使用::RegisterWindowMessage来得到该范围的消息ID。
<3>.窗口消息,命令消息,控件通知消息:根据处理过程的不同,可以分为三类:窗口消息,命令消息,控件通知消息。
(1).窗口消息
一般以WM_开头,如WM_CREATE, WM_SIZE, WM_MOUSEMOVE等标准的Windows消息, 用于窗口相关的事件通知,窗口消息将由系统分配到该窗口的窗口过程处理。
(2).命令消息 (WM_COMMAND)
一种特殊的窗口消息,它从一个窗口发送到另一个窗口以处理来自用户的请求,通常是从子窗口发送到父窗口,例如,点击按钮时,按钮的父窗口会收到WM_COMMAND消息,用以通知父窗口按钮被点击,经测试:子窗口向父窗口发送WM_COMMAND消息,或者称为父窗口会收到WM_COMMAND消息,操作系统并不是通过将WM_COMMAND消息放入到父窗口的消息队列中去,而是直接调用了父窗口的窗口过程,以 WM_COMMAND 为消息标识参数(UINT uMsg),实现这个功能的API函数正是: LRESULT DispatchMessage(const MSG *lpmsg);
(3).控件通知消息
WM_NOTIFY消息,当用户与控件交互(Edit, Button...)时,通知消息会从控件窗口发送到父窗口,这种消息的目的不是为了处理用户命令,而是为了让父窗 口能够适时的改变控件。
3. 什么是窗口过程函数
我们鼠标、键盘消息是在窗口上产生的,windows会把消息传给该窗口所属线程的消息队列中。然后这个消息在某个时间被这个窗口对应的处理函数来“搞定”。
这个和窗口绑定的“处理消息的函数”就是“窗口过程函数”。
一般的,一个窗口类绑定一个窗口过程函数。
4. 怎么创建窗口
首先,对于windows系统来讲,不管你是C#还是C++还是JAVA,要创建一个标准的窗口,都是调用相同的API的~
只是在VC、C# .NET或用JAVA开发windows时,开发工具都帮你把创建窗口“通用的部分”封装了起来,你是看不到的。
在我们C#的代码中,在Main函数里已经给我们创建好了第一个窗口Fomr1。为我们免去了N多初始化的麻烦,但是我们也就不知道窗口是怎么被创造出来的了。
因此,这里的示例代码使用的是最原始,最明显的方法来创建一个窗口,显示出来并处理你发出的消息。我们用C语言来调用API创建窗口。
创建窗口的4个步骤:
1) 初始化窗口类(实际上是一个结构体)
WNDCLASS wndclass;
2) 注册这个类
RegisterClass(……);
3) 创建窗口
hwnd = CreateWindow(……);
4) 显示
ShowWindow(hwnd, iCmdShow);
精简后的创建窗口程序主干:
WinMain(……)
{
……
HWND hwnd;//用来存放窗口句柄
MSG msg;//用来存放消息的结构体
WNDCLASS wndclass;// 1)窗口结构体
RegisterClass(……);// 2)注册窗口类
hwnd = CreateWindow(……);// 3)创建窗口
ShowWindow(hwnd, iCmdShow);// 4)显示
UpdateWindow(hwnd);
//下面是消息循环
while(GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return msg.wParam;
}
说明:
1) WNDCLASS wndclass;
这个结构体里存放了窗口的各种初始数据。如窗体类的风格,窗体上显示的图标,鼠标的图标等。
2) RegisterClass(……);
调用该函数通知系统被注册类的窗口的消息,windows为后面创建窗口准备一系列资源。
3) hwnd = CreateWindow(……);
调用该函数创建较迭的、弹出的或子窗口。它指定窗口类、窗口标题、窗口风格和窗口的初始大小与位置等。
创建出来的窗口以属于“活动”状态,已经开始进行消息处理。只是没有被“显示”出来。
4) ShowWindow(hwnd, iCmdShow);
该函数用来设置指定窗口的“显示状态”,如大小、位置、是否隐藏等。
5. 消息是怎么传递并处理的
消息(Message)在窗口中产生后,系统把它放入到该窗口所属线程的消息队列中,等待处理。
线程将队列中的消息取得后,在翻译后,将它“投放”到相应的窗口过程函数中进行处理。
消息处理机制的核心代码:
while(GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
根据MSDN,上面几个函数的作用:
1.GetMessage只获取自己所属线程的消息队列中的消息,包括windows消息和通过PostThreadMessage发送的线程消息。
2.TranslateMessage是为了将虚拟键消息“翻译”成字符消息,然后把字符消息放回到线程的消息队列中,在下一次被GetMessage取得。(具体内容见MSDN)
3.DispatchMessage的作用是将从GetMessage获得的消息分发给相应的窗口处理函数(WndProc),然后窗口过程函数对消息进行处理。
上面代码中使用while循环的意义是:循环是应用程序能够持续存在的根本原因。如果循环退出,则应用程序就结束了。
消息是根据窗口进行分发的,而不考虑该窗口的从属关系。也就是说在子窗口中产生的消息只在子窗口函数中处理,处理完后不会再把消息传递给父窗口(除非你自己做反射)。
对窗口、线程、消息的关系总结:
1.每一个窗口都是属于某一个线程的,注意是线程
2.每一个窗口都“绑定”了一个“窗口函数”!也就是在每个窗口上产生的消息,会发往对应的函数来处理!
3.每一个线程都有一个属于自己的“消息队列”
图示: