一句话总结

  • MVC:Controller 手动”指挥” View 和 Model,View 的更新靠代码一行行写。
  • MVVM:View 和 ViewModel 通过数据绑定自动同步,几乎不需要手写”更新界面”的代码。

两种架构各是什么

MVC(Model-View-Controller)

角色 职责
Model 数据和业务逻辑
View 显示界面
Controller 接收用户操作,修改 Model,并通知 View 刷新

数据流:用户点击 → Controller 处理 → 改 Model → Controller(或 Model 的信号)手动调用 View 更新。

MVVM(Model-View-ViewModel)

角色 职责
Model 数据和业务逻辑
View 只负责声明”界面长什么样”,并绑定到 ViewModel 的属性
ViewModel 把 Model 的数据加工成”界面直接能用的状态”,并暴露操作命令

数据流:ViewModel 的属性一变,绑定机制自动刷新 View;用户操作调用 ViewModel 的方法。ViewModel 完全不知道 View 的存在。

关键区别

要解决的问题 MVC 风格 MVVM 风格
原始计数存在哪 Model Model
用户点击交给谁 Controller ViewModel 的操作方法
View 怎么更新 Controller 手动调用,如 label->setText() 属性绑定自动更新
中间层是否认识 View Controller 通常持有 View 的指针 ViewModel 不引用 View
View 里有多少逻辑 常有少量逻辑(事件处理) 几乎没有,只做声明
可测试性 Controller 依赖 View,较难单测 ViewModel 是纯逻辑,易单测
样板代码 多(大量 connect 和 set) 少
样板代码(boilerplate code)

样板代码(boilerplate code)指的是:为了让功能跑起来,必须反复写、但本身没有”业务含义”的重复代码。它不是你真正想实现的逻辑,只是”把东西连起来”的体力活。

MVC 里为什么多:

QWidget + MVC 中,界面上每个会变化的东西,都要你手动写两类代码:

  1. connect:把控件的信号、Model 的信号和 Controller 的槽一根根连起来
  2. set:数据变了之后,手动调用 setText()、setEnabled()、setVisible() 等去刷新控件

比如一个登录页,有”状态文字、按钮是否可用、进度条是否显示”三处要随数据变化:

1
2
3
4
5
6
7
8
9
10
// 每多一个要变化的界面元素,就多几行 connect + set
connect(model, &UserModel::statusChanged, this, [=]{
view->statusLabel->setText(model->status()); // set
});
connect(model, &UserModel::busyChanged, this, [=]{
view->loginButton->setEnabled(!model->busy()); // set
view->progressBar->setVisible(model->busy()); // set
});
connect(view->loginButton, &QPushButton::clicked, // connect
this, &LoginController::onLoginClicked);

界面越复杂,这类代码越多,而且它们都是同一种模式的重复,这就是”样板”。

MVVM 里为什么少:

QML 里一句绑定就代替了上面的 connect + set:

1
2
3
Label { text: vm.status }                  // 代替 connect + setText
Button { enabled: !vm.busy } // 代替 connect + setEnabled
ProgressBar { visible: vm.busy } // 代替 connect + setVisible

“数据变了就更新界面”这件事由 QML 的绑定机制自动完成,你不用再写。

Qwidget + MVC 对比 QML + MVVM

QWidget + MVC 的典型写法

1
2
3
4
5
6
7
8
9
10
11
12
13
// Controller:持有 View 和 Model,负责"粘合"
class LoginController : public QObject {
public:
LoginController(LoginWidget* view, UserModel* model) {
connect(view->loginButton, &QPushButton::clicked, this, [=]{
bool ok = model->login(view->nameEdit->text(),
view->pwdEdit->text());
// 手动更新界面
view->statusLabel->setText(ok ? "成功" : "失败");
view->loginButton->setEnabled(!ok);
});
}
};

特点:每个界面变化都要手动 setText / setEnabled / setVisible,Controller 必须认识具体的控件。

QML + MVVM 的典型写法

1
2
3
4
5
6
7
8
9
10
11
// ViewModel:纯 C++,通过 Q_PROPERTY 暴露状态
class LoginViewModel : public QObject {
Q_OBJECT
Q_PROPERTY(QString status READ status NOTIFY statusChanged)
Q_PROPERTY(bool busy READ busy NOTIFY busyChanged)
public:
Q_INVOKABLE void login(const QString& name, const QString& pwd);
signals:
void statusChanged();
void busyChanged();
};
1
2
3
4
5
6
7
8
9
10
// View:只做声明式绑定,没有"更新界面"的代码
Column {
TextField { id: nameEdit }
Button {
text: "登录"
enabled: !vm.busy // 自动跟随 vm.busy
onClicked: vm.login(nameEdit.text, pwdEdit.text)
}
Label { text: vm.status } // vm.status 变了自动刷新
}

