Win32 的 MVVM:没有内置绑定的落地方式

一句话

Win32 可以用 MVVM,而且非常适合中大型桌面程序;区别只在 View 层——Win32 没有 Qt/WPF 那样的内置数据绑定,需要自己实现 Property / Command / Event / Observer / DataBinding 五种机制,并把 WndProc 拆成「界面创建 + 事件处理 + 数据刷新」三职责。 分层本身与 GUI 库无关,换 Qt 时只需重写 View,ViewModel / Service / Domain / Infrastructure 基本不动。


一、结论:Win32 能用 MVVM 吗

可以,而且很适合。MVVM 的分层原则(View 只管显示与转发、ViewModel 管状态与编排、Domain 管数据、Service 管业务、Infrastructure 管平台)与具体 GUI 库无关——它约束的是职责怎么分,不是界面怎么画

三个 GUI 技术栈的区别只在 View 层:

1
2
3
4
Qt:   控件(QWidget)+ 信号槽(Signal/Slot)——内置绑定,开箱即用
WPF: XAML + Binding——内置绑定,声明式
Win32:HWND + WndProc + 消息循环——没有内置绑定,需要自己实现
观察者 / 命令 / 数据同步

所以真正的差异不是「能不能分层」,而是:

Qt/WPF 把「状态变化 → 界面刷新」这条纽带内置了;Win32 需要自己把它搭起来。

搭起来并不难,本质就是观察者模式——这正是本篇文章要展开的内容。


二、Win32 下 MVVM 各层职责

分层与 Qt 文章(10-桌面端架构/02)完全一致,这里补上每层在 Win32 下的具体形态:

1
2
3
4
5
View            创建控件 + WndProc 接收消息 + 转发事件给 ViewModel;不写业务
ViewModel 按钮事件 → 调用 Service → 更新状态 → 通知 UI 刷新;不知道 ListView 等具体控件
Domain / Model 纯数据(ProcessInfo、MemoryBlock);没有 Win32、HWND、MessageBox
Service 真正业务(EnumProcess 等),内部封装 CreateToolhelp32Snapshot 等 API;UI 完全不知道
Infrastructure 封装平台(Win32 / OpenGL / D3D11 / Filesystem / Network / Thread / Logger / Database)

各层代码示例

View:只创建控件 + 转发事件

1
2
3
4
5
6
7
8
// MainWindow 持有控件,WndProc 收到 WM_COMMAND 后只做转发
case WM_COMMAND:
switch (LOWORD(wParam)) {
case IDC_BTN_SCAN:
viewModel_->onScanClicked(); // 转发给 ViewModel,不在这里写扫描逻辑
break;
}
break;

ViewModel:编排 + 状态通知

1
2
3
4
5
6
7
8
9
void MainViewModel::onScanClicked() {
m_state.isScanning = true;
notify("Scanning", m_state); // 通知观察者(View)刷新
service_->scanAsync([this](Result r) {
m_state.result = r;
m_state.isScanning = false;
notify("ScanDone", m_state); // 状态变化 → 通知界面
});
}

Domain:纯数据,不依赖 Win32

1
2
struct ProcessInfo { DWORD pid; std::wstring name; size_t memory; };
// 没有 HWND、没有 MessageBox、没有 CreateToolhelp32Snapshot

Service:封装真正业务

1
2
3
4
std::vector<ProcessInfo> ProcessService::listProcesses() {
// 内部使用 CreateToolhelp32Snapshot / Process32First / Process32Next
// UI 和 ViewModel 都不知道这些 API 的存在
}

Infrastructure:封装平台能力

1
// infrastructure/win32/ 下按能力分类:memory/、process/、thread/、file/、network/ ...

分层核心约束: Domain 里没有 Win32,ViewModel 里不知道控件,View 里没有业务——每一层都只依赖它下面一层


三、Win32 最大的缺口:没有 Property Binding

这是 Win32 落地 MVVM 时唯一真正要自己动手的地方。

