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

11

免责声明:

正如前面的答案所指出的那样,在当前示例中使用MVC是过度的。问题的目标是理解基本概念,并通过简单的示例来使用它们,以便在修改更复杂的数据(数组、对象)的更大程序中使用它们。


我正在尝试在C++和QT中实现MVC模式,类似于此处的问题:

其他MVC问题

该程序有两个行编辑框:
  • mHexLineEdit
  • mDecLineEdit
3个按钮:
  • mConvertToHexButton
  • mConvertoDecButton
  • mClearButton
并且只修改字符串。

enter image description here

与其他问题的不同之处在于,我正在尝试实现主题/观察者模式,在模型更改后更新视图。

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

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");
}

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

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;
    }
}

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

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();
}

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();
}

如果它们变得相关,我可以稍后发布Subject/Observer文件。

除了一般性的评论外,有人能回答以下问题吗:

1)将按钮信号直接连接到控制器插槽是否更好(如在View::setController中注释掉的部分)?控制器需要知道哪个视图被调用,以便可以使用视图的正确信息,不是吗?这意味着要么:

a) 重新实现QSignalMapper 或者

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

2)更新时知道发生了什么变化的最佳方法是什么?是开关/循环遍历所有可能性,预定义映射……?

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

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


编辑:

特别是在数据修改非常频繁的情况下。例如,有一个加载按钮和一个整个对象/数组被修改了。通过信号/槽机制将副本传递给视图会耗费时间。

来自ddriver的回答

现在,如果您拥有传统的“项目列表”模型,并且您的视图是列表/树/表格,则情况将不同,但您的情况是单个表单之一。


4) 视图是否需要引用模型?因为它只与一个控制器交互?(View::setModel())

如果不需要,它如何将自己注册为模型的观察者?

4个回答

5
你正在过度思考一些实际上微不足道的事情,同时也在过度工程化。
是的,在抽象逻辑和UI方面总是一个好主意,但在你特定的例子中,数据的额外抽象层并不必要,主要是因为你没有不同的数据集,你只有两个值,这些值实际上是逻辑的一部分,不值得使用数据抽象层。
现在,如果你有传统的“项目列表”模型,并且你的视图是一个列表/树形/表格,则情况将有所不同,但你的情况是单个表单之一。
在你的情况下,正确的设计应该是一个包括当前模型数据、控制器和转换逻辑的“Converter”类,以及一个基本上是你的视图表单的“ConverterUI”类。你可以节省样板代码和组件互连。
话虽如此,你可以自由地采取不必要的步骤和过度的设计。

1 - 你将修改数据从视图传递到控制器连接,因此它总是来自适当的视图,控制器不关心它是哪个视图,有多少个视图或者是否存在视图。 QSignalMapper 是一个选项,但它相当有限 - 它只支持单个参数和仅几种参数类型。我个人更喜欢一行槽,它们更灵活,编写起来也不那么难,而且它们是可重用代码,有时会很方便。Lambda是一个新的、酷炫的功能,在你的特定情况下使用它们确实会让你看起来更酷,但它们本身并不值得切换到Qt5。话虽如此,除了Lambda之外,升级到Qt5还有很多其他原因。

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

3 - 通过信号传递值更加优雅,并且不需要你的控制器保留对视图的引用并管理它是哪个视图,如第1点所述。

4 - 如MVC图表所示,视图仅具有对模型的只读引用。因此,如果您想要符合书本上的MVC,那就是你需要的。

enter image description here

我已经改进了之前的示例(虽然还未经测试),现在有一个名为的普通结构体,如果您要使用多个,则绝对不希望它派生自,因为会占用大量内存。此外还有,它维护数据集;,它迭代基础数据集并读取和写入数据;

