Windows消息机制(转)1

时间:2024-12-25 20:04:37

Windows的应用程序一般包含窗口(Window),它主要为用户提供一种可视化的交互方式,窗口是总是在某个线程(Thread)内创建的。Windows系统通过消息机制来管理交互,消息(Message)被发送,保存,处理,一个线程会维护自己的一套消息队列(Message Queue),以保持线程间的独占性。队列的特点无非是先进先出,这种机制可以实现一种异步的需求响应过程。

PS 常见的错误的理解:

1) 每个窗口有自己的消息队列 (我加的)

消息的是什么样子的?

消息由一个叫MSG的结构体定义,包括窗口句柄(HWND),消息ID(UINT),参数(WPARAM, LPARAM)等等:

struct MSG
{
HWND hwnd;
UINT message;
WPARAM wParam;
LPARAM lParam;
DWORD time;
POINT pt;
};

消息ID是消息的类型标识符,由系统或应用程序定义,消息ID为消息划分了类型。同时,也可以看出消息是对应于特定的窗口(窗口句柄)的。(此话并不完全对,其实我们除了可以向某个窗口投递消息,也可以向某个线程投递消息,这个时候,消息是没有对应的窗口句柄的,句柄为空,要处理此类消息,必须在GetMessage或PostMessage之后,判断窗口句柄是否为空,如果为空,就要处理该线程消息,不必再往下DispatchMessage()派发消息,因为派发消息函数一看到消息窗口句柄为空,直接丢弃了该消息,不往下派发,因为没有某个窗口过程处理该消息,所以没必要派发【参考文献 4】)

消息是如何分类的?其前缀都代表什么含义?

消息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 calendar 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, ,  ) != FALSE)
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}

这里GetMessage()从队列里取出一条消息,经过TranslateMessage(),主要是将虚拟按键消息(WM_KEYDOWN等)翻译成字符消息(WM_CHAR等)。

DispatchMessage()将调用消息处理函数。这里有一个灵活性,消息从队列拿出之后,也可以不分发,进行一些别的特殊操作。

下面在看看GetMessage()的细节:

BOOL GetMessage(
LPMSG lpMsg,
HWND hWnd,
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),消息被发送时,这个句柄就已经被指定了。所以当子窗口收到一个消息时,其父窗口不会也收到这个消息,除非子窗口手动的转发。

关于更详细的窗口和控件,会在另一篇中讨论。

PS:

关系的消息的一个很详细的描述文章在最新新出的《windows编程启示录》里面有。

作者模拟了 SendMessage(),PostMessage()PeekMessage(),GetMessage()的实现,相当于给出了源码,一目了然!

谁来处理消息?消息处理函数能发送消息么?

由消息处理函数(Window Procedure)来处理。消息处理函数是一个回调函数,其地址在注册窗口类的时候注册,只有在线程内才能调用。

其原型为:

typedef LRESULT (CALLBACK* WNDPROC)(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam);

处理函数内部一般是一个switch-case结构,来针对不同的消息类型进行处理。Windows系统还为所有窗口预定义了一个默认的处理函数 DefWindowProc(),它提供了最基本的消息处理,一般在不需要特殊处理的时候(即在switch的default分支)会调用这个函数。

由同一个窗口类创建的一组窗口共享一个消息处理函数,所以在编写处理函数的时候要小心处理窗口实例的局部变量。

处理函数里可以发送消息,但是可以想象有可能出现循环。另外处理函数还常常被递归调用,所以要减少局部变量的使用,以避免递归过深是栈溢出。

最后关于处理函数特化的问题将在另外的文章讨论。