一、Windows GUI 最底层先记 5 个东西
Window / HWND
Windows 里的真正窗口对象通常有一个 HWND。
HWND hwnd;可以把 HWND 理解成:
Windows 给窗口对象的句柄 / 身份标识。
传统 Win32 里,主窗口、Button、Edit 等都可能各自有自己的 HWND。
Main HWND
├─ Button HWND
├─ Edit HWND
└─ ListBox HWNDUI Thread
窗口不会执行代码,线程才会执行代码。
哪个线程创建了窗口,通常哪个线程就负责这个窗口的消息处理,因此这个线程通常叫:
UI Thread / GUI Thread
关系大致是:
Process
├─ UI Thread
│ ├─ Window A
│ ├─ Window B
│ └─ Window C
│
└─ Worker Thread一个 UI Thread 可以管理多个窗口。
Message
Windows GUI 是事件驱动的。
用户进行操作:
鼠标点击
键盘输入
窗口缩放
窗口重绘
关闭窗口Windows 会把这些事情表示为消息,例如:
WM_LBUTTONDOWN
WM_MOUSEMOVE
WM_KEYDOWN
WM_PAINT
WM_SIZE
WM_CLOSE消息大致包含:
HWND hwnd; // 给哪个窗口
UINT message; // 什么事情
WPARAM wParam; // 参数
LPARAM lParam; // 参数Message Queue
消息通常会进入目标 UI 线程的消息队列。
重点:
不是“一窗口一个消息队列”,主要是“线程拥有消息队列”。
例如:
UI Thread Message Queue
WM_MOUSEMOVE → Window A
WM_KEYDOWN → Window B
WM_LBUTTONDOWN → Window AMessage Loop
UI Thread 会不断从队列里取消息:
MSG msg;
while (GetMessage(&msg, nullptr, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}这就是:
Message Loop / Message Pump
注意:
消息循环不是另一个东西在运行。
而是:
UI Thread 本身正在执行这段循环。
二、DispatchMessage 和 WndProc
这是整个 Windows GUI 最核心的一条调用链:
UI Thread
↓
GetMessage
↓
拿到 MSG
↓
DispatchMessage
↓
根据 msg.hwnd 找到窗口
↓
调用这个窗口对应的 WndProc
↓
执行你的代码你之前问:
DispatchMessage(&msg);是不是相当于调用窗口回调?
答案就是:
对,可以这样理解。
概念上类似:
WndProc(
msg.hwnd,
msg.message,
msg.wParam,
msg.lParam
);真正执行 WndProc 的仍然是:
当前 UI Thread。
三、WndProc 是干嘛的
WndProc 就是窗口处理 Windows 消息的回调。
例如:
LRESULT CALLBACK WndProc(
HWND hwnd,
UINT msg,
WPARAM wParam,
LPARAM lParam)
{
switch (msg)
{
case WM_LBUTTONDOWN:
// 鼠标点击
break;
case WM_PAINT:
// 绘制
break;
case WM_SIZE:
// 窗口尺寸变化
break;
}
return DefWindowProc(hwnd, msg, wParam, lParam);
}所以可以记:
Message Loop
负责:
“下一条消息是什么?”
WndProc
负责:
“这条消息具体怎么处理?”四、整个鼠标点击流程
这是最值得记住的一张图:
用户按鼠标
↓
鼠标硬件 / 驱动
↓
Windows 输入系统
↓
Windows 判断点到了哪个窗口
↓
生成 WM_LBUTTONDOWN
↓
目标 UI Thread 的消息队列
↓
GetMessage
↓
DispatchMessage
↓
目标窗口 WndProc
↓
case WM_LBUTTONDOWN
↓
你的业务逻辑所以几个角色的关系:
| 东西 | 作用 |
|---|---|
| Window | 被操作的对象 |
| HWND | Window 的句柄 |
| Thread | 真正执行代码 |
| UI Thread | 负责 GUI 的线程 |
| Message | “发生了什么” |
| Message Queue | UI Thread 的待办队列 |
| Message Loop | UI Thread 不断取待办 |
| WndProc | 处理窗口消息的函数 |
五、为什么 UI Thread 不能卡住
因为:
UI Thread
↓
GetMessage
↓
DispatchMessage
↓
WndProc
↓
回 GetMessage全部基本是在同一个线程里执行。
如果你写:
case WM_LBUTTONDOWN:
{
Sleep(30000);
break;
}UI Thread 就卡在这里 30 秒。
结果:
WM_PAINT
WM_MOUSEMOVE
WM_KEYDOWN
WM_SIZE
...没人处理。
所以界面:
无法刷新
无法拖动
按钮无反应
Not Responding因此:
“不要阻塞 UI Thread”本质上就是“不要阻塞消息循环”。
六、PostMessage 和 SendMessage
先简单这么记:
PostMessage
≈ 异步
SendMessage
≈ 同步PostMessage
PostMessage(hwnd, msg, wParam, lParam);大概是:
把消息投递过去
↓
我自己继续执行
↓
目标线程以后处理SendMessage
SendMessage(hwnd, msg, wParam, lParam);大概是:
让目标窗口处理
↓
等待处理完成
↓
我再继续而且:
SendMessage不应该简单理解成“把消息放进队列”。
很多情况下它属于同步消息调用机制。
七、然后进入 DuiLib
DuiLib 本质上可以理解为:
建立在 Win32 上的一套 Windows UI 框架。
它比较靠近底层。
底下还是:
HWND
WndProc
WM_xxx
Message Loop但是上面提供:
Button
Label
List
Edit
Layout
XML
Event八、DuiLib 最重要的思想:Windowless / DirectUI
传统 Win32 可能:
Windows
│
└─ Main HWND
├─ Button HWND
├─ Edit HWND
└─ ListBox HWND也就是说 Button 本身也是 Windows 真正的窗口对象。
但 DuiLib 可以:
Windows
│
└─ Main HWND
│
└─ DuiLib
├─ CButtonUI
├─ CLabelUI
├─ CListUI
└─ ...这些:
CButtonUI
CLabelUI
CListUI可能只是普通 C++ 对象。
它们:
没有自己的 HWND。
因此叫:
Windowless Control
不是说它们“不显示”。
而是:
它们虽然显示在窗口里,但它们本身不是 Windows Window。
九、DuiLib Button 是怎么画出来的
例如:
┌───────────────────┐
│ │
│ [ 登录 ] │
│ │
└───────────────────┘Windows 可能只知道外面这个:
Main HWND里面的 Button 是 DuiLib:
画矩形
+
画文字“登录”
+
维护 pressed / hover 状态做出来的。
所以:
Windows:
“这里有一个窗口,里面有一些像素。”
DuiLib:
“这里其实有 Button、Label、List 等控件。”十、那没有 HWND,怎么知道按钮被点了?
靠:
Hit Test,命中测试。
例如:
for (Control* control : controls)
{
if (control->rect.contains(mousePoint))
{
// 点中了这个 control
}
}意思:
鼠标坐标 = (50, 60)
↓
看看 Button A 的矩形包含它吗?
↓
不包含
↓
看看 Button B
↓
包含!
↓
用户点到了 Button B所以:
Windows
↓
“Main HWND 的 (50,60) 被点了”
↓
DuiLib
↓
Hit Test
↓
“(50,60) 是 LoginButton”
↓
触发 Click
↓
Login()Windows 并不知道:
“用户点的是 CButtonUI。”
这个判断是 DuiLib 自己完成的。
十一、所以 DuiLib 在 Windows 上干了什么
可以总结成:
Windows
↓
WM_MOUSEMOVE
WM_LBUTTONDOWN
WM_PAINT
WM_KEYDOWN
...
↓
Main HWND / WndProc
↓
DuiLib
↓
维护控件树
↓
布局
绘制
Hit Test
事件分发
状态管理
↓
Button / Label / List
↓
业务代码所以 DuiLib 很像:
在一个 Windows HWND 里面,自己实现了一个小型 UI 系统。
十二、Qt 又是什么可以,把前面内容压成一套“从 Windows 底层到 DuiLib / Qt”的知识框架。
Qt 的层级更高。
DuiLib 更像:
Windows UI Library
Qt 更像:
完整的跨平台 C++ Application Framework
Qt 不只是 UI,还有:
Qt Core
Qt GUI
Qt Widgets
Qt Quick / QML
Qt Network
Qt SQL
Qt Multimedia
Qt Thread
Qt WebEngine
...而且可以跑:
Windows
Linux
macOS
嵌入式
移动端十三、Qt 也有 Event Loop
典型 Qt:
QApplication app(argc, argv);
MainWindow window;
window.show();
return app.exec();这里:
app.exec();就是进入:
Qt Event Loop。
Windows 平台下面,Qt 仍然得和 Windows 的:
Message Queue
WM_xxx
HWND
Windows Event打交道。
但是 Qt 把这些封装起来:
Windows
↓
Win32 消息
↓
Qt Windows Platform Layer
↓
Qt Event Loop
↓
QEvent
↓
QWidget / QObject
↓
Signal / Slot
↓
你的业务逻辑于是你写 Qt 时通常不需要天天处理:
WM_LBUTTONDOWN
WM_PAINT
WndProc十四、Qt 的 Signal / Slot
Qt 里你更可能写:
connect(
button,
&QPushButton::clicked,
this,
&MainWindow::login
);意思:
Button Click
↓
clicked signal
↓
login slot
↓
Login()所以 Qt 又给事件机制加了一层更高级抽象。
十五、DuiLib vs Qt
可以这样记:
| DuiLib | Qt | |
|---|---|---|
| 定位 | Windows UI 库 | 完整应用框架 |
| 平台 | Windows | 跨平台 |
| 和 Win32 距离 | 很近 | 比较远 |
| HWND / 消息 | 比较容易接触 | 大量封装 |
| UI | DirectUI | Widgets / Qt Quick |
| UI 描述 | XML | C++ / .ui / QML |
| 事件机制 | Notify / Event | Signal / Slot |
| 网络等 | 自己解决较多 | Qt 提供完整模块 |
| 学 Windows 原理 | 很适合 | 不如 DuiLib 直观 |
| 做跨平台产品 | 不适合 | 很适合 |
十六、最后建立这一层级感
你现在学的这些其实可以全部放在一张图里:
你的业务代码
│
┌────────────┴────────────┐
│ │
DuiLib Qt
│ │
CButtonUI QPushButton
CListUI QWidget
XML QML
Notify Signal/Slot
│ │
│ Qt Platform Layer
│ │
└────────────┬────────────┘
│
Win32
│
HWND / Window / WndProc
│
Windows Message
│
Message Queue
│
UI Thread
│
Message Loop
│
Windows如果你现在准备 Windows 客户端面试/基础学习,最重要的是先彻底理解这一条:
Process → Thread → UI Thread → Window → HWND → Message Queue → Message Loop → DispatchMessage → WndProc → UI Framework → 控件 → 业务代码
这条链串起来以后,后面再看 MFC、DuiLib、Qt、WPF、WinForms、Chromium,你会发现它们很多时候只是“在 Win32 之上做了不同程度的封装”。