Windows窗口消息和消息队列

消息队列

所有基于事件驱动的操作系统中的GUI程序,都会在主线程中运行一个消息泵来从消息队列中取出消息并执行对应的处理逻辑。消息队列中的消息除了由系统产生外,还提供了对应的API接口来将消息存放到消息队列中去。在Windows中所有线程中都可以有消息队列,并且可以建立消息泵来从消息队列中取消息,通过消息队列来进行数据的传递也是一种线程同步的机制。每个线程在建立时都会有一个THREADINFO结构,这个结构是一个未公开的内部数据结构。这个结构的定义大概如下:

struct THREADINFO
{
   登记消息队列指针(posted-message queue)
   发送消息队列指针(sended-message queue)
   应答消息队列指针(reply-message queue)
   虚拟输入队列指针(virtualized-input queue)
   唤醒标志(wake flag)
   结束代码(nExitCode)

// 下面是用来存放GUI线程的信息

   线程的活动窗口句柄
   线程的焦点窗口句柄
   线程鼠标捕获窗口句柄
   线程的拥有提示符的窗口句柄
   
   其他线程的状态变量

};

从上面看出每个线程有四个队列:一个登记消息队列,一个发送消息队列,一个应答消息队列,一个虚拟输入消息队列。每个线程在建立初始时并不会为线程建立消息队列,直到线程调用了与图形用户界面有关的函数(例如:CreateWindow, GetMessage)就会为线程建立消息队列。操作系统维护着一个系统消息队列和分别为每个GUI线程维护消息队列。当系统收到用户键盘和鼠标的输入时,键盘鼠标的驱动程序就会产生一个消息,并将消息投递到系统消息队列中,系统每一次从系统消息队列中检查一个消息,确定接收消息的目标线程,然后将消息从系统消息队列中删除,并把消息投递到线程的登记消息队列中。系统在运行的时候一个时间内只能有一个线程的窗口具有活动窗口,键盘输入焦点,鼠标捕获,和提示符。虽然每个TRHEADINFO都维护线程的这些信息,但当线程不具备活动窗口,键盘焦点,鼠标捕获和提示符时这些成员变量的值都将为NULL。下面将列出所有对消息队列操作的API。

消息队列函数

    1. 向登记消息队列投递消息的函数
BOOL PostMessage(
  HWND hWnd,      //NULL为向当前线程投递消息,为HWND_BROADCAST则向系统的所有Top-Level窗口发送消息
  UINT Msg,       // message
  WPARAM wParam,  // first message parameter
  LPARAM lParam   // second message parameter
);  //函数立即返回,失败返回0。 一般不要用这个函数来投递WM_QUIT消息
  • 2.将消息投递给指定的线程的函数。
BOOL PostThreadMessage(
  DWORD idThread, // thread identifier
  UINT Msg,       // message
  WPARAM wParam,  // first message parameter
  LPARAM lParam   // second message parameter
);

对于发送给线程的消息,由于没有窗口回调过程(WndCallback)会处理,因此需要在消息循环中处理发送给线程的消息。

  • 3.投递线程结束请求消息的函数
VOID PostQuitMessage(
  int nExitCode   // exit code
);  //这个函数通常是在程序主窗口的WM_DESTROY消息处理中调用

这个函数的内部实现类似(注意只是类似): PostThreadMessage(GetCurrentThreadId(), WM_QUIT, nExitCode, 0);

但是PostQuitMessage函数并没有将WM_QUIT消息放到线程的登记消息队列中去,而只是将线程的THREADINFO结构中的唤醒标志设置为QS_QUIT。并将THREADINFO中的退出代码设置为nExitCode, 这样当线程调用GetMessage函数时若检测到唤醒标志为QS_QUIT则将THREADINFO结构的退出代码赋值给MSG的wParam成员,并且返回FALSE,这样就可以使消息循环结束,并调用: return msg.wParam;

  • 4.向窗口发送消息的函数
LRESULT SendMessage(
  HWND hWnd,      // handle to destination window
  UINT Msg,       // message
  WPARAM wParam,  // first message parameter
  LPARAM lParam   // second message parameter
);  