1
2
3
4
5
6
7
8
Qt:  Q_PROPERTY + signal/slot——属性变了自动发信号,界面自动刷新
WPF: Binding——声明式绑定,属性变了自动更新
Win32:都没有——需要自己实现五种机制:
Property(属性)
Command(命令)
Event(事件)
Observer(观察者)
DataBinding(数据绑定,可以简化)

五种机制逐一说明

1. Property(属性)——带通知的字段。普通成员变量变成「setter 里触发通知」的封装:

1
2
3
4
5
6
7
8
9
10
11
class MainViewModel {
public:
void setStatus(const std::wstring& s) {
if (m_status != s) {
m_status = s;
notify(L"Status", m_status); // 变化时通知观察者
}
}
private:
std::wstring m_status;
};

2. Command(命令)——把「用户操作」封装成可调用的动作,而不是直接调函数。ViewModel 暴露 Command,View 把按钮绑到命令上:

1
2
3
4
struct Command {
std::function<void()> execute;
std::function<bool()> canExecute; // 决定按钮是否可用
};

3. Event(事件)——通知的载体。用 std::function + 回调列表实现,配合 lambda 可以模拟 Qt 的 Signal/Slot:

1
2
3
4
5
6
class Observable {
std::vector<std::function<void()>> observers_;
public:
void addObserver(std::function<void()> fn) { observers_.push_back(fn); }
void notify() { for (auto& fn : observers_) fn(); }
};

4. Observer(观察者)——View 订阅 ViewModel 的状态,状态一变自动回调:

1
2
viewModel_.addObserver([this] { refreshListView(); });  // 订阅
// ViewModel 状态变化 → notify() → 这里刷新控件

5. DataBinding(数据绑定)——把「属性 ↔ 控件」的同步简化封装。完整版是双向绑定,实际可以简化成「属性变化 → 刷新对应控件」:

1
2
3
// 简化版:属性名 → 控件更新函数 的映射
std::map<std::wstring, std::function<void()>> bindings_;
// ViewModel 通知 "Status" → bindings_["Status"]() → SetWindowText

Event + lambda 模拟 Qt Signal/Slot

Qt 的信号槽本质就是「事件 + 回调」。Win32 下用同样的思想:

1
2
3
// Qt:  connect(button, &QPushButton::clicked, vm, &MainViewModel::onScanClicked);
// Win32:
button.clicked = [this] { viewModel_->onScanClicked(); }; // 事件 + lambda

绑定机制的本质就是观察者模式——属性是「可观察的状态」,Command 是「可调用的动作」,Event 是「通知的通道」,Observer 是「订阅关系」,DataBinding 是「属性与控件的映射」。五者合起来就是把 Qt 内置的东西按需搭出来。


四、完整调用流程

1
2
3
4
5
6
7
8
9
10
11
12
用户点击按钮
→ Windows 把 WM_COMMAND 投递到消息队列
→ WinMain 的消息循环 GetMessage / DispatchMessage
→ WndProc 收到 WM_COMMAND
→ 转发给 ViewModel 的 Command
→ ViewModel 调用 Service
→ Service 调用 Infrastructure
→ Infrastructure 调用 Win32 API(如 EnumProcess)
→ 结果逐层返回
→ ViewModel 修改状态并 notify
→ View 的观察者回调刷新控件
→ 用户看到结果
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
sequenceDiagram
participant U as 用户
participant OS as 消息队列
participant WM as WinMain 消息循环
participant WP as WndProc
participant VM as ViewModel
participant S as Service/Infra
participant API as Win32 API

U->>OS: 点击按钮
OS->>WM: WM_COMMAND
WM->>WP: DispatchMessage
WP->>VM: 转发命令
VM->>S: 调用业务
S->>API: EnumProcess 等
API-->>S: 返回结果
S-->>VM: 结果
VM->>VM: 修改状态 + notify
VM-->>WP: 观察者回调(刷新控件)
WP-->>U: 界面更新

关键点:

用户操作进入程序的唯一通道是消息循环 + WndProc;结果返回界面的唯一通道是观察者回调(notify → 刷新)。 消息循环负责「进」,观察者机制负责「出」。


五、View 的拆分:界面创建与事件处理分离

