Windows程序的消息机制是指在Windows操作系统下,应用程序与操作系统之间的一种通信方式。通过消息机制,应用程序可以接收来自操作系统的各种事件和请求,以便做出相应的响应和处理。
在Windows程序中,消息机制的实现是基于消息队列和消息循环的。消息队列是一个用于存储消息的缓冲区,当操作系统有消息需要传递给应用程序时,消息会被放入消息队列中。消息循环是应用程序的主循环,在消息循环的过程中,应用程序从消息队列中取出消息并进行处理。
Windows程序中的各种事件和请求都经过消息机制进行传递,比如鼠标点击、键盘输入、窗口尺寸改变等等。当发生这些事件时,操作系统会将相应的消息放入消息队列中,并通知应用程序有新的消息到达。应用程序在消息循环中不断地检查消息队列,处理队列中的消息,直到队列为空或者收到退出消息。
应用程序可以通过消息处理函数来响应不同类型的消息。当该类型的消息到达时,消息处理函数就会被调用,可以在该函数中编写相应的逻辑来处理消息。消息处理函数可以是在创建窗口时指定的窗口过程函数,或者是通过注册消息回调函数的方式来实现。绝大多数消息都是由Windwos系统默认的窗口过程来处理的。
本节必须掌握的知识点:
消息队列
消息循环
2.3.1 消息队列
Windows程序中的消息队列是属于线程的。每个线程初始化时都可以创建一个线程所属的消息队列。但通常在Windows程序中,只有负责处理窗口消息的主线程创建消息队列,因此,我们将线程消息队列也称之为窗口消息队列。通常Windows程序将与用户交互的消息送入消息队列进行同步。例如鼠标消息、键盘消息、绘图消息等。
此外,一些不需要同步的消息会直接分发给窗口过程进行处理,不进入消息队列。
■Windows系统消息队列
Windows操作系统是多任务系统,通常同时有多个窗口程序在运行。所有进程的消息都会被送入Windows系统内的总消息队列。然后再将总消息队列中的消息按照所属的窗口分发给每个进程的窗口消息队列。这就需要判断消息应该属于哪个窗口。
在Windows程序中,消息是通过窗口句柄来进行分发和处理的。每个窗口都有一个唯一的窗口句柄(HWND),它用于标识和引用该窗口。通过窗口句柄,可以将收到的消息正确地分发给相应的窗口。
当Windows操作系统产生一个消息并将其放入消息队列时,消息中会包含目标窗口的句柄。然后,在应用程序的消息循环中,通过处理该消息的程序会根据消息中的窗口句柄,将消息分发给对应的窗口进行处理。
在处理消息的过程中,应用程序可以使用窗口句柄来识别消息应该属于哪个窗口。通常,在处理消息的代码中,可以使用如下方式来判断消息属于哪个窗口:
●检查消息中的窗口句柄与已创建的窗口句柄是否匹配。这可以通过保存窗口句柄的变量或数据结构来完成,然后将收到的消息中的窗口句柄与保存的窗口句柄进行比较。
●使用GetWindowLongPtr函数或GetWindowLong函数获取窗口的扩展信息(例如窗口的额外数据)。可以根据扩展信息中的标识或其他自定义数据来判断消息是属于哪个窗口。
●使用其它窗口属性或特征来判断。(例如窗口类名、窗口标题等)。
问题似乎回到了原点,最初消息被Windows操作系统捕获时就应该确定其属于哪个窗口。我们分类来讨论。
1.如果是鼠标消息,Windows系统会根据鼠标消息的坐标位置来确定它所处的位置属于哪个窗口。当然也有另外一种情况,就是一个窗口会主动捕获并拥有一个鼠标消息。
2.如果是键盘消息,那么该键盘消息一定是属于当前处于焦点状态的窗口,即当前拥有键盘输入焦点的窗口。如果要想改变键盘消息所属的窗口,只能是切换焦点窗口。
3.如果是子窗口控件、菜单、快捷键消息,其本身就属于指定的窗口,毋庸置疑。
■窗口消息队列
窗口消息队列是Windows操作系统中负责存储消息的一个缓冲区。每个线程都有自己的消息队列,用于接收和存储操作系统发送给应用程序的消息。在Windows程序中,消息队列起到了重要的作用,用于传递各种事件和请求,例如键盘输入、鼠标点击、窗口尺寸改变等。
消息队列是一个先进先出(FIFO)的数据结构,新的消息会被添加到队列的尾部,而从队列头部会取出最早进入队列的消息。当操作系统接收到一个消息时,会将该消息放入相应线程的消息队列中,然后通知该线程有新消息到达。
2.3.2 消息循环
应用程序可以通过消息循环来检查和处理消息队列中的消息。在消息循环中,应用程序会不断地从消息队列中取出消息进行处理,直到队列为空或者收到退出消息。
一般来说,消息循环具有以下形式:
while (GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
在这个消息循环中,GetMessage函数会从消息队列中取出一条消息,并将其存储在 msg 变量中。
■键盘消息
如果是键盘按键消息,TranslateMessage函数会将虚拟键消息翻译为字符消息。它会检查消息的类型,如果是某些特殊键(如功能键、方向键等),则生成一个 WM_KEYDOWN 或 WM_KEYUP 消息;如果是一个字符键消息,则生成一个 WM_CHAR 消息。TranslateMessage 的作用是为了在键盘输入时生成合适的字符消息。接着将WM_CHAR 消息重新送入消息队列。
■其他消息
其他消息则直接由DispatchMessage 函数将消息分发到相应的窗口过程函数进行处理。它会根据消息中的窗口句柄找到对应的窗口过程函数,并将消息传递给它。窗口过程函数负责处理具体的消息,它会根据消息类型和需要执行相应的逻辑,比如绘图、更新窗口状态、响应用户输入等。
总结一下,窗口消息队列是用于存储消息的缓冲区,当操作系统有消息需要传递给应用程序时,会将消息放入消息队列中。应用程序在消息循环中检查消息队列,从中取出消息并将其发送给相应的窗口过程函数进行处理。这样,应用程序能够接收并响应用户输入和操作系统事件。
在Windows程序中,如果是由我们主动发送消息呢?则需要调用下面两个函数。
●PostMessage函数:
PostMessage是一个Windows API函数,用于将一个消息放入与指定窗口关联的线程的消息队列中,等待线程处理。此消息并不会直接发送到窗口过程函数,而是立即返回,消息的处理可能会在稍后才进行。
以下是PostMessage函数的原型:
BOOL PostMessage(
HWND hWnd, // 窗口句柄
UINT Msg, // 消息
WPARAM wParam, // 额外的消息相关信息
LPARAM lParam // 额外的消息相关信息
);
参数含义:
hWnd:接收消息的窗口句柄。这可以是具体的窗口句柄,也可以是一些特殊值,如HWND_BROADCAST(指所有窗口)。如果此参数为NULL,则消息会发送给调用线程的消息队列。
Msg:需要发送的消息码,如WM_CLOSE。
wParam,lParam:与消息有关的额外信息。
PostMessage函数将消息投递到消息队列后就会立即返回,不管消息是否被目标窗口过程函数处理。也就是说PostMessage是异步的,主要用于在不要求立即处理消息的情况下通知其他窗口一个事件发生,比如通知其他窗口更新显示内容。
如果你希望立即发送并处理消息,应该使用SendMessage函数。不同于PostMessage,SendMessage会立即触发窗口的窗口过程函数处理消息,并等待处理完毕后返回。
●SendMessage函数:
SendMessage是一个Windows API函数,用于将一个消息发送到指定的窗口,并等待接收消息的窗口处理完该消息后返回。这个函数与PostMessage的最大区别就是:SendMessage是同步的,消息会立即被处理,而不是被投递到消息队列中等待处理。
以下是SendMessage函数的原型:
LRESULT SendMessage(
HWND hWnd, // 窗口句柄
UINT Msg, // 消息
WPARAM wParam, // 额外的消息相关信息
LPARAM lParam // 额外的消息相关信息
);
参数含义:
hWnd:接收消息的窗口句柄。这可以是具体的窗口句柄,也可以是一些特殊值。
Msg:需要发送的消息码,如WM_CLOSE。
wParam,lParam:与消息有关的额外信息。
SendMessage函数会立即调用目标窗口的窗口过程,传递给它消息码和额外信息,等待窗口过程处理完消息后再返回。这意味着在消息处理完成前,SendMessage函数会一直被阻塞。
请注意,对于可能会导致长时间等待的操作(如网络操作或大量计算),不应使用SendMessage进行处理,因为这可能造成应用程序的阻塞。此时,应使用PostMessage进行异步操作,或者设计一种将这种操作异步化的机制。
■窗口程序运行过程
如图2-10所示,我们把Windows程序分为4个部分:Windows操作系统、主程序、窗口过程、其他应用程序。
●主程序我们已经非常熟悉了,分为五个固定的步骤,主程序的核心是一个消息循环,调用GetMessage函数不间断的从窗口消息队列获取消息。
●窗口过程负责处理消息,switch结构不处理的消息都交给DefWindProc默认窗口过程处理,处理后返回Windows系统。
●Windows操作存在一个总的消息队列和各个不同进程的窗口消息队列。Windows系统负责捕获消息或产生消息。如果是鼠标键盘消息则送入消息队列,非队列消息直接传递给窗口过程处理。Windows系统中的USER32.dll负责处理窗口界面处理。
●其他应用程序也可能向本进程发送消息。如果调用PostMessage函数向指定窗口过程发送消息,该消息将被分发到本进程的窗口消息队列。如果调用的是SendMessage函数发送的消息,则该消息会被直接发送给指定的窗口过程处理。
图2-10 窗口程序运行过程
举例
一个完整的消息传递过程:
第一步:Windows操作系统捕获键盘消息WM_KEYDOWN,并将该消息送入操作系统总消息队列。
第二步:Windows系统将键盘消息分发给当前焦点窗口的窗口消息队列。
第三步:主程序的消息循环GetMessage函数从消息队列中获取键盘消息。
第四步:TranslateMessage函数将WM_KEYDOWN消息转换为WM_CHAR消息,并将WM_CHAR消息重新送入消息队列。
第五步:DispatchMessage函数将WM_CHAR消息分发给Windows系统。
第六步:Windows系统将WM_CHAR消息传递给窗口过程WndProc函数处理。
第七步:WndProc函数的switch结构中处理WM_KEYDOWN消息(MessageBox窗口显示按键消息的虚拟键码),处理完之后将控制器交还给Windows操作系统。
第八步:Windows系统将任务切换到主程序,消息循环GetMessage函数继续从消息队列中获取消息,如果消息队列中没有消息,则继续等待。
请读者写一个实例程序,测试上述消息循环过程。请单步跟踪程序执行的过程。
本文摘自编程达人系列教材《Windows API每日一练》。公众号滴水编程达人。