要点回顾
1)一个GUI线程包含一个消息队列
普通线程
↓
GUI线程
↓
THEWAD.W32THREAD
↓
THREADINFO
↓
消息队列
2)一个线程可以包含多个窗口,所有窗口共享一个消息队列
_WINDOW_OBJECT //0环创建
↓
PTHREADINFO pti //所属线程
↓
WNDPROC lpfnWndProc //窗口过程(窗口回调函数)
消息循环
MSG msg;
while(GetMessage(&msg, NULL, 0, 0)) //从消息队列中取出消息
{
TranslateMessage(&msg); //加工消息
DispatchMessage(&msg); //分发消息
}
思考:使用SendMessage与PostMessage发送的消息位于同一组队列中吗?
答案:不同类型的消息被置于不同队列。
消息队列
描述:消息队列共有七组,用于存放不同类型的消息。
例如:
1)SentMessagesListHead //接收SendMessage发来的消息
2)PostedMessagesListHead //接收PostMessage发来的消息
3)HardwareMessagesListHead //接收键盘、鼠标等硬件设备的消息
...
完整队列:
//ReactOS v3.12
typedef struct _USER_MESSAGE_QUEUE
{
...
/* Owner of the message queue */
struct _ETHREAD *Thread;
/* Queue of messages sent to the queue. */
LIST_ENTRY SentMessagesListHead;
/* Queue of messages posted to the queue. */
LIST_ENTRY PostedMessagesListHead;
/* Queue of sent-message notifies for the queue. */
LIST_ENTRY NotifyMessagesListHead;
/* Queue for hardware messages for the queue. */
LIST_ENTRY HardwareMessagesListHead;
...
/* messages that are currently dispatched by other threads */
LIST_ENTRY DispatchingMessagesHead;
/* messages that are currently dispatched by this message queue, required for cleanup */
LIST_ENTRY LocalDispatchingMessagesHead;
...
}
消息的接收
GetMessage
描述:从消息队列中取出消息
BOOL WINAPI GetMessage(
LPMSG lpMsg, //返回从队列中取出的消息
HWND hWnd, //过滤条件一:窗口句柄
UINT wMsgFilterMin, //过滤条件二:最小值
UINT wMsgFilterMax //过滤条件三:最大值
);
主要功能:循环判断是否存在属于该窗口的消息,若存在,则将消息存储到MSG指定的结构中,并将消息从列表中删除。
注意:事实上,GetMessage还做了一件很重要的事情,即在接收消息时,将SentMessagesListHead中的消息进行处理
大致流程:
User32!GetMessage
↓
w32k!NtUsrGetMessage
↓
do
{
//先判断SentMessagesListHead中是否存在消息,若存在则进行处理
do
{
...
KeUserModeCallback(USER32_CALLBACK_WINDOWPROC,
Arguments,
ArgumentLength,
&ResultPinter,
&ResultLength);
...
}
}while(SentMessagesListHead != NULL)
源码(ReactOS v3.12):
BOOL APIENTRY
NtUserGetMessage( PNTUSERGETMESSAGEINFO UnsafeInfo,
HWND hWnd,
UINT MsgFilterMin,
UINT MsgFilterMax )
{
...
do
{
GotMessage = co_IntPeekMessage(&Msg, Window, MsgFilterMin, MsgFilterMax, PM_REMOVE);
...
}while (! GotMessage); //对应外层循环
...
}
BOOL FASTCALL
co_IntPeekMessage( PUSER_MESSAGE Msg,
PWINDOW_OBJECT Window,
UINT MsgFilterMin,
UINT MsgFilterMax,
UINT RemoveMsg )
{
...
/* Dispatch sent messages here. */
while (co_MsqDispatchOneSentMessage(ThreadQueue)); //对应内层循环
...
}
BOOLEAN FASTCALL
co_MsqDispatchOneSentMessage(PUSER_MESSAGE_QUEUE MessageQueue)
{
...
if (Message->HookMessage == MSQ_ISHOOK)
{
...
}
else if (Message->HookMessage == MSQ_ISEVENT)
{
...
}
else
{
/* 发送消息 */
Result = co_IntSendMessage(Message->Msg.hwnd,
Message->Msg.message,
Message->Msg.wParam,
Message->Msg.lParam);
}
/* 从消息队列中删除该消息 */
RemoveEntryList(&Message->ListEntry);
...
/* 调用回调函数 */
if (Message->CompletionCallback != NULL)
{
co_IntCallSentMessageCallback(Message->CompletionCallback,
Message->Msg.hwnd,
Message->Msg.message,
Message->CompletionCallbackContext,
Result);
}
...
}
VOID APIENTRY
co_IntCallSentMessageCallback(SENDASYNCPROC CompletionCallback,
HWND hWnd,
UINT Msg,
ULONG_PTR CompletionCallbackContext,
LRESULT Result)
{
...
/* 回到三环进行处理 */
Status = KeUserModeCallback(USER32_CALLBACK_SENDASYNCPROC,
&Arguments,
sizeof(SENDASYNCPROC_CALLBACK_ARGUMENTS),
&ResultPointer,
&ResultLength);
...
}
实验1:理解GetMessage
第一步:编译并运行程序A
#include <windows.h>
LRESULT CALLBACK WindowProc(
IN HWND hwnd,
IN UINT uMsg,
IN WPARAM wParam,
IN LPARAM lParam
){
switch(uMsg)
{
case 0x401:
MessageBoxA(NULL, "测试窗口接收到消息", "新消息", MB_OK);
return false;
}
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
int APIENTRY WinMain(
HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nShowCmd
){
//窗口的类名
TCHAR className[] = "My First Window";
//创建一个自己的窗口
WNDCLASS wndclass = {0};
wndclass.hbrBackground = (HBRUSH)COLOR_MENU;
wndclass.lpfnWndProc = WindowProc;
wndclass.lpszClassName = className;
wndclass.hInstance = hInstance;
//注册
RegisterClass(&wndclass);
//创建窗口
HWND hwnd = CreateWindow(
className,
TEXT("测试窗口"),
WS_OVERLAPPEDWINDOW,
10,
10,
600,
300,
NULL,
NULL,
hInstance,
NULL);
if(hwnd == NULL)
return 0;
//显示窗口
ShowWindow(hwnd, SW_SHOW);
//消息循环
MSG msg;
while(GetMessage(&msg, NULL, 0, 0))
{
//TranslateMessage(&msg);
//DispatchMessage(&msg);
}
return 0;
}
运行结果:
注意:不要关闭窗口
第二步:编译并运行程序B
#include <stdio.h>
#include <windows.h>
int main()
{
HWND hwnd = FindWindow("My First Window", "测试窗口");
SendMessage(hwnd, 0x401, 0, 0);
return 0;
}
运行结果:
同步与异步
描述:
- 使用SendMessage发送消息时,会等待消息处理之后再返回,这个过程称为同步。
- 使用PostMessage发送消息时,不会等待消息处理,直接返回,这种方式称为异步。
实验2:理解同步与异步
第一步:编译并运行实验1中的程序A
运行结果:
第二步:编译并运行实验1中的程序B
在return处设置断点:
运行结果:
此时,程序B并未继续向下执行。
第三步:点击确定按钮
执行结果:
程序A处理消息后,程序B继续向下执行。
第四步:修改程序B的代码,再次运行
新代码:
#include <stdio.h>
#include <windows.h>
int main()
{
HWND hwnd = FindWindow("My First Window", "测试窗口");
PostMessage(hwnd, 0x401, 0, 0); //SendMessage改为PostMessage
return 0;
}
执行结果:
程序B发送消息后未等待目标处理继续向下执行,程序A并未处理消息。
消息的分发
描述:GetMessage能够处理SentMessagesListHead队列中的消息,其他队列中的消息则由DispatchMessage进行分发处理。
大致流程:
- 根据窗口句柄找到窗口对象。
- 根据窗口对象得到窗口过程函数,由0环进行调用。
源码(ReactOS v3.12):
LRESULT APIENTRY
NtUserDispatchMessage(PMSG UnsafeMsgInfo)
{
...
if (!Hit) Res = IntDispatchMessage(&SafeMsg);
...
}
LRESULT
FASTCALL
IntDispatchMessage(PMSG pMsg)
{
...
retval = co_IntCallWindowProc( Window->Wnd->lpfnWndProc,
!Window->Wnd->Unicode,
pMsg->hwnd,
pMsg->message,
pMsg->wParam,
lParamPacked,
lParamBufferSize);
...
}
LRESULT APIENTRY
co_IntCallWindowProc(WNDPROC Proc,
BOOLEAN IsAnsiProc,
HWND Wnd,
UINT Message,
WPARAM wParam,
LPARAM lParam,
INT lParamBufferSize)
{
...
/* 回到三环,调用窗口过程函数 */
Status = KeUserModeCallback(USER32_CALLBACK_WINDOWPROC,
Arguments,
ArgumentLength,
&ResultPointer,
&ResultLength);
...
}
DispatchMessage
描述:用于消息分发,根据窗口句柄调用相关的窗口过程,通常用于分发由GetMessage函数检索到的消息。
LRESULT DispatchMessage(
CONST MSG *lpmsg // message information
);
思考:为什么DispatchMessage要对消息进行分发而不统一处理?
答案:所有窗口共享一个消息队列,且每个窗口都拥有属于自己的回调函数。
MSG结构体成员:
typedef struct tagMSG {
HWND hwnd; //窗口句柄
UINT message;
WPARAM wParam;
LPARAM lParam;
DWORD time;
POINT pt;
} MSG, *PMSG;
DispatchMessage根据MSG->hwnd调用各句柄对应窗口对象的回调函数。
思考:既然GetMessage已经得到了句柄信息,为什么不能直接调用对应的回调函数而需要分发。
答案:因为句柄指向的是句柄表,而不是窗口对象,需要进入0环通过句柄表找到窗口对象,才能调用对应的回调函数。
实验3:理解DispatchMessage
第一步:编译并运行以下代码
#include <stdio.h>
#include <windows.h>
LRESULT CALLBACK WindowProc(
IN HWND hwnd,
IN UINT uMsg,
IN WPARAM wParam,
IN LPARAM lParam
){
switch(uMsg)
{
case 0x401:
{
MessageBoxA(NULL, "测试窗口接收到消息", "新消息", MB_OK);
return 0;
}
case WM_DESTROY:
{
ExitProcess(0);
return 0;
}
}
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
int APIENTRY WinMain(
HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nShowCmd
){
//窗口的类名
TCHAR className[] = "My First Window";
//创建一个自己的窗口
WNDCLASS wndclass = {0};
wndclass.hbrBackground = (HBRUSH)COLOR_MENU;
wndclass.lpfnWndProc = WindowProc;
wndclass.lpszClassName = className;
wndclass.hInstance = hInstance;
//注册
RegisterClass(&wndclass);
//创建窗口
HWND hwnd = CreateWindow(
className,
TEXT("测试窗口"),
WS_OVERLAPPEDWINDOW,
10,
10,
600,
300,
NULL,
NULL,
hInstance,
NULL);
if(hwnd == NULL)
return 0;
//显示窗口
ShowWindow(hwnd, SW_SHOW);
//消息循环
MSG msg;
while(GetMessage(&msg, NULL, 0, 0))
{
//TranslateMessage(&msg);
//DispatchMessage(&msg);
}
return 0;
}
运行结果:
第二步:操作窗口
拖动窗口、点击最小化、缩放、关闭按钮均无反应。
第三步:去掉DispatchMessage注释,重新运行
MSG msg;
while(GetMessage(&msg, NULL, 0, 0))
{
//TranslateMessage(&msg);
DispatchMessage(&msg);
}
第四步:再次操作窗口
拖动窗口、点击最小化、缩放、关闭按钮均得到相应。
TranslateMessage
描述:用于将虚拟键码转换为字符消息。该字符消息又被发送给对应线程(调用TranslateMessage的线程)的消息队列,当线程再次调用GetMessage函数或PeekMessage函数获取消息的时候被读取。
实验4:理解TranslateMessage
第一步:编译并运行以下代码
#include <stdio.h>
#include <windows.h>
LRESULT CALLBACK WindowProc(
IN HWND hwnd,
IN UINT uMsg,
IN WPARAM wParam,
IN LPARAM lParam
){
TCHAR szBuffer[MAX_PATH];
switch(uMsg)
{
/*
case WM_KEYDOWN:
{
sprintf(szBuffer, "键%d被你按下了\x00", wParam);
MessageBox(NULL, szBuffer, "WM_KEYDOWN", MB_OK);
return 0;
}
*/
case WM_CHAR:
{
sprintf(szBuffer, "你按下了%c键\x00", wParam);
MessageBox(NULL, szBuffer, "WM_CHAR", MB_OK);
return 0;
}
}
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
int APIENTRY WinMain(
HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nShowCmd
){
//窗口的类名
TCHAR className[] = "My First Window";
//创建一个自己的窗口
WNDCLASS wndclass = {0};
wndclass.hbrBackground = (HBRUSH)COLOR_MENU;
wndclass.lpfnWndProc = WindowProc;
wndclass.lpszClassName = className;
wndclass.hInstance = hInstance;
//注册
RegisterClass(&wndclass);
//创建窗口
HWND hwnd = CreateWindow(
className,
TEXT("测试窗口"),
WS_OVERLAPPEDWINDOW,
10,
10,
600,
300,
NULL,
NULL,
hInstance,
NULL);
if(hwnd == NULL)
return 0;
//显示窗口
ShowWindow(hwnd, SW_SHOW);
//消息循环
MSG msg;
while(GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
return 0;
}
运行结果:
第二步:敲击不同按键
1)小写模式,按下"A":
2)按下大小写键,切换到大写模式:
无弹窗
3)大写模式,按下”A“:
4)按下shift键:
无弹窗
5)小写模式,按下shift+“A”:
第三步:修改代码并重新运行程序
新代码:
#include <stdio.h>
#include <windows.h>
LRESULT CALLBACK WindowProc(
IN HWND hwnd,
IN UINT uMsg,
IN WPARAM wParam,
IN LPARAM lParam
){
TCHAR szBuffer[MAX_PATH];
switch(uMsg)
{
case WM_KEYDOWN:
{
sprintf(szBuffer, "键%d被你按下了\x00", wParam);
MessageBox(NULL, szBuffer, "WM_KEYDOWN", MB_OK);
return 0;
}
/*
case WM_CHAR:
{
sprintf(szBuffer, "你按下了%c键\x00", wParam);
MessageBox(NULL, szBuffer, "WM_CHAR", MB_OK);
return 0;
}
*/
}
return DefWindowProc(hwnd, uMsg, wParam, lParam);
}
int APIENTRY WinMain(
HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nShowCmd
){
//窗口的类名
TCHAR className[] = "My First Window";
//创建一个自己的窗口
WNDCLASS wndclass = {0};
wndclass.hbrBackground = (HBRUSH)COLOR_MENU;
wndclass.lpfnWndProc = WindowProc;
wndclass.lpszClassName = className;
wndclass.hInstance = hInstance;
//注册
RegisterClass(&wndclass);
//创建窗口
HWND hwnd = CreateWindow(
className,
TEXT("测试窗口"),
WS_OVERLAPPEDWINDOW,
10,
10,
600,
300,
NULL,
NULL,
hInstance,
NULL);
if(hwnd == NULL)
return 0;
//显示窗口
ShowWindow(hwnd, SW_SHOW);
//消息循环
MSG msg;
while(GetMessage(&msg, NULL, 0, 0))
{
//TranslateMessage(&msg);
DispatchMessage(&msg);
}
return 0;
}
运行结果:
第四步:敲击不同按键
1)小写模式,按下"A":
2)按下大小写键,切换到大写模式:
3)大写模式,按下”A“:
4)按下shift键:
总结
- GetMessage的不止能够取出消息,还能够处理消息。
- GetMessage处理消息时,从0环回到3环调用窗口过程函数进行处理。
- SendMessage发送消息后会等待目标处理完毕,PostMessage不会。
- GetMessage只会处理SentMessagesListHead列表中的消息。
- DispatchMessage的作用是分发除了SentMessagesListHead队列以外的消息给各窗口对象进行处理。
- TranslateMessage只处理键盘消息,将虚拟键码转换为字符消息,再由DispatchMessage进行分发。
- 未经过TranslateMessage处理的键盘消息,虚拟键码由WM_KEYDOWN进行处理;经过TranslateMessage处理的键盘消息,字符消息由WM_CHAR进行处理