一、Windows GUI 最底层先记 5 个东西

Window / HWND

Windows 里的真正窗口对象通常有一个 HWND。

HWND hwnd;

可以把 HWND 理解成:

Windows 给窗口对象的句柄 / 身份标识。

传统 Win32 里,主窗口、Button、Edit 等都可能各自有自己的 HWND。

Main HWND
├─ Button HWND
├─ Edit HWND
└─ ListBox HWND

UI 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 A

Message 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被操作的对象
HWNDWindow 的句柄
Thread真正执行代码
UI Thread负责 GUI 的线程
Message“发生了什么”
Message QueueUI Thread 的待办队列
Message LoopUI 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

可以这样记:

DuiLibQt
定位Windows UI 库完整应用框架
平台Windows跨平台
和 Win32 距离很近比较远
HWND / 消息比较容易接触大量封装
UIDirectUIWidgets / Qt Quick
UI 描述XMLC++ / .ui / QML
事件机制Notify / EventSignal / 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 之上做了不同程度的封装”。