一谈到设计模式, 很多人都觉得,设计模式只是GoF书上那23种。 但我认为广义的设计模式包含架构模式, 即MVC, MVVM之类的设计理念。 使用C++开发应用程序时, 往往需要应用这些广艺设计模式,自己搭建一个应用程序框架。 这里我提供一个适用于Qt库的MVC设计模式样板代码, 这种设计已经在我们的工业级软件中广泛应用, 并经过实践考验, 结构清晰明了。
首先,我们使用 Qt Designer 制作界面,Qt Designer 生成的 .ui 文件作为 MVC 模式中的 View 层。
然后,手写 Controller 层的代码。 Controller 层作为 View 层 和 Model 层的衔接, 非常重要。
为了增强语义, 我们做两个 type declaration, 为 MyWidget
起一个别名叫Self
, 为QWidget
起一个别名叫Super
。这么做的目的是区分我们自己写的成员函数和基类的成员函数, 在多层继承的环境下, 一定要区分成员函数, 不然一不小心,你可能就 overwrite 了一个基类的虚函数。
class MyWidget : public QWidget
{
Q_OBJECT
using Self = MyWidget;
using Super = QWidget;
public:
MyWidget(QWidget* parent = nullptr);
~MyWidget();
private:
Ui::MyWidgetClass ui;
}
回看这个类, ui
这个成员可以理解成 View 层的依赖注入,也就是通过这个变量, 我们可以访问 View 层的任何控件。
接下来, 我们做几个成员函数,函数见名知意。
class MyWidget : public QWidget
{
// ...
public slots:
void updateWidgets();
protected:
virtual void showEvent(QShowEvent* e) override;
virtual void closeEvent(QCloseEvent* e) override;
private:
void makeConnections();
void setupUi();
// ...
}
对于虚函数我们一律使用override 关键字, 结合了 virtual 关键字, 我们只为强调这是一个在MyWidget
类中被重新实现的虚函数, 语义更强。
updateWidgets()
函数写在 public section, 既可以被外部调用, 也可以被内部调用。 这个函数就是用来给界面上数据。 如果你的应用程序需要打开一个工程文件(比如 Visual Studio 可以打开 .sln 文件), 在把工程文件读入内存后, 就可以调用这个函数, 更新界面上的内容。
在点击关闭按钮时, 我们不想让Qt框架析构掉这个窗口的资源, 所以重写了closeEvent
屏蔽关闭事件, 顺便可以做点自己的实现(比如在用户点关闭按钮时, 可以向用户确认是否真的要退出)。
在.cpp文件中,我们首先实现构造函数。
// MyWidget.cpp
MyWidget::MyWidget (QWidget* parent)
: QWidget(parent)
{
/* Qt框架做的 View 层初始化代码 */
ui.setupUi(this);
/* 我们对 View 层初始化的补充 */
Self::setupUi();
}
这时, 你已经看到了 type declaration 的妙用。 除了框架的初始化代码, 我们自己还要做点补充。 比如你的界面上有ComboBox
存在, 那么在 setupUi()函数里, 你需要给它上选项。
void MyWidget::setupUi()
{
QStringList optionList;
optionList.append("Option1");
optionList.append("Option2");
ui.myCombo->addItems(optionList);
Self::makeConnections();
}
对 View 层做完初始化, 我们要连接信号槽。 makeConnections()
里面, 用connect()
实现了数据的单向绑定。 我们结合 Lambda 函数, 可以把单向绑定做得很简洁。 在 Lambda 函数里, 直接把数据写入 Model 层。 Model 层的访问, 你可以选择用单例模式访问, 也可以把 Model 层注入到MyWidget
类来, 并通过指针或引用访问 。
- 使用单例模式访问 Model 层
void MyWidget::makeConnections()
{
connect(ui.myCombo, &QComboBox::activated, [this](int index){
DataModel::get().option = index;
});
}
- 使用依赖注入访问 Model 层
// MyWidget.h
class MyWidget : public QWidget
{
// ...
private:
DataModel* _dataModel;
// ...
}
// MyWidget.cpp
MyWidget::MyWidget (QWidget* parent, DataModel* dataModel)
: QWidget(parent)
{
// ...
_dataModel = dataModel;
// ...
}
void MyWidget::makeConnections()
{
connect(ui.myCombo, &QComboBox::activated, [this](int index){
_dataModel->option = index;
});
}
对于_dataModel
, 我没有使用智能指针。 因为在这一层里, delete _dataModel
这种行为是无意义的。
对于 DataModel
里的 option
成员,我们没设计getter
和setter
, 而是直接开放为 public data member, 这不仅简化了代码, 而且强调 option
成员是个可读可写的。
然后我们来实现 showEvent()
函数。 我们把上数据这个操作, 推迟到 showEvent()
里进行。
void MyWidget::showEvent(QShowEvent* e)
{
Q_UNUSED(e);
Self::updateWidgets();
}
界面上显示的数据, 有可能来自应用程序读入的项目文件, 也有可能是个一成不变的值。 如果是来自项目, 那我们直接从DataModel
里获取就好了。 但是如果是常量, 为了让代码中不出现魔法数字,我们最好组织一下代码。
class MyWidget : public QWidget
{
// ...
private:
struct
{
int option;
} _d;
// ...
};
MyWidget::MyWidget(QWidget* parent)
: QWidget(parent)
{
// ...
_d.option = 1;
// ...
}
void MyWidget::updateWidgets()
{
ui.myCombo->setCurIndex(_d.option);
}
先做一个 Anonymouse struct 组织这些属于 Controller 层的变量, 然后在构造函数中给它默认值。 在updateWidgets()
里面, 对控件进行更新。
最后实现closeEvent()
逻辑。
void MyWidget::closeEvent(QCloseEvent* e)
{
/* 调用 QWidget 的成员函数 */
Super::hide();
/* 事件传播到此为止 */
e->ignore(();
}
在这个函数里, 我们只对控件做隐藏操作, 而不是析构,这确保了 MyWidget 对象的生命周期。
Model 层的设计与实现我想另开一贴, 毕竟有很多内容要说。 至于 View 层和 Controller 层的设计, 在此之上增加任何业务,都不会使代码变得杂乱。