C++和QT中的MVC和Subject-Observer模式

Sma*_*ash 7 c++ model-view-controller qt

免责声明:

正如第一个答案已经适当注意到的那样,在当前的示例案例中使用MVC是过度的.问题的目标是通过一个简单的例子来理解底层概念,以便能够在更大的程序中使用它们来修改更复杂的数据(数组,对象).


我试图在C++和QT中实现MVC模式,类似于这里的问题:

其他MVC问题

该程序有2行编辑:

  • mHexLineEdit
  • mDecLineEdit

3个按钮

  • mConvertToHexButton
  • mConvertoDecButton
  • mClearButton

并只修改字符串.

在此输入图像描述

与另一个问题的区别在于,我试图实现Subject/Observer模式,以便在模型更改后更新View.

Model.h

#ifndef MODEL_H
#define MODEL_H

#include <QString>
#include <Subject>

class Model : virtual public Subject
{
public:
    Model();
    ~Model();
    void convertDecToHex(QString iDec);
    void convertHexToDec(QString iHex);
    void clear();

    QString getDecValue() {return mDecValue;}
    QString getHexValue() {return mHexValue;}
private:
    QString mDecValue;
    QString mHexValue;
};
#endif // MODEL_H
Run Code Online (Sandbox Code Playgroud)

Model.cpp

#include "Model.h"

Model::Model():mDecValue(""),mHexValue(""){}
Model::~Model(){}

void Model::convertDecToHex(QString iDec)
{
    mHexValue = iDec + "Hex";

    notify("HexValue");
}

void Model::convertHexToDec(QString iHex)
{
    mDecValue = iHex + "Dec";

    notify("DecValue");
}

void Model::clear()
{
  mHexValue = "";
  mDecValue = "";

  notify("AllValues");
}
Run Code Online (Sandbox Code Playgroud)

View.h

#ifndef VIEW_H
#define VIEW_H

#include <QtGui/QMainWindow>
#include "ui_View.h"
#include <Observer>

class Controller;
class Model;
class View : public QMainWindow, public Observer
{
    Q_OBJECT

public:
    View(QWidget *parent = 0, Qt::WFlags flags = 0);
    ~View();
    void setController(VController* iController);
    void setModel(VModel* iModel);
    QString getDecValue();
    QString getHexValue();
public slots:
    void ConvertToDecButtonClicked();
    void ConvertToHexButtonClicked();
    void ClearButtonClicked();
private:

    virtual void update(Subject* iChangedSubject, std::string iNotification);

    Ui::ViewClass ui;

    Controller*  mController;
    Model*        mModel;
};

#endif // VIEW_H
Run Code Online (Sandbox Code Playgroud)

View.cpp

#include "View.h"
#include "Model.h"
#include "Controller.h"
#include <QSignalMapper>

VWorld::VWorld(QWidget *parent, Qt::WFlags flags)
: QMainWindow(parent, flags)
{
    ui.setupUi(this);

    connect(ui.mConvertToHexButton,SIGNAL(clicked(bool)),this,SLOT(ConvertToHexButtonClicked()));
    connect(ui.mConvertToDecButton,SIGNAL(clicked(bool)),this,SLOT(ConvertToDecButtonClicked()));
    connect(ui.mClearButton,SIGNAL(clicked(bool)),this,SLOT(ClearButtonClicked()));
}

View::~View(){}

void View::setController(Controller* iController)
{
    mController = iController;

    //connect(ui.mConvertToHexButton,SIGNAL(clicked(bool)),this,SLOT(mController->OnConvertToHexButtonClicked(this)));
    //connect(ui.mConvertToDecButton,SIGNAL(clicked(bool)),this,SLOT(mController->OnConvertToDecButtonClicked(this)));
    //connect(ui.mClearButton,SIGNAL(clicked(bool)),this,SLOT(mController->OnClearButtonClicked(this)));
}

void View::setModel(Model* iModel)
{
    mModel = iModel;

    mModel->attach(this);
}

QString View::getDecValue()
{
    return ui.mDecLineEdit->text();
}

QString View::getHexValue()
{
    return ui.mHexLineEdit->text();
}

void View::ConvertToHexButtonClicked()
{
    mController->OnConvertToHexButtonClicked(this);
}

void View::ConvertToDecButtonClicked()
{
    mController->OnConvertToDecButtonClicked(this);
}

void VWorld::ClearButtonClicked() 
{
    mController->OnClearButtonClicked(this);
}