这个函数要直到相应的消息处理完成后才返回。这个函数对于处理发送给本线程的消息和发送给别的线程的消息的处理不同。对于发送给本线程的消息(hWnd是本线程的窗口句柄),则SendMessage函数直接调用相应的窗口的回调函数,让后将回调函数的返回作为SendMessage函数的返回;而对于将消息发送给别的线程的窗口(hWnd并不是本线程的窗口)时则处理比较复杂:

一. 发送线程将消息追加到接收线程的发送消息队列中,并将接收线程的唤醒标志设置为QS_SENDMESSAGE,然后发送线程进入阻塞状态。

二. 接收线程等到调用GetMessage(或PeekMessage, WaitMessage)时先检查本线程的唤醒标志是否为QS_SENDMESSAGE,若是则将本线程中的发送消息队列中的消息取出,一直处理完发送消息队列中的所有消息,当每完成一条消息的处理后,接收线程就会将处理的结果追加到发送线程的应答消息队列中。

三. 当发送线程检测到本线程的应答消息队列中有消息时就将本线程激活并将应答消息队列中的结果做为发送线程的SendMessage函数的返回值。

四. 在发送线程等待结果前进入阻塞的这段时间里,若发现本线程的发送消息对列中有消息时,也会去处理本线程的所有发送消息队列中的消息,也就是说,在SendMessage函数返回前,可以确保本线程的发送消息队列中没有任何消息,并且应答消息队列中也没有任何消息。

发送消息的函数SendMessage可能造成线程之间进入死锁状态,因此可以使用下面一系列函数来防止发送消息时产生死锁(用于不同线程之间发送):

(1). 具有超时机制的发送消息函数

BOOL SendMessageTimeout(
  HWND hWnd,            // handle to window
  UINT Msg,             // message
  WPARAM wParam,        // first message parameter
  LPARAM lParam,        // second message parameter
  UINT fuFlags,         // send options
  UINT uTimeout,        // time-out duration
  PDWORD_PTR lpdwResult //[OUT] return value for synchronous call
);  //函数调用失败或超时返回0,正确返回TRUE.

fuFlags: 可以是如下联合: SMTO_NORMAL: 正常为0, 不与其他联合 SMTO_ABORTIFHUNG: 若是接收线程正处于挂起状态时则只将消息放入接收线程的发送消息队列中,并且函数立即返回 SMTO_BLOCK: 在等待应答时,不处理本线程发送消息队列中的消息 SMTO_NOTIMEOUTIFNOTHUNG: 若接收线程没有挂起时,则忽略uTimeOut参数

uTimeout: 发送线程等待的时间,为豪秒 lpdwResult: 保存消息返回的结果

(2).具有回调函数的发送消息函数

BOOL SendMessageCallback(
  HWND hWnd,                // handle to window
  UINT Msg,                 // message
  WPARAM wParam,            // first message parameter
  LPARAM lParam,            // second message parameter
  SENDASYNCPROC lpCallBack, // callback function
  DWORD dwData          // 传递给回调函数的参数
);  //函数不是返回消息的结果

这个函数会立即返回,而当接收消息的线程处理完后会将一个应答消息放入发送消息的应答消息队列中,直到发送消息线程有函数来取应答消息队列中的消息时,回调函数才能调用,当发送广播消息时,每个顶级窗口处理完后都会使的发送线程执行一次回调函数。回调函数的格式如下:

VOID CALLBACK SendAsyncProc(
  HWND hwnd,        // handle to destination window
  UINT uMsg,        // message
  ULONG_PTR dwData, // application-defined value
  LRESULT lResult   // result of message processing, 消息处理产生的结果
);  //对于本线程发送消息来说,当调用完窗口过程后,立即调用这个回调函数,回调函数执行完后,继续执行SendMessage后的代码
    1. 发送通知消息的函数
BOOL SendNotifyMessage(
  HWND hWnd,      // handle to window
  UINT Msg,       // message
  WPARAM wParam,  // first message parameter
  LPARAM lParam   // second message parameter
);

