Qt Model/View编程介绍
Qt中包含了一系列的项视图类,它们使用model/view的架构去管理数据之间的关系以及它们被展示给用户的方式。这种由这种架构引进的功能分离特性给了开发者很大的灵活性去自定义自己的展示方式,并且提供了一个编制的模型接口以使很多种的数据源都可以和现存的视图相结合。那么,今天我们就来简单的看下这种model/view范例,相关的概念,并简要描述一下项视图系统的架构。
模型/视图 架构
Model-View-Controller(MVC)是一种起源于Smalltalk编程语言的设计模式,主要被用来构建用户界面。对于这种设计模式,Gamma et al 写道:
MVC由3中对象组成。模型(Model)是应用程序的数据,视图(View)是它的屏幕展示,控制器(Controller)则定义了用户界面响应用户输入的方式。在MVC之前,用户界面的设 计倾向于将这些东西杂揉到一起。MVC则通过对它们进行解耦来提高开发的灵活性和组件的复用性。
如果视图和控制器被组合在了一起,结果就是model/view架构了。这仍然区分了数据的存储方式和它被展示的方式,不过是基于相同的原则提供了一个更简单的框架。这种分离使我们有可能用多种不同的视图来展示同一种数据,并且可以在不改变底层数据结构的情况下实现新的视图类型。而为了灵活的处理用户输入,我们又引入了代理(Delegate)的概念。在model/view框架中引入代理的好处是允许数据项的渲染和编辑可以被自定义。所以,传统的MVC在Qt中就变成了MVD,其工作原理图如下:
其中,模型和数据源进行交互,为架构中的其他组件提供一个接口。当然,交互的本质取决于数据源的类型和模型被实现的方式;视图从模型中获得模型下标(model indexes),这些下标是对数据项的引用。通过为模型应用模型下标,视图就可以从数据源中获得数据项。在标志视图下,会有一个代理来渲染这些数据项。当数据项被编辑时,代理又会使用模型下标直接和模型进行交互。
通常情况下,model/view相关的类可以分为三组:模型,视图和代理。这些组件中的每一个都有相关的抽象类来定义,以此来提供一些通用的接口,并在某些情况下,提供一些特性的默认实现。这些抽象类可以被子类化以为其他组件提供完全的功能支持,也可以借此实现一些特定的组件。
模型,视图和代理彼此使用信号和槽进行通信:
- 来自模型的信号会通知视图关于数据源中数据的改变
- 来自视图的信号提供了用户和视图项发生交互的信息
- 来自代理的信号被用于在编辑过程中告知模型和视图当前编辑器的状态。
- QStringListModel:被用来存储一个QString项的简单列表。
- QStanderItemModel:可以用来管理更复杂的树型数据结构,每一个数据项可以包含任意数据。
- QFileSystemModel:提供本地文件系统中文件和目录的信息。
- QSqlQueryModel,QSqlTabelModel和QSqlRelationalTableModel 被用作访问数据库的方便方法。
int main(int argc, char *argv[]){QApplication app(argc, argv);QSplitter *splitter = new QSplitter;QFileSystemModel *model = new QFileSystemModel;model->setRootPath(QDir::currentPath());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()));splitter->setWindowTitle("Two views onto the same file system model");splitter->show();return app.exec();}
我们实例化了一个QFileSystemModel以便使用,并创建了相应的视图来展示一个目录下的内容。这是使用模型的简单的方式。该模型被初始化为使用一个文件系统的数据。调用setRootPath()告诉模型要将文件系统上的哪一个驱动器展示到视图中。我们在此创建了两个视图以便于用两种方式测试存储在模型中的数据。
QAbstractItemModel *model = index.model();
模型下标提供了对信息片段的临时引用,可以被用来通过模型去获取或修改数据。因为模型随时都有可能重新组织它们的内部结构,模型下标可能会失效,所以不应该存储它们到一个变量中。如果需要对某种信息的长期引用,必须创建一个持久性的模型下标。这种下标会提供一个对模型信息的引用,并保持更新。临时模型下标由QModelIndex类表示,持久性的模型下标由QPersistentModelIndex类表示。
QModelIndex index = model->index(row, column, ...);
对于那些为简单、单级数据结构如列表和表格提供接口的模型来说,在访问一个数据项时,除了行号和列号,不需要提供其他的信息。但是,像上面的代码显示的,我们需要提供其他的信息才能获得一个模型下标。
上图表示了一个基本的表格模型,其中的每一项由一个行和列来确定。我们通过传递相对于该模型的行和列来得到一个引用某个数据项的模型下标。
QModelIndex indexA = model->index(0, 0, QModelIndex());QModelIndex indexB = model->index(1, 1, QModelIndex());QModelIndex indexC = model->index(2, 1, QModelIndex());
对应模型中的顶层项来说,我们总是传入一个QModelIndex()来作为它们的父项。我们会在后面的部分继续讨论这个问题。
QModelIndex index = model->index(row, column, parent);
上图展示了一个树型视图,其中的每一项都由一个父项,一个行号,一个列号来确定。
QModelIndex indexA = model->index(0, 0, QModelIndex());QModelIndex indexC = model->index(2, 1, QModelIndex());
项"A"有一系列的孩子。其中,"B"的模型下标可以用下面的方式获得:
QModelIndex indexB = model->index(1, 0, indexA);
项的角色
QVariant value = model->data(index, role);
对于大部分常见用途的项数据,Qt::ItemDataRole类型的定义中均已包括。通过为每一种角色提供合适的项数据,模型可以为视图和代理提供提示该如果向用户展示该项。不同种类的视图均可以接受或忽略这些需要的信息。当然,也可以为特定的应用程序目的定义特定的角色。
自此,对于模型类的使用,我们可以进行一下总结:
- 模型下标以独立于底层数据结构的方式向视图和代理提供了模型中的数据项的信息。
- 项是由它们在模型中的行号和列号,以及它们的父项所指定的。
- 模型下标在其他组件,如视图和代理,发出请求时被模型创建出来。
- 当我们使用index() 函数请求一个下标时,若传递了一个有效的模型下标做为父项,那么返回的下标引用了一个在模型中位于父项下面的某项。这个下标引用着那个项的一个孩子。
- 若使用index() 函数时,传入了一个无效的模型下标做为父项,那么返回的下标引用着模型中的一个顶层项
- 角色区分了一个项所关联的不同数据。
使用模型下标
QFileSystemModel *model = new QFileSystemModel;QModelIndex parentIndex = model->index(QDir::currentPath());int numRows = model->rowCount(parentIndex);
如上面的代码所示,我们创建了一个默认的QFileSystemModel,使用模型类的函数index()获取一个父下标,然后,我们使用模型类的rowCount()函数获取了该下标中的行数。
for (int row = 0; row < numRows; ++row) {QModelIndex index = model->index(row, 0, parentIndex);QString text = model->data(index, Qt::DisplayRole).toString();// Display the text in a widget.}
为了得到一个模型下标,我们指定了一个行号,一个列号(0代表第一个列),和一个代表我们想获得的所有数据的父项的模型下标。而每一项中存储的文本可以使用模型的data()函数获取到。我们通过指定一个模型下标和相应的DisplayRole已字符串的形式来获得某项存储的数据。
- 一个模型的维度可以由rowCount() 和 columnCount()获得。这些函数通常需要指定一个父项的模型下标。
- 模型下标被用来访问模型中的项。行号、列号和父项的模型下标是必须的,对于定位一个模型项来说。
- 要访问模型中的顶层项目,可以使用QModelIndex()指定一个空的模型下标作为父项。
- 项可以为不同的角色保存数据。为了获得特定角色的数据,需同时指定模型下标和角色。
视图类
int main(int argc, char *argv[]){QApplication app(argc, argv);QStringList numbers;numbers << "One" << "Two" << "Three" << "Four" << "Five";QAbstractItemModel *model = new StringListModel(numbers);QListView *view = new QListView;view->setModel(model);view->show();return app.exec();}
如上面代码所示,我们先创建了一个字符串列表模型(自定义),再为它初始化一些数据,然后创建一个视图显示这个模型的内容。
QTableView *firstTableView = new QTableView;QTableView *secondTableView = new QTableView;firstTableView->setModel(model);secondTableView->setModel(model);
在模型/视图中,信号和槽的使用意味着模型的改变可以被传播到所有相关的视图上,以此来确保无论使用哪个视图,我们总能访问到相同的数据。
secondTableView->setSelectionModel(firstTableView->selectionModel());
第二个视图的选中模型被设置为第一个视图的选中模型。此时,两个视图就工作与同一个选中模型了。它们会在数据和选择操作上保存一致。效果如下:
在上面的例子中,我们使用了两个同样类型的视图和同一个模型数据。但是,如果使用两种不同类型的视图,那么这些选中的项在两个视图将会得到不同的展现;例如,在表格视图中一系列连续的选中项,在一个树型视图可能被展示为了一系列的高亮选项的片段。
class SpinBoxDelegate : public QStyledItemDelegate{Q_OBJECTpublic:SpinBoxDelegate(QObject *parent = 0);QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option,const QModelIndex &index) const Q_DECL_OVERRIDE;void setEditorData(QWidget *editor, const QModelIndex &index) const Q_DECL_OVERRIDE;void setModelData(QWidget *editor, QAbstractItemModel *model,const QModelIndex &index) const Q_DECL_OVERRIDE;void updateEditorGeometry(QWidget *editor,const QStyleOptionViewItem &option, const QModelIndex &index) const Q_DECL_OVERRIDE;};
注意,当代理被创建时,并不创建相关的编辑控件。我们只在需要时才创建一个编辑控件。
QWidget *SpinBoxDelegate::createEditor(QWidget *parent,const QStyleOptionViewItem &/* option */,const QModelIndex &/* index */) const{QSpinBox *editor = new QSpinBox(parent);editor->setFrame(false);editor->setMinimum(0);editor->setMaximum(100);return editor;}
注意,我们不需要保存执行该控件的指针,因为视图会负责在不需要它时将其释放。
我们在编辑器上安装代理默认的事件过滤器,类确保它提供用户期望的标准的编辑快捷键。也可以向编辑器添加额外的快捷键来提供更复杂的行为。
void SpinBoxDelegate::setEditorData(QWidget *editor, const QModelIndex &index) const{int value = index.model()->data(index, Qt::EditRole).toInt();QSpinBox *spinBox = static_cast<QSpinBox*>(editor);spinBox->setValue(value);}
在这个例子中,我们知道编辑器控件是一个spix box,但是我们可能为模型中不同的数据类型提供了不同的代理,所以,我们需要先将控件转化成正确的类型才能访问它的成员函数。
void SpinBoxDelegate::setModelData(QWidget *editor, QAbstractItemModel *model,const QModelIndex &index) const{QSpinBox *spinBox = static_cast<QSpinBox*>(editor);spinBox->interpretText();int value = spinBox->value();model->setData(index, value, Qt::EditRole);}
既然是视图帮代理管理着编辑器控件,所以我们只需要使用编辑器提供的值去更新模型即可。在这种情况下,我们要确保spin box 是最新的,并使用它包含的值通过指定的下标来更新模型。
void SpinBoxDelegate::updateEditorGeometry(QWidget *editor,const QStyleOptionViewItem &option, const QModelIndex &/* index */) const{editor->setGeometry(option.rect);}
在这个例子中,我们仅仅使用了view option 对象在item rectangle 中提供的尺寸信息。使用多种元素渲染数据项的代理可能不直接使用item rectangle。它可能会相对于该数据项中其他的元素来安置这个编辑器。
当前项 | 选中项 |
同一时间只有一个当前项 | 同一时间可以有多个选中项 |
当前项会随着键盘导航或鼠标点击而改变 |
每一项的选中状态的设置或取消由几个预定义的模式决定, 比如,单选模式,多选模式,等等。 |
当前项会在按下编辑键F2或鼠标双击时被编辑 |
当前项可以和一个锚点一起使用来指定一个应该被选中或取消选中的范围 (或者是两个范围的组合) |
当前项通过当前的矩形框来表明 | 选中项通过选中矩形来表明 |
TableModel *model = new TableModel(8, 4, &app);QTableView *table = new QTableView(0);table->setModel(model);QItemSelectionModel *selectionModel = table->selectionModel();
我们获得了表格视图的默认选择模型以备后续使用。我们不修改模型中的任何项,只是选择表格视图中左上角的一些项。为实现这个功能,我们需要获得将要被选中的区域的左上角和右下角模型下标:
QModelIndex topLeft = model->index(0, 0, QModelIndex());QModelIndex bottomRight = model->index(5, 2, QModelIndex());
为了在模型中选中这些项,并且在表格视图中看到相应的变化,我们需要构建一个选择对象,在把它应用到选择模型上:
QItemSelection selection(topLeft, bottomRight);selectionModel->select(selection, QItemSelectionModel::Select);
该选集被应用到选择模型,通过一个预定义的选集标志。在上面这种情况下,所使用的的标志导致我们所传入的选集对象中的模型项被包含进了选择模型,无论他们先前是什么状态。结果显示如下图:
这些项的选集可以使用多种预定义好的选集标志进行修改。而这些操作所产生的选集的可能会有一个复合结构,但它仍可以被选中模型高效的展示。
读取选集状态
存储在选择模型中的模型下标可以使用selectedIndexed()方法读取。返回的是一个无序的列表,其中包含了我们可以迭代的模型下标,只要我们知道它们属于哪个模型:
QModelIndexList indexes = selectionModel->selectedIndexes();QModelIndex index;foreach(index, indexes) {QString text = QString("(%1,%2)").arg(index.row()).arg(index.column());model->setData(index, text);}
选择模型也是通过发射信号来表现选择的变化。这可以通知其他组件有关于模型中整个选集和当前有焦点的项的变化。我们可以连接selectionChanged()信号到一个槽函数上,来测试当选集发 发生变化时模型中的项是被选中了还是被取消选中了。这个槽函数会接收两个QItemSelection对象:一个包含新选中项的模型下标的列表;另一个包含的是相应的被取消选中的模型项的下标。
下面的代码中,我们为selectionChanged()信号提供了一个槽函数,来为选中的项填充一个字符串,并清空取消选中的项。
void MainWindow::updateSelection(const QItemSelection &selected, const QItemSelection &deselected){QModelIndex index;QModelIndexList items = selected.indexes();foreach (index, items) {QString text = QString("(%1,%2)").arg(index.row()).arg(index.column());model->setData(index, text);}items = deselected.indexes();foreach (index, items)model->setData(index, "");}
我们可以通过连接currentChanged()信号到一个槽函数来跟踪当前焦点项的改变,这个槽函数接受两个模型下标作为参数,分别对应着前一个焦点项和当前焦点项。
下面的代码中,我们连接到currentChanged()信号,然后使用接收到的信息更新主窗口的状态栏:
void MainWindow::changeCurrent(const QModelIndex &t, const QModelIndex &previous){statusBar()->showMessage(tr("Moved from (%1,%2) to (%3,%4)").arg(previous.row()).arg(previous.column()).arg(current.row()).arg(current.column()));}
更新选集
选择命令通过选择标志的组合来提供,选项标志由QItemSelectionModel::SelectionFlag定义。每一个选择标志都告诉选择模型当任何一个select()函数被调用时,该怎么更新其内部的选中项的记录集。最常使用的标志是Select标志,它指导选择模型去把指定的模型项记录为已选中。Toggle标志使选择模型去翻转指定项的状态,选中任何给定的未选中项,取消选中当前已选中的项。Deselect标志取消选中所有指定的项。
选择模型中个别的模型项也可以通过创建一个选集来更改,并把它们应用到一个选择模型上。下面的代码,我们为上面的表格模型应用第二个选集,使用Toggle标志来翻转给定项的选中状态:
QItemSelection toggleSelection;topLeft = model->index(2, 1, QModelIndex());bottomRight = model->index(7, 3, QModelIndex());toggleSelection.select(topLeft, bottomRight);selectionModel->select(toggleSelection, QItemSelectionModel::Toggle);
运行结果如下:
默认情况下,选择命令仅仅作用于由模型下标指定的个别项上。但是,描述选择命令的标志可以和其他的标志进行组合,以此来改变整行和整列。例如,如果你只使用一个下标来调用select()函数,但传入的选择命令是Select和Rows的组合,那么包含该行的整行都被选中。下面的代码展示了Rows和Columns标志的使用:
QItemSelection columnSelection;topLeft = model->index(0, 1, QModelIndex());bottomRight = model->index(0, 2, QModelIndex());columnSelection.select(topLeft, bottomRight);selectionModel->select(columnSelection,QItemSelectionModel::Select | QItemSelectionModel::Columns);QItemSelection rowSelection;topLeft = model->index(0, 0, QModelIndex());bottomRight = model->index(1, 0, QModelIndex());rowSelection.select(topLeft, bottomRight);selectionModel->select(rowSelection,QItemSelectionModel::Select | QItemSelectionModel::Rows);
虽然只向选择模型提供了4个下标,但使用了Columns和Rows标志,意味着2行和2列被选中。运行结果如下:
在上面例子模型中展示的命令都涉及到了在模型中累加一个选集中的模型项。当然,我们也可以清空一个选集,或用一个新的选集来替代当前的选集。要用一个新的选集替换当前的选集,可以在选择标志中组合Current标志。该标志就是告诉选择模型去用调用select()函数传入的模型下标替换它当前选集中的模型下标。要清空一个选集,可以在选择标志中组合Clear标志,这会重置选择模型中的模型下标的集合。
选择模型中的所有项
为了选中模型中的所有项,我们需要为每一个层级创建一个能覆盖该层级中所有项的选集。我们通过获得左上角和右下角的项的模型下标来完成这件事:
QModelIndex topLeft = model->index(0, 0, parent);QModelIndex bottomRight = model->index(model->rowCount(parent)-1,model->columnCount(parent)-1, parent);
然后,我们用这些下标来创建一个选集,使在该范围中的模型项都被选中:
QItemSelection selection(topLeft, bottomRight);selectionModel->select(selection, QItemSelectionModel::Select);
这需要应用于模型中的所有层级。对于顶层项来说,我们可以使用下面的方式为它们定义一个父下标:
QModelIndex parent = QModelIndex();
对于有层级的模型,函数hasChildren()可以用来测试一个给定的项是否是另一个层级的父。
自定义模型
模型/视图中功能的分离允许我们创建自定义的模型,同时还可以利用已存在的视图。这中方法可以让我们使用标准的图形用户界面组件,如QListView,QTableView和QTreeView,来展示来自不同数据源的数据。
QAbstractItemModel类提供了一个足够灵活的接口来支持一层级的方式安排信息的数据源,允许数据以某种方式被插入,删除,修改或排序。它还支持拖放操作。
QAbstractListModel和QAbstractTableModel类为更简单的非层级数据结构提供了接口支持,对于简单的列表和表格模型这也是一个更容易使用的开始点。
下面,我们先创建一个简单的只读模型来展示基本的模型/视图架构的惯例。在后面,我们会改变这个简单模型,使它的项可以被用户修改。
设计一个模型
当为现存的数据结构创建一个新模型时,考虑哪种模型应该用来为数据提供接口是很重要的。如果该数据结构可以被展示为一个列表或表格,那么可以子类化QAbstractListModel 或者 QAbstractTableModel,因为这些类为很多操作提供了默认实现。
但是,如果底层的数据结构只能被展示为一个层级的树形结构,那就必须子类化QAbstractItemModel。
在这里,我们基于字符串列表实现一个简单的模型,所以QAbstractListModel是很好的一个基类选择。
其实,无论底层的数据结构是什么样的,在一个特化的模型中,为了更自然的访问底层数据结构,补充实现一些标准的QAbstractItemModel API总是一个好主意。这会让模型的填充更容易,也能使其他常规的模型/视图组件使用标准的API与它交互。下面自定义的模型出于目的提供了一个构造函数。
一个只读的模型
这儿实现的模型是一个简单的,非层级的,只读的,基于QStringListModel的模型。它有一个QStringList作为它的内部数据结构,只实现了能让模型运行的必须的函数。为了使实现更简单,我们子类化QAbstractListModel。当实现一个模型时,要注意的是QAbstractItemModel并不存储数据本身,它仅仅提供一个视图访问数据的接口。对于一个最小化的只读模型来说,我们只需要实现很少的几个函数,因为大部分接口都有了默认的实现。该类的声明如下:
class StringListModel : public QAbstractListModel{Q_OBJECTpublic: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()返回模型中数据的行数,data()返回模型中相对于特定模型下标的项。
行为良好的模型,还应该实现headerData()函数,来为树形或表格视图提供一些显示在头部的信息。
注意,这是一个非层级模型,所以我们不必关心父子关系。如果我们的模型是层级模型,可能还要实现index()和parent()函数。
字符串被存储在内部的stringList私有成员变量中。
模型的维度
我们想要让模型的行数和字符串链表中的项数一样。所以,我们如下实现rowCount()函数:
int StringListModel::rowCount(const QModelIndex &parent) const{return stringList.count();}
因为该模型是非层级的,所以我们可以安全的忽略父项的模型下标。默认情况下,从QAbstractListModel类派生的模型只包含一列,所以我们也没必要实现columnCount()函数。
模型头信息和数据的获取
对于视图中的项,我们想要返回字符串列表中的字符串。data()函数负责返回对应于index参数的项的数据。
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)return stringList.at(index.row());elsereturn QVariant();}
我们只是在index有效,行数在有效范围内,请求的角色是模型所支持的情况下返回一个有效的QVariant类型的变量。
还有一些视图,例如QTreeView和QTableView,同数据一道还可以显示一些头部信息。如果我们的模型被显示在一个具有头信息的视图中,并我们想在表头显示行号和列号。我们可以实现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);elsereturn QString("Row %1").arg(section);}
再一次,我们在角色是模型所支持的情况下,返回一个有效的QVariant变量。并且,在返回具体的数据时我们也考虑了头部的方向问题。
不是所有的视图都显示头部信息,还有一些视图会隐藏它们。尽管如此,我们还是推荐你去实现headerData()函数,来为模型提供的数据提供一些相关的描述信息。
一个模型项可以有多个角色,对于指定的不同的角色要返回不同的数据。在我们自定义的模型中只有一种角色,DisplayRole,所以我们为每一项返回数据而不关心其指定的角色。但是,我们还是可以将我们为DisplayRole提供的数据用于其他角色的,比如ToolTipRole,这样,视图在一个提供框中显示一些关于当前项的信息。
可编辑的模型
上面的只读模型可以向用户展示一些简单的信息,但是,对于很多应用程序来说,一个可编辑的列表模型是更有用的。我们可以通过修改为只读模型实现的data()函数来使模型中的项可以被编辑,当然,还有提供另外两个函数:flags()和setData()。我们先将下面的函数添加到类声明中:
Qt::ItemFlags flags(const QModelIndex &index) const;bool setData(const QModelIndex &index, const QVariant &value, int role = Qt::EditRole);
代理会在创建编辑器之前检测一个模型项是否可编辑。模型必须让代理只读它的项是可编辑的。我们可以通过为模型中的每一项返回一个正确的标志来实现这个功能;在这种情况下,我们使能所有的模型项,并且使它们可以被选择和编辑:
Qt::ItemFlags StringListModel::flags(const QModelIndex &index) const{if (!index.isValid())return Qt::ItemIsEnabled;return QAbstractItemModel::flags(index) | Qt::ItemIsEditable;}
我们不需要知道代理具体是怎么处理编辑过程的。我们只需给代理提供一个方式将数据设置会模型。这是通过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;}
在这个模型中,我们使用函数提供的值来替换字符串列表中对应位置的值。但是,我们必须确保下标是有效的,项的类型是正确的,角色是受支持的。按照惯例,我们强调角色是EditRole,因为这是标准代理支持的角色。但对于boolean值来说,你可以使用Qt::CheckStateRole角色和设置Qt::ItemIsUserCheckable标志;然后,提供一个复选框来编辑数据。因为此处模型中底层数据是同一个角色,所以这些细节使它更容易与标准组件集成。
当数据被给设置后,模型必须让视图知道一些数据已经改变了。我们通过发射dataChanged()信号完成这个功能。因为此处只有一项发生了变化,所以信号携带的参数被限制为一个模型下标。
下面,来修改data()函数,加入Qt::EditRole的判断:
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());elsereturn QVariant();}
传入和删除行
模型中的行列数是可以被改变的。但在上面我们自定义的字符串模型中,只有一列,所以我们只要实现插入和删除行的函数即可。在类声明中加入下面这些函数的声明:
bool insertRows(int position, int rows, const QModelIndex &index = QModelIndex());bool removeRows(int position, int rows, const QModelIndex &index = QModelIndex());
因为行在这个模型中就对应于列表中的字符串,insertRows()函数就是想列表中指定位置前面插入一些空字符串。插入的字符串数量等于指定的行的数量。
父下标通常被用于决定这些行被插入到模型中哪个地方。在这个例子中,我们只有一个顶层列表,所以,我们只需将空字符串插入列表即可。
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()函数来通知其他组件行数将要发生变化。这个函数指定将要插入的第一行和最后一行的行号,以及它们的父下标。修改字符串列表之后,又调用endInsertRows()来完成操作和通知其他组件模型的维度发生了变化,返回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;}
Qt中视图控件
Qt的基于项的控件的名字就反应它们各自的作用:QListWidget提供了一个项的列表,QTreeWidget展示了一个多级的树形结构,QTableWidget提供了项的表格展示。每一个类都继承了QAbstractItemView类的行为,该类为项的选择和头信息的管理提供了公共行为。
列表控件
单层级的项列表通常使用QListWidget和一系列的QListWidgetItem来展示。List Widget的构建和其他控件一样:
QListWidget *listWidget = new QListWidget(this);
列表项可以在它们被构造时就直接加入列表控件:
new QListWidgetItem(tr("Sycamore"), listWidget);new QListWidgetItem(tr("Chestnut"), listWidget);new QListWidgetItem(tr("Mahogany"), listWidget);
也可以在构建列表项时不给它们指定父,而在以后的某个时候通过函数将它们插入到列表控件中:
QListWidgetItem *newItem = new QListWidgetItem;newItem->setText(itemText);listWidget->insertItem(row, newItem);
列表控件中的每一项可以显示一个文件标签和一个图标。文本的颜色和字体可以改变,从而为每一项提供一个自定义的外观。Tooltips,status tips和“What's This?”帮助也是很容易配置的:
newItem->setToolTip(toolTipText);newItem->setStatusTip(toolTipText);newItem->setWhatsThis(whatsThisText);
默认情况下,列表中的项是按它们被插入的顺序放置的。项列表可以根据Qt::SortOrder中给出的标准排序,从而产生一个按字母正序或逆序放置的列表:
listWidget->sortItems(Qt::AscendingOrder);listWidget->sortItems(Qt::DescendingOrder);
树形控件
树或层级的列表是由QTreeWidget和QTreeWidgetItem类来提供的。树形控件中的每一项都可以有它们自己的孩子,并可以显示多列的信息。树形控件的创建和其他控件一样:
QTreeWidget *treeWidget = new QTreeWidget(this);
在项被插入树形控件之前,必须先设置列数。例如,我们可以定义两列,并为每一列提供一个头信息:
treeWidget->setColumnCount(2);QStringList headers;headers << tr("Subject") << tr("Default");treeWidget->setHeaderLabels(headers);
为每一列提供信息的最简单的办法就是使用一个字符串列表。对应更复杂的头部来说,你可以创建一个树项,按你希望的方式装饰它,然后把它作为树控件的头。
树形控件的顶层项以树形控件为父控件进行创建。它们可以以任意的顺序被插入,或者你可以通过在创建每一个树项时指定父项的方式让它们按一定的顺序排列:
QTreeWidgetItem *cities = new QTreeWidgetItem(treeWidget);cities->setText(0, tr("Cities"));QTreeWidgetItem *osloItem = new QTreeWidgetItem(cities);osloItem->setText(0, tr("Oslo"));osloItem->setText(1, tr("Yes"));QTreeWidgetItem *planets = new QTreeWidgetItem(treeWidget, cities);
树形控件处理顶层项和其他更深层的项有一点不同。项可以从树的顶层被删除通过调用树形控件的takeTopLevelItem(),但更底层的项是通过调用它们父项的takeChild()函数来删除的。顶层的项是用过insertTopLevelItem()函数插入的,更底层的项是通过它们的父项的insertChild()来插入的。
我们可以很容易的删除树形控件中的顶层项和底层项。只需要判断一下这个项是否是顶层项即可,而这个信息可以由每一个项的parent()函数提供。例如,下面的代码从树控件中删除一项:
QTreeWidgetItem *parent = currentItem->parent();int index;if (parent) {index = parent->indexOfChild(treeWidget->currentItem());delete parent->takeChild(index);} else {index = treeWidget->indexOfTopLevelItem(treeWidget->currentItem());delete treeWidget->takeTopLevelItem(index);}
在树形控件中的某个位置插入一项也是如此:
QTreeWidgetItem *parent = currentItem->parent();QTreeWidgetItem *newItem;if (parent)newItem = new QTreeWidgetItem(parent, treeWidget->currentItem());elsenewItem = new QTreeWidgetItem(treeWidget, treeWidget->currentItem());
表格控件
表格控件是由QTableWidget和QTableWidgetItem类提供的。这些类提供了一个滚动的保护头信息和项的表格控件。
表格控件可以使用一个行数和列数作为参数来创建:
QTableWidget *tableWidget;tableWidget = new QTableWidget(12, 3, this);
表格中项的创建如下:
QTableWidgetItem *newItem = new QTableWidgetItem(tr("%1").arg(pow(row, column+1)));tableWidget->setItem(row, column, newItem);
水平的和垂直的头也可以在表格外创建,然后把它们用作头部信息:
QTableWidgetItem *valuesHeaderItem = new QTableWidgetItem(tr("Values"));tableWidget->setHorizontalHeaderItem(0, valuesHeaderItem);
注意,表格中的行和列从0开始。
公共特性
在这些基于项的控件中有一些通用的基于项的特性可以使用同一个接口来获得。我们下面来具体看一看。
隐藏项
有时候在一个视图控件中隐藏一个项比把它们从控件删除要好的多。上面提到的控件的项都可以被隐藏和重新显示。可以使用isItemHidden()函数来判断一个项是否被隐藏了;可以使用setItemHidden()来隐藏一个项。
因为这个操作是基于项的,所以上面提供的三个类都有这个接口。
选择
项被选中的方式由控件的选择模式控制(QAbstractItemView;:SelectionMode)。这个属性控制着用户是否可以选中一个或多个项,以及在多项选择中,被选中的项是否必须连续。上面提到的三个类有同样的选择模式。
控件中被选中的项可以通过selectedItems()函数获取到,其返回一个可被迭代的包含相关项的列表。例如,我们可以使用下面的代码来计算选中项的数据的总和:
QList<QTableWidgetItem *> selected = tableWidget->selectedItems();QTableWidgetItem *item;int number = 0;double total = 0;foreach (item, selected) {bool ok;double value = item->text().toDouble(&ok);if (ok && !item->text().isEmpty()) {total += value;number++;}}
注意,对应单选模式来说,当前项会在选集中。对应多先模型和扩展选择模式,当前项可能不在选集中,取决于用户组织选项的方式。
搜索
无论是对于开发者和是对于用户来说,可以在视图控件中查找某一项是一个很重要的功能。上面的三个控件类都提供了一个通用的方法findItems()来完成这个功能。
控件中的行通过它们包含的文本被搜索到,当然还要在调用findItems()函数时传入一个特定的匹配标准。该函数会返回一个满足条件的项组成的列表:
QTreeWidgetItem *item;QList<QTreeWidgetItem *> found = treeWidget->findItems(itemText, Qt::MatchWildcard);foreach (item, found) {treeWidget->setItemSelected(item, true);// Show the item->text(0) for each item.}
上面的代码会使树控件中包含特定文件的项被选中。这种模式也被用于列表和表格控件。
在视图中应用拖放功能
模型/视图框架完全支持Qt的拖放功能。列表,表格和树形控件中的项可以在视图中被拖动,数据可以被作为MIME-encoded数据来导入或导出。
标准视图内部自动的支持拖放,它们的项可以移动来改变它们被显示的顺序。默认情况下,这些视图的拖放功能未被启用,因为它们被配置为最简单,最通用的控件。为了使项可以被拖动,视图的某些属性需要被启用,并且项本身也必须支持拖动。
视图控件中的拖放
默认情况下,QListWidget,QTableWidget和QTreeWidget所使用的每一个项的类型被配置为使用不同的标志。例如,每一个QListWidgetItem或QTreeWidgetItem初始条件下是使能的,可以选中的,可以选择的,也可以做为一个拖放操作的源;每一个QTableWidgetItem是可以被编辑的,可以作为拖放操作的目标的。
尽管所有的标准项具有一个或两个标志被设置用于拖放,你通常还需要设置视图本身的多个属性,以此来利用内建的拖放支持:
- 使项可以拖动,设置视图的dragEnabled属性。
- 使用户可以在视图上放置一个内部或外部的项,设置视图的viewport()的acceptDrops属性为真。
- 为了给用户展示当前被拖动的项将被放在哪里,设置视图的showDropIndicator属性。这会向用户提供项被拖动的项在视图中的位置的实时信息。
QListWidget *listWidget = new QListWidget(this);listWidget->setSelectionMode(QAbstractItemView::SingleSelection);listWidget->setDragEnabled(true);listWidget->viewport()->setAcceptDrops(true);listWidget->setDropIndicatorShown(true);
上面代码的执行结果是,列表控件允许其中的项在 视图中被复制,甚至允许用户在相同类型的数据的视图之间拖动项。在这两种情况中,项是被复制而不是被移动。
listWidget->setDragDropMode(QAbstractItemView::InternalMove);
模型/视图类中的拖放
QListView *listView = new QListView(this);listView->setSelectionMode(QAbstractItemView::ExtendedSelection);listView->setDragEnabled(true);listView->setAcceptDrops(true);listView->setDropIndicatorShown(true);
因为视图展示的数据是由模型控制的,所以我们还要让所使用的模型支持拖放操作。这可以通过重新实现QAbstractItemModel::supportedDropActions()。例如,可以使用下面的代码启用复制和移动操作:
Qt::DropActions DragDropListModel::supportedDropActions() const{return Qt::CopyAction | Qt::MoveAction;}
尽管可以提供Qt::DropActions中的任意组合,但模型必须支持所提供的所有组合。例如,为了在模型中正确的使用QtMoveAction标志,模型必须提供QAbstractItemModel::remveRows()方法,无论是直接实现还是间接的从基类继承。
启用模型项的拖放
Qt::ItemFlags DragDropListModel::flags(const QModelIndex &index) const{Qt::ItemFlags defaultFlags = QStringListModel::flags(index);if (index.isValid())return Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled | defaultFlags;elsereturn Qt::ItemIsDropEnabled | defaultFlags;}
注意,项可以被放置到模型的顶层,但是只要有效的项才能被拖动。在上面的例子中,因为模型是从QStringListModel派生下来的,所以我们先调用了基类的flags()方法来获得默认的标志集合。
QStringList DragDropListModel::mimeTypes() const{QStringList types;types << "application/vnd.text.list";return types;}
该模型还必须提供广告格式的代码编码数据。这是通过实现QAbstractItemModel::mimeData()函数完成的,该函数会返回一个QMimeData对象。
QMimeData *DragDropListModel::mimeData(const QModelIndexList &indexes) const{QMimeData *mimeData = new QMimeData();QByteArray encodedData;QDataStream stream(&encodedData, QIODevice::WriteOnly);foreach (const QModelIndex &index, indexes) {if (index.isValid()) {QString text = data(index, Qt::DisplayRole).toString();stream << text;}}mimeData->setData("application/vnd.text.list", encodedData);return mimeData;}
因为该函数接受一个下标列表,所以该函数可以应用于层级的和非层级的模型中。
项模型中插入拖放的数据
bool DragDropListModel::canDropMimeData(const QMimeData *data,Qt::DropAction action, int row, int column, const QModelIndex &parent){Q_UNUSED(action);Q_UNUSED(row);Q_UNUSED(parent);if (!data->hasFormat("application/vnd.text.list"))return false;if (column > 0)return false;return true;}bool DragDropListModel::dropMimeData(const QMimeData *data,Qt::DropAction action, int row, int column, const QModelIndex &parent){if (!canDropMimeData(data, action, row, column, parent))return false;if (action == Qt::IgnoreAction)return true;
对于一个简单的单列字符串列表模型来说,如果提供的数据不是纯文本,或者给定的列号无效,就代表拖放无效。
int beginRow;if (row != -1)beginRow = row;
我们先检查所提供的行号,看我们是否可以使用它来讲相应项插入到模型中,而不关心父项的下标是否有效。
else if (parent.isValid())beginRow = parent.row();
如果父项的下标是有效的,那就代表该次放置发生在一个现存的项上。在这个简单列表模型中,我们找出项的行号,以此来将项插入到模型的顶层。
elsebeginRow = rowCount(QModelIndex());
当放置操作发生在视图的其他地方时,并且行号不可用,我们就将该项追加到模型的顶层项中。
QByteArray encodedData = data->data("application/vnd.text.list");QDataStream stream(&encodedData, QIODevice::ReadOnly);QStringList newItems;int rows = 0;while (!stream.atEnd()) {QString text;stream >> text;newItems << text;++rows;}
接下来,将这些字符串插入到底层的数据存储中。出于一致性,可以使用模型自己的接口来完成这件事:
insertRows(beginRow, rows, QModelIndex());foreach (const QString &text, newItems) {QModelIndex idx = index(beginRow, 0, QModelIndex());setData(idx, text);beginRow++;}return true;}
注意,模型通常也需要实现QAbstractItemModel::insertRows()和QAbstractItemModel::setData()函数。
QSortFilterProxyModel *filterModel = new QSortFilterProxyModel(parent);filterModel->setSourceModel(stringListModel);QListView *filteredView = new QListView;filteredView->setModel(filterModel);
因为代理模型也是从QAbstractItemModel类继承而来,所以它们可以连接到任意视图上,也可以在视图间共享。它们也可以被用来处理经由管道传来的其他代理模型的信息。
- filterAcceptsColumn() 被用于从源模型中过滤特定的列
- filterAcceptsRow() 被用于从源模型中过滤特定的行
Qt Model/View编程介绍相关推荐
- QT Model/View 编程:MVC模型视图编程:实例实现(二)
目录 样例001:现有模型中使用视图Using views with an existing model 样例002:使用模型索引 样例003:使用模型 样例004:使用模型的多个视图 样例005:委 ...
- Qt Model/View教程
修正版已转移到 Qt中文文档 目录 修正版已转移到 [Qt中文文档](https://www.qtdoc.cn/Src/M/Model_View_Tutorial/Model_View_Tutoria ...
- Qt Model/View 学习(4) - 实现自己的QAbstractTableModel类(支持显示与修改)
目录 0. 前言 1. Data设计 2. Model类设计 2.1 数据显示与对齐.字体修改 2.2 数据修改 3. 小结 0. 前言 可算到了这一篇了! 上一篇文章中把Qt::ItemDataRo ...
- Qt Model/View(MVD)模型分析
最近在看Qt的Model/View Framework,在网上搜了搜,好像中文的除了几篇翻译没有什么有价值的文章.E文的除了Qt的官方介绍,其它文章也很少.看到一个老外在blog中写道M ...
- (一) Qt Model/View 的简单说明
目录: (一) Qt Model/View 的简单说明 .预定义模型 (二)使用预定义模型 QstringListModel例子 (三)使用预定义模型QDirModel的例子 (四)Qt实现自定义模型 ...
- Flask-admin Model View字段介绍
Model View字段介绍 can_create = True 是否可以创建can_edit = True 是否可以编辑can_delete = True 是否可以删除list_template = ...
- Qt Model/View 学习笔记 (四)
创建新的Models 介绍 model/view组件之间功能的分离,允许创建model利用现成的views.这也可以使用标准的功能 图形用户接口组件像QListView,QTableView和Q ...
- Qt Model/View/Delegate浅谈 - QAbstractListModel
为什么80%的码农都做不了架构师?>>> 待补充... ##子类化 当子类化QAbstractListModel时,必须提供rowCount()和data()这2个函数的实现, ...
- 【QT Model/View】QTableView中使用委托实现表格中插入箭头
一.应用场景 在QTableView表格中,右键插入一行数据,需要在表格上标记待插入的位置,插入完成后标记消除 二.源码实现 箭头代理继承QItemDelegate,重写paint事件,画出箭头形状 ...
最新文章
- Codeforces Round #698 (Div. 2) D. Nezzar and Board(一步步推出来,超级清晰,不猜结论,看不懂来打我 ~ 好题 )
- python画动态爱心-使用Python画出小人发射爱心的代码
- 如何处理数据中心电缆管理问题?
- c语言24点游戏流程图,C语言解24点游戏程序
- ARMV7,ARMV8
- centos下安装PHP的IDE,如何在 CentOS 8 上安装和使用 PHP 编辑器
- 实现Linux select IO复用C/S服务器代码
- 安装php7的mysql扩展,php7安装mysql扩展的方法是什么
- python 与_Python基础 第一个 Python 程序
- Python库collections中的计数器(Counter)
- Java常用加密解密算法全解
- oracle mysql sqlserver对比_Mysql、Oracle、SqlServer的JDBC连接实现和对比(提供驱动包)...
- ICC 图文学习——LAB2:Design Planning 设计规划
- 程序员常用mysql命令
- Qml 中用 Shader 实现圣诞树旋转灯
- 计算机课是怎样查出勤的,基于计算机视觉技术的课堂自动考勤系统研究
- php添加浮动广告,漂浮广告是什么?漂浮广告如何设置
- 传说她是上海大学校花
- 马未都的《量力而行》有感
- matlab验证对称三相电路,实验四period;三相交流电路 - 范文中心
热门文章
- 【课程汇总】OpenHarmony全场景Demo数字管家系列课(附链接)
- STM32按键的检测IO口上拉下拉电阻
- Halcon 算子 gen_disc_se
- Unity编辑器内搜索小技巧
- php制作万年历的步骤_PHP制作万年历_php实例
- 老毛桃PE系统安装篡改主页3456.com和强制安装绿色浏览器lvseie.exe
- 普通计算机硬件cpu,DIY硬件搭配技巧:组装电脑如何搭配CPU和显卡才最合适?
- 学会Linux,看完这篇就行了!
- 动态word文档 下载
- 基金考试可以用计算机吗,2021年基金从业机考常见问题,计算器可以带吗?