03-Win32与自实现绑定
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 | Qt: 控件(QWidget)+ 信号槽(Signal/Slot)——内置绑定,开箱即用 |
所以真正的差异不是「能不能分层」,而是:
Qt/WPF 把「状态变化 → 界面刷新」这条纽带内置了;Win32 需要自己把它搭起来。
搭起来并不难,本质就是观察者模式——这正是本篇文章要展开的内容。
二、Win32 下 MVVM 各层职责
分层与 Qt 文章(10-桌面端架构/02)完全一致,这里补上每层在 Win32 下的具体形态:
1 | View 创建控件 + WndProc 接收消息 + 转发事件给 ViewModel;不写业务 |
各层代码示例
View:只创建控件 + 转发事件
1 | // MainWindow 持有控件,WndProc 收到 WM_COMMAND 后只做转发 |
ViewModel:编排 + 状态通知
1 | void MainViewModel::onScanClicked() { |
Domain:纯数据,不依赖 Win32
1 | struct ProcessInfo { DWORD pid; std::wstring name; size_t memory; }; |
Service:封装真正业务
1 | std::vector<ProcessInfo> ProcessService::listProcesses() { |
Infrastructure:封装平台能力
1 | // infrastructure/win32/ 下按能力分类:memory/、process/、thread/、file/、network/ ... |
分层核心约束: Domain 里没有 Win32,ViewModel 里不知道控件,View 里没有业务——每一层都只依赖它下面一层。
三、Win32 最大的缺口:没有 Property Binding
这是 Win32 落地 MVVM 时唯一真正要自己动手的地方。
1 | Qt: Q_PROPERTY + signal/slot——属性变了自动发信号,界面自动刷新 |
五种机制逐一说明
1. Property(属性)——带通知的字段。普通成员变量变成「setter 里触发通知」的封装:
1 | class MainViewModel { |
2. Command(命令)——把「用户操作」封装成可调用的动作,而不是直接调函数。ViewModel 暴露 Command,View 把按钮绑到命令上:
1 | struct Command { |
3. Event(事件)——通知的载体。用 std::function + 回调列表实现,配合 lambda 可以模拟 Qt 的 Signal/Slot:
1 | class Observable { |
4. Observer(观察者)——View 订阅 ViewModel 的状态,状态一变自动回调:
1 | viewModel_.addObserver([this] { refreshListView(); }); // 订阅 |
5. DataBinding(数据绑定)——把「属性 ↔ 控件」的同步简化封装。完整版是双向绑定,实际可以简化成「属性变化 → 刷新对应控件」:
1 | // 简化版:属性名 → 控件更新函数 的映射 |
Event + lambda 模拟 Qt Signal/Slot
Qt 的信号槽本质就是「事件 + 回调」。Win32 下用同样的思想:
1 | // Qt: connect(button, &QPushButton::clicked, vm, &MainViewModel::onScanClicked); |
绑定机制的本质就是观察者模式——属性是「可观察的状态」,Command 是「可调用的动作」,Event 是「通知的通道」,Observer 是「订阅关系」,DataBinding 是「属性与控件的映射」。五者合起来就是把 Qt 内置的东西按需搭出来。
四、完整调用流程
1 | 用户点击按钮 |
1 | sequenceDiagram |
关键点:
用户操作进入程序的唯一通道是消息循环 + WndProc;结果返回界面的唯一通道是观察者回调(notify → 刷新)。 消息循环负责「进」,观察者机制负责「出」。
五、View 的拆分:界面创建与事件处理分离
Win32 最常见的坏味道:所有东西都堆在 WndProc 里——创建控件、处理命令、处理通知、处理 resize、刷新数据全在一个巨型 switch 里。View 要拆成三职责:
1 | View = UI结构(创建) + UI呈现(刷新) + UI事件转发(处理消息) |
1. Create():只建界面
1 | void MainWindow::create(HWND parent) { |
2. WndProc 拆分:Handle* 系列
不要在一个 WndProc 里 switch 一切,按消息类型拆成函数:
1 | LRESULT MainWindow::wndProc(HWND hwnd, UINT msg, WPARAM w, LPARAM l) { |
3. Refresh():数据刷新与 UI 绘制分开
1 | void MainWindow::refresh(const ViewState& s) { |
拆分的收益
1 | 不拆分:WndProc 里一个巨型 switch,业务、布局、刷新全混在一起 |
「先只画窗口 UI、再分离数据」完全可以做到——先写 Create() 把界面画出来,验证界面没问题,再把 Handle* 的转发、Refresh 的刷新逐步接上。这正是最小闭环在 Win32 落地的方式。
六、四阶段自顶向下实现
把上面所有内容固定成可执行的顺序:
1 | 阶段 1:画 UI 骨架 |
每一步都是「可运行、可验证」的闭环,与 最小闭环/ 主题的思路一致——先画窗口验证,再往下接,每一次都是增量更新的可验证闭环。
与其他线的关系
- 与 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 桌面程序的具体展开
