文章最先发表于用C++实现MVVM.
欢迎关注博客.
用C++实现MVVM
序
MVVM
(Model-View-ViewModel
)是现在比较流行的GUI程序的框架。
通过代码的编写,我谈谈我对于MVVM的理解。
整体代码的sample在Graphics Editor可以看到。
GUI库使用了QT5.9
,功能代码主要使用了OpenCV
库。
后面一些功能的编写不是我写的,所以代码风格可能有些不和谐,这里主要集中精力于整个框架的实现,忽略其各项功能的实现。
如果有任何理解不对的地方,欢迎您批评指出。
MVVM
在阮一峰的"MVC,MVP 和 MVVM 的图示"中, 介绍了三个架构之间的区别。
总结来说,就是在Model,View,ViewModel三个模块之间,View与ViewModel之间的数据通过双向绑定进行联系,View与Model之间不产生联系,ViewModel操作Model进行数据处理。
(这里实际写代码的时候好像跟阮老师所说的有一些区别:按照阮老师所说,应该是ViewModel在功能上相当于MVP模式中的Presenter,所有逻辑都部署在这里,实际上写的时候应该是大部分逻辑都部署在Model层进行数据操作,然后通知ViewModel和View进行更新,不知道是否是在我的理解中出现问题……)
项目目录
.
├── app.cpp
├── app.h
├── command.cpp
├── command.h
├── Commands
│ ├── alter_bright_command.cpp
│ ├── alter_bright_command.h
│ ├── crop_command.cpp
│ ├── crop_command.h
│ ├── detect_face_command.cpp
│ ├── detect_face_command.h
│ ├── filter_command.cpp
│ ├── filter_command.h
│ ├── open_file_command.cpp
│ ├── open_file_command.h
│ ├── reset_command.cpp
│ ├── reset_command.h
│ ├── rotate_command.cpp
│ ├── rotate_command.h
│ ├── save_bmp_command.cpp
│ ├── save_bmp_command.h
│ ├── save_file_command.cpp
│ └── save_file_command.h
├── common.cpp
├── common.h
├── GraphicsEditor.pro
├── GraphicsEditor.pro.user
├── LICENSE
├── main.cpp
├── model.cpp
├── model.h
├── MyView.cpp
├── MyView.h
├── notification.cpp
├── notification.h
├── parameters.cpp
├── parameters.h
├── README.md
├── test.pro
├── test.pro.user
├── view.cpp
├── view.h
├── viewmodel.cpp
├── viewmodel.h
└── view.ui
项目架构介绍
各个类以及之间关系如下:
App
class App
{
private:
std::shared_ptr<View> view;
std::shared_ptr<Model> model;
std::shared_ptr<ViewModel> viewmodel;
public:
App();
void run();
};
在构造函数中,对各项需要初始化和绑定的数据进行绑定:
App::App():view(new View),model(new Model), viewmodel(new ViewModel)
{
viewmodel->bind(model);
view->set_img(viewmodel->get());
view->set_open_file_command(viewmodel->get_open_file_command());
view->set_alter_bright_command(viewmodel->get_alter_bright_command());
view->set_filter_rem_command(viewmodel->get_filter_rem_command());
view->set_reset_command(viewmodel->get_reset_command());
view->set_detect_face_command(viewmodel->get_detect_face_command());
view->set_save_file_command(viewmodel->get_save_file_command());
view->set_save_bmp_file_command(viewmodel->get_save_bmp_file_command());
view->set_rotate_command(viewmodel->get_rotate_command());
view->set_crop_command(viewmodel->get_crop_command());
viewmodel->set_update_view_notification(view->get_update_view_notification());
model->set_update_display_data_notification(viewmodel->get_update_display_data_notification());
}
View
class View : public QMainWindow
{
Q_OBJECT
public:
explicit View(QWidget *parent = 0);
~View();
void update();
void set_img(std::shared_ptr<QImage> image);
void set_open_file_command(std::shared_ptr<Command>);
void set_alter_bright_command(std::shared_ptr<Command>);
void set_filter_rem_command(std::shared_ptr<Command>);
void set_reset_command(std::shared_ptr<Command>);
void set_detect_face_command(std::shared_ptr<Command>);
void set_save_file_command(std::shared_ptr<Command>);
void set_save_bmp_file_command(std::shared_ptr<Command>);
void set_rotate_command(std::shared_ptr<Command>);
void set_crop_command(std::shared_ptr<Command>);
std::shared_ptr<Notification> get_update_view_notification();
private slots:
void on_button_open_clicked();
void on_brightSlider_valueChanged(int value);
void on_contrastSlider_valueChanged(int value);
void on_filter_1_clicked();
void on_reset_clicked();
void on_actionOpen_File_triggered();
void on_button_detect_face_clicked();
void on_actionSave_triggered();
void on_action_bmp_triggered();
void on_action_png_triggered();
void on_action_jpeg_triggered();
void on_rotateSlider_valueChanged(int value);
private:
Ui::View *ui;
MyView* canvas;
std::shared_ptr<QImage> q_image;
std::shared_ptr<Command> open_file_command;
std::shared_ptr<Command> alter_bright_command;
std::shared_ptr<Command> filter_rem_command;
std::shared_ptr<Command> reset_command;
std::shared_ptr<Command> detect_face_command;
std::shared_ptr<Command> save_file_command;
std::shared_ptr<Command> save_bmp_file_command;
std::shared_ptr<Command> rotate_command;
std::shared_ptr<Command> crop_command;
std::shared_ptr<Notification> update_view_notification;
};
本身提供一个用于更新的notification
, 并提供get()
方法交给ViewModel
层进行绑定,如此可以实现ViewModel
通知View
进行更新。
同时,本身提供很多Command
的成员变量,这些变量本省并不属于View
层,本身属于ViewModel
层,并在ViewModel
层提供get
方法给View
层进行set
绑定,这样就实现了View
发送command
给ViewModel
层,View
就可以在不知道Command具体派生类的情况下写代码。
ViewModel
class ViewModel
{
private:
std::shared_ptr<QImage> q_image;
std::shared_ptr<Model> model;
std::shared_ptr<Command> open_file_command;
std::shared_ptr<Command> alter_bright_command;
std::shared_ptr<Command> filter_rem_command;
std::shared_ptr<Command> reset_command;
std::shared_ptr<Command> detect_face_command;
std::shared_ptr<Command> save_file_command;
std::shared_ptr<Command> save_bmp_file_command;
std::shared_ptr<Command> rotate_command;
std::shared_ptr<Command> crop_command;
std::shared_ptr<Notification> update_display_data_notification;
std::shared_ptr<Notification> update_view_notification;
public:
ViewModel();
void bind(std::shared_ptr<Model> model);
void exec_open_file_command(std::string path);
void exec_alter_bright_command(int nBright, int nContrast);
void exec_filter_rem_command();
void exec_reset_command();
void exec_detect_face_command();
void exec_save_file_command(std::string path);
void exec_save_bmp_file_command(std::string path);
void exec_rotate_command(int angle);
void exec_crop_command(double x_s, double y_s, double x_e, double y_e);
void set_update_view_notification(std::shared_ptr<Notification> notification);
std::shared_ptr<Command> get_open_file_command();
std::shared_ptr<Command> get_alter_bright_command();
std::shared_ptr<Command> get_filter_rem_command();
std::shared_ptr<Command> get_reset_command();
std::shared_ptr<Command> get_detect_face_command();
std::shared_ptr<Command> get_save_file_command();
std::shared_ptr<Command> get_save_bmp_file_command();
std::shared_ptr<Command> get_rotate_command();
std::shared_ptr<Command> get_crop_command();
std::shared_ptr<Notification> get_update_display_data_notification();
std::shared_ptr<QImage> get();
void notified();
};
与View
层之间的通信在之前已经讲过,在构造函数中初始化具体的命令,然后get
交给View
的set
进行绑定。这其中有一个向基类指针的转换,我是这么写的:
open_file_command = std::static_pointer_cast<Command, OpenFileCommand>(std::shared_ptr<OpenFileCommand> (new OpenFileCommand(std::shared_ptr<ViewModel>(this))));
然后与Model
间的通信没有通过Command
,而是直接获得一个Model
的指针,调用它的功能函数即可。
Model
class Model
{
private:
cv::Mat image;
std::shared_ptr<Notification> update_display_data_notification;
public:
Model(){}
void set_update_display_data_notification(std::shared_ptr<Notification> notification);
void open_file(std::string path);
cv::Mat& get();
cv::Mat& getOrigin();
void notify();
void save_file(std::string path);
void save_bmp_file(std::string path);
void alterBrightAndContrast(int nbright, int nContrast);
void detect_face();
void filterReminiscence(); //Filter No.1
void reset();
void rotate(double angle);
void crop(int x1, int y1, int x2, int y2);
};
Model
层本身又一个set一个notification的接口,这个notification用于通知ViewModel
进行更新数据。
其他的就是针对数据的一些功能代码。
Command
本身可以写为纯虚类,我是写了一个成员变量是一个基类参数的指针,然后所有具体的command都是派生于此,提供exec()
方法。
class Command
{
protected:
std::shared_ptr<Parameters> params;
public:
Command();
void set_parameters(std::shared_ptr<Parameters> parameters){
params = parameters;
}
virtual void exec() = 0;
};
Notification
class Notification
{
public:
Notification();
virtual void exec() = 0;
};
class UpdateDisplayDataNotification: public Notification{
private:
std::shared_ptr<ViewModel> viewmodel;
public:
UpdateDisplayDataNotification(std::shared_ptr<ViewModel> vm):viewmodel(vm){}
void exec(){
viewmodel->notified();
}
};
class UpdateViewNotification: public Notification{
private:
std::shared_ptr<View> view;
public:
UpdateViewNotification(std::shared_ptr<View> v):view(v){}
void exec(){
view->update();
}
};
Parameters
class Parameters
{
public:
Parameters();
};
class PathParameters: public Parameters{
private:
std::string path;
public:
PathParameters(std::string _path):path(_path){
}
std::string get_path(){
return path;
}
};
以PathParameters
为例表示了一般的新的参数的派生方法。
common
实现了cv::Mat
与QImage
之间的转换代码。
整体流程
在View
层进行操作之后,会触发对应槽函数,该槽函数会准备好参数Parameter
交给对应的Command
,然后执行exec()
这个command,exec会解出参数交给ViewModel
层,ViewModel
调用Model
里对应的方法,进行数据操作,Model
操作完之后会通知ViewModel
更新显示数据,ViewModel
会通知View
刷新显示。