第一阶段 · 第 4 课:MVC、MVP 与 MVVM 架构
上一课我们学习了分层架构,把 Qt 工程拆成 UI 层、业务层、设备层和公共基础层。
这一课进一步解决一个实际问题:Qt 界面类越来越臃肿,按钮槽函数里既有业务逻辑,又有设备操作和数据处理,应该怎么办?
我们以你熟悉的 Qt/C++ 电源控制程序为例,学习三种常见架构。
一、先理解三种架构的区别
1. MVC --- Model-View-Controller
模型-视图-控制器
- Model:保存数据、处理业务状态。
- View:显示界面。
- Controller:接收操作并协调 Model 与 View。
适合:理解传统桌面应用和 Web 应用的职责划分。
2. MVP --- Model-View-Presenter
模型-视图-展示器
- Model:数据与业务。
- View:负责显示和转发用户操作。
- Presenter:处理界面相关逻辑,调用 Model,再通知 View 更新。
适合:希望把界面逻辑独立出来,便于单元测试的桌面程序。
3. MVVM --- Model-View-ViewModel
模型-视图-视图模型
- Model:数据与业务规则。
- View:显示界面。
- ViewModel:向界面提供可显示的数据和操作接口。
适合:数据状态较多、界面需要频繁更新的应用。
注意:三者并非互相排斥的绝对标准。不同项目可能采用混合结构,尤其在 Qt Widgets 项目中很常见。
二、先看最常见的问题:界面类承担太多职责
例如下面这种写法:
cpp
void FormPower::on_pushButtonSet_clicked()
{
double voltage = ui->doubleSpinBoxVoltage->value();
// 检查参数
if (voltage < 0 || voltage > 10)
return;
// 设备通信
m_power->setVoltage(voltage);
// 更新界面
ui->labelStatus->setText("设置完成");
// 保存配置
m_settings->setValue("Voltage", voltage);
}
这个槽函数同时承担了参数校验、设备控制、界面更新和配置保存。
问题是:如果以后要增加自动测试、定时测试或命令行控制,也需要重复这些逻辑。
我们可以先把界面逻辑抽出去。对于 Qt Widgets 项目,MVP 是一种容易上手的选择。
三、用 MVP 改造 Qt 程序
- 定义 View 接口
让 Presenter 不直接操作 QLabel、QDoubleSpinBox 等具体控件。
cpp
class IPowerView
{
public:
virtual ~IPowerView() = default;
virtual double voltage() const = 0;
virtual void showStatus(const QString& text) = 0;
};
这里的 IPowerView 只定义 Presenter 需要的界面能力。
- 定义 Presenter
Presenter 接收用户操作,执行校验、调用业务对象,并将结果交给 View。
cpp
class PowerPresenter
{
public:
PowerPresenter(IPowerView* view, IPowerDevice* power)
: m_view(view), m_power(power)
{
}
void setVoltage()
{
if (!m_view || !m_power)
return;
const double voltage = m_view->voltage();
if (voltage < 0.0 || voltage > 10.0) {
m_view->showStatus("电压范围错误");
return;
}
if (!m_power->setVoltage(voltage)) {
m_view->showStatus("设置失败");
return;
}
m_view->showStatus("设置成功");
}
private:
IPowerView* m_view; // 非拥有指针
IPowerDevice* m_power; // 非拥有指针
};
这里继续使用了前面学过的接口抽象和依赖注入。Presenter 不需要知道具体是哪一个 FormPower,也不需要知道具体电源型号。
- 让 Qt 窗口实现 View
cpp
class FormPower : public QWidget, public IPowerView
{
Q_OBJECT
public:
explicit FormPower(QWidget* parent = nullptr)
: QWidget(parent)
{
// setupUi(this);
}
double voltage() const override
{
return ui->doubleSpinBoxVoltage->value();
}
void showStatus(const QString& text) override
{
ui->labelStatus->setText(text);
}
private slots:
void on_pushButtonSet_clicked()
{
if (m_presenter)
m_presenter->setVoltage();
}
private:
PowerPresenter* m_presenter = nullptr; // 非拥有指针
};
实际工程中,记得在构造函数或初始化函数中创建并设置 m_presenter。Presenter 的生命周期必须覆盖它被使用的时间。
- 在组装位置连接对象
cpp
Power2657 power;
FormPower form;
PowerPresenter presenter(&form, &power);
// presenter 在 form 使用期间保持存活
form.setPresenter(&presenter);
form.show();
上面示例中的 setPresenter() 需要在 FormPower 中提供对应的 setter:
void setPresenter(PowerPresenter* presenter)
{
m_presenter = presenter;
}
实际项目可以使用构造函数注入,让依赖关系更加明确。
现在职责变成:
- FormPower:读取控件值、显示状态、转发点击操作。
- PowerPresenter:处理界面相关流程和输入校验。
- IPowerDevice:抽象电源能力。
- Power2657:负责具体设备操作。
四、MVP 和 MVVM 到底有什么区别?
MVP 中,Presenter 通常会主动调用 View 的方法来更新界面。
MVVM 则通常让 ViewModel 暴露状态和命令,View 通过数据绑定或信号槽响应这些状态变化。
比较 MVP MVVM
界面更新 Presenter 调用 View 接口 View 响应 ViewModel 的状态
依赖关系 Presenter 持有 View 接口 View 观察或绑定 ViewModel
测试 可以独立测试 Presenter 可以独立测试 ViewModel
Qt Widgets 很容易用接口实现 可用属性、信号槽实现
Qt Quick/QML 可用 通常很自然
在 Qt 中,MVVM 不要求必须使用 QML。你也可以在 Qt Widgets 中使用 QObject、Q_PROPERTY、signals 和 slots 实现 ViewModel。
例如:
cpp
class PowerViewModel : public QObject
{
Q_OBJECT
Q_PROPERTY(QString status READ status NOTIFY statusChanged)
public:
explicit PowerViewModel(QObject* parent = nullptr)
: QObject(parent)
{
}
QString status() const
{
return m_status;
}
void setStatus(const QString& status)
{
if (m_status == status)
return;
m_status = status;
emit statusChanged();
}
signals:
void statusChanged();
private:
QString m_status;
};
当 m_status 变化时,ViewModel 发出 statusChanged(),界面便可以连接这个信号并刷新状态显示。若需要完整的数据绑定体验,Qt Quick/QML 提供了更直接的机制。
五、结合你的 Qt 项目,怎么选?
我的建议是:
- 普通 Qt Widgets 工具界面:先使用分层架构,复杂界面可以引入 MVP。
- 大量参数、设备状态、异步采集和实时曲线:可以把数据状态抽成独立 Model 或 ViewModel,避免窗口类越来越大。
- Qt Quick/QML 项目:优先考虑 MVVM 风格。
- 已有大型项目:不必为了模式而整体重写。先把最臃肿的槽函数拆出去,逐步引入接口和业务类。
尤其要注意:MVP 的 Presenter 不应承担所有设备协议和底层通信。Presenter 负责界面交互流程,设备控制流程仍然可以由上一课的 PowerController 或 TestService 处理。
六、课后练习
问题:你的红外相机界面需要显示采集状态、温度、实时图像,并响应"开始采集"按钮。下面哪种设计更合理?
A. 所有逻辑全部写进 FormCamera 的槽函数
B. 界面负责显示和交互,控制类负责采集流程,设备类负责相机通信;状态通过信号槽传递
C. 每个控件单独创建一个线程,避免界面阻塞
D. 所有类都继承 QObject,就自然实现了分层架构
提交答案
下一课可以进入第一阶段的最后一个主题:如何设计模块边界、头文件依赖和 .pro 工程结构,把这些架构原则落到实际 Qt 工程中。