介绍
Qt 4推出了一组新的item view类,它们使用model/view结构来管理数据与表示层的关系。这种结构带来的功能上的分离给了开发人员更大的弹性来定制数据项的表示,它也提供一个标准的model接口,使得更多的数据源可以被这些item view使用。这里对model/view的结构进行了描述,结构中的每个组件都进行了解释,给出了一些例子说明了提供的这些类如何使用。
Model/View 结构
Model-View-Controller(MVC), 是从Smalltalk发展而来的一种设计模式,常被用于构建用户界面。经典设计模式的著作中有这样的描述:
MVC由三种对象组成。Model是应用程序对象,View是它的屏幕表示,Controller定义了用户界面如何对用户输入进行响应。在MVC之前,用户界面设计倾向于三者结合在一起,MVC对它们进行了解耦,提高了灵活性与重用性。
假如把view与controller结合在一起,结果就是model/view结构。这个结构依然是把数据存储与数据表示进行了分离,它与MVC都基于一样的思想,但它更简单一些。这种分离使得在几个不同的view上显示同一个数据成为可能,也可以重新实现新的view,而不必改变底层的数据结构。为了更灵活的对用户输入进行处理,引入了delegate这个概念。它的好处是,数据项的渲染与编程可以进行定制。
如上图所示,model与数据源通讯,并提供接口给结构中的其他组件使用。通信的性质依赖于数据源的种类与model实现的方式。view从model获取model indexes,后者是数据项的引用。通过把model indexes提供给model,view可以从数据源中获取数据。在标准的views中,delegate会对数据项进行渲染,当某个数据项被选中时,delegate通过model indexes与model直接进行交流。
总的来说,model/view相关类可以被分成上面所提到的三组:models、views、delegates。这些组件通过抽象类来定义,它们提供了共同的接口,在某些情况下,还提供了缺省的实现。抽象类意味着需要子类化以提供完整的其他组件希望的功能。这也允许实现定制的组件。
Models、views、delegates之间通过信号—槽机制来进行通信:
从model发出的信号通知view数据源中的数据发生了改变。
从view发出的信号提供了有关被显示的数据项与用户交互的信息。
从delegate发生的信号被用于在编辑时通知model和view关于当前编辑器的状态信息。
Models(模型)
所有的item models都基于QAbstractItemModel类,这个类定义了用于views和delegates访问数据的接口。数据本身不必存储在model,数据可被置于一个数据结构或另外的类、文件、数据库、或其它的程序组件中。
QAbstractItemModel提供给数据一个接口,它非常灵活,基本满足views的需要,无论数据用以下任何形式表现,如table、list、trees。然而,当你重新实现一个model时,如果它基于table或list形式的数据结构,最好从QAbstractListModel、QAbstractTableModel开始做起,因为它们提供了适当的常规功能的缺省实现。这些类可以被子类化以支持特殊的定制需求。
Qt提供了一些现成的models用于处理数据项:
QStringListModel 用于存储简单的QString项目列表。
QStandardItemModel管理更复杂的树型结构数据项,每项都可以包含任意数据。
QFileSystemModel提供本地文件系统中的文件与目录信息。
QSqlQueryModel、QSqlTableModel、QSqlRelationTableModel用来访问数据库。
如果这些标准models不能满足需要,可以使用子类化QAbstractItemModel、QAbstractListModel或是QAbstractTableModel来定制models。
Views(视图)
不同的view都完整实现了各自的功能:
QListView把数据显示为一个列表,QTableView把Model 中的数据以table的形式表现,QTreeView用具有层次结构列表来显示model中的数据。这些类都基于QAbstractItemView抽象基类。尽管这些类都已经完整地得到了实现,但它们仍然可以用于子类化以满足定制需求。
Delegates(委托)
QAbstractItemDelegate是model/view架构中的用于delegate的抽象基类。缺省的delegate实现在QStyledItemDelegate类中提供,它可以用于Qt标准views的缺省 delegate。但是,QStyledItemDelegate和QItemDelegate是相互独立的用于实现视图(views)中items的描绘和编辑功能的方法。它们两者的不同在于,QStyledItemDelegate使用当前的样式(style)来描绘items。因此,当我们实现定制委托(delegate)或者使用Qt Style Sheets时,我们建议使用QStyledItemDelegate作为基类。
Sorting(排序)
在model/view架构中,有两种方法进行排序,选择哪种方法依赖于你的底层Model。
假如你的model是可排序的,也就是它重新实现了QAbstractItemModel::sort()函数,QTableView与QTreeView都提供了API,允许你以编程的方式对model数据进行排序。另外,你也可以进行交互方式下的排序(例如,允许用户通过点击view表头的方式对数据进行排序),具体方法是:把QHeaderView::sectionClicked()信号与QTableView::sortByColum()槽或QTreeView::sortByColumn()槽进行关联。
另一种方法是,假如你的model没有提供需要的接口或是你想用list view表示数据,可以用一个代理model在用view表示数据之前对你的model数据结构进行转换。
Convenience classes(便利类)
许多便利类都源于标准的view类,它们方便了那些使用Qt中基于项的view与table类,它们不应该被子类化,它们只是为Qt 3的等价类提供一个熟悉的接口。这些类有QListWidget、QTreeWidget、QTableWidget,它们提供了如Qt 3中的QListBox、 QlistView、QTable相似的行为。这些类比View类缺少灵活性,不能用于任意的models,推荐使用model/view的方法处理数据。
Using models and views(使用模型和视图)
Qt提供了两个标准的models:QStandardItemModel和QFileSystemModel。
QStandardItemModel是一个多用途的model,可用于表示list,table,tree views所需要的各种不同的数据结构。这个model也持有数据。
QFileSystemModel维护相关的目录内容的信息,它本身不持有数据,仅是对本地文件系统中的文件与目录的描述。QFileSystemModel是一个现成的model,很容易进行配置以利用现有的数据。使用这个model,可以很好地展示如何给一个现成的view设定model,研究如何用model indexes来操纵数据。
model与views的搭配使用
QListView与QTreeView很适合与QFileSystemModel进行搭配。下面的例子在tree view与list view显示了相同的信息,QFileSystemModel提供了目录内容数据。这两个Views共享用户选择,因此每个被选择的项在每个view中都会被高亮。
/*先创建QF<span style="font-family:Times New Roman;">i</span>leSystemModel以供使用,再创建views去显示目录的内容。这里展示了使用model的最简单的方式。model的创建与使用都在main()函数中完成:*/ int main(int argc, char *argv[]) { QApplication app(argc, argv); QSplitter *splitter = new QSplitter; /*此模型model的创建是为了使用特定文件系统的数据。setRootPath()调用告诉驱动此文件系统的model将数据呈现给哪些视图(Views)*/ QFileSystemModel *model = new QFileSystemModel; model->setRootPath(QDir::currentPath()); /*创建两个视图(Views),分别是TreeView和ListView*/ /*配置一个View以显示模型model中的数据,只需简单调用setModel()函数,并将目录model作为参数。setRootIndex()告诉views显示哪个目录的信息,这需要提供一个model index,然后用这个model index去model中去获取数据。index()函数是QFileSystemModel特有的,通过把一个目录做为参数,得到了需要的model index。*/ QTreeView *tree = new QTreeView(splitter); tree->setModel(model); tree->setRootIndex(model->index(QDir::currentPath())); QListView *list = new QListView(splitter); list->setModel(model); list->setRootIndex(model->index(QDir::currentPath())); /*将所有的widgets置于splitter中*/ splitter->setWindowTitle("Two views onto the same file system model"); splitter->show(); return app.exec(); }
上面的例子并没有展示如何处理数据项的选择,这包括很多细节,以后会提到。
Model类
基本概念
在model/view构架中,model为view和delegates使用数据提供了标准接口。在Qt中,标准接口QAbstractItemModel类中被定义。不管数据在底层以何种数据结构存储,QAabstractItemModel的子类会以层次结构的形式来表示数据,结构中包含了数据项表。我们按这种约定来访问model中的数据项,但这个约定不会对如何显示这些数据有任何限制。数据发生改变时,model通过信号槽机制来通知关联的views。
Model Indexes
为了使数据存储与数据访问分开,引入了model indexes的概念。通过model indexes,可以引用model中的数据项,views和delegates都使用indexes来访问数据项,然后再显示出来。因此,只有model需要了解如何获取数据,被model管理的数据类型可以非常广泛地被定义。Model indexes包含一个指向创建它们的model的指针,这会在配合多个model工作时避免混乱。
QAbstractItemModel *model = index.model();
Model indexes提供了对一项数据信息的临时引用,通过它可以访问或是修改model中的数据。既然model可能不时重新组织内部的数据结构,model indexes可能会失效,因此不应该保存临时的model indexes。假如需要一个对数据信息的长期的引用,那么应该创建一个persistent model index。这个引用会保持更新。临时的model indexes由QModelIndex提供,而具有持久能力的model indexes则由QPersistentModelIndex提供。
在获取对应一个数据项的model index时,需要考虑有关于model的三个属性:行数,列数,父项的model index。
行与列
在最基本的形式中,一个model可作为一个简单的表来访问,每个数据项由行数,列数来定位。这并不意味着底层的数据用数组结构来存储。行和列的使用仅仅是一种约定,它允许组件之间相互通信。可以通过指定model中的行列数来获取任一项数据,可以得到与数据项一一对应的那个index。
QModelIndex index = model->index(row, column, ...);
Model为简单的、单级的数据结构如list与tables提供了接口,它们如上面代码所显示的那样,不再需要别的信息被提供。当我们在获取一个model index时,我们需要提供另外的信息。
上图代表一个基本的table model,它的每一项用一对行列数来定位。通过行数和列数,可以获取代表一个数据项的model index :
QModelIndex indexA = model->index(0, 0,QModelIndex()); QModelIndex indexB = model->index(1, 1,QModelIndex()); QModelIndex indexC = model->index(2, 1,QModelIndex());
一个model的顶层项由QModelIndex()取得,它们被用作父项。
父项
类似于表的接口在搭配使用table或list view时是理想的,这种行列系统与view显示的方式是确切匹配的。然而,像tree views这种结构需要model提供更为灵活的接口来访问数据项。每个数据项可能是其它的项的父项,上级的项可以获取下级项的列表。当获取model中数据项的index时,我们必须指定关于数据项的父项的信息。在model外部,引用一个数据项的唯一方法就是通过model index,因此需要在求取model index时指定父项的信息。
QModelIndex index = model->index(row, column, parent);
上图中,A项和C项作为model中顶层的兄弟项:
QModelIndex indexA = model->index(0, 0, QModelIndex()); QModelIndex indexC = model->index(2, 1, QModelIndex());
A有许多孩子,它的一个孩子B用以下代码获取:
QModelIndex indexB = model->index(1, 0, indexA);
项角色
model中的项可以作为各种角色来使用,这允许为不同的环境提供不同的数据。举例来说,Qt::DisplayRole被用于访问一个字符串,它作为文本会在view中显示。典型地,每个数据项都可以为许多不同的角色提供数据,标准的角色在Qt::ItemDataRole中定义。
我们可以通过指定model index与角色来获取我们需要的数据:
QVariant value = model->data(index, role);
角色指出了从model中引用哪种类型的数据。views可以用不同的形式显示角色,因此为每个角色提供正确的信息是非常重要的。通过为每个角色提供适当数据,model也为views和delegates提供了暗示,以指示如何正确地把这些数据项显给用户。不同的views可以自由地解析或忽略这些数据信息,对于特殊的应用要求,也可以定义一些附加的角色。
基本概念总结:
1、Model indexes为views与delegates提供model中数据项定位的信息,它与底层的数据结构无关。
2、通过指定行数、列数以及父项的model index来引用数据项。
3、依照其它的组件的要求,model indexes被model构建,如views和delegates。
4、使用index()时,如果指定了有效的父项的model index,那么返回得到的model index对应于父项的某个孩子。
5、使用index()时,如果指定了无效的父项的model index,那么返回得到的model index对应于顶层项的某个孩子。
6、角色对一个数据项包含的不同类型的数据给出了区分。
使用Model Indexes
为了演示数据是如何通过model indexes来从model中获取数据,我们创建了一个没有views的QFileSystemModel ,这个Model用于在一个widget中显示文件名和目录路径。尽管这个例子并没有展现出一个使用Model的正常方法,但是我们可以通过此例来了解处理Model indexes的一些约定和规范。
首先,我们创建一个文件系统Model。
QFileSystemModel *model = new QFileSystemModel; QModelIndex parentIndex = model->index(QDir::currentPath()); /*计算Model中的行数*/ int numRows = model->rowCount(parentIndex); /*这里,为了简约,我们仅仅只关注Model每一列的第一项。我们依次检查每一行,获取该行第一项的Model index。然后在读取该项的内容。*/ for (int row = 0; row < numRows; ++row) { QModelIndex index = model->index(row, 0, parentIndex); /*在这里,理解data()函数的用法,data()函数用于获取指定index处的数据,同时,通过Qt::DisplayRole来以String的方式获取该项内容。*/ QString text = model->data(index, Qt::DisplayRole).toString(); // Display the text in a widget. }
以上的例子说明了从model中获取数据的基本原则:
1、Model的尺寸可以从rowCount()与columnCount()中得出。这些函数通常都需要一个表示父项的model index。
2、Model indexes用来从model中访问数据项,数据项用行数,列数以及父项来实现model index定位。
3、为了访问model顶层项,可以使用QModelIndex()指定一个Null Model Index。
4、数据项为不同的角色提供不同的数据。为了获取数据,除了model index之外,还要指定角色。
创建新的Models
model/view组件之间功能的分离,允许创建model利用现成的views。这也可以使用标准的功能图形用户接口组件像QListView、QTableView和QTreeView来显示来自各种数据源的数据。
QAbstractListModel类提供了非常灵活的接口,允许数据源以层次结构的形式来管理信息,也允许以某种方式对数据进行插入、删除、修改和排序。它也提供了对拖拽操作的支持。
QAbstractListModel与QAbstractTableModel为简单的非层次结构的数据提供了接口,对于比较简单的list和table models来说,这是不错的一个开始点。
设计一个Model
当我们为存在的数据结构新建一个model时,首先要考虑的问题是应该选用哪种model来为这些数据提供接口。
假如数据结构可以用数据项的列表或表格来表示,那么可以考虑子类化QAbstractListModel或QAbstractTableModel,因为这些类已经合理地对许多功能提供缺省实现。
然而,假如底层的数据结构只能表示成具有层次结构的树型结构,那么必须得子类化QAbstractItemModel。
无论底层的数据结构采取何种形式,在特定的model中实现标准的QAbstractItemModel API总是一个不错的主意,这使得可以使用更自然的方式对底层的数据结构进行访问。这也使得用数据构建model更为容易,其他的model/view组件也可以使用标准的API与之进行交互。
一个只读model示例
这个示例实现了一个简单的、非层次结构的、只读的数据model,它基于QStringListModel类。它有一个QStringList作为它内部的数据源,只实现了一些必要的接口。为了简单化,它子类化了QAbstractListModel,这个基类提供了合理的缺省行为,对外提供了比QAbstractItemModel更为简单的接口。当我们实现一个model时,不要忘了QAbstractItemModel本身不存储任何数据,它仅仅提供了给views访问数据的接口。
class StringListModel : public QAbstractListModel { Q_OBJECT public: StringListModel(const QStringList &strings, QObject *parent = 0) : QAbstractListModel(parent), stringList(strings) {} int rowCount(const QModelIndex &parent = QModelIndex()) const; QVariant data(const QModelIndex &index, int role) const; QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const; private: QStringList stringList; };
除了构造函数,我们仅需要实现两个函数:rowCount()返回model中的行数,data()返回与特定model index对应的数据项。
具有良好行为的model也会实现headerData(),它返回tree和table views需要的在标题中显示的数据(标题栏)。
因为这是一个非层次结构的model,我们不必考虑父子关系。假如model具有层次结构,我们也应该实现index()与parent()函数。
Model的尺寸
我们认为model中的行数与string list中的string数目一致:
int StringListModel::rowCount(const QModelIndex &parent) const { return stringList.count(); }
在缺省情况下,从QAbstractListModel派生的model只具有一列,因此不需要实现columnCount()。
Model 标题与数据
QVariant StringListModel::data(const QModelIndex &index, int role) const { if (!index.isValid()) return QVariant();// QVariant is invalid data. if (index.row() >= stringList.size()) return QVariant(); if (role == Qt::DisplayRole) return stringList.at(index.row()); else return QVariant(); }
对于一些视图(Views),如QTreeView和QTabeleView,我们需要设置对应的数据项的标题。这里,我们使用headerData()函数来实现添加行列标题的目的:
QVariant StringListModel::headerData(int section, Qt::Orientation orientation, int role) const { if (role != Qt::DisplayRole) return QVariant(); if (orientation == Qt::Horizontal) return QString("Column %1").arg(section); else return QString("Row %1").arg(section); }
一个数据项可能有多个角色,根据角色的不同输出不同的数据。上例中,model中的数据项只有一个角色DisplayRole,然而我们也可以重用提供给DisplayRole的数据,作为别的角色使用,如我们可以作为ToolTipRole来用。
可编辑的model
上面我们演示了一个只读的model,它只用于向用户显示,对于许多程序来说,可编辑的list model可能更有用。我们只需要给只读的model提供另外两个函数flags()与setData()的实现。下列函数声明被添加到类定义中:
Qt::ItemFlags flags(const QModelIndex &index) const; bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole);
使model可编辑
delegate会在创建编辑器之前检查数据项是否是可编辑的。model必须得让delegate知道它的数据项是可编辑的。这可以通过为每一个数据项返回一个正确的标记得到,在本例中,我们假设所有的数据项都是可编辑可选择的:
Qt::ItemFlags StringListModel::flags(const QModelIndex &index) const { if (!index.isValid()) return Qt::ItemIsEnabled; return QAbstractItemModel::flags(index) | Qt::ItemIsEditable; }
我们不必知道delegate执行怎样实际的编辑处理过程,我们只需提供给delegate一个方法,delegate会使用它对model中的数据进行设置。这个特殊的函数就是setData():
bool StringListModel::setData(const QModelIndex &index, const QVariant &value, int role) { if (index.isValid() && role == Qt::EditRole) { stringList.replace(index.row(), value.toString()); emit dataChanged(index, index); return true; } return false; }
当数据被设置后,model必须得让views知道一些数据发生了变化,这可通过发射一个dataChanged()信号实现。因为只有一个数据项发生了变化,因此在信号中说明的变化范围只限于一个model index。
同时,data()函数也需要进行更改来添加Qt::Editable测试:
QVariant StringListModel::data(const QModelIndex &index, int role) const { if (!index.isValid()) return QVariant(); if (index.row() >= stringList.size()) return QVariant(); if (role == Qt::DisplayRole || role == Qt::EditRole) return stringList.at(index.row()); else return QVariant(); }
插入,删除行
在model中改变行数与列数是可能的。当然在本列中,只考虑行的情况,我们只需要重新实现插入、删除的函数就可以了,下面应在类定义中声明:
bool insertRows(int position, int rows, const QModelIndex &index = QModelIndex()); bool removeRows(int position, int rows, const QModelIndex &index = QModelIndex());
既然model中的每行对应于列表中的一个string,因此,insertRows()函数在string list 中指定位置插入一个空的string。
Parent和index通常用于决定model中行列的位置。本例中只有一个单独的顶级项,因此只需要在list中插入空string。
bool StringListModel::insertRows(int position, int rows, const QModelIndex &parent) { beginInsertRows(QModelIndex(), position, position+rows-1); for (int row = 0; row < rows; ++row) { stringList.insert(position, ""); } endInsertRows(); return true; }
函数首先调用beginInsertRows()通知其他组件行数将会改变。然后确定插入的第一行和最后一行所对应的行号,以及父项的Model Index。在插入所有字符串表项后,调用endInseRows()来完成所有操作同时通知其他组件model尺寸的改变,返回TRUE来表示插入操作的成功。
删除操作与插入操作类似:
bool StringListModel::removeRows(int position, int rows, const QModelIndex &parent){ beginRemoveRows(QModelIndex(), position, position+rows-1); for (int row = 0; row < rows; ++row) { stringList.removeAt(position); } endRemoveRows(); return true; }