void View::update(Subject* iChangedSubject, std::string     iNotification)
{
    if(iNotification.compare("DecValue") == 0)
    {
        ui.mDecLineEdit->setText(mModel->getDecValue());
    }
    else if(iNotification.compare("HexValue") == 0)
    {
        ui.mHexLineEdit->setText(mModel->getHexValue());
    }
    else if(iNotification.compare("AllValues") == 0)
    {
        ui.mDecLineEdit->setText(mModel->getDecValue());
        ui.mHexLineEdit->setText(mModel->getHexValue());
    }
    else
    {
        //Unknown notification;
    }
}
Run Code Online (Sandbox Code Playgroud)

或者Controller.h

#ifndef CONTROLLER_H
#define CONTROLLER_H

//Forward Declaration
class Model;
class View;

class Controller 
{
public:
    Controller(Model* iModel);
    virtual ~Controller();
    void OnConvertToDecButtonClicked(View* iView);
    void OnConvertToHexButtonClicked(View* iView);
    void OnClearButtonClicked(View* iView);
private:
    Model* mModel;
};
#endif // CONTROLLER_H
Run Code Online (Sandbox Code Playgroud)

Controller.cpp

#include "Controller.h"
#include "Model.h"
#include "View.h"

Controller::Controller(Model* iModel):mModel(iModel){}

Controller::~Controller(){}

void Controller::OnConvertToDecButtonClicked(View* iView) 
{
  QString wHexValue = iView->getHexValue();

  mModel->convertHexToDec(wHexValue);
}

void Controller::OnConvertToHexButtonClicked(View* iView) 
{
  QString wDecValue = iView->getDecValue();

  mModel->convertDecToHex(wDecValue);
}

void Controller::OnClearButtonClicked(View* iView) 
{
  mModel->clear();
}
Run Code Online (Sandbox Code Playgroud)

main.cpp中

#include "View.h"
#include "Model.h"
#include "Controller.h"
#include <QtGui/QApplication>

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    Model wModel;
    View wView;
    Controller wCtrl(&wModel);
    wView.setController(&wCtrl);
    wView.setModel(&wModel);
    wView.show();
    return a.exec();
}
Run Code Online (Sandbox Code Playgroud)

如果它们变得相关,我可以稍后发布主题/观察者文件.

除了一般性评论,有人可以回答这些问题:

1)将按钮信号直接连接到控制器插槽(如在注释掉的部分中)会更好View::setController吗?Controller需要知道哪个View被调用,所以它可以使用来自View的正确信息呢?这意味着:

a)重新实现QSignalMapper

b)升级到Qt5和VS2012以便直接连接lambdas(C++ 11) ;

2)在模型调用更新时,了解更改内容的最佳方法是什么?它是一个切换/循环所有可能性,一个预定义的地图......?

3)另外,我应该通过更新功能传递必要的信息,还是让View在收到通知后检查模型的必需值?

在第二种情况下,View需要访问Model数据......


编辑:

特别是在有大量数据被修改的情况下.例如,有一个加载按钮,整个对象/数组被修改.通过信号/插槽机制将副本传递给View将非常耗时.

来自ddriver的回答

现在,如果您有一个传统的"项目列表"模型并且您的视图是列表/树/表,那么将是另一回事,但您的案例是单个表单之一.


4)View是否需要引用模型?因为它只对控制器起作用?(查看::则setModel())

如果没有,它如何将自己注册为模型的观察者?

dte*_*ech 5

你过度思考了一些几乎微不足道的事情。你也过度设计了。

是的,从 UI 中抽象逻辑总是一个好主意,但是在您的特定示例的情况下,不需要额外的数据抽象层,主要是因为您没有不同的数据集,您只有两个值实际上是逻辑的一部分,不需要数据抽象层。

现在,如果您有一个传统的“项目列表”模型并且您的视图是一个列表/树/表,那将是另一回事,但您的案例是单一形式之一。

在您的情况下,正确的设计应该是一个Converter包含您当前模型数据、控制器和转换逻辑的ConverterUI类,以及一个本质上是您的视图表单的类。您可以节省样板代码和组件互连。

话虽如此,你可以自由地经历不必要的长度和矫枉过正。