这个函数对于发送给本线程时跟SendMessage相同,而发送给不同线程时,则将消息追加到接收线程的发送消息队列,然后立即返回。这个函数的返回值不是消息的返回值而是判断函数调用正确与否的返回值。这个函数不会使发送线程进入阻塞,也不会在本线程中加入应答消息.

  • 5.应答一个消息 我们都知道在不同线程之间发送消息时,发送线程一旦检测到本线程的应答消息队列中有消息时就会返回,而函数:
BOOL ReplyMessage(
  LRESULT lResult   // message-specific reply
);

当一个线程调用ReplyMessage时,它是要告诉系统,为了知道消息结果,它已经完成了足够的工作,结果应该包装起来并登记到发送线程的应答消息队列中。这将使发送线程醒来,获得结果,并继续执行。调用ReplyMessage的线程在lResult参数中指出消息处理的结果。在调用ReplyMessage之后,发送消息的线程恢复执行,而处理消息的线程继续处理消息。两个线程都不会被挂起,都可以正常执行。当处理消息的线程从它的窗口过程返回时,它返回的任何值都被忽略。这里的问题是, ReplyMessage必须在接收消息的窗口过程中调用,而不是由调用某个SendXXX函数的线程调用。为了编写保护性代码,最好不要用前面讨论过的三个SendXXX函数中的一个代替对SendMessage的调用,而是依靠窗口过程的实现者来调用ReplyMessage。还应该知道,如果在处理一个由同一线程发送来的消息时调用ReplyMessage,则该函数什么也不做。实际上,这就是ReplyMessage的返回值所指出的。如果你在处理线程间的消息发送时调用了ReplyMessage,则它返回TRUE ,如果你在处理线程内的消息发送时调用ReplyMessage,它返回FALSE

  • 6.在处理一个消息时,确认此消息时由本线程发送的还是由其他线程发送而来的。
BOOL InSendMessage(VOID);   //是其他线程发来的消息返回TRUE,本线程发来或登记的消息返回FALSE

DWORD InSendMessageEx(
  LPVOID lpReserved      // not used; must be NULL
);

扩展函数的返回值若为ISMEX_NOSEND(0)则表示是线程内发来或登记的消息,否则为如下联合:

ISMEX_CALLBACK: 表明发送线程是调用SendMessageCallback函数发送的,发送线程没有处于阻塞状态. ISMEX_NOTIFY : 表示发送线程是调用的SendNotifyMessage函数发送的 ISMEX_REPLIED: 表明接收线程已经调用了ReplyMessage函数,而发送线程也没有处于阻塞状态了 ISMEX_SEND :发送线程使用了SendMessage或SendMessageTimeout函数发来的消息,并且发送线程正处于阻塞状态,等待着应答

  • 7.注册一个消息
UINT RegisterWindowMessage(
  LPCTSTR lpString   // message string
);

函数调用成功返回0XC000到0XFFFF之间的值,失败返回0, 对于不同进程之间使用相同的消息名称所得到的消息ID的值是一样的.消息的分配如下:

0x0000-0x03FF: 为系统定义的消息 0x0400-0x7FFF: 为每个窗口类私有的消息 #define WM_USER 0x0400 0x8000-0xBFFF: 为应用程序私有的消息 #define WM_APP 0x8000 0xC000-0xFFFF: 为调用RegisterWindowMessage函数产生的系统唯一的消息 0xFFFF- : 保留给系统未来使用

  • 8.获取最近的GetMessage所检索的消息的信息

一. 获取最近的GetMessage函数所检索到的消息时的cursor的位置

DWORD GetMessagePos(VOID);   //低字X坐标, 高字Y坐标

二. 获取最近的GetMessage函数所检索的消息放置在队列中的时间

LONG GetMessageTime(VOID);   //单位豪秒,这个时间若超过了某个时间又会归0
    1. 设置和获取与本线程的消息队列相关联的附加信息
LPARAM SetMessageExtraInfo(
  LPARAM lParam  // application-defined value ,将一个程序定义的附加信息的值与调用函数的线程的消息队列关联起来
);  //函数返回前一个关联的信息

