在讲本程序的消息循环之前,我想先谈一下Dos与Windows驱动机制的区别:
DOS程序主要使用顺序的,过程驱动的程序设计方法。顺序的,过程驱动的程序有一个明显的开始,明显的过程及一个明显的结束,因此程序能直接控制程序事件或过程的顺序。虽然在顺序的过程驱动的程序中也有很多处理异常的方法,但这样的异常处理也仍然是顺序的,过程驱动的结构。
而Windows的驱动方式是事件驱动,就是不由事件的顺序来控制,而是由事件的发生来控制,所有的事件是无序的,所为一个程序员,在你编写程序时,你并不知道用户先按哪个按纽,也不知道程序先触发哪个消息。你的任务就是对正在开发的应用程序要发出或要接收的消息进行排序和管理。事件驱动程序设计是密切围绕消息的产生与处理而展开的,一条消息是关于发生的事件的消息。
C语言编程至少有一个主程序,其名字是main()。Windows程序则至少两个主程序,一个是WinMain(),
int WINAPI WinMain(
HINSTANCE hInstance, // handle to current instance
HINSTANCE hPrevInstance, // handle to previous instance
LPSTR lpCmdLine, // pointer to command line
int nCmdShow // show state of window
);
另一个是窗口过程函数WindowProc,它的函数原型为:
LRESULT CALLBACK WindowProc(
HWND hwnd, // handle to window
UINT uMsg, // message identifier
WPARAM wParam, // first message parameter
LPARAM lParam // second message parameter
);
Windows应用程序的编程就围绕这两个部份进行的。其中WinMain函数为应用程序的入口点,它的名字一定要是WinMain。
在Windows中,应用程序通过要求Windows完成指定操作,而承担这项通信任务的API函数就是Windows的相应窗口函数WindowProc。在dos里,程序能直接控制事件的发生顺序,结果等。而在Windows里,应用程序不直接调用任何窗口函数,而是等待Windows调用窗口函数,请求完成任务或返回信息。为保证Windows调用这个窗口函数,这个函数必须先向Windows登记,然后在Windows实施相应操作时回调,所以窗口函数又称为回调函数。WindowProc是一个主回调函数,Windows至少有一个回调函数。
回调函数WindowProc在哪里定义的呢,请看这个语句:wc.lpfnWndProc = WindowProc ;将在第七讲里详谈.
实例:在Windows中,能多次同时运行同一个应用程序,即运行多个副本,每个副本叫做一个“实例”。
现在让我们把这个程序层层剥解开来,我把自己的理解慢慢地展示给你:
我把这个程序支解为四块:(一)建立,注册窗口类.(二)创建窗口.(三)显示和更新窗口.(四)创建消息循环.(五)终止应用程序.(六)窗口过程.(七)处理消息.
(1)建立窗口类
WinMain()是程序的入口,它相当于一个中介人的角色,把应用程序(指小窗口)介绍给windows.首要的一步是登记应用程序的窗口类.
窗口种类是定义窗口属性的模板,这些属性包括窗口式样,鼠标形状,菜单等等,窗口种类也指定处理该类中所有窗口消息的窗口函数.只有先建立窗口种类,才能根据窗口种类来创建Windows应用程序的一个或多个窗口.创建窗口时,还可以指定窗口独有的附加特性.窗口种类简称窗口类,窗口类不能重名.在建立窗口类后,必须向Windows登记.
建立窗口类就是用WNDCLASS结构定义一个结构变量,在这个程序中就是指 WNDCLASS wc ;然后用自己设计的窗口属性的信息填充结构变量wc的域.
要WinMain登记窗口类,首先要填写一个WNDCLASS结构,其定义如下所示:
typedef struct _WNDCLASSA
{
UINT style ; //窗口类风格
WNDPROC lpfnWndProc ; //指向窗口过程函数的指针
int cbClsExtra ; //窗口类附加数据
int cbWndExtra ; //窗口附加数据
HINSTANCE hInstance ; //拥有窗口类的实例句柄
HICON hIcon ; //最小窗口图标
HCURSOR hCursor ; //窗口内使用的光标
HBRUSH hbrBackground ; //用来着色窗口背景的刷子
LPCSTR lpszMenuName ; //指向菜单资源名的指针
LPCSTR lpszClassName ; // 指向窗口类名的指针
}WNDCLASS;
// 加强版
typedef struct _WNDCLASSEX {
UINT cbSize;
UINT style;
WNDPROC lpfnWndProc;
int cbClsExtra;
int cbWndExtra;
HANDLE hInstance;
HICON hIcon;
HCURSOR hCursor;
HBRUSH hbrBackground;
LPCTSTR lpszMenuName;
LPCTSTR lpszClassName;
HICON hIconSm;
} WNDCLASSEX;
在VC6.0里面,把光标定位在WNDCLASS上,按F1,即可启动MSDN,在MSDN里你可看到这个结构原形.在下节讲解这些参数在本程序中的具体用法.
(2)注册窗口类
(1)第一个参数:成员style控制窗口的某些重要特性,在WINDOWS.H中定义了一些前缀为CS的常量,在程序中可组合使用这些常量.也可把sytle设为0.本程序中为wc.style = CS_HREDRAW | CS_VREDRAW
,它表示当窗口的纵横坐标发生变化时要重画整个窗口。你看:无论你怎样拉动窗口的大小,那行字都会停留在窗口的正中部,而假如把这个参数设为0的话,当改动窗口的大小时,那行字则不一定处于中部了。
(2)第二个参数:lpfnWndProc包括一个指向该窗口类的消息处理函数的指针,此函数称为窗口过程函数。它将接收Windows发送给窗口的消息,并执行相应的任务。其原型为:
long FAR PASCAL WndProc(HWND ,unsigned,WORD,LONG);并且必须在模快定义中回调它。WndProc是一个回调函数(见第五节),如果暂时无法理解这个模糊的概念意味着什么,可先放过,等到讲消息循环时再详谈。
(3)第三,四个参数:cbWndExtra域指定用本窗口类建立的所有窗口结构分配的额外字节数。当有两个以上的窗口属于同一窗口类时,如果想将不同的数据和每个窗口分别相对应。则使用该域很有用。这般来讲,你只要把它们设为0就行了,不必过多考虑。
(4)第五个参数:hInstance域标识应用程序的实例hInstance,当然,实例名是可以改变的。wc.hInstance = hInstance ;这一成员可使Windows连接到正确的程序。
(5)第六个参数:成员hIcon被设置成应用程序所使用图标的句柄,图标是将应用程序最小化时出现在任务栏里的的图标,用以表示程序仍驻留在内存中。Windows提供了一些默认图标,我们也可定义自己的图标,VC里面专有一个制作图标的工具。
(6)第七个参数: hCursor域定义该窗口产生的光标形状。LoadCursor可返回固有光标句柄或者应用程序定义的光标句柄。IDC_ARROW表示箭头光标.
(7)第八个参数:wc.hbrBackground域决定Windows用于着色窗口背景的刷子颜色,函数GetStockObject返回窗口的颜色,本程序中返回的是白色,你也可以把它改变为红色等其他颜色.试试看
(8)第九个参数:lpszMenuName用来指定菜单名,本程序中没有定义菜单,所以为NULL。
(9)第十个参数:lpszClassName指定了本窗口的类名。
当对WNDCLASS结构域一一赋值后,就可注册窗口类了,在创建窗口之前,是必须要注册窗口类的,注册窗口类用的API函数是RegisterClass,注册失败的话,就会出现一个对话框如程序所示,函数RegisterClass返回0值,也只能返回0值,因为注册不成功,程序已经不能再进行下去了。
在本程序中注册窗口类如下:
if (!RegisterClass (&wc))
{
MessageBox (NULL,
TEXT ("This program requires Windows NT!"),
szAppName,MB_IConERROR) ;
return 0 ;
}
注册窗口类后,就可以创建窗口了,本程序中创建窗口的有关语句如下:
HWND CreateWindow(
LPCTSTR lpClassName, // pointer to registered class name
LPCTSTR lpWindowName, // pointer to window name
DWORD dwStyle, // window style
int x, // horizontal position of window
int y, // vertical position of window
int nWidth, // window width
int nHeight, // window height
HWND hWndParent, // handle to parent or owner window
HMENU hMenu, // handle to menu or child-window identifier
HANDLE hInstance, // handle to application instance
LPVOID lpParam // pointer to window-creation data
);
参数1:登记的窗口类名,这个类名刚才咱们在注册窗口时已经定义过了。
参数2:用来表明窗口的标题。
参数3: 用来表明窗口的风格,如有无最大化,最小化按纽啊什么的。
参数4,5: 用来表明程序运行后窗口在屏幕中的坐标值。
参数6,7: 用来表明窗口初始化时(即程序初运行时)窗口的大小,即长度与宽度。
参数8: 在创建窗口时可以指定其父窗口,这里没有父窗口则参数值为0。
参数9: 用以指明窗口的菜单,菜单以后会讲,这里暂时为0。
最后一个参数是附加数据,一般都是0。
CreateWindow()的返回值是已经创建的窗口的句柄,应用程序使用这个句柄来引用该窗口。如果返回值为0,就应该终止该程序,因为可能某个地方出错了。如果一个程序创建了多个窗口,则每个窗口都有各自不同的句柄.
API函数CreateWindow创建完窗口后,要想把它显示出现,还必须调用另一个API函数ShowWindows.形式为:
ShowWindow (hwnd, iCmdShow);
其第一个参数是窗口句柄,告诉ShowWindow()显示哪一个窗口,而第二个参数则告诉它如何显示这个窗口:最小化(SW_MINIMIZE),普通(SW_SHOWNORMAL),还是最大化(SW_SHOWMAXIMIZED)。WinMain在创建完窗口后就调用ShowWindow函数,并把iCmdShow参数传送给这个窗口。你可把iCmdShow改变为这些参数试试。
WinMain()调用完ShowWindow后,还需要调用函数UpdateWindow,最终把窗口显示了出来。调用函数UpdateWindow将产生一个WM_PAINT消息,这个消息将使窗口重画,即使窗口得到更新.
主窗口显示出来了,WinMain就开始处理消息了,怎么做的呢?
Windows为每个正在运行的应用程序都保持一个消息队列。当你按下鼠标或者键盘时,Windows并不是把这个输入事件直接送给应用程序,而是将输入的事件先翻译成一个消息,然后把这个消息放入到这个应用程序的消息队列中去。应用程序又是怎么来接收这个消息呢?这就讲讲消息循环了。
应用程序的WinMain函数通过执行一段代码从她的队列中来检索Windows送往她的消息。然后WinMain就把这些消息分配给相应的窗口函数以便处理它们,这段代码是一段循环代码,故称为”消息循环”。这段循环代码是什么呢?好,往下看:
在咱们的第二只小板凳中,这段代码就是:
……
MSG msg; //定义消息名
while (GetMessage (&msg, NULL, 0, 0))
{
TranslateMessage (&msg) ; //翻译消息
DispatchMessage (&msg) ; //撤去消息
}
return msg.wParam ;
MSG结构在头文件中定义如下:
typedef struct tagMSG { // msg
HWND hwnd;
UINT message;
WPARAM wParam;
LPARAM lParam;
DWORD time;
POINT pt;
} MSG;
MSG数据成员意义如下:
参数1:hwnd是消息要发送到的那个窗口的句柄,这个窗口就是咱们用CreateWindows函数创建的那一个。如果是在一个有多个窗口的应用程序中,用这个参数就可决定让哪个窗口接收消息。
参数2:message是一个数字,它唯一标识了一种消息类型。每种消息类型都在Windows文件中定义了,这些常量都以WM_开始后面带一些描述了消息特性的名称。比如说当应用程序退出时,Windows就向应用程序发送一条WM_QUIT消息。
参数3:一个32位的消息参数,这个值的确切意义取决于消息本身。
参数4:同上。
参数5:消息放入消息队列中的时间,在这个域中写入的并不是日期,而是从Windows启动后所测量的时间值。Windows用这个域来使用消息保持正确的顺序。
参数6:消息放入消息队列时的鼠标坐标.
消息循环以GetMessage调用开始,它从消息队列中取出一个消息:
GetMessage(&msg,NULL,0,0)
,第一个参数是要接收消息的MSG结构的地址,第二个参数表示窗口句柄,NULL则表示要获取该应用程序创建的所有窗口的消息;第三,四参数指定消息范围。后面三个参数被设置为默认值,这就是说你打算接收发送到属于这个应用程序的任何一个窗口的所有消息。在接收到除WM_QUIT之外的任何一个消息后,GetMessage()都返回TRUE。如果GetMessage收到一个WM_QUIT消息,则返回FALSE,如收到其他消息,则返回TRUE。因此,在接收到WM_QUIT之前,带有GetMessage()的消息循环可以一直循环下去。只有当收到的消息是WM_QUIT时,GetMessage才返回FALSE,结束消息循环,从而终止应用程序。 均为NULL时就表示获取所有消息。
消息用GetMessage读入后(注意这个消息可不是WM_QUIT消息),它首先要经过函数TranslateMessage()进行翻译,这个函数会转换成一些键盘消息,它检索匹配的WM_KEYDOWN和WM_KEYUP消息,并为窗口产生相应的ASCII字符消息(WM_CHAR),它包含指定键的ANSI字符.但对大多数消息来说它并不起什么作用,所以现在没有必要考虑它。
下一个函数调用DispatchMessage()要求Windows将消息传送给在MSG结构中为窗口所指定的窗口过程。我们在讲到登记窗口类时曾提到过,登记窗口类时,我们曾指定Windows把函数WindosProc作为咱们这个窗口的窗口过程(就是指处理这个消息的东东)。就是说,Windows会调用函数WindowsProc()来处理这个消息。在WindowProc()处理完消息后,代码又循环到开始去接收另一个消息,这样就完成了一个消息循环。
下一个出场的东东就是窗口过程了,先歇一会儿再说吧??
Windows是一种非剥夺式多任务操作系统。只有的应用程序交出CPU控制权后,Windows才能把控制权交给其他应用程序。当GetMessage函数找不到等待应用程序处理的消息时,自动交出控制权,Windows把CPU的控制权交给其他等待控制权的应用程序。由于每个应用程序都有一个消息循环,这种隐式交出控制权的方式保证合并各个应用程序共享控制权。一旦发往该应用程序的消息到达应用程序队列,即开始执行GetMessage语句的下一条语句。
当WinMain函数把控制返回到Windows时,应用程序就终止了。应用程序的启动消息循环前要检查引导出消息循环的每一步,以确保每个窗口已注册,每个窗口都已创建。如存在一个错误,应用程序应返回控制权,并显示一条消息。
但是,一旦WinMain函数进入消息循环,终止应用程序的唯一办法就是使用PostQuitMessage把消息WM_QUIT发送到应用程序队列。当GetMessage函数检索到WM_QUIT消息,它就返回NULL,并退出消息外循环。通常,当主窗口正在删除时(即窗口已接收到一条WM_DESTROY消息),应用程序主窗口的窗口函数就发送一条WM_QUIT消息。
虽然WinMain指定了返回值的数据类型,但Windows并不使用返回值。不过,在调试一应用程序时,返回值地有用的。通常,可使用与标准C程序相同的返回值约定:0表示成功,非0表示出错。PostQuitMessage函数允许窗口函数指定返回值,这个值复制到WM_QUIT消息的wParam参数中。为了的结束消息循环之后返回这个值,我们的第二只小板凳中使用了以下语句:
return msg.wParam ; //表示从PostQuitMessage返回的值
例如:当Windows自身终止时,它会撤消每个窗口,但不把控制返回给应用程序的消息循环,这意味着消息循环将永远不会检索到WM_QUIT消息,并且的循环之后的语句也不能再执行。Windows的终止前的确发送一消息给每个应用程序,因而标准C程序通常会的结束前清理现场并释放资源,但Windows应用程序必须随每个窗口的撤消而被清除,否则会丢失一些数据。
如前所述,函数GetMessage负责从应用程序的消息队列中取出消息,而函数DispatchMessage()要求Windows将消息传送给在MSG结构中为窗口所指定的窗口过程。然后出台的就是这个窗口过程了,这个窗口过程的任务是干什么呢?就是最终用来处理消息的,就是消息的处理器而已,那么这个函数就是WindowProc,在Visual C++6.0中按F1启动MSDN,按下面这个路径走下来:
PlatForm SDK–>User Interface services–>Windows user Interface–>Windowing–>Window Procedures–>Window Procedure Reference–>Windows Procedure Functions–>WindowProc
啊,太累了,不过我们终于的MSDN中找到了这个函数,前几次我讲解这些API函数的时候,都是的知道的情况下搜索出来的,所以没有详细给出每个函数的具体位置,而这次我却是一点点去找的,还好,没被累死,体会到MSDN的庞大了吧,不过我用的是MSDN2000,是D版的,三张光盘装。你用的MSDN如果按这个路径走下去的话,可能会找不到,不过我想大致也是在这个位置了,找找看!!!
LRESULT CALLBACK WindowProc
(
HWND hwnd, // handle to window
UINT uMsg, // message identifier
WPARAM wParam, // first message parameter
LPARAM lParam // second message parameter
);
这个函数我们的第二只小板凳里被我们称为WndProc.
不知你注意到了没有,这个函数的参数与刚刚提到的GetMessage调用把返回的MSG结构的前四个成员相同。如果消息处理成功,WindowProc的返回值为0.
Windows的启动应用程序时,先调用WinMain函数,然后调用窗口过程,注意:在我们的这个程序中,只有一个窗口过程,实际上,也许有不止一个的窗口过程。例如,每一个不同的窗口类都 有一个与之相对应的窗口过程。无论Windows何时想传递一个消息到一窗口,都将调用相应的窗口过程。当Windows从环境,或从另一个应用程序,或从用户的应用程序中得到消息时,它将调用窗口过程并将信息传给此函数。总之,窗口过程函数处理所有传送到由此窗口类创建的窗口所得到的消息。并且窗口过程有义务处理Windows扔给它的任何消息。我们在学习Windows程序设计的时候,最主要的就是学习这些消息是什么以及是什么意思,它们是怎么工作的。
令我们不解的是,在程序中我们看不出来是哪一个函数在调用窗口过程。它其实是一个回调函数.前面已经提到,Windows把发生的输入事件转换成输入消息放到消息队列中,而消息循环将它们发送到相应的窗口过程函数,真正的处理是在窗口过程函数中执行的,在Windows中就使用了回调函数来进行这种通信。
回调函数是输出函数中特殊的一种,它是指那些在Windows环境下直接调用的函数。一个应用程序至少有一个回调函数,因为在应用程序处理消息时,Windows调用回调函数。这种回调函数就是我们前面提到的窗口过程,它对对应于一个活动的窗口,回调函数必须向Windows注册,Windows实施相应操作即行回调。
每个窗口必须有一个窗口过程与之对应,且Windows直接调用本函数,因此,窗口函数必须采用FAR PASCAL调用约定。在我们的第二只小板凳中,我们的窗口函数为WndProc,必须注意这里的函数名必须是前面注册的窗口类时,向域wc.lpfnWndProc所赋的WndProc。函数WndProc就是前面定义的窗口类所生成的所有窗口的窗口函数。
在我们的这个窗口函数中,WndProc处理了共有两条消息:WM_PAINT和WM_DESTROY.
窗口函数从Windows中接收消息,这些消息或者是由WinMain函数发送的输入消息,或者是直接来自Windows的窗口管理消息。窗口过程检查一条消息,然后根据这些消息执行特定的动作未被处理的消息通过DefWindowProc函数传回给Windows作缺海上处理。
可以发送窗口函数的消息约有220种,所有窗口消息都以WM_开头,这些消息在头文件中被定义为常量。引起Windows调用窗口函数的原因有很多,,如改变窗口大小啊,改变窗口在屏幕上的位置啊什么的。
Windows已经把任务扔给窗口过程了,窗口过程是怎么处理消息的呢?稍息一下,让我们进行下一节:处理消息……
注:可能你看这些东西的时候有些乱,不过没关系,这很正常,多看几下MSDN就慢慢明白了,有我写这个专题的时候,很多概念也太不清楚,不过等我查资料写下来后,感觉渐渐有些东西也有了点眉目,因为这本身也是个进步的过程。 —小朱
(七)处理消息
窗口过程处理消息通常以switch语句开始,对于它要处理的每一条消息ID都跟有一条case语句。大多数windows proc都有具有下面形式的内部结构:
switch(uMsgId)
{
case WM_(something):
//这里此消息的处理过程
return 0;
case WM_(something else):
//这里是此消息的处理过程
ruturn 0;
default:
//其他消息由这个默认处理函数来处理
return DefWindowProc(hwnd,uMsgId,wParam,lParam);
}
在处理完消息后,要返回0,这很重要—–它会告诉Windows不必再重试了。对于那些在程序中不准备处理的消息,窗口过程会把它们都扔给DefWindowProc进行缺省处理,而且还要返回那个函数的返回值。在消息传递层次中,可以认为DefWindowProc函数是最顶层的函数。这个函数发出WM_SYSCOMMAND消息,由系统执行Windows环境中多数窗口所公用的各种通用操作,例如,画窗口的非用户区,更新窗口的正文标题等等等等。
再提示一下,以WM_的消息在Windows头文件中都被定义成了常量,如WM_QUIT=XXXXXXXXXXX,但我们没有必要记住这个数值,也不可能记得住,我们只要知道WM_QUIT就OK了。
在第二只小板凳中我们只让窗口过程处理了两个消息:一个是WM_PAINT,另一个是WM_DESTROY,先说说第一个消息—WM_PAINT.
无论何时Windows要求重画当前窗口时,都会发该消息。也可以这样说:无论何时窗口非法,都必须进行重画。 哎呀,什么又是”非法窗口”?什么又是重画啊?你这人有没有完,嗯?
稍安勿燥,我比你还烦呢?我午饭到现在还没吃呢!你有点耐心,来点专业精神好不好???我开始在MSDN里面找有关这个方面的内容了,别急,我找找看:
Platform SDK–>Graphics and Multimedia Services–>Windows GDI–>Painting and Drawing–>Using the WM_PAINT Message—–终于找到了。
让我们把Windows的屏幕想像成一个桌面,把一个窗口想像成一张纸。当我们把一张纸放到桌面上时,它会盖住其他的纸,这样被盖住的其他纸上的内容都看不到了。但我们只要把这张纸移开,被盖住的其他纸上的内容就会显示出来了—这是一个很简单的道理,谁都明白。
对于我们的屏幕来说,当一个窗口被另一窗口盖住时,被盖住的窗口的某些部分就看不到了,我们要想看到被盖住的窗口的全部面貌,就要把另一个窗口移开,但是当我们移开后,事情却起了变化—–很可能这个被盖住的窗口上的信息被擦除了或是丢失了。当窗口中的数据丢失或过期时,窗口就变成非法的了—或者称为”无效”。于是我们的任务就来了,我们必须考虑怎样在窗口的信息丢失时”重画窗口”–使窗口恢复成以前的那个样子。这也就是我们在这第二只小板凳中调用UpdateWindow的原因。
你忘记了吗?刚才我们在(三)显示和更新窗口中有下面的一些文字:
WinMain()调用完ShowWindow后,还需要调用函数UpdateWindow,最终把窗口显示了出来。调用函数UpdateWindow将产生一个WM_PAINT消息,这个消息将使窗口重画,即使窗口得到更新.—这是程序第一次调用了这条消息。
为重新显示非法区域,Windows就发送WM_PAINT消息实现。要求Windows发送WM_PAINT的情况有改变窗口大小,对话框关闭,使用了UpdateWindows和ScrollWindow函数等。这里注意,Windows并非是消息WM_PAINT的唯一来源,使用InvalidateRect或InvalidateRgn函数也可以产生绘图窗口的WM_PAINT消息……
通常情况下用BeginPaint()来响应WM_PAINT消息。如果要在没有WM_PAINT的情况下重画窗口,必须使用GetDC函数得到显示缓冲区的句柄。这里面不再扩展。详细见MDSN。
这个BeginPaint函数会执行准备绘画所需的所有步骤,包括返回你用于输入的句柄。结束则是以EndPaint();
在调用完BeginPaint之后,WndProc接着调用GetClientRect:
GetClientRect(hwnd,&rect);
第一个参数是程序窗口的句柄。第二个参数是一个指针,指向一个RECT类型的结构。查MSDN,可看到这个结构有四个成员。
WndProc做了一件事,他把这个RECT结构的指针传送给了DrawText的第四个参数。函数DrawText的目的就是在窗口上显示一行字—-“你好,欢迎你来到VC之路!”,有关这个函数的具体用法这里也没必要说了吧。
这个消息要比WM_PAINT消息容易处理得多:只要用户关闭窗口,就会发送WM_DESTROY消息(在窗口从屏幕上移去后)。
程序通过调用PostQuitMessage以标准方式响应WM_DESTROY消息:
PostQuitMessage (0) ;
这个函数在程序的消息队列中插入一个WM_QUIT消息。在(四)创建消息循环中我们曾有这么一段话:
消息循环以GetMessage调用开始,它从消息队列中取出一个消息:
在接收到除WM_QUIT之外的任何一个消息后,GetMessage()都返回TRUE。如果GetMessage收到一个WM_QUIT消息,则返回FALSE,如收到其他消息,则返回TRUE。因此,在接收到WM_QUIT之前,带有GetMessage()的消息循环可以一直循环下去。只有当收到的消息是WM_QUIT时,GetMessage才返回FALSE,结束消息循环,从而终止应用程序。
主要介绍了Win32 API主消息循环的两种处理方法:使用GetMessage方法构造主消息循环、使用PeekMessage方法构造主消息循环。
一般应用程序都使用用GetMessage方法构造主消息循环,该方法是获得一条线程 的消息。对于VS2005自动生成的Win32 Windows程序上面有些不足。
因为VS2005生成的主消息循环如下;
// Main message loop:
while (GetMessage(&msg, NULL, 0, 0))
{
if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
简单看看的确没有问题,但是当我们去查阅MSDN文档看到GetMessage消息时候可以看到这样一段
If there is an error, the return value is -1.
所以我们应该把上面这个主循环修改为下面这样的形式,增加一个临死变量。
// Main message loop:
BOOL bRet;//临时变量,存储GetMessage方法返回值
// Main message loop:
while ((bRet = GetMessage(&msg, NULL, 0, 0))!=0)
{
if(bRet==-1)
{
//表示GetMessage获得的信息有错误
}
else
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
(2)使用PeekMessage方法构造主消息循环
PeekMessage常常用于Windows开发游戏中,PeekMessage在处理获得消息时候和GetMessage一样,关键不同的是PeekMessage在没有消息处理的时候还会继续保持循环激活状态,并且继续占用资源。
// Main message loop:
while (true)
{
if(PeekMessage(&msg,NULL,0,0,PM_REMOVE))
{
if(msg.message == WM_QUIT)
{
break;
}
else //表示GetMessage获得的信息有错误
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
else
{
//循环处理的函数
}
}
1:Win32是个多任务抢占式操作系统,每运行一个程序(可执行文件),操作系统就创建一个进程和主线程,把程序的代码和数据映射到该进程地址空间,并为每个线程分配了一个时间片,一个线程放弃CPU的处理权有、可以是时间片完了,I/O请求,还有就是程序自己要求放弃处理权,而GetMessage函数是一个阻塞函数,也就是你调用他就相当于主动放弃了CPU,引起线程上下文切换,从而其他线程可以得到CPU,但该函数会在有消息的时间激活而继续执行。如果你是获取消息用PeekMessage函数,那么你打开任务管理器,才知道什么叫做真正的浪费资源;
2:
while(1)
{
id=getMessage(...);
if(id == quit)
break;
translateMessage(...);
}
当该程序没有消息通知时getMessage就不会返回,也就不会占用系统的CPU时间。
在16位的系统中系统中只有一个消息队列,所以系统必须等待当前任务处理消息后才可以发送下一消息到相应程序,如果一个程序陷如死循环或是耗时操作时系统就会得不到控制权。这种多任务系统也就称为协同式的多任务系统。Windows3.X就是这种系统。
而32位的系统中每一运行的程序都会有一个消息队列,所以系统可以在多个消息队列中转换而不必等待当前程序完成消息处理就可以得到控制权。这种多任务系统就称为抢先式的多任务系统。Windows95/Windows98/NT就是这种系统。
3:曾有这样的疑问,为什么很多资料中都有关于windows中的While(getmessage(&msg,Null,0,0)){..}
消息循环不占用CPU的说法?今天特有关此事查了一下资料,原来是这样子啊!
说,其实这里的while(){}循环是占用cpu的,只是getmessage()是一个阻塞型的函数,当消息队列中没有消息时,它会检查确认,当确认消息队列为空时,则进行V操作,从而使线程外于阻塞状态,不被激发,另外我们知道外于sleep状态的线程是不占cpu的,是故当getmessage无返回值时,while()也不执行。整个线程被阻塞,从而不占用CPU资源。
当Winows程序启动时,会注册一个窗口类,注册的窗口类中包括当前窗口的风格、消息处理函数等等。然后,程序创建一个该注册窗口类的主窗口,接着,显示这个主窗口并进入到消息循环。在消息循环中,将不断地从窗口自身的消息队列中读取消息,并调用注册的窗口消息处理函数对不同的消息进行处理。
GetMessage函数是一个阻塞型的函数,当消息队列中没有消息时,GetMessage会处于阻塞状态。一旦有消息到达,进程会被唤醒,GetMessage马上返回。实现时,使用了一个信号量, GetMessage函数在确定没有消息可读时,对这个信号量进行一个V操作,从而使线程阻塞。而PostMessage、SendNotifyMessage、SendSyncMessage等任何一个发送消息函数在发送完消息之后,都会读取这个信号量的值,当发现这个值等于零时,即表示读消息的线程当前已阻塞,这时就会作一次P操作,来唤醒睡眠的线程。
PeekMessage 返回 TRUE 的条件是有消息,如果没有消息返回 FALSE
GetMessage 返回 TRUE 的条件是有消息且该消息不为 WM_QUIT
返回 FALSE 的条件是有消息且该消息 为 WM_QUIT
GetMessage不将控制传回给程序,直到从程序的消息队列中取得消息,但是PeekMessage总是立刻传回,而不论一个消息是否出现。当消息队列中有一个消息时,PeekMessage的传回值为TRUE(非0),并且将按通常方式处理消息。当队列中没有消息时,PeekMessage传回FALSE(0)。
这使得我们可以改写普通的消息循环。我们可以将如下所示的循环:
while (GetMessage (&msg, NULL, 0, 0))
{
TranslateMessage (&msg) ;
DispatchMessage (&msg) ;
}
return msg.wParam ;
替换为下面的循环:
while (TRUE)
{
if (PeekMessage (&msg, NULL, 0, 0, PM_REMOVE))
{
if (msg.message == WM_QUIT)
break ;
TranslateMessage (&msg) ;
DispatchMessage (&msg) ;
}
else
{
// 完成某些工作的其它行程序
}
}
return msg.wParam ;
注意,WM_QUIT
消息被另外挑出来检查。在普通的消息循环中您不必这么作,因为如果GetMessage
接收到一个WM_QUIT
消息,它将传回0,但是PeekMessage
用它的传回值来指示是否得到一个消息,所以需要对WM_QUIT
进行检查。
如果PeekMessage
的传回值为TRUE
,则消息按通常方式进行处理。如果传回值为FALSE
,则在将控制传回给Windows之前,还可以作一点工作(如显示另一个随机矩形)。
(尽管Windows文件上说,您不能用PeekMessage从消息队列中删除WM_PAINT
消息,但是这并不是什么大不了的问题。毕竟,GetMessage
并不从消息队列中删除WM_PAINT
消息。从队列中删除WM_PAINT
消息的唯一方法是令窗口显示区域的失效区域变得有效,这可以用ValidateRect
和ValidateRgn
或者BeginPaint
和EndPaint
对来完成。如果您在使用PeekMessage
从队列中取出WM_PAINT
消息后,同平常一样处理它,那么就不会有问题了。所不能作的是使用如下所示的程序代码来清除消息队列中的所有消息:
while (PeekMessage (&msg, NULL, 0, 0, PM_REMOVE)) ;
这行叙述从消息队列中删除WM_PAINT
之外的所有消息。如果队列中有一个WM_PAINT
消息,程序就会永远地陷在while循环中。
我们也可以说,PeekMessage是一个具有线程异步行为的函数,不管消息队列中是否有消息,函数都会立即返回。而GetMessage则是一个具有线程同步行为的函数,如果消息队列中没有消息的话,函数就会一直等待,直到消息队列中至少有一条消息时才返回。
如果消息队列中没有消息,PeekMessage总是能返回,这就相当于在执行一个循环,如果消息队列一直为空, 它就进入了一个死循环。GetMessage则不可能因为消息队列为空而进入死循环。
在Windows的内部,两个函数执行着相同的代码。
具体情况具体分析,无法说明到底哪一个更好一些,这要根据实际的应用情况而定。
SendMessage、PostMessage原理
本文讲解SendMessage、PostMessage两个函数的实现原理,分为三个步骤进行讲解,分别适合初级、中级、高级程序员进行理解,三个步骤分别为:
1、SendMessage、PostMessage的运行机制。
2、SendMessage、PostMessage的运行内幕。
3、SendMessage、PostMessage的内部实现。
注:理解这篇文章之前,必须先了解Windows的消息循环机制。
我们先来看最简单的。
SendMessage可以理解为,SendMessage函数发送消息,等待消息处理完成后,SendMessage才返回。稍微深入一点,是等待窗口处理函数返回后,SendMessage就返回了。
PostMessage可以理解为,PostMessage函数发送消息,不等待消息处理完成,立刻返回。稍微深入一点,PostMessage只管发送消息,消息有没有被送到则并不关心,只要发送了消息,便立刻返回。
对于写一般Windows程序的程序员来说,能够这样理解也就足够了。但SendMessage、PostMessage真的是一个发送消息等待、一个发送消息不等待吗?具体细节,下面第2点将会讲到。
在写一般Windows程序时,如上第1点讲到的足以应付,其实我们可以看看MSDN来确定SendMessage、PostMessage的运行内幕。
在MSDN中,SendMessage解释如为:The SendMessage function sends the specified message to a window or windows. It calls the window procedure for the specified window and does not return until the window procedure has processed the message.
翻译成中文为:SendMessage函数将指定的消息发到窗口。它调用特定窗口的窗口处理函数,并且不会立即返回,直到窗口处理函数处理了这个消息。
再看看PostMessage的解释:The PostMessage function places (posts) a message in the message queue associated with the thread that created the specified window and returns without waiting for the thread to process the message.
翻译成中文为:PostMessage函数将一个消息放入与创建这个窗口的消息队列相关的线程中,并立刻返回不等待线程处理消息。
仔细看完MSDN解释,我们了解到,SendMessage的确是发送消息,然后等待处理完成返回,但发送消息的方法为直接调用消息处理函数(即WndProc函数),按照函数调用规则,肯定会等消息处理函数返回之后,SendMessage才返回。而PostMessage却没有发送消息,PostMessage是将消息放入消息队列中,然后立刻返回,至于消息何时被处理,PostMessage完全不知道,此时只有消息循环知道被PostMessage的消息何时被处理了。
至此我们拨开了一层疑云,原来SendMessage只是调用我们的消息处理函数,PostMessage只是将消息放到消息队列中。下一节将会更深入这两个函数,看看Microsoft究竟是如何实现这两个函数的。
Windows内部运行原理、机制往往是我们感兴趣的东西,而这些东西又没有被文档化,所以我们只能使用Microsoft提供的工具自己研究了。
首先,在基本Win32工程代码中,我们可以直接看到消息处理函数、消息循环,所以建立一个基本Win32工程(本篇文章使用VS2005),为了看到更多信息,我们需要进行设置,让VS2005载入Microsoft的Symbol(pdb)文件[1]。为了方便,去除了一些多余的代码,加入了两个菜单,ID分别为:IDM_SENDMESSAGE
、IDM_POSTMESSAGE
。如下列出经过简化后的必要的代码。
消息循环:
Ln000:while (GetMessage(&msg, NULL, 0, 0))
Ln001:{
Ln002: TranslateMessage(&msg);
Ln003: DispatchMessage(&msg);
Ln004:}
消息处理函数:
Ln100:LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
Ln101:{
Ln102: int wmId, wmEvent;
Ln103: switch (message)
Ln104: {
Ln105: case WM_COMMAND:
Ln106: wmId = LOWORD(wParam);
Ln107: wmEvent = HIWORD(wParam);
Ln108: switch (wmId)
Ln109: {
Ln110: case IDM_EXIT:
Ln111: DestroyWindow(hWnd);
Ln112: break;
Ln113: case IDM_SENDMESSAGE:
Ln114: SendMessage(hWnd, WM_SENDMESSAGE, 0, 0);
Ln115: break;
Ln116: case IDM_POSTMESSAGE:
Ln117: PostMessage(hWnd, WM_POSTMESSAGE, 0, 0);
Ln118: break;
Ln119: default:
Ln120: return DefWindowProc(hWnd, message, wParam, lParam);
Ln121: }
Ln122: break;
Ln123:
Ln124: case WM_SENDMESSAGE:
Ln125: MessageBox(hWnd, L"SendMessage", L"Prompt", MB_OK);
Ln126: break;
Ln127:
Ln128: case WM_POSTMESSAGE:
Ln129: MessageBox(hWnd, L"PostMessage", L"Prompt", MB_OK);
Ln130: break;
Ln131:
Ln132: case WM_DESTROY:
Ln133: PostQuitMessage(0);
Ln134:
Ln135: default:
Ln136: return DefWindowProc(hWnd, message, wParam, lParam);
Ln137: }
Ln138: return 0;
Ln139:}
下面一步步分析这两个函数的内部情况,先讨论 SendMessage。
第一步,在DispatchMessage(Ln003)函数处下个断点,F5进行调试,当程序运行到断点后,查看 CallStack 窗口,可得如下结果:
#003:MyProj.exe!wWinMain(HINSTANCE__ * hInstance=0x00400000, HINSTANCE__ * hPrevInstance=0x00000000, wchar_t * lpCmdLine=0x000208e0, int nCmdShow=0x00000001) Line 49 C++
#002:MyProj.exe!__tmainCRTStartup() Line 589 + 0x35 bytes C
#001:MyProj.exe!wWinMainCRTStartup() Line 414 C
#000:kernel32.dll!_BaseProcessStart@4() + 0x23 bytes
我们可以看到,进程先调用 kernel32.dll 中的 BaseProcessStart 函数,然后调用的 Startup Code 的函数 wWinMainCRTStartup,然后调用 _tmainCRTStartup 函数,最终调用我们的 wWinMain 函数,我们的程序就运行起来了。
第二步,去除第一步下的断点,在 WndProc(Ln101) 函数入口处下个断点,F5 继续运行,运行到新下的断点处,查看 CallStack 窗口,可得如下结果:
#008:MyProj.exe!WndProc(HWND__ * hWnd=0x00050860, unsigned int message=0x00000101, unsigned int wParam=0x00000074, long lParam=0xc03f0001) Line 122 C++
#007:user32.dll!_InternalCallWinProc@20() + 0x28 bytes
#006:user32.dll!_UserCallWinProcCheckWow@32() + 0xb7 bytes
#005:user32.dll!_DispatchMessageWorker@8() + 0xdc bytes
#004:user32.dll!_DispatchMessageW@4() + 0xf bytes
#003:MyProj.exe!wWinMain(HINSTANCE__ * hInstance=0x00400000, HINSTANCE__ * hPrevInstance=0x00000000, wchar_t * lpCmdLine=0x000208e0, #000:int nCmdShow=0x00000001) Line 49 + 0xc bytes C++
#002:MyProj.exe!__tmainCRTStartup() Line 589 + 0x35 bytes C
#001:MyProj.exe!wWinMainCRTStartup() Line 414 C
#000:kernel32.dll!_BaseProcessStart@4() + 0x23 bytes
#000~#003 跟第一步相同,不再解释。在 #004、#005,可以看到,函数运行到 DispatchMessage 的内部了,DispatchMessageW、DispatchMessageWorker 是 user32.dll 中到处的函数,而且函数前部字符串相等,在此猜想应该是 DispatchMessage 的内部处理。#008 为我们消息处理函数,所以推想而知,#006、#007 是为了调用我们的消息处理函数而准备的代码。
第三步,去除第二步下的断点,在Ln003、Ln114、Ln115、Ln125 处分别下一个断点,在菜单中选择对应项,使程序运行至 Ln114,F10下一步,可以看到并没有运行到 break(Ln115),直接跳到了 Ln125 处,由此可知目前 SendMessage 已经在等待了,查看 CallStack 窗口,可得如下结果:
#013:MyProj.exe!WndProc(HWND__ * hWnd=0x00050860, unsigned int message=0x00000500, unsigned int wParam=0x00000000, long lParam=0x00000000) Line 147 C++
#012:user32.dll!_InternalCallWinProc@20() + 0x28 bytes
#011:user32.dll!_UserCallWinProcCheckWow@32() + 0xb7 bytes
#010:user32.dll!_SendMessageWorker@20() + 0xc8 bytes
#009:user32.dll!_SendMessageW@16() + 0x49 bytes
#008:MyProj.exe!WndProc(HWND__ * hWnd=0x00050860, unsigned int message=0x00000111, unsigned int wParam=0x00008003, long lParam=0x00000000) Line 136 + 0x15 bytes C++
#007:user32.dll!_InternalCallWinProc@20() + 0x28 bytes
#006:user32.dll!_UserCallWinProcCheckWow@32() + 0xb7 bytes
#005:user32.dll!_DispatchMessageWorker@8() + 0xdc bytes
#004:user32.dll!_DispatchMessageW@4() + 0xf bytes
#003:MyProj.exe!wWinMain(HINSTANCE__ * hInstance=0x00400000, HINSTANCE__ * hPrevInstance=0x00000000, wchar_t * lpCmdLine=0x000208e0, #000:int nCmdShow=0x00000001) Line 49 + 0xc bytes C++
#002:MyProj.exe!__tmainCRTStartup() Line 589 + 0x35 bytes C
#001:MyProj.exe!wWinMainCRTStartup() Line 414 C
#000:kernel32.dll!_BaseProcessStart@4() + 0x23 bytes
#000~#008 跟上面的相同,不再解释。在 #009、#010,可以看到,函数调用到 SendMessage 内部了,在此猜想应该是 SendMessage 的内部处理。#011、#012 跟第二步中的 #006、#007 一样,在第二部中,这两个函数是为了调用消息处理函数而准备的代码,#013 也是我们的消息处理函数,所以此两行代码的功能相等。
至此,我们证明了 SendMessage 的确是直接调用消息处理函数的,在消息处理函数返回前,SendMessage 等待。在所有的操作中,Ln003 断点没有去到,证明 SendMessage 不会将消息放入消息队列中(在 PostMessage 分析中,此断点将会跑到,接下来讲述)。
第四步,F5继续运行,此时弹出对话框,点击对话框中的确定后,运行到断点 Ln115 处。查看 CallStack 窗口,可得如下结果:
#008:MyProj.exe!WndProc(HWND__ * hWnd=0x00050860, unsigned int message=0x00000111, unsigned int wParam=0x00008003, long lParam=0x00000000) Line 137 C++
#007:user32.dll!_InternalCallWinProc@20() + 0x28 bytes
#006:user32.dll!_UserCallWinProcCheckWow@32() + 0xb7 bytes
#005:user32.dll!_DispatchMessageWorker@8() + 0xdc bytes
#004:user32.dll!_DispatchMessageW@4() + 0xf bytes
#003:MyProj.exe!wWinMain(HINSTANCE__ * hInstance=0x00400000, HINSTANCE__ * hPrevInstance=0x00000000, wchar_t * lpCmdLine=0x000208e0, int nCmdShow=0x00000001) Line 49 + 0xc bytes C++
#002:MyProj.exe!__tmainCRTStartup() Line 589 + 0x35 bytes C
#001:MyProj.exe!wWinMainCRTStartup() Line 414 C
#000:kernel32.dll!_BaseProcessStart@4() + 0x23 bytes
#000~008 跟第二步的完全相同,此时 SendMessage 也已经返回,所调用的堆栈也清空了。
至此,我们彻底拨开了 SendMessage
的疑云,了解了 **SendMessage**
函数的运行机制,综述为,SendMessage 内部调用 SendMessageW、SendMessageWorker 函数做内部处理,然后调用 UserCallWinProcCheckWow、InternalCallWinProc 来调用我们代码中的消息处理函数,消息处理函数完成之后,SendMessage 函数便返回了。
SendMessage 讨论完之后,现在讨论 PostMessage,将上面的所有断点删除,关闭调试。
第一步,在DispatchMessage(Ln003)函数处下个断点,F5进行调试,此处结果跟 SendMessage 一样,不再说明。
第二步,去除第一步下的断点,在 WndProc(Ln101) 函数入口处下个断点,F5 继续运行,此处结果跟 SendMessage 一样,不再说明。
第三步,去除第二步下的断点,在 Ln003、Ln117、Ln118、Ln129 处分别下一个断点,在菜单中选择对应项,使程序运行至 Ln117,F10 下一步,可以看到已经运行到 break,PostMessage 函数返回了,此时 CallStack 没有变化。
第四步,F5 继续运行,此时程序运行到 Ln003,CallStack 跟第一步刚起来时一样。
第五步,F5 继续运行(由于有多个消息,可能要按多次),让程序运行到 Ln129,此时 CallStack 跟第二步相同,为了方便说明,再次列举如下:
#008:MyProj.exe!WndProc(HWND__ * hWnd=0x00070874, unsigned int message=0x00000501, unsigned int wParam=0x00000000, long lParam=0x00000000) Line 151 C++
#007:user32.dll!_InternalCallWinProc@20() + 0x28 bytes
#006:user32.dll!_UserCallWinProcCheckWow@32() + 0xb7 bytes
#005:user32.dll!_DispatchMessageWorker@8() + 0xdc bytes
#004:user32.dll!_DispatchMessageW@4() + 0xf bytes
#003:MyProj.exe!wWinMain(HINSTANCE__ * hInstance=0x00400000, HINSTANCE__ * hPrevInstance=0x00000000, wchar_t * lpCmdLine=0x000208e0, int nCmdShow=0x00000001) Line 49 + 0xc bytes C++
#002:MyProj.exe!__tmainCRTStartup() Line 589 + 0x35 bytes C
#001:MyProj.exe!wWinMainCRTStartup() Line 414 C
#000:kernel32.dll!_BaseProcessStart@4() + 0x23 bytes
由此可以看到,此调用是从消息循环中调用而来,DispatchMessageW、DispatchMessageWorker 是 DispatchMessage 的内部处理,UserCallWinProcCheckWow、InternalCallWinProc是为了调用我们的消息处理函数而准备的代码。
至此,我们再次彻底拨开了 PostMessage 的疑云,了解了 PostMessage 函数的运行机制,综述为,PostMessage 将消息放入消息队列中,自己立刻返回,消息循环中的 GetMessage(PeekMessage 也可,本例中为演示)处理到我们发的消息之后,便按照普通消息处理方法进行处理。
[1]关于如何设置,让VS2005载入Symbol,可以查看我写的另外一篇文章:“让Visual Studio载入Symbol(pdb)文件”,地址:http://blog.csdn.net/xt_xiaotian/archive/2010/03/16/5384111.aspx
原创博客:
http://blog.chinaunix.net/uid-20496675-id-1664090.html
http://blog.itpub.net/7668308/viewspace-853757/
http://zhidao.baidu.com/link?url=fQT7pgXRKoFnpQlzYRjhdqJtPe8E1Lp1G8t1M0Sg8lm-mfNC5zF2D83FFxwWtJA5UL_E81lHT9uxuITlMIlLg_
http://blog.sina.com.cn/s/blog_4ba53587010007lb.html
http://www.cnblogs.com/scope/archive/2009/06/14/1503088.html
http://blog.csdn.net/gencheng/article/details/9376881
http://blog.csdn.net/xt_xiaotian/article/details/5384137