Win32 最常见的坏味道:所有东西都堆在 WndProc 里——创建控件、处理命令、处理通知、处理 resize、刷新数据全在一个巨型 switch 里。View 要拆成三职责:

1
View = UI结构(创建) + UI呈现(刷新) + UI事件转发(处理消息)

1. Create():只建界面

1
2
3
4
5
6
void MainWindow::create(HWND parent) {
CreateWindow(L"STATIC", L"状态", ...);
CreateWindow(L"BUTTON", L"扫描", ...);
CreateWindow(L"LISTVIEW", L"", ...);
// 只负责建控件,不处理任何业务
}

2. WndProc 拆分:Handle* 系列

不要在一个 WndProc 里 switch 一切,按消息类型拆成函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
LRESULT MainWindow::wndProc(HWND hwnd, UINT msg, WPARAM w, LPARAM l) {
switch (msg) {
case WM_COMMAND: return handleCommand(hwnd, w, l); // 按钮/菜单命令
case WM_NOTIFY: return handleNotify(hwnd, w, l); // 控件通知(列表选择等)
case WM_SIZE: return handleResize(hwnd, w, l); // 窗口尺寸变化
case WM_TIMER: return handleTimer(hwnd, w, l); // 定时任务
}
return DefWindowProc(hwnd, msg, w, l);
}

// handleCommand:把命令转发给 ViewModel
// handleNotify:把控件状态变化转发给 ViewModel
// handleResize:重新布局控件

3. Refresh():数据刷新与 UI 绘制分开

1
2
3
4
5
void MainWindow::refresh(const ViewState& s) {
SetWindowText(statusLabel_, s.status.c_str());
ListView_SetItemCount(listView_, s.processes.size());
// 只做「把 ViewModel 的状态画到控件上」,不在这里算业务
}

拆分的收益

1
2
3
不拆分:WndProc 里一个巨型 switch,业务、布局、刷新全混在一起
拆分后:Create 只管结构、Handle* 只管转发、Refresh 只管呈现
换 Qt 时只重写 View 这一层

「先只画窗口 UI、再分离数据」完全可以做到——先写 Create() 把界面画出来,验证界面没问题,再把 Handle* 的转发、Refresh 的刷新逐步接上。这正是最小闭环在 Win32 落地的方式。


六、四阶段自顶向下实现

把上面所有内容固定成可执行的顺序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
阶段 1:画 UI 骨架
只写 Create() + 空 WndProc + 消息循环
验证:窗口能显示、控件能点击(不做事)

阶段 2:接 ViewModel
WndProc 拆成 Handle*,转发给 ViewModel
ViewModel 先返回假数据 / 空实现
验证:点击按钮 → ViewModel 被调用 → 状态通知 → 界面能刷新

阶段 3:接 Service
ViewModel 调 Service(可以先返回模拟数据)
验证:调用链通,UI 表现符合预期

阶段 4:接业务 / 系统 API
Service 接 Infrastructure,Infrastructure 接真实 Win32 API
验证:真实数据、真实功能

每一步都是「可运行、可验证」的闭环,与 最小闭环/ 主题的思路一致——先画窗口验证,再往下接,每一次都是增量更新的可验证闭环


与其他线的关系

  • 与 Qt 桌面端架构(10-桌面端架构/02):同一个 MVVM 分层,两种落地——Qt 有内置绑定、Win32 自实现绑定;换 GUI 库只重写 View
  • 与 GUI 的 MVVM(04-系统角色/08):那边讲「View 与 ViewModel 通过状态变化同步」的原理;本篇讲「没有内置绑定时,状态变化怎么自己实现」
  • 与依赖注入(01-依赖关系/02):ViewModel 持有 Service 引用 = 接口依赖注入的实例;换 Service 实现不改 ViewModel
  • 与基础设施层:Infrastructure 封装 Win32 API,让 Domain/Service 不依赖平台——换平台(Win32 → 其他)只换 Infrastructure
  • 与最小闭环(最小闭环/):四阶段实现 = 最小闭环在 Win32 桌面程序的具体展开