声明:
正如第一个答案已经适当注意到的那样,在当前的示例案例中使用MVC是过度的。问题的目标是通过一个简单的例子来理解底层概念,以便能够在更大的程序中使用它们,在这个程序中修改更复杂的数据(数组,对象)。
我正在尝试用C ++和C ++实现MVC模式。 QT,类似于这里的问题:
该程序有两行编辑:
3个按钮
并且只修改字符串。
与另一个问题的区别在于,我试图实现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
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();
}
如果它们变得相关,我可以稍后发布主题/观察者文件。
除了一般性意见外,有人可以回答这些问题:
1)将按钮信号直接连接到控制器插槽(如View::setController
中注释掉的部分)会更好吗? Controller需要知道哪个View被调用,所以它可以使用来自View的正确信息呢?这意味着:
a)Reimplement a QSignalMapper或
b)Upgrade to Qt5 and VS2012 in order to connect directly with lambdas (C++11);
2)在模型调用更新时,了解更改内容的最佳方法是什么?它是一个切换/循环所有可能性,一个预定义的地图......?
3)另外,我是否应该通过更新功能传递必要的信息,或者让View在收到通知后检查模型所需的值?
在第二种情况下,View需要访问Model数据......
编辑:
特别是在修改了很多数据的情况下。例如,有一个加载按钮,整个对象/数组被修改。通过信号/插槽机制将副本传递给视图将非常耗时。
来自ddriver的回答
现在,如果您有一个传统的“项目列表”模型并且您的视图是列表/树/表,那么将是另一回事,但您的案例是单一表单之一。
4)视图是否需要引用模型?因为它只对控制器起作用? (View :: setModel())
如果没有,它如何将自己注册为模型的观察者?
答案 0 :(得分:3)
你正在过度思考几乎无足轻重的事情。你也过度工程了。
是的,从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));
}
};
答案 1 :(得分:3)
我将在Passive-View和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_;
};
演示者:
此处Presenter是Observer,而不是View。演示者的工作是将模型的整数值转换为文本表示以供显示。这里我们确实有两个控制器,一个用于十进制表示法,一个用于十六进制表示法。虽然可能过度设计了这个简单的情况,但我们为Presenter创建了一个抽象的基类。
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 );
}
};
最后是聚合的演示者
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)将按钮信号直接连接到Controller插槽(如View :: setController中注释掉的部分)会更好吗?
由于我们想要一个没有逻辑的“被动视图”,如果控制参数合适,则可以直接连接到控制器。如果你必须将std :: string转换为QString,你可以创建一个本地插槽来执行转换并传递值,或者在Qt5中使用lambda作为作业。
Controller需要知道哪个View被调用,所以它可以使用View中的正确信息呢?
不,不。如果需要做不同的事情,应该是单独的演示者或演示者,每个案例都有不同的方法。
2)在模型调用更新时,了解更改内容的最佳方法是什么?它是一个切换/循环所有可能性,一个预定义的地图......?
最佳方式是模型告诉观察者改变了什么。这可以通过不同的信号或包含信息的事件来完成。在这种情况下,只有一个值,所以没有区别。
3)另外,我是否应该通过更新功能传递必要的信息,或者让View在收到通知后检查模型所需的值?
传递信息以避免演示者中的冗余更改计算。
4)视图是否需要引用模型?
不,至少不是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
<强> Live on Coliru 强>
答案 2 :(得分:0)
1)将按钮信号直接连接到Controller插槽会更好吗(比如在View :: setController中注释掉的部分)?
是的,因为你打电话的插槽只是一行。
Controller需要知道哪个View被调用,以便它可以使用View中的正确信息呢?
不一定。你不应该在你的信号中通过this
。您应该传递已更改的数据。例如,在控制器类中,您可以拥有一个名为void SetDecValueTo(int)
或void SetDecValueTo(QString)
的广告位,只需从您的视图中调用它,而不是传递this
。
这意味着:
a)重新实现QSignalMapper或
b)升级到Qt5和VS2012以便直接连接lambdas(C ++ 11);
如上所述,你真的不需要这个。但总的来说,lambdas是未来的方式。
2)在模型调用更新时,了解更改内容的最佳方法是什么?它是一个切换/循环所有可能性,一个预定义的地图......?
将相关数据传递到信号/插槽中。例如,在您的模型中,您可以发出信号void DecValueChanged(int)
和void HexValueChanged(int)
。您可以将这些广告连接到您观看的广告位void UpdateDecValue(int)
和void UpdateHexValue(int)
。
3)另外,我是否应该通过更新功能传递必要的信息,或者让View在收到通知后检查模型所需的值?
见上段。
在第二种情况下,View需要访问Model数据......
4)视图是否需要引用模型?因为它只对控制器起作用? (查看::则setModel())
如果没有,它如何将自己注册为模型的观察者?
在这种特殊情况下,它不需要对模型进行ref。您可以在main()
中执行所有连接,也可以在视图中执行此操作,而不是将ref保留在模型中。
最后,由于没有很多需要完成的控制,您可以放弃控制器类并在视图中实现其功能,这在Qt中经常进行。请参阅Model/View Programming。
答案 3 :(得分:0)
不同的MVC架构存在重大差异,尤其是组件应该如何协作。我更喜欢模型和视图彼此独立以及与控制器独立的方法。这种设计的好处是您可以轻松地将模型与另一个视图一起使用,反之亦然;重复使用另一个模型的视图。
这是Apple建议的MVC组件协作:https://developer.apple.com/library/ios/documentation/General/Conceptual/DevPedia-CocoaCore/MVC.html
那它是如何运作的?
视图。 视图可以被认为只是一个假人。它现在不涉及外部世界,只能输出由模型决定的信息表示。
模特。 这是主要组件,它是您的软件。它管理应用程序的数据,逻辑和规则。
控制器。 控制器的职责是确保模型和视图相互理解。
将视图视为您的身体,将模型视为您的大脑(您是谁),控制器将是来自您大脑的电子信号。
示例强>
我目前没有编译器方便,所以无法测试,但我会在明天上班时尝试。需要注意的重要部分是视图和模型彼此独立且与控制器无关。
<强>模型强>
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建议作为答案。