特点:ViewModel 里只管改属性、发 NOTIFY 信号,界面自己会跟着变,ViewModel 完全不碰任何 UI 控件。

为什么 QWidget 通常搭配 MVC?

核心原因:QWidget 是命令式(imperative)UI,没有原生的数据绑定。

  1. 界面是”对象 + 方法调用” :按钮、标签都是 C++ 对象,要改显示就得调用 setText() 等方法,天然需要”某个东西”来负责调用,这个角色自然就是 Controller。
  2. 没有绑定机制:想让 View 跟 Model 同步,只能靠 connect 信号槽 + 手写刷新代码,这正是 MVC 的 Controller 在做的事。
  3. 控件本身就带逻辑:QWidget 的事件处理(mousePressEvent 等)和 View 绑得很紧,View 和 Controller 很难彻底分开。
  4. Qt 自带的 Model/View 框架本身就是 MVC 的变体(QAbstractItemModel + QListView/QTableView + QStyledItemDelegate,Controller 的职责被合并进 View/Delegate),所以 QWidget 开发者的思维习惯自然偏向 MVC。

为什么 QML 通常搭配 MVVM?

核心原因:QML 是声明式(declarative)UI,原生支持属性绑定。

  1. 绑定是语言级特性:text: vm.status 这一句就建立了自动同步,不需要 Controller 去”推”数据,MVC 里 Controller 最重的工作被语言本身接管了。
  2. Q_PROPERTY + NOTIFY 信号就是为绑定设计的:C++ 对象一暴露属性,QML 立刻能绑定,这个 C++ 对象就是现成的 ViewModel。
  3. QML 不适合写复杂逻辑:JS 里堆业务逻辑难维护、难测试,所以把逻辑放进 C++ ViewModel,QML 只留声明,职责划分非常清晰。
  4. C++ 与 QML 天然解耦:ViewModel 不持有任何 QML 对象指针,可以脱离界面单独测试,也可以换一套 QML 界面复用同一个 ViewModel。

一个形象的比喻

  • MVC:像餐厅里有个服务员(Controller),客人点菜后,服务员跑去厨房(Model)传话,菜好了再亲自端到桌上(更新 View)。
  • MVVM:像自助点餐屏 + 出餐口显示屏,屏幕(View)直接绑定订单状态(ViewModel),状态一变,屏幕自动刷新,不需要人去通知。

补充说明

这是”惯例“而不是”规定“:

  • QWidget 也可以做 MVVM,但要自己写绑定(用信号槽模拟,或借助第三方库),收益不大,反而增加复杂度。
  • QML 也可以用 MVC,但等于没有利用绑定,是在浪费 QML 的优势。
  • 实际项目中,两者的边界也常常混合,比如 QML 里用 QAbstractListModel 做列表数据,再配合 ViewModel 管理页面状态。

选择依据一句话:UI 框架有没有声明式绑定,决定了”中间层”是需要手动指挥(Controller),还是只需要暴露状态(ViewModel)。

补充:Qt 的 Model/View/Delegate 与 MVC 有什么区别

前面的 MVC、MVVM 用来讨论界面与业务代码如何组织。
Qt Widgets 的 Model/View/Delegate 主要用于列表、表格、树形控件的数据展示与编辑:

  • Model:提供数据访问接口。
  • View:管理列表、表格或树的整体展示,以及选中、滚动等交互。
  • Delegate:负责单个数据项怎样绘制,以及编辑时使用什么编辑器。
    Qt 将传统 MVC 的 View 与部分 Controller 职责合到 View 中,再通过 Delegate 定制数据项的显示和编辑。

例如表格里有一个“年龄”单元格:

角色 做什么
Model 提供年龄 18,接受修改后的年龄
View 确定这一格的位置,处理选中和开始编辑
Delegate 平时绘制 18,编辑时提供数字输入框,提交新值

Delegate 不是改名后的 Controller。 它专门处理数据项的绘制和编辑,不负责整个应用的登录、设备连接等业务流程。

常用的 QStyledItemDelegate 提供这些定制入口:

方法 作用
paint() 绘制数据项
sizeHint() 提供建议尺寸
createEditor() 创建编辑控件
setEditorData() 将 Model 的数据填入编辑控件
setModelData() 将编辑结果写回 Model