转换器只是一个非常简单的例子,用于理解底层逻辑,这将在一个更大的程序中使用。我现在会仔细阅读你的答案。 ;) - Smash
嗯,如果没有涉及数据集的示例,你是无法理解逻辑的,因为显示和修改数据集正是MVC的主要目的。此外,不要觉得自己必须坚持某一特定范式,甚至不要局限于自己对范式的理解,MS MVC设计是由其工具集的功能和限制所决定的。除非你有一个愚蠢的老板,他什么都不懂,但却读了些东西,让你做那个确切的事情,否则你应该选择你理解并认为是清晰高效的设计。 - dtech
@Smash,我添加了一个基本示例,请记住它没有经过彻底测试。 - dtech

3

我将在被动视图和模型-视图-控制器的背景下回答这个问题。

Model View Presenter

这里是维基百科的链接

它是模型-视图-控制器(MVC)架构模式的一个派生,主要用于构建用户界面。

模型:

必须能够观察到对模型/主题的更改。大多数主题/观察者的详细信息都由信号/插槽机制处理,因此对于这个简单的用例,只需通过给模型发出值的信号使其可观察即可。由于在线编译器不支持Qt,因此我将使用boost::signals2和std::string。

class Model
{
public:

    Model(  )
    {
    }

    void setValue( int value )
    {
        value_ = value;
        sigValueChanged( value_ );
    }

    void clear()
    {
        value_ = boost::none;
        sigValueChanged( value_ );
    }

    boost::optional<int> value() const
    {
        return value_;
    }

    boost::signals2::signal< void( boost::optional<int> ) > sigValueChanged;

private:

    boost::optional<int> value_;
};

演示者:

在这里,演示者是观察者,而不是视图。演示者的工作是将模型的整数值转换为文本表示以供显示。在这里,我们真正拥有两个控制器,一个用于十进制表示法,另一个用于十六进制表示法。虽然对于这个简单的情况可能过度设计了,但我们为演示者创建了一个抽象基类。

class AbstractPresenter
{
public:

    AbstractPresenter()
        : model_( nullptr )
        , view_( nullptr )
    {
    }

    void setModel( Model& model )
    {
        model_ = &model;
        model.sigValueChanged.connect( [this]( int value ){
            _modelChanged( value ); } );
    }

    void setView( TextView& view )
    {
        view_ = &view;
    }

    void editChanged( std::string const& hex )
    {
        _editChanged( hex );
    }

private:

    virtual void _editChanged( std::string const& ) = 0;
    virtual void _modelChanged( boost::optional<int> ) = 0;

protected:

    Model *model_;
    TextView  *view_;
};

和一个十进制 Presenter 的实现

class DecPresenter
    : public AbstractPresenter
{
    void _editChanged( std::string const& dec ) override
    {
        int value;
        std::istringstream( dec ) >> value;

        model_->setValue( value );
    }

    void _modelChanged( boost::optional<int> value ) override
    {
        std::string text;

        if( value )
        {            
            text = std::to_string( *value );;
        }

        view_->setEdit( text );
    }
};

还有一个十六进制情况的实现。

class HexPresenter
    : public AbstractPresenter
{
    void _editChanged( std::string const& hex ) override
    {
        int value;
        std::istringstream( hex ) >> std::hex >> value;

        model_->setValue( value );
    }

    void _modelChanged( boost::optional<int> value ) override
    {
        std::string text;

        if( value )
        {
            std::stringstream stream;
            stream << std::hex << *value;

            text = stream.str();
        }

        view_->setEdit( text );
    }
};

最后是一个聚合的Presenter。
class Presenter
{
public:

    Presenter()
        : model_( nullptr )
    {
    }

    void setModel( Model& model )
    {
        model_ = &model;
        hexPresenter.setModel( model );
        decPresenter.setModel( model );    
    }

    void setView( View& view )
    {
        hexPresenter.setView( view.hexView );
        decPresenter.setView( view.decView );    
    }

    HexPresenter hexPresenter;
    DecPresenter decPresenter;    

    void clear()
    {
        model_->clear();
    }

private:

