学习笔记——Windows消息机制与程序结构
一条消息的一生
我是一条消息。随着鼠标在按钮上的一次点击,我出生了,刚睁开眼,身上就被塞了一张证件,上面是我的身份信息:
- hwnd: 我出生的窗口的句柄,也可以说是我的户籍地址
- message: 我的身份证号,独一无二的标识了我的身份
- wParam和lParam: 出生时附加的一些信息。不同消息对应的内容不一定相同,在我这里存储的是被点击的按钮id
- time: 我的出生日期
- pt: 我出生时鼠标光标所处的位置
看我还在发愣,给我发证件的鼠标驱动程序急了:“愣着干嘛,一会赶不上消息处理了,还不快去系统队列报到!”
我连忙灰头土脸地赶去排队。到了地方抬头一看,前面已经排了很多各种类型的消息,其中最常见的是窗口消息,他们是由操作系统和控制其他窗口的窗口所使用的消息。其次是命令消息,这是一些特殊的窗口消息,用来处理一个窗口发送到另一个窗口的用户请求。最后是像我一样的控件通知消息:由操作某个控件(如单击一个按钮)所产生的消息。
看着长长的队列,我又迷茫了。好在前面的老哥及时替我解惑:
“这里是windows老大维护的系统消息队列,集中了来自各个窗口、各个线程的消息,轮到我们的时候,老大会根据身份证件来确定将我们送到哪个窗口,然后我们还要在窗口对应的线程消息队列排队,并由对应的线程决定哪一个窗口过程(消息处理函数)来处理我们。”
“这么说所有消息都要经过‘系统消息队列——线程消息队列——窗口过程’这个流程才能被处理呀” 虽然过程繁琐了点,不过大家都一样,也就不觉得难以忍受。
“呵呵,你太天真了,只有我们这些用PostMessage函数发送的队列消息要排这么久的队,有一些关系户,用SendMessage函数发送,叫非队列消息,直接就能被送到窗口过程那里,什么队都不需要排!”
还有这种事?我叹了口气,只能耐心等待。
还好我所在的计算机CPU比较快,没等太久就排到我了,操作系统查看了我的证件后,又把我送回了出生的那个应用程序。这里的队列就短多了,我很快就排完了队,进入了WinMain函数,这里有一个消息循环:
MSG msg;//这个msg用来存放我
while (GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);//用来将虚拟键消息转换为字符消息,这是键盘消息需要用的,与我无关
DispatchMessage(&msg);//将我分派给窗口的消息处理函数,即WndProc函数
}
WndProc(消息处理)函数:
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
int wmId;
PAINTSTRUCT ps;
HDC hdc;
switch (message)//这里分出三条岔路
{
case WM_COMMAND://我走这一条路
wmId = LOWORD(wParam);//解读出我wParam中蕴含的信息:被点击的按钮ID
switch (wmId)
{
case ID_BUTTON_DRAW://如果是ID_BUTTON_DRAW按钮被点击
OnButtonWhite();//调用这个函数,让背景变白
break;
case ID_BUTTON_SWEEP://如果是ID_BUTTON_SWEEP被点击
OnButtonGray();//调用这个函数,让背景变灰
break;
default://如果都不是,调用系统默认的消息处理函数,以免消息无人处理,产生bug
return DefWindowProc(hWnd, message, wParam, lParam);
}
break;
case WM_PAINT://用来绘制窗口的消息
hdc = BeginPaint(hWnd, &ps);
OnDraw(hdc);
EndPaint(hWnd, &ps);
break;
case WM_DESTROY://用来关闭窗口的消息
PostQuitMessage(0);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
return 0;
}
我对应的按钮是ID_BUTTON_DRAW,这意味这需要调用OnButtonWhite()函数,让窗口变成白色。
到了这一步,我的使命也就结束了,剩下的工作就交给绘图程序去做。
回想我的一生,真如白驹过隙,忽然而已。
一个简单的win32程序的结构
全局变量:
HINSTANCE hInst; // 当前实例
TCHAR szTitle[MAX_LOADSTRING] = TEXT("Message process"); // 标题栏文本
TCHAR szWindowClass[MAX_LOADSTRING] = TEXT("AppTest"); // 主窗口类名
WinMain函数:
int APIENTRY _tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow)
/*
参数解释:
HINSTANCE hInstance: 当前实例的句柄。
HINSTANCE hPrevInstance 前一个实例的句柄。在Win32环境下,这个参数不再起作用。
LPSTR lpCmdLine: 一个指向字符串的指针,表示一个命令行参数,即C或C++中的main函数中的参数char *argv[]。
int nCmdShow: 窗口显示形式,最大化显示、最小化显示、正常大小显示还是隐藏显示。
APIENTRY:表明此函数是应用程序的入口点 详见 https://blog.csdn.net/guoyong10721073/article/details/52399823
*/
{
// 注册窗口类
if(!AppRegisterClass(hInstance))
{
return FALSE;
}
// 初始化应用程序窗口
if (!InitInstance (hInstance, nCmdShow))
{
return FALSE;
}
// 消息循环
MSG msg;
while (GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);//用来把键盘事件的虚拟键消息转换为字符消息。
DispatchMessage(&msg);//把消息发送到窗口的消息处理函数,此函数在窗口注册时已经指定。
}
return (int) msg.wParam;
}
注册窗口类:
ATOM AppRegisterClass(HINSTANCE hInstance)
{
WNDCLASSEX wcex;
wcex.cbSize = sizeof(WNDCLASSEX);
wcex.style = CS_HREDRAW | CS_VREDRAW; //指定窗口类的类型
wcex.lpfnWndProc = WndProc; //指定消息处理函数,是一个回调函数, 需要自己定义
wcex.cbClsExtra = 0; //类的附加内存,通常数情况下为0
wcex.cbWndExtra = 0; //窗口附加内存,通常情况下为0
wcex.hInstance = hInstance; //当前实例句柄,用WinMain中的形参给它赋值
wcex.hIcon = LoadIcon(NULL, IDI_APPLICATION); //图标句柄,用于指示应用程序所用的是什么图标
wcex.hIconSm = NULL;
wcex.hCursor = LoadCursor(NULL, IDC_ARROW); //光标句柄,用于指示鼠标进入应用程序窗口区域时的显示
wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW+1); //用于指示程序的背景颜色
wcex.lpszMenuName = NULL; //指定菜单的名字
wcex.lpszClassName = g_szWindowClass; //指定类的名字
return RegisterClassEx(&wcex);
}
保存实例句柄并创建主窗口:
BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
{
HWND hWnd;
// 将实例句柄存储在全局变量中
hInst = hInstance;
//创建窗口,详见 https://blog.csdn.net/lizijie7471619/article/details/51058095
hWnd = CreateWindow(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInstance, NULL);
if (!hWnd)
{
return FALSE;
}
ShowWindow(hWnd, nCmdShow);//显示窗口
UpdateWindow(hWnd);//更新窗口
return TRUE;
}
消息循环与消息处理函数:
参见上文《一条消息的一生》点击跳转