1 - 你从视图到控制器连接发出修改数据,所以它总是来自适当的视图,控制器不关心它是哪个视图,可能有多少视图,或者是否有视图. QSignalMapper是一个选项,但它相当有限——它只支持单个参数和几个参数类型。老实说,我自己更喜欢单行插槽,它们更灵活,而且并不难编写,而且它们是可重用的代码,有时会很方便。Lambdas 是一个很酷的新特性,使用它们肯定会让你看起来更酷,但在你的特定情况下,它们不会产生太大的不同,而且单独的 lambdas 不值得切换到 Qt5。话虽如此,除了 lambdas 之外,还有更多理由更新到 Qt5。

2 - 信号和槽,你知道你在编辑什么,所以你只更新

3 - 通过信号传递值更优雅,它不需要您的控制器保持对视图的引用并管理它是哪个视图,如 1 中所述

4 - 从 MVC 图中可以明显看出,视图对模型的引用仅供阅读。因此,如果您想要一个“书本上的”MVC,那就是您所需要的。

在此处输入图片说明

我对前面的例子进行了改进(有点,仍然未经测试),现在Data它只是一个常规结构,QObject如果你要拥有很多结构,你肯定不希望它是一个派生的,因为这QObject是巨大的内存开销,的Model,其保持的数据集,所述Controller它迭代底层Model数据集和读取和写入数据时,View它被绑定到控制器,并且App它汇集了一个模型和两个独立的控制器以及两个独立的视图。功能有限 - 您可以转到下一个可用的数据集条目,修改或删除,在此示例中没有添加或重新排序,您可以将它们作为练习来实现。更改将向下传播回模型,从而反映在每个控制器和相关视图中。您可以将多个不同的视图绑定到单个控制器。控制器的模型目前是固定的,但是如果你想改变它,你必须经历一个类似于为视图设置控制器的过程——也就是说,在连接到新的之前断开旧的,尽管如果你是删除旧的,它会自动断开连接。

struct Data {
    QString d1, d2;
};

class Model : public QObject {
    Q_OBJECT
    QVector<Data> dataSet;
  public:
    Model() {
      dataSet << Data{"John", "Doe"} << Data{"Jane", "Doe"} << Data{"Clark", "Kent"} << Data{"Rick", "Sanchez"};
    }
    int size() const { return dataSet.size(); }
  public slots:
    QString getd1(int i) const { return i > -1 && i < dataSet.size() ? dataSet[i].d1 : ""; }
    QString getd2(int i) const { return i > -1 && i < dataSet.size() ? dataSet[i].d2 : ""; }
    void setd1(int i, const QString & d) {
      if (i > -1 && i < dataSet.size()) {
        if (dataSet[i].d1 != d) {
          dataSet[i].d1 = d;
          emit d1Changed(i);
        }
      }
    }
    void setd2(int i, const QString & d) {
      if (i > -1 && i < dataSet.size()) {
        if (dataSet[i].d2 != d) {
          dataSet[i].d2 = d;
          emit d2Changed(i);
        }
      }
    }
    void remove(int i) {
      if (i > -1 && i < dataSet.size()) {
        removing(i);
        dataSet.remove(i);
        removed();
      }
    }
  signals:
    void removing(int);
    void removed();
    void d1Changed(int);
    void d2Changed(int);
};

class Controller : public QObject {
    Q_OBJECT
    Model * data;
    int index;
    bool shifting;
  public:
    Controller(Model * _m) : data(_m), index(-1), shifting(false) {
      connect(data, SIGNAL(d1Changed(int)), this, SLOT(ond1Changed(int)));
      connect(data, SIGNAL(d2Changed(int)), this, SLOT(ond2Changed(int)));
      connect(data, SIGNAL(removing(int)), this, SLOT(onRemoving(int)));
      connect(data, SIGNAL(removed()), this, SLOT(onRemoved()));
      if (data->size()){
        index = 0;
        dataChanged();
      }
    }
  public slots:
    QString getd1() const { return data->getd1(index); }
    QString getd2() const { return data->getd2(index); }
    void setd1(const QString & d) { data->setd1(index, d); }
    void setd2(const QString & d) { data->setd2(index, d); }
    void remove() { data->remove(index); }
  private slots:
    void onRemoving(int i) { if (i <= index) shifting = true; }
    void onRemoved() {
      if (shifting) {
        shifting = false;
        if ((index > 0) || (index && !data->size())) --index;
        dataChanged();
      }
    }
    void ond1Changed(int i) { if (i == index) d1Changed(); }
    void ond2Changed(int i) { if (i == index) d2Changed(); }
    void fetchNext() {
      if (data->size()) {
        index = (index + 1) % data->size();
        dataChanged();
      }
    }
  signals:
    void dataChanged();
    void d1Changed();
    void d2Changed();
};

