消息队列
typedef struct _Color
{
DWORD r,g,b;
}Color;
typedef struct _WindowClass
{
DWORD x;
DWORD y;
DWORD width;
DWORD hight;
Color color;
}WindowClass;
//画窗口
void PaintWindows(HDC hdc,WindowClass* Win)
{
//取图形对象
HBRUSH hBrush = (HBRUSH)GetStockObject(DC_BRUSH);
SelectObject(hdc,hBrush);//画刷
SetDCBrushColor(hdc,RGB(Win->color.r, Win->color.g, Win->color.b));
MoveToEx(hdc,Win->x,Win->y,NULL);
LineTo(hdc,Win->x+Win->width,Win->y);
LineTo(hdc, Win->x+Win->width,Win->y+Win->hight);
LineTo(hdc, Win->x, Win->y+Win->hight);
LineTo(hdc, Win->x, Win->y);
Rectangle(hdc,Win->x,Win->y,Win->x+Win->width,Win->y+Win->hight+1);
DeleteObject(hBrush);
}
void main()
{
char Message;
HWND hwnd;
HDC hdc;
//设置窗口参数
WindowClass win;
win.x = 10;
win.y = 10;
win.width = 300;
win.hight = 300;
win.color.r = 0x10;
win.color.g = 0x20;
win.color.b = 0x30;
hwnd = GetDesktopWindow();//获取桌面句柄
hdc = GetWindowDC(hwnd);
while (true)
{
PaintWindows(hdc, &win);
//接收消息
Message = getchar();
switch (Message)
{
case 'a':
win.color.r += 0x10;
win.color.g += 0x20;
win.color.b += 0x30;
break;
case 'b':
win.color.r -= 0x10;
win.color.g -= 0x20;
win.color.b -= 0x30;
break;
}
}
getchar();
}
Windows所有图像界面编程的底层实现都是通过Gdi画出来的。
但我们通过微软提供的Api来创建窗口可以接收到,鼠标、键盘、或者其他进程发送的消息等等,上面的代码只能接收到键盘的消息。
唯一的解决分案就是给它提供一块内存,保所有接收到的消息都往这块内存里放。
消息队列
KTHREAD.Win32Thread如果你的进程调用了图形界面的api这个成员会指向_THREADINFO结构体,这个结构体的MessageQueue成员就是“消息队列”,
没调用图形界面的api, Win32Thread就为NULL。
GUI线程
<1> 当线程刚创建的时候,都是普通线程:
Thread.ServiceTable-> KeServiceDescriptorTable(ssdt)
<2> 当线程第一次调用Win32k.sys时,会调用一个函数:PsConvertToGuiThread //当前线程转为GUI线程
主要做几件事:
- 扩充内核栈,必须换成64KB的大内核栈,因为普通内核栈只有12KB大小。
- 创建_THREADINFO结构体,这结构体包含消息队列,并挂到KTHREAD上。
- Thread.ServiceTable-> KeServiceDescriptorTableShadow
- 把需要的内存数据映射到本进程空间
每一个GUI线程对应1个消息队列
普通线程是看不了SSSDT表的,调用了win32k里函数的线程才能看到SSSDT表。
窗口与线程
消息是程序发送的,可以通过GetMessage获取。
点击关闭后产生消息,这消息会存储到对应窗口的线程的消息队列中。
调用w32k的模块会先调用InitInputImpl()初始化,这函数启动了两条线程,一条用来监控键盘,一条监控鼠标
某些原因导致程序突然卡死了,但鼠标还可以动,这是因为鼠标是有着自己独立的线程
创建窗口过程:
user32.dll CreateWindowEx
1. CreateWindowEx()
2. _VerNtUserCreateWindowEx()
3. _NtUserCreateWindowEx() 这里开始进入0环
窗口对象:
_WINDOW_OBJECT
...
PTHREADINFO pti; //所属线程
...
创建窗口要提供一个窗口过程(回调函数)WNDPROC lpfnWndProc
Windows建立了一张表,所有窗口对应的结构体地址都在里面,返回3环只是返回了索引
窗口句柄是全局的,窗口创建后句柄就已经确定,你在别的进程中通过FindWindow获取它得到的值是一样的。
- 窗口是在0环创建的
- 窗口句柄是全局的
- 一个线程可以用多个窗口,但每个窗口只能属于一个线程
消息的接收
创建窗口就是在0环创建_WINDOW_OBJECT结构体,然后赋上该有的值
MSG msg;
while(GetMessage(&msg,NULL,0,0))
{
TranslateMessage(&msg); //翻译消息
DispatchMessage(&msg); //分发消息
}
GetMessage(LPMSG lpMsg, //返回从队列中摘下来的消息
HWND hWnd, //过滤条件一:发个这个窗口的消息
UNIT wMsgFilterMin, //过滤条件
UNIT wMsgFilterMax //过滤条件
);
GetMessage的主要功能:
循环判断是否有该窗口的消息,如果有,将消息存储到MSG指定的结构,并将消息从列表中删除。
还会处理掉SentMessages()发送来的消息
MSG msg;
while(GetMessage(&msg,NULL,0,0))
{
//TranslateMessage(&msg); //翻译消息
//DispatchMessage(&msg); //分发消息
}
也就是说就是你将这里面两行注释掉一样会处理SentMessagesListHead里的消息
消息队列的结构
1. SentMessagesListHead //接到SendMessage发来的消息
2. PostedMessagesListHead //接到PostMessage发来的消息
3. HardwareMessagesListHead //接到鼠标、键盘的消息
略...
消息队列有7组,我这只列出了3组
NtUserGetMessage的执行流程:
User32!GetMessage() 调用 w32k!NtUserGetMessage()
co_IntGetPeekMessage()
co_IntPeekMessage()
co_MsqDispatchOneSentMessage()
co_IntCallSentMessageCallback()
KeUserModeCallback()//返回3环调用3环窗口过程
do
{
//先判断SentMessagesListHead是否有消息 如果有处理掉
do
{
....
KeUserModeCallback(USER32_CALLBACK_WINDOWPROC,
Arguments,
ArgumentLength,
&ResultPointer,
&ResultLength);
....
}while(SentMessagesListHead != NULL)
//依次判断其他的6个队列,里面如果有消息 返回 没有继续
}while(其他队列!=NULL)
详细版伪代码和上那个一样的
co_IntCallSentMessageCallback(SENDASYNCPROC CompletionCallback,
HWND hWnd,
UINT Msg,
ULONG_PTR CompletionCallbackContext,
LRESULT Result)
{
略......
KeUserModeCallback(USER32_CALLBACK_SENDASYNCPROC,
&Arguments,
sizeof(SENDASYNCPROC_CALLBACK_ARGUMENTS),
&ResultPointer,
&ResultLength);
略......
}
co_MsqDispatchOneSentMessage(_In_ PTHREADINFO pti)
{
略......
co_IntCallSentMessageCallback(Message->CompletionCallback,
Message->Msg.hwnd,
Message->Msg.message,
Message->CompletionCallbackContext,
Message->lResult);
略......
}
co_IntPeekMessage(PMSG Msg,
PWND Window,
UINT MsgFilterMin,
UINT MsgFilterMax,
UINT RemoveMsg,
LONG_PTR *ExtraInfo,
BOOL bGMSG )
{
略......
do
{
略......
while ( co_MsqDispatchOneSentMessage(pti) )
{
if (HIWORD(RemoveMsg) && !bGMSG) Hit = TRUE;
}
略......
}while(SentMessagesListHead != NULL)
略.....
}
co_IntGetPeekMessage(PMSG pMsg,
HWND hWnd,
UINT MsgFilterMin,
UINT MsgFilterMax,
UINT RemoveMsg,
BOOL bGMSG )
{
略......
do
{
Present = co_IntPeekMessage( pMsg,
Window,
MsgFilterMin,
MsgFilterMax,
RemoveMsg,
&ExtraInfo,
bGMSG );
略......
//依次判断其他的6个队列,里面如果有消息 返回 没有继续
}while(其他队列!=NULL)
略......
}
SendMessage与PostMessage的区别(同步与异步):
1. 同步
SendMessage()发送消息,GetMessage();接收,进入0环要遍历SentMessagesListHead有没有消息,
有就处理,没有就返回,
必须要处理完才会返回结果,SendMessage()要接收到结果才会结束否则会一直 堵塞 在这
2. 异步
PostMessage()发送消息,GetMessage()只会接收它的消息,不会处理,
它的消息由TranslateMessage(&msg)与DispatchMessage(&msg)来处理
PostMessage()发送完后是不会等待你的处理结果的,发完立马就结束
消息的分发
MSG msg;
while(GetMessage(&msg,NULL,0,0))
{
TranslateMessage(&msg); //翻译消息 (处理键盘码的)
DispatchMessage(&msg); //分发消息 根据窗口句柄调用窗口过程
}
一个线程可以有多个窗口(按钮之类的控件也算窗口),它们共享着一个消息队列,
GetMessage()会把这个线程的所有消息都取出来,所以叫“分发消息”
TranslateMessage(&msg); //翻译消息
如果注释掉TranslateMessage(&msg)它就只能接收到键盘码
case WH_KEYDOWN:
{
sprintf(szBuffer,"你按下了%d键",wParam);
MessageBox(hwnd,szBuffer,NULL,MB_OK);
}
加了TranslateMessage(&msg)它就能帮你翻译为字符
case WH_CHAR:
{
sprintf(szBuffer,"你按下了%c键",wParam);
MessageBox(hwnd,szBuffer,NULL,MB_OK);
}
其他队列的处理流程:
1. User32!DispatchMessage() 调用 w32k!NtUserDispatchMessage()
2. IntDispatchMessage()
3. co_IntCallWindowProc()
4. KeUserModeCallback() //返回3环调用3环窗口过程
<1> 根据窗口句柄找到窗口对象
<2> 根据窗口对象得到窗口过程函数,由0环发起调用
在回调函数里我们只处理我们需要的消息,别的消息给默认的窗口过程处理函数来处理
//默认窗口过程处理函数
return DefWindowProc(hWnd, uMsg, wParam, lParam);