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进行更新,不知道是否是在我的理解中出现问题……)

项目目录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
.
├── 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

1
2
3
4
5
6
7
8
9
10
11
class App
{
private:
std::shared_ptr<View> view;
std::shared_ptr<Model> model;
std::shared_ptr<ViewModel> viewmodel;

public:
App();
void run();
};

在构造函数中,对各项需要初始化和绑定的数据进行绑定:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
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发送commandViewModel层,View就可以在不知道Command具体派生类的情况下写代码。

ViewModel

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
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交给Viewset进行绑定。这其中有一个向基类指针的转换,我是这么写的:

1
open_file_command = std::static_pointer_cast<Command, OpenFileCommand>(std::shared_ptr<OpenFileCommand> (new OpenFileCommand(std::shared_ptr<ViewModel>(this))));

然后与Model间的通信没有通过Command,而是直接获得一个Model的指针,调用它的功能函数即可。

Model

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

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()方法。

1
2
3
4
5
6
7
8
9
10
11
12

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

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::MatQImage之间的转换代码。

整体流程

View层进行操作之后,会触发对应槽函数,该槽函数会准备好参数Parameter交给对应的Command,然后执行exec()这个command,exec会解出参数交给ViewModel层,ViewModel调用Model里对应的方法,进行数据操作,Model操作完之后会通知ViewModel更新显示数据,ViewModel会通知View刷新显示。