说明:这篇博客基本都是翻译于Qt官方的Model/View Tutorial教程,无法理解的地方建议转到原文,同时,由于译者水平有限,如有差错欢迎指出。
原文:http://qt-project.org/doc/qt-5/modelview.html
转载注明出处:http://www.cnblogs.com/bestheart/p/3707584.html
须知符号:(:…译者自己的理解…*)
每一个UI开发者都应该知道Model/View编程,这个教程的目的就是简单易懂的介绍这个主题。
表格(table),列表(list) 和树(tree)组件是在GUIs中频繁使用的组件。这些组件有两种不同的方法能访问它们的数据。传统方法涉及的组件包含一些存储数据的内部容器,这种方法非常直观,但是,在许多较大的应用程序中,这种方法将会引发数据同步性问题。 第二种方法就是我们要说的model/view编程了,在这种方法中,组件不需要维护内部的数据容器,组件可以通过一个标准化的接口访问外部数据,从而避免数据复制(:因为第一种方法为了解决数据同步问题不得不频繁的复制数据*)。后一种方法初看起来挺复杂的并且不容易掌握,但是一旦你认真理解了,model/view编程将后惠无穷。
以下,我们将会学习到一些基本的Qt技术,例如:
1.标准组件和model/view组件的不同。
2.窗体和模型间的适配器。
3.开发一个简单的model/view程序。
4.中间主题,例如:
a.Tree 视图
b.选择
c.委托
d.模型测试调试
你也会学习到,能否用model/view更简单的编写你的应用程序,或者,传统的组件是否一样好。
这篇教程包含了一些例子和源代码,你可以把它们集成到自己的工程当中,例子的源代码在:
examples/widgets/tutorials/modelview 。
你也可以访问reference documentation了解更详细地信息。
Model/View是一种(在处理数据集的组件)从视图中分离数据的技术。标准的组件不从视图中分离数据,这就是为什么Qt 4(:Qt 5*)有两种不同类型的组件。这两种组件看起来一样,但是它们的数据交互方式不同。
让我们来细看一下标准的table 组件吧。Table 组件是一个二维的数组,它的数据元素可以被用户改变。Table 组件通过读写自己的数据元素将其集成到程序流中。这种方法非常的直观,同时在许多应用程序中也是非常的有用,但是用标准的table组件展示和编辑一个数据库表格将会遇到麻烦。数据的两份拷贝不得不协调:一份在组件外,一份在组件内部。开发者必须自己为两个版本的数据同步负责。除此之外,紧密相连的两份数据将使单元测试变得更加艰难。
Model/view 使用了一种更加通用的体系结构来解决这个问题。Model/view 消除了发生在标准组件间的数据一致性问题。Model/view 也使几个视图拥有一份数据变得更加容易,因为一个模型能够被嫁接给许多视图。最重要的不同是model/view 组件在表格单元中不存储数据,实际上,组件直接操作你的数据。因为视图类不知道你的数据结构,所以需要提供包装从而让数据遵守QAbstractItemModel接口,视图通过这个接口读写数据。任何一个实现QAbstractItemModel类的实例都称为模型。
下面是model/view 组件和它们相应的标准组件的概况。
下面将简述窗体和模型间的适配器是怎样派上用场的。
我们可以在表格当中直接编辑存储在表格中的数据,但是在文本域中编辑数据将会更加符合我们的习惯。然而model/view 没有直接的相应部分能够为操作一个值而不是数据集的组件分离数据和视图(:也就是说,我们上面所述的MV是通过接口来分离数据集和视图的,但是这里我们要分离的是视图和单个数据,这就要用到适配器了*),所以我们需要一个适配器来把窗体连接到数据源。
QDataWidgetMapper 是一个主要的解决方法,它从窗体部件映射到表格行,同时,使为数据库表格创建窗体变得更加容易。
另一个适配器的例子是QCompleter。Qt 利用QCompleter在Qt部件中提供自动匹配,例如部件QComboBox和QLineEdit,如下所示。QCompleter使用模型作为它的数据源
如果想开发一个model/view 程序,从哪开始呢?建议从一个简单的例子开始,然后一步步扩展它。这样会更加容易理解M/V体系结构。对许多开发者来说,在引入IDE之前尝试理解model/view 体系通常是不适合的。从一个简单的且有样本数据的model/view 应用程序开始将会更加容易。开始吧!小伙伴们,只需要简单的用你自己的数据替换下面的例子数据。
下面将有7个简单的相互独立的应用程序,它们展示了model/view 编程的不同方面,源代码在Qt安装目录examples/widgets/tutorials/modelview
我们从一个使用QTableView来展示数据的应用程序开始。我们在后面将会增加编辑功能。(文件源代码: examples/widgets/tutorials/modelview/1_readonly/main.cpp)
// main.cpp #include <QtWidgets/QApplication> #include <QtWidgets/QTableView> #include "mymodel.h" int main(int argc, char *argv[]) { QApplication a(argc, argv); QTableView tableView; MyModel myModel(0); tableView.setModel( &myModel ); tableView.show(); return a.exec(); }
这是一个惯例的main( )函数:
非常有趣的一个部分:我们创建了MyModel的一个实例并且使用tableView.setModel(&myModel);来传递它的一个指针给tableView,tableView将会调用已经接受了指针的函数setModel来发现两件事:
模型需要一些代码来响应上述内容。
因为我们有一个表格数据集,所以更应该从QAbstractTableModel开始,因为它将比更加普遍的QAbstractItemModel模型更容易使用
(file source: examples/widgets/tutorials/modelview/1_readonly/mymodel.h )
// mymodel.h #include <QAbstractTableModel> class MyModel : public QAbstractTableModel { Q_OBJECT public: MyModel(QObject*parent); int rowCount(const QModelIndex &parent = QModelIndex()) const ; int columnCount(const QModelIndex &parent = QModelIndex()) const; QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const; };
QAbstractTableModel 需要实现三个抽象方法(:上述三个函数是纯虚函数必须重新实现*)。
(文件源码: examples/widgets/tutorials/modelview/1_readonly/mymodel.cpp)
// mymodel.cpp #include "mymodel.h" MyModel::MyModel(QObject *parent) :QAbstractTableModel(parent) { } int MyModel::rowCount(const QModelIndex & /*parent*/) const { return 2; } int MyModel::columnCount(const QModelIndex & /*parent*/) const { return 3; } QVariant MyModel::data(const QModelIndex &index, int role) const { if (role == Qt::DisplayRole) { return QString("Row%1, Column%2") .arg(index.row() + 1) .arg(index.column() +1); } return QVariant(); }
MyModel::rowCount()和MyModel::columnCount()提供行数和列数,视图调用函数MyModel::data()来获取表格单元的存储数据,参数index指定行和列的信息,同时参数role被设定为Qt::DisplayRole,其他的roles由下一部分来处理。在我们的例子当中,显示的数据使我们产生的,但在现实的程序中,MyModel 应该有一个成员MyData,它为读写操作服务。
这个例子展现了一个被动的模型,不知道什么时候这个模型应该使用,也不知道需要什么数据,它仅仅在视图需要的时候提供数据。
当模型的数据需要改变的时候会是什么情况?当数据改变需要重新读取时,视图是怎么实现的?模型需要发送一个信号表明哪些表格单元改变了,这些将会在2.3部分说明。
除了控制显示什么文本外,模型也能控制文本的外观。我们只需要简单改变一下模型,就能得到下面的结果。
实际上,除了改变data()函数(设置字体,背景颜色,对齐方式,和复选框)外,我们什么也不用做。下面就是修改后的data()函数,它可以产生上面的结果。与2.1的不同之处在于,此时我们使用了role里的参数来返回不同的信息块。
(源文件: examples/widgets/tutorials/modelview/2_formatting/mymodel.cpp)
// mymodel.cpp QVariant MyModel::data(const QModelIndex &index, int role) const { int row = index.row(); int col = index.column(); // generate a log message when this method gets called qDebug() << QString("row %1, col%2, role %3") .arg(row).arg(col).arg(role); switch(role){ case Qt::DisplayRole: if (row == 0 && col == 1) return QString("<--left"); if (row == 1 && col == 1) return QString("right-->"); return QString("Row%1, Column%2") .arg(row + 1) .arg(col +1); break; case Qt::FontRole: if (row == 0 && col == 0) //change font only for cell(0,0) { QFont boldFont; boldFont.setBold(true); return boldFont; } break; case Qt::BackgroundRole: if (row == 1 && col == 2) //change background only for cell(1,2) { QBrush redBackground(Qt::red); return redBackground; } break; case Qt::TextAlignmentRole: if (row == 1 && col == 1) //change text alignment only for cell(1,1) { return Qt::AlignRight + Qt::AlignVCenter; } break; case Qt::CheckStateRole: if (row == 1 && col == 0) //add a checkbox to cell(1,0) { return Qt::Checked; } } return QVariant(); }
模型每次调用data()函数时,每个格式化的属性将被要求设置,role参数可以通知模型哪一个属性被请求。
参考Qt 域名空间文档可以学习更多关于Qt::ItemDataRole枚举的功能。
现在我们需要知道怎样使用分离的模型来影响程序的外观,所以跟踪data()函数的调用频率非常重要,为了跟踪视图多长时间调用一次模型,我们可以在data()函数中放置一条debug语句,该语句可以记录错误输出流。例子中,data()函数会被调用42次,每当在作用域移动鼠标光标时,data()函数都会被调用(每个单元调用7次)。这就说明为什么当调用data()函数时,确保数据有效很重要。
上面讨论的仍然是只读表格,这次我们每秒都改变单元格的内容,因为我们下面要显示当前的时间。
(源文件:examples/widgets/tutorials/modelview/3_changingmodel/mymodel.cpp)
QVariant MyModel::data(const QModelIndex &index, int role) const { int row = index.row(); int col = index.column(); if (role == Qt::DisplayRole) { if (row == 0 && col == 0) { return QTime::currentTime().toString(); } } return QVariant(); }
貌似我们还没有让时钟滴答起来,是的,每秒都需要通知视图时间已经改变了,从而需要重新读取显示。这可以用一个定时器来实现,在构造函数里,我们设置定时器间隔为一秒,同时连接它的timeout信号。
(源文件: examples/widgets/tutorials/modelview/3_changingmodel/mymodel.cpp)
MyModel::MyModel(QObject *parent) :QAbstractTableModel(parent) { // selectedCell = 0; timer = new QTimer(this); timer->setInterval(1000); connect(timer, SIGNAL(timeout()) , this, SLOT(timerHit())); timer->start(); }
下面是相应的槽函数。
void MyModel::timerHit() { //we identify the top left cell QModelIndex topLeft = createIndex(0,0); //emit a signal to make the view reread identified data emit dataChanged(topLeft, topLeft); }
我们通过发送dataChanged()信号来请求视图重读左上角单元格的数据。注意到我们没有显式的连接信号dataChanged()到视图,这是因为当调用setModel()时会自动连接。
标题可以被隐藏,通过:tableView->verticalHeader()->hide();
但是,标题的内容是通过模型设置的,所以我们要重新实现方法headerData()
(file source: examples/widgets/tutorials/modelview/4_headers/mymodel.cpp)
QVariant MyModel::headerData(int section, Qt::Orientation orientation, int role) const { if (role == Qt::DisplayRole) { if (orientation == Qt::Horizontal) { switch (section) { case 0: return QString("first"); case 1: return QString("second"); case 2: return QString("third"); } } } return QVariant(); }
注意到,方法headerData()也有一个参数role,这个参数的意义和MyModel::data()中的是一样的。
在这个例子中,我们将要创建一个应用程序,它可以利用输入到表格单元值来自动填充窗口标题。为了能够容易访问窗口标题,可以把QTableView 放在QMainWindow里。
模型决定了是否有能力编辑,我们必须修改模型以使编辑可用,这个可以同时重新实现虚函数setData() 和 flags()做到。
(file source: examples/widgets/tutorials/modelview/5_edit/mymodel.h)
// mymodel.h #include <QAbstractTableModel> #include <QString> const int COLS= 3; const int ROWS= 2; class MyModel : public QAbstractTableModel { Q_OBJECT public: MyModel(QObject *parent); int rowCount(const QModelIndex &parent = QModelIndex()) const ; int columnCount(const QModelIndex &parent = QModelIndex()) const; QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const; bool setData(const QModelIndex & index, const QVariant & value, int role = Qt::EditRole); Qt::ItemFlags flags(const QModelIndex & index) const ; private: QString m_gridData[ROWS][COLS]; //holds text entered into QTableView signals: void editCompleted(const QString &); };
我们使用二维数组QStringm_gridData 来存储数据,这样m_gridData就成为MyModel的核心了。MyModel其余的函数就像一个包装,它们使 m_gridData 适应QAbstractItemModel的接口。顺便介绍下editCompleted() 信号,它使传递修改的文本到窗口标题成为可能。
(file source: examples/widgets/tutorials/modelview/5_edit/mymodel.cpp)
bool MyModel::setData(const QModelIndex & index, const QVariant & value, int role) { if (role == Qt::EditRole) { //save value from editor to member m_gridData m_gridData[index.row()][index.column()] = value.toString(); //for presentation purposes only: build and emit a joined string QString result; for(int row= 0; row < ROWS; row++) { for(int col= 0; col < COLS; col++) { result += m_gridData[row][col] + " "; } } emit editCompleted( result ); } return true; }
每次用户编辑单元格时,都会调用setData()函数,参数index告诉用户编辑哪一个区域,value 提供了编辑程序的结果。Role将总是被设置成Qt::EditRole,因为我们的单元格仅仅包含文本。如果需要显示复选框同时用户权限允许选择复选框,就可以把role设置成Qt::CheckStateRole来调用。
(file source: examples/widgets/tutorials/modelview/5_edit/mymodel.cpp)
Qt::ItemFlags MyModel::flags(const QModelIndex &index) const { return Qt::ItemIsEditable | QAbstractTableModel::flags(index); }
单元格的不同属性可以通过flags()来调整设置。返回Qt::ItemIsSelectable | Qt::ItemIsEditable | Qt::ItemIsEnabled 足够显示一个可选编辑器。如果编辑一个单元格,但修改的却不仅仅是这个单元格的话,模型必须发送一个dataChanged()信号以使被改变的数据可以重读。