Windows 窗口的诞生过程
定义窗口类结构(WNDCLASS) -> 注册窗口类(RegisterClass) -> 创建窗口(CreateWindow) -> 显示窗口(ShowWindow) -> 更新窗口(UpdateWindow) -> 消息循环(GetMessage -> TranslateMessage ->DispatchMessage)
当调用完 CreateWindow 函数的时候,应用程序实例以及相应的消息队列已经诞生了。
实现代码
//字符串数组长度
#define MAX_LOADSTRING 100
// 全局变量:
HINSTANCE hInst; // 当前实例句柄
WCHAR szTitle[MAX_LOADSTRING]; // 标题栏文本
WCHAR szWindowClass[MAX_LOADSTRING]; // 主窗口类名
// 此代码模块中包含的函数的前向声明:
ATOM MyRegisterClass(HINSTANCE hInstance); //注册窗口类
BOOL InitInstance(HINSTANCE, int); //初始化实例 创建窗口
LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM); //主窗口消息回调
INT_PTR CALLBACK About(HWND, UINT, WPARAM, LPARAM); //关于窗口消息回调
int APIENTRY wWinMain(_In_ HINSTANCE hInstance,
_In_opt_ HINSTANCE hPrevInstance,
_In_ LPWSTR lpCmdLine,
_In_ int nCmdShow)
{
//屏蔽未引用参数的警告
UNREFERENCED_PARAMETER(hPrevInstance);
UNREFERENCED_PARAMETER(lpCmdLine);
// TODO: 在此处放置代码。
// 初始化全局字符串
LoadStringW(hInstance, IDS_APP_TITLE, szTitle, MAX_LOADSTRING);
LoadStringW(hInstance, IDC_WINDOWSPROJECT1, szWindowClass, MAX_LOADSTRING);
//注册窗口
MyRegisterClass(hInstance);
// 执行应用程序初始化:
if (!InitInstance (hInstance, nCmdShow))
{
return FALSE;
}
//用来从程序的资源文件里查找到快捷键定义
HACCEL hAccelTable = LoadAccelerators(hInstance, MAKEINTRESOURCE(IDC_WINDOWSPROJECT1));
MSG msg;
// 主消息循环:
while (GetMessage(&msg, nullptr, 0, 0)) //从当前线程的消息队列里获取一个消息并填入 MSG 结构 中
{
//当应用程序运行时,用户按下快捷键,这样就产生了一个按键消息,
//那么Windows是怎么样把它转化为快捷键响应的消息呢?这就需要使用TranslateAccelerator函数。
//TranslateAccelerator函数主要的作用就是把消息跟快捷键表里定义的按键进行比较,
//如果发现有快捷键,就会把这个按键消息转换为WM_COMMAND或者WM_SYSCOMMAND消息给窗口的消息处理函数发送过去。
if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg))
{
//将虚拟键消息转换为字符消息,字符消息被寄送到当前线程的消息队列里,比如将键盘码转换成ASCII码
TranslateMessage(&msg);
//分派一个消息给窗口过程(回调函数)
DispatchMessage(&msg);
}
}
return (int) msg.wParam;
}
//
// 函数: MyRegisterClass()
//
// 目标: 注册窗口类。
//
ATOM MyRegisterClass(HINSTANCE hInstance)
{
WNDCLASSEXW wcex;
wcex.cbSize = sizeof(WNDCLASSEX);
//指定窗口类型,各种“类风格”(详见下方↓)可以使用按位或操作符组合起来
//CS_HREDRAW 移动或者调整窗口的宽度(水平方向)时,重绘整个窗口
//CS_VREDRAW 移动或者调整窗口的高度(垂直方向)时,重绘整个窗口
wcex.style = CS_HREDRAW | CS_VREDRAW;
//lpfnWndProc 指定窗口过程(必须是回调函数)
wcex.lpfnWndProc = WndProc;
//cbClsExtra 预留的额外空间,一般为 0
wcex.cbClsExtra = 0;
//cbWndExtra 预留的额外空间,一般为 0
wcex.cbWndExtra = 0;
//hInstance 应用程序的实例句柄
wcex.hInstance = hInstance;
//为所有基于该窗口类的窗口设定一个图标
wcex.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_WINDOWSPROJECT1));
//为所有基于该窗口类的窗口设定一个鼠标指针
wcex.hCursor = LoadCursor(nullptr, IDC_ARROW);
// 指定窗口背景色
wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW+1);
//指定窗口菜单
wcex.lpszMenuName = MAKEINTRESOURCEW(IDC_WINDOWSPROJECT1);
//指定窗口类名
wcex.lpszClassName = szWindowClass;
//和窗口类关联的小图标。如果该值为NULL。则把hIcon中的图标转换成大小合适的小图标
wcex.hIconSm = LoadIcon(wcex.hInstance, MAKEINTRESOURCE(IDI_SMALL));
//注册一个窗口类
return RegisterClassExW(&wcex);
}
//
// 函数: InitInstance(HINSTANCE, int)
//
// 目标: 保存实例句柄并创建主窗口
//
// 注释:
//
// 在此函数中,我们在全局变量中保存实例句柄并
// 创建和显示主程序窗口。
//
BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
{
hInst = hInstance; // 将实例句柄存储在全局变量中
//创建窗口
//WS_OVERLAPPEDWINDOW 相当于(WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_THICKFRAME | WS_MINIMIZEBOX | WS_MAXIMIZEBOX),与 WS_TILEDWINDOW 风格相同
//如果该参数被设为 CW_USEDEFAULT 则系统为窗口选择缺省的左上角坐标并忽略 y 参数
HWND hWnd = CreateWindowW(szWindowClass, szTitle, WS_OVERLAPPEDWINDOW,
CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, nullptr, nullptr, hInstance, nullptr);
if (!hWnd)
{
return FALSE;
}
//用于设置窗口的显示状态
ShowWindow(hWnd, nCmdShow);
//绕过应用程序的消息队列,直接发送 WM_PAINT 消息给指定窗口的窗口过程
UpdateWindow(hWnd);
return TRUE;
}
//
// 函数: WndProc(HWND, UINT, WPARAM, LPARAM)
//
// 目标: 处理主窗口的消息。
//
// WM_COMMAND - 处理应用程序菜单
// WM_PAINT - 绘制主窗口
// WM_DESTROY - 发送退出消息并返回
//
//
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
switch (message)
{
case WM_COMMAND: //点击菜单, 点击加速键,点击子窗口按钮,点击工具栏按钮
{
int wmId = LOWORD(wParam);
// 分析菜单选择:
switch (wmId)
{
case IDM_ABOUT:
DialogBox(hInst, MAKEINTRESOURCE(IDD_ABOUTBOX), hWnd, About);
break;
case IDM_EXIT:
DestroyWindow(hWnd);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
}
break;
case WM_PAINT: //窗口绘制事件
{
PAINTSTRUCT ps;
HDC hdc = BeginPaint(hWnd, &ps);
// TODO: 在此处添加使用 hdc 的任何绘图代码...
EndPaint(hWnd, &ps);
}
break;
case WM_LBUTTONDOWN: //鼠标左键事件
{
MessageBox(hWnd, TEXT("单机左键"), TEXT("提示"), MB_OK);
return 0;
}
break;
case WM_CLOSE: //关闭窗口事件
{
if (MessageBox(hWnd, TEXT("确定要关闭该窗口吗?"), TEXT("提示"), MB_YESNO) == IDYES)
{
DestroyWindow(hWnd);
}
else
{
return 0;
}
}
break;
case WM_DESTROY: //窗口销毁事件
PostQuitMessage(0);
break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
return 0;
}
// “关于”框的消息处理程序。
INT_PTR CALLBACK About(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)
{
UNREFERENCED_PARAMETER(lParam);
switch (message)
{
case WM_INITDIALOG:
return (INT_PTR)TRUE;
case WM_COMMAND:
if (LOWORD(wParam) == IDOK || LOWORD(wParam) == IDCANCEL)
{
EndDialog(hDlg, LOWORD(wParam));
return (INT_PTR)TRUE;
}
break;
}
return (INT_PTR)FALSE;
}
Windows的消息机制
系统消息队列
Windows 是一个事件驱动,基于消息的操作系统,用户的任何操作都被看作一个事件,操作系统会自动将该事件转换为相应的消息并投入该应用程序的消息队列等待处理。
用户的鼠标、键盘事件都会放到操作系统的消息队列中,等待分配处理
用户消息队列
应用程序的消息机制主要由消息循环来处理:
while (GetMessage(&msg, NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
etMessage 函数从应用程序的队列里取出一个消息,如果这个消息是 WM_QUIT(程序退出消息)就返回 0,结束 while 循环。如果不是退出消息,那么就执行 TranslateMessage 翻译消息,这个操作主要是对一些消息进行转换,例如把键盘的虚拟键消息转换为字符消息。
接着调用 DispatchMessage 将消息分派给相应的窗口过程。
注意:他不是直接通过 DispatchMessage 调用我们的窗口过程 WndProc,这里其实 DispatchMessage 是带着消息去找操作系统,然后再由操作系统调用 WndProc 窗口过程。看起来有点纠结哈,不过这就是消息机制的一个真实面目,操作系统为了绝对的控制权,时时刻刻都监控着应用程序的运行。
在窗口过程中,我们对感兴趣的消息进行监控并部署相应的代码,对不感兴趣的消息我们都扔给DefWindowProc,让操作系统以默认的方式来处理消息。
注意点
- 消息队列是FIFO的形式
- WM_PAINT,WM_TIMER 和 WM_QUIT 这三个消息属于特例,操作系统会把它们时刻放在消息队列的最后
- 消息其实会细分为队列化消息和非队列化消息
系统如何销毁一个窗口
DestroyWindow 函数来销毁一个窗口
窗口销毁过程
队列消息和非队列消息
消息既可以是“队列消息”,也可以是“非队列消息”,队列消息是指那些由 Windows 放入消息队列的消息,主要由用户的输入产生,例如按键点击、鼠标移动、窗口重绘、还有定时器消息等。这些消息都是要经过消息循环,通过 GetMessage 检索消息,到 DisapatchMessage 将消息投递到窗口过程中处理。
队列消息以外的其他所有消息我们称之为非队列消息,他们通常是由调用特定的 Windows 函数引起的。例如上节课我们讲到的当 WinMain 调用 CreateWindow 函数时,Windows 就会创建窗口,并在创建过程中向窗口过程直接发送一条 WM_CREATE,注意,这条消息是不用进入消息队列的。