    Model * model_;
};

视图:

视图的唯一工作是显示文本值,因此我们可以在两种情况下使用相同的视图。

class TextView
{
public:

    TextView( std::string which )
        : which_( which )
    {
    }

    void setPresenter( AbstractPresenter& presenter )
    {
        presenter_ = &presenter;
    }

    void setEdit( std::string const& string )
    {
        std::cout << which_ << " : " << string << "\n";
    }

private:

    AbstractPresenter* presenter_;
    std::string which_;
};

和聚合视图。

class View
{
public:

    View()
        : hexView( "hex" )
        , decView( "dec" )
    {
    }

    TextView hexView;
    TextView decView;
};

在Qt应用程序中,每个视图都会有一个指向相应标签的指针,并设置标签的文本。
    void setEdit( std::string const& string )
    {
        label->setText( QSting::fromStdString( string ) );
    }

在这种情况下,我们也可以回答问题1。
1)直接将按钮信号连接到控制器插槽中是否更好(例如在View :: setController中已注释的部分)?
由于我们想要一个没有逻辑的“被动视图”,如果控件参数适合,则直接连接到控制器是完全可以的。如果您需要转换,例如将std :: string转换为QString,则可以创建一个本地插槽来执行转换并传递值,或者在Qt5中使用lambda完成此工作。
控制器需要知道哪个视图调用了它,以便它可以使用视图的正确信息,不是吗?
不需要。如果它需要执行不同的操作,则应该有单独的Presenter或每种情况具有单独方法的Presenter。
2)知道Model何时调用更新时已更改的最佳方法是什么?是切换/循环所有可能性,预定义映射...?
最佳方法是让模型告诉观察者发生了什么变化。这可以通过不同的信号或包含信息的事件来完成。在这种情况下,只有一个值,因此没有区别。

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

传递信息以避免Presenter中重复的更改计算。

4) View是否需要引用Model?

不需要,在MVP中至少不需要。

可以这样组合:

int main()
{
    Model model;
    Presenter presenter;
    View view;

    presenter.setModel( model );
    presenter.setView( view );

    view.decView.setPresenter( presenter.decPresenter );
    view.hexView.setPresenter( presenter.hexPresenter );

    // simulate some button presses

    presenter.hexPresenter.editChanged( "42" );
    presenter.clear();
    presenter.decPresenter.editChanged( "42" );
}

它创建了以下输出

hex : 42
dec : 66
hex :
dec :
hex : 2a
dec : 42

在Coliru上直播


在关于Qt的问题中使用boost信号有点违背初衷。 - dtech
1
@ddriver 不完全是这样。它不会影响设计,实际上更多关于信号/槽而非Qt。 - Thomas

1
有不同的MVC架构,特别是组件如何协作方面存在显著差异。我更喜欢一种方法,其中模型和视图彼此独立,也与控制器独立。这种设计的好处在于,您可以轻松地将模型与另一个视图重用,或者反之;重用视图与另一个模型。

这是苹果建议的MVC组件协作方式:https://developer.apple.com/library/ios/documentation/General/Conceptual/DevPedia-CocoaCore/MVC.html

Apples MVC

那么它是如何工作的呢?

视图(View)。 可以将视图视为一个虚拟组件,它对外部世界一无所知,只能输出由模型决定的信息表示。

模型(Model)。 这是主要组件,也就是您的软件。它管理应用程序的数据、逻辑和规则。

控制器(Controller)。 控制器的职责是确保模型和视图相互理解。

将视图视为您的身体,模型视为您的大脑(定义了您是谁),而控制器则是与大脑相互交流的电信号。

示例

我目前手头没有编译器,所以无法测试这个例子���但明天上班时我会尝试。需要注意的重点是,视图和模型都独立于控制器。

模型(Model)

class PersonModel : public QObject
{
    Q_OBJECT