LPARAM GetMessageExtraInfo(VOID);  //获取
  • 10.获取线程消息队列的状态信息
BOOL GetInputState(VOID);  判断当前消息队列中是否有键盘和鼠标的消息,有返回TRUE,没有返回FALSE


DWORD GetQueueStatus(
  UINT flags   // message types
);  //线程的消息队列的状态存放在THREADINFO结构的唤醒标志成员中,用户可以联合指定属性,函数的高字返回指定属性的一个子集,低字返回自上一次调用GetMessage/PeekMessage/GetQueueStatus函数以来存放在消息队列中的消息的类型的联合
  • 11.从消息队列中获取消息的函数
BOOL GetMessage(
  LPMSG lpMsg,         //[OUT] message information
  HWND hWnd,           // handle to window,指定只获取属于窗口和窗口子窗口的消息,若为NULL则所有的窗口的消息和线程消息
  UINT wMsgFilterMin,  // first message
  UINT wMsgFilterMax   // last message  //若两个都为0则表示不限制
);  //若消息队列中没有投递消息则调用线程将会被挂起,若函数检测到QS_QUIT标志则返回FALSE, 否则返回TRUE,函数调用失败返回-1


typedef struct tagMSG {
    HWND   hwnd; 
    UINT   message; 
    WPARAM wParam; 
    LPARAM lParam; 
    DWORD  time;   //消息存放在队列中的时间
    POINT  pt;    //消息产生时鼠标的位置
} MSG, *PMSG; 



BOOL PeekMessage(
  LPMSG lpMsg,         // message information
  HWND hWnd,           // handle to window
  UINT wMsgFilterMin,  // first message
  UINT wMsgFilterMax,  // last message
  UINT wRemoveMsg      // removal options
);   //若是消息队列中没有投递消息则函数立即返回


PeekMessage和GetMessage都会在内部处理完线程的所有发送消息队列中的消息
  • 12.等待消息
BOOL WaitMessage(VOID);

当调用这个函数后线程将会挂起,直到线程的消息队列中有消息时线程才会恢复执行

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏WebDeveloper

Thinkphp的cron计划任务

cron1默认在ThinkPHP\Library\Cron\cron1.php如果没有cron目录则新建一个,cron1.php自己所要执行的脚本

1503
来自专栏Ken的杂谈

ASP.NET Core 入门教程 3、ASP.NET Core MVC路由入门

本篇代码基于上一篇进行调整:https://github.com/ken-io/asp.net-core-tutorial/tree/master/chapte...

1453
来自专栏iOS开发攻城狮的集散地

runloop的解读

1916
来自专栏walterlv - 吕毅的博客

如何创建一个基于命令行工具的跨平台的 NuGet 工具包

发布于 2018-05-12 01:09 更新于 2018-09...

782
来自专栏有困难要上,没有困难创造困难也要上!

Pyinstaller ERROR: Assembly amd64_Microsoft.VC90.CRT_1fc8b3b9a1e18e3b_9.0.21022.8_none not found

4088
来自专栏Ken的杂谈

ASP.NET Core 入门教程 2、使用ASP.NET Core MVC框架构建Web应用

ASP.NET Core 默认集成了DI。所有官方模块的引入都要使用DI的方式引入。

1242
来自专栏ml

ijg库的使用的几点注意

ijg库(http://www.ijg.org/)是用于处理jpeg解码和压缩的库,最新版本为2014发布的版本,可以在官网中下载jpegsr9a.zip 使用...

3415
来自专栏圣杰的专栏

Shadow Copying导致ASP.NET应用启动很慢的解决办法

我们安装一个应用程序并启动后,我们是无法更新应用程序安装目录中程序集文件的。如果强制替换会提示文件正在使用,如下图所示。

761
来自专栏章鱼的慢慢技术路

Linux操作_常用命令操作练习

1664
来自专栏吴伟祥

常用的 Java核心包 原

JVM的常用包一般在C:\Program Files\Java\jre1.5.0_04\lib\rt.jar 一般都会放在C:\Program Files\J...

1613

扫码关注云+社区

领取腾讯云代金券