Windows消息机制
参考⼀:
Windows消息机制要点
1. 窗⼝过程 每个窗⼝会有⼀个称为窗⼝过程的回调函数(WndProc),它带有四个参数,分别为:窗⼝句柄(Window Handle),消息
ID(Message ID),和两个消息参数(wParam, lParam), 当窗⼝收到消息时系统就会调⽤此窗⼝过程来处理消息。(所以叫回调函数)
2 消息类型 1) 系统定义消息(System-Defined Messages)
在SDK中事先定义好的消息,⾮⽤户定义的,其范围在[0x0000, 0x03ff]之间, 可以分为以下三类:
1> 窗⼝消息(Windows Message)
与窗⼝的内部运作有关,如创建窗⼝,绘制窗⼝,销毁窗⼝等。可以是⼀般的窗⼝,也可以是Dialog,控件等。
如:WM_CREATE, WM_PAINT, WM_MOUSEMOVE, WM_CTLCOLOR,
2> 命令消息(Command Message)
与处理⽤户请求有关, 如单击菜单项或⼯具栏或控件时, 就会产⽣命令消息。
WM_COMMAND, LOWORD(wParam)表⽰菜单项,⼯具栏按钮或控件的ID。如果是控件, HIWORD(wParam)表⽰控件消息类型
3> 控件通知(Notify Message)
控件通知消息, 这是最灵活的消息格式, 其Message, wParam, lParam分别为:WM_NOTIFY, 控件ID,指向NMHDR的指针。NMHDR 包含控件通知的内容, 可以任意扩展。
2) 程序定义消息(Application-Defined Messages)
⽤户⾃定义的消息, 对于其范围有如下规定:
WM_USER: 0x0400-0x7FFF (ex. WM_USER+10)
WM_APP(winver> 4.0): 0x8000-0xBFFF (ex.WM_APP+4)
RegisterWindowMessage: 0xC000-0xFFFF
3 消息队列(Message Queues) Windows中有两种类型的消息队列
1) 系统消息队列(System Message Queue)
这是⼀个系统唯⼀的Queue,设备驱动(mouse, keyboard)会把操作输⼊转化成消息存在系统队列中,然后系统会把此消息放到⽬标窗⼝所在的线程的消息队列(thread-specific message queue)中等待处理
2) 线程消息队列(Thread-specific Message Queue)
每⼀个GUI线程都会维护这样⼀个线程消息队列。(这个队列只有在线程调⽤GDI函数时才会创建,默认不创建)。然后线程消息队列中的消息会被送到相应的窗⼝过程(WndProc)处理.
注意: 线程消息队列中WM_PAINT,WM_TIMER只有在Queue中没有其他消息的时候才会被处理,WM_PAINT消息还会被合并以提⾼效率。其他所有消息以先进先出(FIFO)的⽅式被处理。
4 队列消息(Queued Messages)和⾮队列消息(Non-Queued Messages) 1)队列消息(Queued Messages) 消息会先保存在消息队列中,消息循环会从此队列中取消息并分发到各窗⼝处理 如⿏标,键盘消息。 2) ⾮队列消息(NonQueued Messages) 消息会绕过系统消息队列和线程消息队列直接发送到窗⼝过程被处理 如: WM_ACTIVATE, WM_SETFOCUS, WM_SETCURSOR,
WM_WINDOWPOSCHANGED 注意: postMessage发送的消息是队列消息,它会把消息Post到消息队列中; SendMessage发送的消息是⾮队列消息, 被直接送到窗⼝过程处理
5 PostMessage(PostThreadMessage), SendMessage PostMessage:把消息放到指定窗⼝所在的线程消息队列中后⽴即返回。PostThreadMessage:把消息放到指定线程的消息队列中后⽴即返回。 SendMessage:直接把消息送到窗⼝过程处理, 处理完了才返回。
6 GetMessage, PeekMessage PeekMessage会⽴即返回 可以保留消息 GetMessage在有消息时返回 会删除消息
7 TranslateMessage, TranslateAccelerator TranslateMessage: 把⼀个virtual-key消息转化成字符消息(character message),并放到当前线程的消息队列中,消息循环下⼀次取出处理。 TranslateAccelerator: 将快捷键对应到相应的菜单命令。它会把WM_KEYDOWN 或 WM_SYSKEYDOWN转化成快捷键表中相应的WM_COMMAND 或WM_SYSCOMMAND消息, 然后把转化后的 WM_COMMAND 或WM_SYSCOMMAND直接发送到窗⼝过程处理, 处理完后才会返回。
8(消息死锁( Message Deadlocks) 假设有线程A和B, 现在有以下下步骤 1) 线程A SendMessage给线程B, A等待消息在线程B中处理后返回 2) 线程B收到了线程A发来的消息,并进⾏处理, 在处理过程中,
B也向线程A SendMessgae,然后等待从A返回。 因为此时, 线程A正等待从线程B返回, ⽆法处理B发来的消息, 从⽽导致了线程A,B相互等待, 形成死锁。多个线程也可以形成环形死锁。 可以使⽤SendNotifyMessage或SendMessageTimeout来避免出现死锁。
9 BroadcastSystemMessage 我们⼀般所接触到的消息都是发送给窗⼝的, 其实, 消息的接收者可以是多种多样的,它可以是应⽤程序(applications), 可安装驱动(installable drivers), ⽹络设备(network drivers), 系统级设备驱动(system-level device drivers)等,BroadcastSystemMessage这个API可以对以上系统组件发送消息。
⼀、引⾔
随着Windows操作系统的不断推⼴,众多软件开发包都提供有开发基于Windows平台应⽤软件的功能。虽然这些开发包不尽相同,流⾏的有Visual C++、Visual Basic、Delphi、C++ Builder 等多种,但由这些不同语⾔开发的软件有⼀点却是相同的--都是运⾏于Windows 操作平台,都必须接受Windows 的运⾏机制。作为Windows 操作系统灵魂的消息机制也就必然为众多⽤不同语⾔开发的Windows操作系统下运⾏的应⽤程序所接受。因此,要编写深⼊的Windows程序,就必须对 Windows的运⾏机制有很好的认识和理解。本⽂下⾯将对Windows操作系统下的消息运⾏机制做较为深⼊的剖析。
⼆、Windows事件驱动机制 我们当中不少使⽤VC、Delphi等作为开发语⾔的程序员是⼀步步从DOS下的Basic、C++中⾛过来的,⽽且⼤多在刚开始学习编程时也是先从 DOS下的编程环境⼊⼿的,因此在习惯了DOS下的过程驱动形式的顺序程序设计⽅法后,往往在向Windows下的开发环境转型的过程中会对 Windows所采取的事件驱动⽅式感到⽆法适应。因为DOS和Windows这两种操作系统的运⾏机制是截然不同的,DOS下的任何程序都是使⽤顺序的、过程驱动的程序设计⽅法。这种程序都有⼀个明显的开始、明显的过程以及⼀个明显的结束,因此通过程序就能直接控制程序事件或过程的全部顺序。即使是在处理异常时,处理过程也仍然是顺序的、过程驱动的结构。⽽Windows的驱动⽅式则是事件驱动的,即程序的流程不是由事件的顺序来控制,⽽是由事件的发⽣来控制,所有的事件是⽆序的,所为⼀个程序员,在编写程序时,并不知道⽤户会先按下哪个按纽,也就不知道程序先触发哪个消息。因此我们的主要任务就是对正在开发的应⽤程序要发出的或要接收的消息进⾏排序和管理。事件驱动程序设计是密切围绕消息的产⽣与处理⽽展开的,⼀条消息是关于发⽣的事件的消息。 三、Windows的消息循环 Windows操作系统为每⼀个正在运⾏的应⽤程序保持有⼀个消息队列。当有事件发⽣后,Windows并不是将这个激发事件直接送给应⽤程序,⽽是先将其翻译成⼀个Windows消息,然后再把这个消息加⼊到这个应⽤程序的消息队列中去。应⽤程序需要通过消息循环来接收这些消息。在MFC中使⽤了对 WinAPI进⾏了很好封装的类库,虽然可以为编程提供⼀个⾯向对象的界⾯,使Windows程序员能够以⾯象对象的⽅式进⾏编程,把那些进⾏SDK编程时最繁琐的部分提供给程序员,使之专注于功能的实现,但是由于引⼊了很好的封装特性,使我们不能直接操纵部分核⼼代码。
对于消息的循环和接收也只是通过类似于下⾯的消息映射予以很简单的表⽰:BEGIN_MESSAGE_MAP(CTEMMSView, CFormView) //{ { AFX_MSG_MAP(CTEMMSView) ON_WM_LBUTTONDOWN()
ON_COMMAND(ID_OPENDATA, OnOpenData) ON_WM_TIMER() ON_WM_PAINT() //} } AFX_MSG_MAP
END_MESSAGE_MAP() 虽然上述消息映射在编程过程中处理消息⾮常简练⽅便,但显然是难于理解消息是如何参与循环和分发的。因此有必要通过SDK(Software Developers Kit,软件开发⼯具箱)代码深⼊到被MFC封装的Windows编程的核⼼中来研究其具体是如何⼯作的。在SDK编程中,⼀般是在Windows应⽤程序的⼊⼝点WinMain函数中添加处理消息循环的代码以检索Windows送来的消息,然后WinMain再把这些消息分配给相应的窗⼝函数并处理它们: …… MSG msg; //定义消息名 while (GetMessage (& msg, NULL, 0, 0)) { TranslateMessage (& msg) ; //翻译消息 DispatchMessage (& msg) ; //撤去消息 } return msg.wParam ; 上述⼏句虽然简单但却是所有Windows程序的关键代码,担负着获取、解释和分发消息的任务,下⾯就重点对其功能和作⽤进⾏分析: MSG结构在头⽂件中定义如下: typedef struct tagMSG { HWND hwnd; UINT message; WPARAM wParam; LPARAM lParam; DWORD time; POINT pt; } MSG, *PMSG; 其数据成员的具体意义如下: hwnd:消息将要发送到的那个窗⼝的句柄,⽤这个参数可以决定让哪个窗⼝接收消息。message:消息号,它唯⼀标识了⼀种消
息类型。每种消息类型都在Windows⽂件进⾏了预定义。 wParam:⼀个32位的消息参数,这个值的确切意义取决于消息本⾝。 lParam:同上。 time:消息放⼊消息队列中的时间,在这个域中写⼊的并⾮当时⽇期,⽽是从Windows启动后所测量的时间值。Windows⽤ 这个域来使⽤消息保持正确的顺序。 pt:消息放⼊消息队列时的⿏标坐标。 消息循环以GetMessage调⽤开始,它从消息队列中取出⼀个消息。该函数的四个参数可以有控制地获取消息,第⼀个参数指定要接收消息的MSG结构的地址,第⼆个参数表⽰窗⼝句柄,⼀般将其设置为空,表⽰要获取该应⽤程序创建的所有窗⼝的消息;第三、四参数⽤于指定消息范围。后⾯三个参数被设置为默认值,⽤于接收发送到属于这个应⽤程序的任何⼀个窗⼝的所有消息。在接收到除WM_QUIT之外的任何⼀个消息后,GetMessage()返回 TRUE;如果GetMessage收到⼀个WM_QUIT消息,则返回FALSE以退出消息循环,终⽌程序运⾏。因此,在接收到WM_QUIT之前,带有GetMessage()的消息循环可以⼀直循环下去。当除WM_QUIT的消息⽤GetMessage读⼊后,⾸先要经过函数 TranslateMessage()对其进⾏解释,但对⼤多数消息来说并不起什么作⽤。这⾥起关键作⽤的是DispatchMessage()函数,把由GetMessage获取的Windows消息传送给在MSG结构中为窗⼝所指定的窗⼝过程。在消息处理函数处理完消息之后,代码⼜循环到开始去接收另⼀个消息,这样就完成了⼀个完整的消息循环。 由于Windows操作系统是⼀种⾮剥夺式多任务操作系统。只有在应⽤程序主动交出CPU控制权后,Windows才能把控制权交给其他应⽤程序。在消息循环中,⼀定要有能交出控制的系统函数才能实现协同式多任务操作。能完成该功能的只有GetMessage、PeekMessage和 WaitMessage这三个函数,如果在应⽤程序中长期不去调⽤
这三个函数之⼀其他任务则⽆法执⾏。GetMessage函数在不到等待应⽤程序处理的消息时,会⾃动交出控制权,由Windows把CPU的控制权交给其他等待获取控制权的应⽤程序。由于任何Windows应⽤程序都含有⼀个消息循环,这种隐式交出控制权的⽅式可以保证合并各个应⽤程序共享控制权。⼀旦发往该应⽤程序的消息到达应⽤程序队列,即开始执⾏GetMessage语句的下⼀条语句。使⽤GetMessage函数的消息循环在消息队列中没有消息时将等待,如果需要,可以利⽤这段时间进⾏I/O端⼝操作等耗时操作,不过需要在消息循环中使⽤PeekMessage函数来代替GetMessage。使⽤PeekMessage的⽅法同GetMessage类似,下⾯是⼀段使⽤ PeekMessage函数的消息循环的典型例⼦: MSG msg; BOOL bDone=FALSE; do{ if(PeekMessage(& msg,NULL,0,0,PM_REMOVE)){ ssage==WM_QUIT)
bDone=TRUE; else{ TranslateMessage(& msg); DispatchMessage(& msg); } } //⽆消息处理,进⾏长时间操作 else{ ……//长时间操作 } } while(!bDone) …… ⽆论应⽤程序消息队列中是否有消息,PeekMessage函数都⽴即返回,如果希望等待新消息⼊队,可以利⽤
⽆返回值的函数WaitMessage配合PeekMessage进⾏消息循环。 四、对Windowds消息的处理 窗⼝过程处理消息通常以switch语句开始,对于它要处理的每⼀条消息ID都跟有⼀条case语句,这在功能上同MFC的消息映射有些类似: switch(uMsgId) { case
WM_TIMER: //对WM_TIMER定时器消息的处理过程 return 0; case WM_LBUTTONDOWN: //对WM_ LBUTTONDOWN⿏标左键单击消息的处理过程 ruturn 0; …… default: //其他消息由这个默认处理函数来处理 return
DefWindowProc(hwnd,uMsgId,wParam,lParam); } 在处理完消息后必须返回0,这很重要,否则Windows将要不停地重试下去。对于那些在程序中不准备处理的消息,窗⼝过程会把它们都扔给 DefWindowProc进⾏缺省处理,⽽且还要返回那个函数的返回值。在消息传递层次中,可以认为DefWindowProc函数是最顶层的函数。该函数发出WM_SYSCOMMAND消息,由系统执⾏Windows环境中多数窗⼝所公⽤的各种通⽤操作,如更新窗⼝的正⽂标题等等。在MFC下可以⽤下述部分代码实现与上述SDK代码相同的功能:
BEGIN_MESSAGE_MAP(CTEMMSView, CFormView) //{ { AFX_MSG_MAP(CTEMMSView) ON_WM_LBUTTONDOWN()
ON_WM_TIMER() //} } AFX_MSG_MAP END_MESSAGE_MAP() ⼩结:Windows环境提供有⾮常丰富的系统资源,在这个基础上可以编制出能满⾜各种各样⽬标功能的应⽤系统。要深⼊Windows编程就必须⾸先对Windows系统的运⾏机理有很好的认识,本⽂仅针对Windows的⼀种重要运⾏机制--消息机制作了较深⼊的剖析和阐述。对培养在Windows 下的编程思想有⼀定的帮助。对某些相关问题的详细论述可以参考MSDN在线帮助的" SDK Reference" 部分。
参考⼆:
Windows消息机制(Windows Messaging)
Windows的应⽤程序⼀般包含窗⼝(Window),它主要为⽤户提供⼀种可视化的交互⽅式,窗⼝是由线程(Thread)创建的。Windows 系统通过消息机制来管理交互,消息(Message)被发送,保存,处理,⼀个线程会维护⾃⼰的⼀套消息队列(Message Queue),以保持线程间的独占性。队列的特点⽆⾮是先进先出,这种机制可以实现⼀种异步的需求响应过程。
消息的是什么样⼦的? 消息由⼀个叫MSG的结构体定义,包括窗⼝句柄(HWND),消息ID(UINT),参数(WPARAM, LPARAM)等等:
1. struct MSG
2. {
3. HWND hwnd;
4. UINT message;
5. WPARAM wParam;
6. LPARAM lParam;
7. DWORD time;
8. POINT pt;
9. };
消息ID是消息的类型标识符,由系统或应⽤程序定义,消息ID为消息划分了类型。同时,也可以看出消息是对应于特定的窗⼝(窗⼝句柄)的。
消息是如何分类的?其前缀都代表什么含义? 消息ID只是⼀个整数,Windows系统预定义了很多消息ID,以不同的前缀来划分,⽐如
WM_*,CB_*等等。 具体见下表:
Prefix Message category ABM Application desktop toolbar BM Button control CB Combo box control CBEM Extended combo box control CDM Common dialog box DBT Device DL Drag list box DM Default push button control DTM Date and time picker control EM Edit control HDM Header control HKM Hot key control IPM IP address control LB List box control LVM List view control MCM Month c
alendar control PBM Progress bar PGM Pager control PSM Property sheet RB Rebar control SB Status bar window SBM Scroll bar control STM Static control TB Toolbar TBM Trackbar TCM Tab control TTM Tooltip control TVM Tree-view control UDM Up-down control WM General window
应⽤程序可以定义⾃⼰的消息,其取值范围必须⼤于WM_USER。 如何通过消息传递任何参数? Windows系统的消息机制都包含2个长整型的参数:WPARAM, LPARAM,可以存放指针,也就是说可以指向任何内容了。 传递的内容因消息各异,消息处理函数会根据消息的类
型进⾏特别的处理,它知道传递的参数是什么含义。 消息在线程内传递时,由于在同⼀个地址空间中,指针的值是有效的。但是夸线程的情况就不能直接使⽤指针了,所以Windows系统提供了 WM_SETTEXT, WM_GETTEXT, WM_COPYDATA等消息,⽤来特殊处理,指针的内容会被放到⼀个临时的内存映射⽂件(Memory-mapped File)⾥⾯,通过它实现线程间的共享数据。 消息队列和线程的关系是什么?消息队列的结构是什么样⼦的? Windows系统本⾝会维护⼀个唯⼀的消息队列,以便于发送给各个线程,这是系统内部的实现⽅式。⽽对于线程来说,每个线程可以拥有⾃⼰的消息队列,它和线程⼀⼀对应。在线程刚创建时,消息队列并不会被创建,⽽是当GDI的函数调⽤发⽣时,Windows系统才认为有必要为线程创建消息队列。 消息队列包含在⼀个叫THREADINFO的结构中,有四个队列: Sent Message Queue Posted Message Queue Visualized Input Queue Reply Message Queue 之所以维护多个队列,是因为不同消息的处理⽅式和处理顺序是不同的。 线程和窗
⼝是⼀⼀对应的吗?如果想要有两个不同的窗⼝对消息作出不同反应,但是他们属于同⼀个线程,可能吗? 窗⼝由线程创建,⼀个线程可以创建多个窗⼝。窗⼝可由CreateWindow()函数创建,但前提是需要提供⼀个已注册的窗⼝类(Window Class),每⼀个窗⼝类在注册时需要指定⼀个窗⼝处理函数(Window Procedure),这个函数是⼀个回调函数,就是⽤来处理消息的。⽽由⼀个线程来创建对应于不同的窗⼝类的窗⼝是可以的。 由此可见,只要注册多个窗⼝类,每个窗⼝都可以拥有⾃⼰的消息处理函数,⽽同时,他们属于同⼀个线程。 如何发送消息? 消息的发送终归通过函数调⽤,⽐较常⽤的有
PostMessage(),SendMessage(),另外还有⼀些Post*或Send*的函数。函数的调⽤者即发送消息的⼈。 这⼆者有什么不同
呢?SendMessage()要求接收者⽴即处理消息,等处理完毕后才返回。⽽PostMessage()将消息发送到接收者队列中以后,⽴即返回,调⽤者不知道消息的处理情况。 他们的的原型如下: LRESULT SendMessage( HWND hwnd, UINT uMsg,
WPARAM wParam, LPARAM lParam); LRESULT PostMessage( HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam); SendMessage()要求⽴即处理,所以它会直接调⽤窗⼝的消息处理函数(Window Procedure),完成后返回处理结果。 但这仅限于线程内的情况,夸线程时
它调不到处理函数,只能把消息发送到接收线程的队列Sent Message Queue⾥。如果接收线程正在处理别的消息,那么它不会被打断,直到它主动去获取队列⾥的下⼀条消息时,它会拿到这⼀条消息,并开始处理,完成后他会通知发送线程结果(猜测是通过ReplyMessage()函数)。 在接收线程处理的过程中,发送线程会挂起等待SendMessage()返回。但是如果这时有其他线程发消息给这个发送线程,它可以响应,但仅限于⾮队列型(Non-queued)消息。 这种机制可能引起死锁,所以有其他函数⽐如SendMessageTimeout(), SendMessageCallback()等函数来避免这种情况。 PostMessage()并不需要同步,所以⽐较简单,它只是负责把消息发送到队列⾥⾯,然后马上返回发送者,之后消息的处理则再受控制。 消息可以不进队列吗?什么消息不进队列? 可以。实际上MSDN把消息分为队列型(Queued Message)和⾮队列型(Non-queued Message),这只是不同的路由⽅式,但最终都会由消息处理函数来处理。 队列型消息包括硬件的输⼊(WM_KEY*等)、WM_TIMER消息、WM_PAINT消息等;⾮队列型的⼀些例⼦有WM_SETFOCUS, WM_ACTIVE, WM_SETCURSOR等,它们被直接发送给处理函数。 其实,按照MSDN的说法和消息的路由过程可以理解为,Posted Message Queue⾥的消息是真正的队列型消息,⽽通过SendMessage()发送到消息,即使它进⼊了Sent Message Queue,由于SendMessage要求的同步处理,这些消息也应该算⾮队列型消息。也许,Windows系统会特殊处理,使消息强⾏绕过队列。 谁来发送消息?硬件输⼊是如何被响应的? 消息可以由Windows系统发送,也可以由应⽤程序本⾝;可以向线程内发送,也可以夸线程。主要是看发送函数的调⽤者。 对于硬件消息,Windows系统启动时会运⾏⼀个叫Raw Input Thread的线程,简
称RIT。这个线程负责处理System Hardware Input Queue(SHIQ)⾥⾯的消息,这些消息由硬件驱动发送。RIT负责把SHIQ⾥的消息分发到线程的消息队列⾥⾯,那RIT是如何知道该发给谁呢?如果是⿏标事件,那就看⿏标指针所指的窗⼝属于哪个线程,如果是键盘那就看哪个窗⼝当前是激活的。⼀些特殊的按键会有所不同,⽐如 Alt+Tab,Ctrl+Alt+Del等,RIT能保证他们不受当前线程的影响⽽死锁。RIT只能同时和⼀个线程关联起来。 有可能,Windows系统还维护了除SHIQ外地其他队列,分发给线程的队列,或者直接发给窗⼝的处理函数。 消息循环是什么样⼦?线程何时挂起?何时醒来? 想象⼀个通常的Windows应⽤程序启动后,会显⽰⼀个窗⼝,它在等待⽤户的操作,并作出反应。 它其实是在⼀个不断等待消息的循环中,这个循环会不断去获取消息并作出处理,当没有消息的时候线程会挂起进⼊等待状态。这就是通常所说的消息循环。 ⼀个典型的消息循环如下所⽰(注意这⾥没有处理GetMessage出错的情况):
while(GetMessage(&msg, NULL, 0, 0 ) != FALSE) { TranslateMessage(&msg); DispatchMessage(&msg); } 这⾥GetMessage()从队列⾥取出⼀条消息,经过TranslateMessage(),主要是将虚拟按键消息(WM_KEYDOWN等)翻译成字符消息(WM_CHAR等)。 DispatchMessage()将调⽤消息处理函数。这⾥有⼀个灵活性,消息从队列拿出之后,也可以不分发,进⾏⼀些别的特殊操作。 下⾯在看看GetMessage()的细节: BOOL GetMessage( LPMSG lpMsg, HWND hWnd,
windows开发平台UINT wMsgFilterMin, UINT wMsgFilterMax ); GetMessage()会从队列中取出消息,填到MSG结构中通过参数返回。如果此时的消息是WM_QUIT,也就标识线程需要结束,则 GetMessage()返回FALSE,那么while循环会终⽌。返回TRUE表⽰取到其他消息,可以继续循环并运⾏⾥⾯的内容。如果返回-1表⽰ GetMessage()出错。 其他⼏个参数是⽤来过滤消息的,可以指定接收消息的窗⼝,以及确定消息的类型范围。 这⾥还需要提到⼀个概念是线程的Wake Flag,这是⼀个整型值,保存在THREADINFO⾥⾯和4个消息队列平级的位置。它的每⼀位(bit)代表⼀个开关,⽐如QS_QUIT, QS_SENDMESSAGE等等,这些开关根据不同的情况会被打开或关闭。GetMessage()在处理的时候会依赖这些开关。 GetMessage()的处理流程如下: 1. 处理Sent Message Queue⾥的消息,这些消息主要是由其他线程的SendMessage()发送,因为他们不能直接调⽤本线程的处理函数,⽽本线程调⽤ SendMessage()时会直接调⽤处理函数。⼀旦调⽤GetMessage(),所有的Sent Message都会被处理掉,并且GetMessage()不会返回; 2. 处理Posted Message Queue⾥的消息,这⾥拿到⼀个消息后,GetMessage()将它拷贝到MSG结构中并返回TRUE。注意有三个消息WM_QUIT, WM_PAINT,
WM_TIMER会被特殊处理,他们总是放在队列的最后⾯,直到没有其他消息的时候才被处理,连续的WM_PAINT消息甚⾄会被合并成⼀个以提⾼效率。从后⾯讨论的这三个消息的发送⽅式可以看出,使⽤Send或Post消息到队列⾥情况不多。 3. 处理QS_QUIT开关,这个开关由PostQuitMessage()函数设置,表⽰线程需要结束。这⾥为什么不⽤Send或Post⼀个 WM_QUIT消息呢?据称:⼀个原因是处理
内存紧缺的特殊情况,在这种情况下Send和Post很可能失败;其次是可以保证线程结束之前,所有Sent 和Posted消息都得到了处理,这是因为要保证程序运⾏的正确性,或者数据丢失?不得⽽知。 如果QS_QUIT打开,GetMessage()会填充⼀个WM_QUIT消息并返回FALSE。
4. 处理Virtualized Input Queue⾥的消息,主要包括硬件输⼊和系统内部消息,并返回TRUE;
5. 再次处理Sent Message Queue,来⾃MSDN却没有解释。难道在检查2、3、4步骤的时候可能出现新的Sent Message?或者是要保证推后处理后⾯两个消息;
6. 处理
QS_PAINT开关,这个开关只和线程拥有的窗⼝的有效性(Validated)有关,不受WM_PAINT的影响,当窗⼝⽆效需要重画的时候这个开关就会打开。当QS_PAINT打开的时候,GetMessage()会返回⼀个WM_PAINT消息。处理QS_PAINT放在后⾯,因为重绘⼀般⽐较慢,这样有助于提⾼效率; 7. 处理QS_TIMER开关,和QS_PAINT类似,返回WM_TIMER消息,之所以它放在QS_PAINT之后是因为其优先级更低,如果Timer消息要求重绘但优先级⼜⽐Paint⾼,那么Paint就没有机会运⾏了。 如果GetMessage()中任何消息可处
理,GetMessage()不会返回,⽽是将线程挂起,也就不会占⽤CPU时间了。 类似的WaitMessage()函
数也是这个作⽤。 还有⼀个PeekMessage(),其原型为: BOOL PeekMessage( LPMSG lpMsg, HWND hWnd, UINT wMsgFilterMin,
UINT wMsgFilterMax, UINT wRemoveMsg ); 它的处理⽅式和GetMessage()⼀样,只是多了⼀个参数wRemoveMsg,可以指定是否移除队列⾥的消息。最⼤的不同应该是,当没有消息可处理时,PeekMessage()不是挂起等待消息的到来,⽽是⽴即返回FALSE。WM_DESTROY, WM_QUIT, WM_CLOSE消息有什么不同? ⽽其他两个消息是关于窗⼝的,WM_CLOSE会⾸先发送,⼀般情况程序接到该消息后可以有机会询问⽤户是否确认关闭窗⼝,如果⽤户确认后才调⽤ DestroyWindow()销毁窗⼝,此时会发送WM_DESTROY消息,这时窗⼝已经不显⽰了,在处理WM_DESTROY消息是可以发送 PostQuitMessage()来设置QS_QUIT开关,WM_QUIT消息会由GetMessage()函数返回,不过此时线程的消息循环可能也即将结束。 窗⼝内的消息的路由是怎样的?窗⼝和其控件的关系是什么? ⼀个窗⼝(Window)可以有⼀个Parent属性,对⼀个Parent窗⼝来说,属于它的窗⼝被称为⼦窗⼝(Child Window)。控件(Control)或对话框(Dialog)也是窗⼝,他们⼀般属于某个⽗窗⼝。 所有的窗⼝都有⾃⼰的句柄(HWND),消息被发送时,这个句柄就已经被指定了。所以当⼦窗⼝收到⼀个消息时,其⽗窗⼝不会也收到这个消息,除⾮⼦窗⼝⼿动的转发。 关于更详细的窗⼝和控件,会在另⼀篇中讨论。 谁来处理消息?消息处理函数能发送消息么? 由消息处理函数(Window Procedure)来处理。消息处理函数是⼀个回调函数,其地址在注册窗⼝类的时候注册,只有在线程内才能调⽤。 其原型为: typedef LRESULT (CALLBACK* WNDPROC)
(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam); 处理函数内部⼀般是⼀个switch-case结构,来针对不同的消息类型进⾏处理。Windows系统还为所有窗⼝预定义了⼀个默认的处理函数 DefWindowProc(),它提供了最基本的消息处理,⼀般在不需要特殊处理的时候(即在switch的default分⽀)会调⽤这个函数。 由同⼀个窗⼝类创建的⼀组窗⼝共享⼀个消息处理函数,所以在编写处理函数的时候要⼩⼼处理窗⼝实例的局部变量。 处理函数⾥可以发送消息,但是可以想象有可能出现循环。另外处理函数还常常被递归调⽤,所以要减少局部变量的使⽤,以避免递归过深是栈溢出。 最后关于处理函数特化的问题将在另外的⽂章讨论。
参考三:
剖析Windows的消息运⾏机制
⼀、引⾔
随着操作系统的不断推⼴,众多软件开发包都提供有开发基于平台应⽤软件的功能。虽然这些开发包不尽相同,流⾏的有Visual 、Visual Basic、Delphi、 Builder 等多种,但由这些不同语⾔开发的软件有⼀点却是相同的--都是运⾏于Windows 操作平台,都必须接受Windows 的运⾏机制。作为Windows 操作系统灵魂的消息机制也就必然为众多⽤不同语⾔开发的Windows操作系统下运⾏的应⽤程序所接受。因此,要编写深⼊的Windows程序,就必须对Windows的运⾏机制有很好的认识和理解。本⽂下
⾯将对Windows操作系统下的消息运⾏机制做较为深⼊的剖析。
⼆、Windows事件驱动机制
我们当中不少使⽤VC、Delphi等作为开发语⾔的程序员是⼀步步从DOS下的Basic、C++中⾛过来的,⽽且⼤多在刚开始学习编程时也是先从DOS下的编程环境⼊⼿的,因此在习惯了DOS下的过程驱动形式的顺序程序设计⽅法后,往往在向Windows下的开发环境转型的过程中会对Windows所采取的事件驱动⽅式感到⽆法适应。因为DOS和Windows这两种操作系统的运⾏机制是截然不同的,DOS下的任何程序都是使⽤顺序的、过程驱动的程序设计⽅法。这种程序都有⼀个明显的开始、明显的过程以及⼀个明显的结束,因此通过程序就能直接控制程序事件或过程的全部顺序。即使是在处理异常时,处理过程也仍然是顺序的、过程驱动的结构。⽽Windows的驱动⽅式则是事件驱动的,即程序的流程不是由事件的顺序来控制,⽽是由事件的发⽣来控制,所有的事件是⽆序的,所为⼀个程序员,在编写程序时,并不知道⽤户会先按下哪个按纽,也就不知道程序先触发哪个消息。因此我们的主要任务就是对正在开发的应⽤程序要发出的或要接收的消息进⾏排序和管理。事件驱动程序设计是密切围绕消息的产⽣与处理⽽展开的,⼀条消息是关于发⽣的事件的消息。
三、Windows的消息循环
Windows操作系统为每⼀个正在运⾏的应⽤程序保持有⼀个消息队列。当有事件发⽣后,Windows并
不是将这个激发事件直接送给应⽤程序,⽽是先将其翻译成⼀个Windows消息,然后再把这个消息加⼊到这个应⽤程序的消息队列中去。应⽤程序需要通过消息循环来接收这些消息。在MFC中使⽤了对WinAPI进⾏了很好封装的类库,虽然可以为编程提供⼀个⾯向对象的界⾯,使Windows程序员能够以⾯象对象的⽅式进⾏编程,把那些进⾏SDK编程时最繁琐的部分提供给程序员,使之专注于功能的实现,但是由于引⼊了很好的封装特性,使我们不能直接操纵部分核⼼代码。对于消息的循环和接收也只是通过类似于下⾯的消息映射予以很简单的表⽰:
BEGIN_MESSAGE_MAP(CTEMMSView, CFormView)
//{{AFX_MSG_MAP(CTEMMSView)
ON_WM_LBUTTONDOWN()
ON_COMMAND(ID_OPENDATA, OnOpenData)
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论