class View : public QWidget {
    Q_OBJECT
    Controller * c;
    QLineEdit * l1, * l2;
    QPushButton * b1, * b2, * bnext, * bremove;
  public:
    View(Controller * _c) : c(nullptr) {
      QVBoxLayout * l = new QVBoxLayout;
      setLayout(l);
      l->addWidget(l1 = new QLineEdit(this));
      l->addWidget(b1 = new QPushButton("set", this));
      connect(b1, SIGNAL(clicked(bool)), this, SLOT(setd1()));
      l->addWidget(l2 = new QLineEdit(this));
      l->addWidget(b2 = new QPushButton("set", this));
      connect(b2, SIGNAL(clicked(bool)), this, SLOT(setd2()));
      l->addWidget(bnext = new QPushButton("next", this));
      l->addWidget(bremove = new QPushButton("remove", this));
      setController(_c);
    }
    void setController(Controller * _c) {
      if (_c != c) {
        if (c) {
          disconnect(c, SIGNAL(d1Changed()), this, SLOT(updateL1()));
          disconnect(c, SIGNAL(d2Changed()), this, SLOT(updateL2()));
          disconnect(c, SIGNAL(dataChanged()), this, SLOT(updateForm()));
          disconnect(bnext, SIGNAL(clicked(bool)), c, SLOT(fetchNext()));
          disconnect(bremove, SIGNAL(clicked(bool)), c, SLOT(remove()));
          c = nullptr;
        }
        c = _c;
        if (c) {
          connect(c, SIGNAL(d1Changed()), this, SLOT(updateL1()));
          connect(c, SIGNAL(d2Changed()), this, SLOT(updateL2()));
          connect(c, SIGNAL(dataChanged()), this, SLOT(updateForm()));
          connect(bnext, SIGNAL(clicked(bool)), c, SLOT(fetchNext()));
          connect(bremove, SIGNAL(clicked(bool)), c, SLOT(remove()));
        }
      }
      updateForm();
    }
  public slots:
    void updateL1() { l1->setText(c ? c->getd1() : ""); }
    void updateL2() { l2->setText(c ? c->getd2() : ""); }
    void updateForm() {
      updateL1();
      updateL2();
    }
    void setd1() { c->setd1(l1->text()); }
    void setd2() { c->setd2(l2->text()); }
};

class App : public QWidget {
    Q_OBJECT
    Model m;
    Controller c1, c2;
  public:
    App() : c1(&m), c2(&m) {
      QVBoxLayout * l = new QVBoxLayout;
      setLayout(l);          
      l->addWidget(new View(&c1));          
      l->addWidget(new View(&c2));
    }
};
Run Code Online (Sandbox Code Playgroud)


Inn*_*der 0

1)直接将按钮信号连接到控制器插槽会更好吗(就像 View::setController 中注释掉的部分)?

是的,因为你所说的老虎机只是一句俏话。

控制器需要知道调用了哪个视图,以便它可以使用来自视图的正确信息,不是吗?

不必要。你不应该传递this你的信号。您应该传递已更改的数据。例如,在控制器类中,您可以有一个名为void SetDecValueTo(int)or的槽void SetDecValueTo(QString),只需从视图中调用它而不是传递this

这意味着:

a) 重新实现 QSignalMapper 或

b) 升级到 Qt5 和 VS2012,以便直接与 lambda (C++11) 连接;

如上所述,您实际上并不需要这个。但总的来说,lambda 是未来的发展方向。

2)当模型调用 update 时,了解发生了什么变化的最佳方法是什么?它是一个切换/循环所有可能性、一个预定义的映射......?

在信号/槽中传递相关数据。例如,在您的模型中,您可以有一个信号void DecValueChanged(int)void HexValueChanged(int)。您将它们连接到视图的插槽void UpdateDecValue(int)void UpdateHexValue(int)

3)另外,我应该通过更新函数传递必要的信息,还是让视图在收到通知后检查模型所需的值?

参见上段。

在第二种情况下,视图需要访问模型数据......

4)视图是否需要引用模型?因为它只作用于控制器?(视图::setModel())

如果不是,它如何将自己注册为 Model 的观察者?

在这种特殊情况下,它不需要引用模型。您可以在视图中进行所有连接main(),也可以在视图中进行,只是不保留对模型的引用。

最后,由于不需要做太多控制,您可以放弃控制器类并在视图中实现其功能,就像 Qt 中经常做的那样。请参阅模型/视图编程