参考:https://www.bilibili.com/video/BV1By4y1r7Cq
写一个简单的窗口程序,流程如下:
启动WinMain
├─ 定义窗口
│
├─ 注册窗口类(RegisterClassW)
│
├─ 创建窗口(CreateWindowW)→ 发送WM_CREATE → WndProc弹窗
│
├─ 显示窗口(ShowWindow)
│
└─ 进入消息循环:
│
├─ 用户操作 → 生成消息 → GetMessage捕获
│
├─ TranslateMessage转换键盘输入
│
├─ DispatchMessage → 调用WndProc处理
│
└─ WM_CLOSE → DestroyWindow
前置知识
Windows 消息处理机制总结
Windows 是一个基于消息驱动的操作系统,应用程序通过消息循环(Message Loop)接收并处理系统或用户输入的事件(如鼠标、键盘、窗口绘制等)。以下是其核心流程和关键概念:
1. 消息处理的核心流程
(1) 消息产生
消息由以下来源产生:
- 用户输入(如鼠标点击
WM_LBUTTONDOWN、键盘按下WM_KEYDOWN)。 - 系统事件(如窗口大小调整
WM_SIZE、窗口关闭WM_CLOSE)。 - 应用程序自身(通过
SendMessage或PostMessage发送)。
(2) 消息进入消息队列
- 系统队列(System Queue):硬件输入(如键盘、鼠标)先进入系统队列,再由 Windows 分发到对应线程的消息队列。
- 线程消息队列(Thread Message Queue):每个 GUI 线程维护自己的消息队列,存储待处理的消息。
(3) 消息循环(Message Loop)
应用程序通过 GetMessage → TranslateMessage → DispatchMessage 循环处理消息:
MSG msg;
while (GetMessage(&msg, NULL, 0, 0)) {
TranslateMessage(&msg); // 转换键盘消息(如 WM_KEYDOWN → WM_CHAR)
DispatchMessage(&msg); // 将消息发送到窗口过程(lpfnWndProc)
}
GetMessage:从消息队列取出消息,若队列为空,线程会等待(不占用 CPU)。TranslateMessage:将键盘按键消息转换为字符消息(如WM_KEYDOWN+WM_KEYUP→WM_CHAR)。DispatchMessage:调用窗口的lpfnWndProc处理消息。
(4) 窗口过程(Window Procedure)
每个窗口都有一个 lpfnWndProc(窗口过程函数),负责处理消息:
LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam) {
switch (msg) {
case WM_CLOSE:
DestroyWindow(hwnd); // 关闭窗口
break;
case WM_DESTROY:
PostQuitMessage(0); // 退出消息循环
break;
default:
return DefWindowProc(hwnd, msg, wParam, lParam); // 默认处理
}
return 0;
}
- 若消息未被处理,必须调用
DefWindowProc执行默认行为(如窗口拖动、最小化等)。
2. 消息的类型
| 消息类型 | 说明 |
|---|---|
| 输入消息 | WM_KEYDOWN, WM_MOUSEMOVE(用户交互) |
| 窗口管理消息 | WM_CREATE, WM_SIZE, WM_CLOSE(窗口生命周期) |
| 绘制消息 | WM_PAINT(窗口需要重绘) |
| 系统命令消息 | WM_COMMAND(菜单/按钮点击) |
| 自定义消息 | WM_USER 或 WM_APP + 自定义值(SendMessage/PostMessage 发送) |
3. 消息的发送方式
| 函数 | 行为 |
|---|---|
PostMessage | 将消息放入队列后立即返回(异步),由消息循环处理。 |
SendMessage | 直接调用窗口过程,等待处理完成(同步),阻塞调用线程。 |
PeekMessage | 检查消息队列但不阻塞(适用于游戏等实时应用)。 |
过程
首先是WinMain,相当于main函数,参数含义已由注释给出。
int WINAPI WinMain(
HINSTANCE hInstance, // 当前程序实例句柄(由系统分配)
HINSTANCE hPreHinstance, // 前一个实例句柄(已废弃,总是NULL)
LPSTR lpCmdLine, // 命令行参数(ANSI字符串)
int nCmdShow // 窗口初始显示状态(最小化/正常等)
)
{
return 0;
}
第一步,定义并初始化窗口类,windows.h给了WNDCLASS以及WNDCLASSEXW、WNDCLASSW、WNDCLASSA等变种,WNDCLASSEXW比较常用,此处我们使用WNDCLASSW。
详见WNDCLASSW (winuser.h) – Win32 apps | Microsoft Learn
需要注意的是初始化时候需要将 lpfnWndProc 设置为自定义的窗口过程函数(此处是WndProc), 此后,所有基于该窗口类创建的窗口都会使用 lpfnWndProc(WndProc) 处理消息。可以理解为,当消息传到某个窗口时,这个窗口就会调用lpfnWndProc(WndProc)来处理消息。
int WINAPI WinMain(
HINSTANCE hInstance, // 当前程序实例句柄(由系统分配)
HINSTANCE hPreHinstance, // 前一个实例句柄(已废弃,总是NULL)
LPSTR lpCmdLine, // 命令行参数(ANSI字符串)
int nCmdShow // 窗口初始显示状态(最小化/正常等)
)
{
// 1. 定义并初始化窗口类结构体
WNDCLASSW myclass = { 0 }; // 清零初始化
myclass.lpszClassName = L"n0ps1ed"; // 窗口类名称(唯一标识)
myclass.lpfnWndProc = WndProc; // 设置窗口消息处理函数
return 0;
}
第二步,注册窗口类,使用的是RegisterClassW函数:RegisterClassW(&myclass);
int WINAPI WinMain(
HINSTANCE hInstance, // 当前程序实例句柄(由系统分配)
HINSTANCE hPreHinstance, // 前一个实例句柄(已废弃,总是NULL)
LPSTR lpCmdLine, // 命令行参数(ANSI字符串)
int nCmdShow // 窗口初始显示状态(最小化/正常等)
)
{
// 1. 定义并初始化窗口类结构体
WNDCLASSW myclass = { 0 }; // 清零初始化
myclass.lpszClassName = L"n0ps1ed"; // 窗口类名称(唯一标识)
myclass.lpfnWndProc = WndProc; // 设置窗口消息处理函数
// 2. 注册窗口类到操作系统
RegisterClassW(&myclass); // 注册后才能创建该类的窗口
return 0;
}
这里可能会有疑惑,为什么不能直接创建窗口实例,而要先注册?这个注册窗口的意义是啥?
答案:当主线程从消息列队获取消息后,会根据句柄(Hwnd)得到一个窗口指针,这个指针指向窗口树的一个节点(系统会把所有的窗口用一个树形链表表示),这个节点含有该窗口的信息(如何窗口的类名称),同时系统会维护一张hash表,作用也就是为每个窗口类找对应的消息处理函数指针,其实你所做的注册窗口类的工作,就是向这张表写数据,然后系统就会为该窗口执行这个消息处理函数。
然后是第三步,创建窗口实例:
// 3. 创建窗口实例
HWND hwindow = CreateWindowW(
myclass.lpszClassName, // 使用已注册的窗口类名
L"n0ps1ed", // 窗口标题文本
WS_OVERLAPPEDWINDOW, // 标准窗口样式(带标题栏、边框等)
CW_USEDEFAULT, // 初始x位置(使用默认值)
0, // 初始y位置
CW_USEDEFAULT, // 窗口宽度(默认)
0, // 窗口高度
NULL, // 父窗口句柄(无)
NULL, // 菜单句柄(无)
hInstance, // 程序实例句柄
0 // 创建参数(无)
);
第四步,显示窗口:
// 4. 显示窗口
ShowWindow(hwindow, SW_SHOWNORMAL); // 以普通状态显示窗口
第五步,消息循环处理:
// 5. 消息循环(程序核心)
MSG msg = { 0 };
while (GetMessageW(&msg, NULL, 0, 0)) // 获取消息,NULL改成hwindow也可以
{
DispatchMessageW(&msg); // 将消息分发到窗口过程WndProc
}
其中,GetMessageW原型如下:
BOOL GetMessageW(
LPMSG lpMsg, // 存储消息的缓冲区
HWND hWnd, // 目标窗口句柄(NULL表示接收所有窗口消息)
UINT wMsgFilterMin, // 最小消息号(过滤范围起点)
UINT wMsgFilterMax // 最大消息号(过滤范围终点)
);
而DispatchMessageW是负责消息分发的,我们点击鼠标键盘对窗口产生的消息不是直接传给窗口,而是需要先传给系统,系统再分发给对应窗口句柄。所以才需要GetMessageW先获取消息然后再DispatchMessageW发消息。(也可以直接发给回调函数 lpfnWndProc)
当调用 DispatchMessageW(&msg) 时,系统首先从 MSG 结构体中提取关键信息(目标窗口句柄、消息类型(如WM_PAINT)、附加信息(如按键码)、附加信息(如鼠标坐标)),然后通过 hwnd 找到对应的窗口类(WNDCLASS),接着获取注册时指定的 lpfnWndProc(即WndProc 函数)并执行。
写到这一步,我们已经创建了一个窗口实例并显示出来,然后使用GetMessageW来获取发往hwindow句柄(即所创建的窗口实例)的信息,然后使用DispatchMessageW函数把消息分发给句柄,调用WndProc 函数执行,所以接下来我们完善一下WndProc 函数:
// 窗口过程函数 - 处理所有发送到窗口的消息
LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
switch (message) // 根据消息类型进行分支处理
{
case WM_CREATE: // 窗口创建完成时收到的第一个消息
MessageBoxW(hwnd, L"窗口创建了", L"提示", MB_OK); // 显示创建提示
break;
case WM_CLOSE: // 当用户点击关闭按钮时触发
MessageBoxW(hwnd, L"窗口关闭了", L"提示", MB_OK); // 显示关闭提示
DestroyWindow(hwnd); // 销毁窗口(会触发WM_DESTROY消息)
break;
case WM_MOUSEMOVE: // 鼠标移动时触发
MessageBoxW(hwnd, L"鼠标移动", L"提示", MB_OK); // 显示关闭提示
break;
default: // 其他未处理的消息交给默认处理函数
return DefWindowProc(hwnd, message, wParam, lParam);
}
return 0; // 已处理的消息返回0
}
此时就已经完成了一个简单的窗口程序,实现的功能是打开窗口时候会弹窗提示“窗口创建了”,关闭窗口时会提示“窗口关闭了”,鼠标在窗口上移动时弹窗提示“鼠标移动”效果图如下:



完整代码:
#include <Windows.h>
// 窗口过程函数 - 处理所有发送到窗口的消息
LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
{
switch (message) // 根据消息类型进行分支处理
{
case WM_CREATE: // 窗口创建完成时收到的第一个消息
MessageBoxW(hwnd, L"窗口创建了", L"提示", MB_OK); // 显示创建提示
break;
case WM_CLOSE: // 当用户点击关闭按钮时触发
MessageBoxW(hwnd, L"窗口关闭了", L"提示", MB_OK); // 显示关闭提示
DestroyWindow(hwnd); // 销毁窗口(会触发WM_DESTROY消息)
break;
case WM_MOUSEMOVE: // 鼠标移动时触发
MessageBoxW(hwnd, L"鼠标移动", L"提示", MB_OK); // 显示关闭提示
break;
default: // 其他未处理的消息交给默认处理函数
return DefWindowProc(hwnd, message, wParam, lParam);
}
return 0; // 已处理的消息返回0
}
// Windows应用程序入口点(不同于控制台程序的main)
int WINAPI WinMain(
HINSTANCE hInstance, // 当前程序实例句柄(由系统分配)
HINSTANCE hPreHinstance, // 前一个实例句柄(已废弃,总是NULL)
LPSTR lpCmdLine, // 命令行参数(ANSI字符串)
int nCmdShow // 窗口初始显示状态(最小化/正常等)
)
{
// 1. 定义并初始化窗口类结构体
WNDCLASSW myclass = { 0 }; // 清零初始化
myclass.lpszClassName = L"n0ps1ed"; // 窗口类名称(唯一标识)
myclass.lpfnWndProc = WndProc; // 设置窗口消息处理函数
// 2. 注册窗口类到操作系统
RegisterClassW(&myclass); // 注册后才能创建该类的窗口
// 3. 创建窗口实例
HWND hwindow = CreateWindowW(
myclass.lpszClassName, // 使用已注册的窗口类名
L"n0ps1ed", // 窗口标题文本
WS_OVERLAPPEDWINDOW, // 标准窗口样式(带标题栏、边框等)
CW_USEDEFAULT, // 初始x位置(使用默认值)
0, // 初始y位置
CW_USEDEFAULT, // 窗口宽度(默认)
0, // 窗口高度
NULL, // 父窗口句柄(无)
NULL, // 菜单句柄(无)
hInstance, // 程序实例句柄
0 // 创建参数(无)
);
// 4. 显示窗口
ShowWindow(hwindow, SW_SHOWNORMAL); // 以普通状态显示窗口
// 5. 消息循环(程序核心)
MSG msg = { 0 };
while (GetMessageW(&msg, hwindow, 0, 0)) // 获取消息
{
DispatchMessageW(&msg); // 将消息分发到窗口过程WndProc
}
return 0; // 程序结束(当GetMessage收到WM_QUIT时退出循环)
}