    QString m_sFirstname;
    QString m_sLastname;

public:
    Model() : m_sFirstname(""), m_sLastname("")
    {}
    ~Model();

    void setFirstname(const QString & sFirstname)
    {
        m_sFirstname = sFirstname;
        emit firstnameChanged(sFirstname);
    }

    void setLastname(const QString & sLastname)
    {
        m_sLastname = sLastname;
        emit lastnameChanged(sLastname);
    }

signals:
    void firstnameChanged(const QString &);
    void lastnameChanged(const QString &);
};

查看

class PersonView : public QWidget
{
    Q_OBJECT

    QLabel * m_pFirstnameLabel; // should be unique_ptrs
    QLabel * m_pLastnameLabel;  //

public:
    PersonView() :
        m_pFirstnameLabel(new QLabel),
        m_pLastnameLabel(new QLabel)
    {
        auto m_pMainLayout = new QHBoxLayout;
        m_pMainLayout->addWidget(m_pFirstnameLabel);
        m_pMainLayout->addWidget(m_pLastnameLabel);
        setLayout(m_pMainLayout);
    }

    ~PersonView()
    {
        delete m_pFirstnameLabel;
        delete m_pLastnameLabel;
    }

public slots:
    void setFirstnameText(const QString & sFirstname)
    {
        m_pFirstnameLabel->setText(sFirstname);
    }

    void setLastnameText(const QString & sLastname)
    {
        m_pLastnameLabel->setText(sLastname);
    }
};

控制器

class PersonController : public QObject
{
    Q_OBJECT

    PersonView * m_pPersonView;     // better off as unique ptr
    PersonModel * m_pPersonModel;

public:
    PersonController() :
        m_pPersonView(new PersonView),
        m_pPersonModel(new PersonModel)
    {
        connect(m_pPersonModel, &PersonModel::firstnameChanged, m_pPersonView, &PersonView::setFirstnameText);
        connect(m_pPersonModel, &PersonModel::lastnameChanged, m_pPersonView, &PersonView::setLastnameText);

        m_pPersonModel->setFirstname("John");
        m_pPersonModel->setLastname("Doe");

        m_pPersonView->show();

    }

    ~PersonController()
    {
        delete m_pPersonView;
        delete m_pPersonModel;
    }
};

注意事项

  • 在更大的项目中,可能不止一个MVC。这种情况下,通信将通过控制器进行。

  • 也可以使用一个控制器添加额外的模型和视图。多个视图可以用于以不同方式显示一组数据。

  • 正如我在开头提到的,还有其他变体的MVC架构。例如,其中一种是由ddriver提出的答案。


使用这种特定的模式,是否可能维护视图到控制器“用户操作”的关系?至少,“View”希望有一些回调。 - Vasiliy Stavenko
@VasiliyStavenko 当然可以。使用Qt,您可以在视图中发出信号以响应用户操作,并在控制器中进行连接。要让视图具有回调功能,只需将函数定义为公共槽,并像以前一样在控制器中进行连接。重要的是要让回调与任何外部控制器或模型无关 :)。 - A.Fagrell

0

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

是的,因为你调用的插槽只有一行代码。

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

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

这意味着:

a)重新实现QSignalMapper或

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

如上所述,你真的不需要这个。但总的来说,lambda是未来的趋势。

2) 在Model调用更新时,了解什么已更改的最佳方法是什么?是通过开关/循环所有可能性,预定义映射...?

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

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

请参见上面的段落。

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

4)视图是否需要引用模型?因为它只与控制器交互?(View::setModel())

如果不需要,它如何将自己注册为模型的观察者?

在这种特定情况下,它不需要引用模型。您可以在main()中完成所有连接,或者在视图中完成并且不保留对模型的引用。

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

1
在最后一段中,您将MVC与Qt的MVD混淆了-后者涉及模型,这些模型是项目列表和列表/表格/树视图,而MVC的情况是单个表单。 - dtech

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接