文章目录
1 “模型/视图”的介绍及其逻辑
1.1 “模型/视图”介绍
Qt包含一套项目视图类,我们可以用一个“模型/视图”的架构来管理数据以及数据展示给用户的方式。将数据和显示分开的架构方式给了开发者在定制项目的显示过程中足够的灵活性。同时Qt还提供了一个标准的模型接口,此接口允许多种数据源为现有的视图提供数据。在本文中,我们提供了一个简短的“模型/视图”的范例,对此思想的概述,还描述了项目视图系统的架构,在架构中的每一个部分都会解释,给出的例子也展示了如果使用这些提供的类
1.2 “模型/视图”架构
模型-视图-控制(Model-View-Controller)(MVC)是一个源于 Smalltalk的设计模式,它通常用来搭建用户结构的时候使用,在《设计模式》一书中,Gamma et al. 说到:
MVC包含3种对象,模型是应用程序对象,视图是模型的对外显示,控制定义了用户接口,和操作输入的接口,在MVC之前,用户接口的设计趋向于将这些对象混合在一起,MVC通过将这些对象解耦合来提高它们的灵活性和复用性
如果视图和控制对象是合在一起的,那么结果将是“模型-视图”架构,不过这仍然实现了数据内容和数据显示对用户分开的架构,实现了思想相似但是实现更简单的方法,这样的分离让同样的数据通过不同的方式来显示出来变为可能,而且可以通过不改变数据源的情况下实现新的显示方法,为了对用户的数据输入有更灵活的操作方法,我们引入了“代理”(delegate)的概念,架构中拥有代理的概念的优势是可以自定义的进行数据的编辑还有显示
1.2.1 架构介绍
模型对象负责与数据源进行通信,并且为架构中的其他组件提供访问接口,沟通的方式取决于数据源的类型还有模型实现的方式。
视图获得模型的索引,相当于对数据源的引用,通过向模型提供模型索引,视图可以从数据源中检索数据。
在标准的视图中,代理负责渲染(render)数据源中的项目,当一个项目在编辑的时候,这个代理会使用模型的索引直接和模型通讯。
通常来讲,“模型-视图”类可以被拆分成3组:模型,视图还有代理,这三个部分都是通过虚基类来提供公共的接口,然后通过重写这些接口来实现的。不过在一些情况中是通过默认的特性类实现的。抽象类意味着需要子类实例化,这样做的目的是为了给其他组件提供完整的访问,这需要写专门的组件。
模型,视图还有代理类通过信号槽来实现相互通讯
- 模型的信号会告诉视图数据的数据源所变化的数据
- 视图的信号包含和用户交互的信息
- 代理的信号告诉模型和视图有关于编辑器的状态
1.2.2 模型
所有的模型都是 QAbstractItemModel 的子类,这个类定义了视图和代理访问数据的接口,数据本身不会储存在模型中,数据可以存放在一个数据结构中,或者在其他类中,在文件中,数据库中,或者其他的应用程序中。
关于模型的基本概念在章节Model Classes中会详细描述。
QAbstractItemModel 的接口很灵活,可以处理表格,列表,和树状列表中的数据,然而当实现新的专门针对于列表和类似表格的数据结构的时候,QAbstractListModel和QAbstractTableModel类是更合适的,因为他们的对相同的函数有更专门的实现,这些类都可以重载,可以为更专门的列表类数据和表格类数据提供更专业的操作方法
在章节 Creating New Models 中将会讨论如何来构建子类
Qt提供了一些现成的模型类,我们可以直接用他们来操作数据
- QStringListModel可以用来储存简单的QString项目
- QStandardItemModel可以管理复杂的树状结构的项目
- QFileSystemModel提供了一些关于文件和目录的信息
- QSqlQueryModel, QSqlTableModel 和 QSqlRelationlTableModel可以用放来访问数据库
如果这些现成的类不能满足你的需求,你可以通过派生QAbstractItemModel和QAbstractListModel或者QAbstractTableModel来创造你自己定制的模型
1.2.3 视图
我们为不同类型的视图提供了不同的实现,QListView用来显示列表项目,QTableView可以显示表格化的数据,QTreeView可以展示多重列表的数据,这些类都是通过继承QAbstractItemView的虚基类来实现的,尽管这些来都可以直接使用,但是他们都可以通过派生子类的方式来提供定制化的视图
可用的视图将会在章节View Classes中解释
1.2.4 代理
QAbstractDelegate是模型-视图架构中代理的虚基类,默认的代理的实现方法在QStyleItemDelegate中提供的,并且他是用在Qt的标准视图中的。然而,QStyleItemDelegate是QItemDelegate在视图中绘制和提供编辑器的替代方法,他们之间的不同是QStyleItemDelegate用当前的风格来绘制项目,因此,我们建议在实现自定义委托或使用Qt样式表时使用QStyledItemDelegate作为基类。
代理将在章节Delegate Classes中详细描述
1.2.5 排序
“模型/视图” 架构中有两种进行排序的方式,选择哪种方式取决于你使用的模型
如果你的模型是可以排序的,比如:如果你重写了 QAbstractItemModel::sort() 函数,所有的QTableView和QTreeView都会提供一个API,允许你以编程的方式来排序你的模型数据。除此之外,你可以使能交互式的排序(比如允许用户通过点击视图的头的方式来排序数据),通过连接QHeaderView::sortIndicatorChanged() 信号和 QTableView::sortByColumn()槽函数或者QTreeView::sortByColumn()槽函数
另一种方式是,如果模型没有需求接口,或者你想去使用列表视图来显示你的数据,需要在模型显示数据之前用代理模型来传递模型的结构,详细的在章节 Proxy Models中描述
1.2.6 便利类
Qt提供了一些由标准视图类派生出来的便利类,方便应用程序的依赖项目视图和表类,他们不打算被子类化。
这些类分别是QListWidget,QTreeWidget和QTableWidget
这些类相对视图类缺乏灵活性,而且不能用于任意的模型。
我们推荐你使用模型-视图的操作数据的方法除非你强烈需要一个项目的类
如果你想充分利用模型/视图的特性,同时仍然想使用一个基于项目的接口,考虑使用视图类,比如将QListView, QTableView,和QTreeView和QStandardItemModel接合起来
2 使用模型和视图
下面的章节将展示如何在Qt中使用模型/视图的模式,每一个章节都包含一个例子
2.1 Qt中的两个模型
Qt中提供了两个标准模型,分别是QStandardItemModel和QFileSystemModel,QStandardItemModel是一个多功能的模型,可以用来代表各种不同的数据结构,比如列表,表格,树表。这些模型同时拥有数据的项目。QFileSystemModel是一个维护有关目录的信息。结果就是,它本身自己不需要储存数据本身,只是简单的代表本地文件系统的文件和路径。
QFileSystemModel提供了开箱即用的模型,而且可以根据现有的数据很快的配置,通过这个模型,我们可以展示展示如何在一个视图中设置这个模型,而且解释如何使用模型的索引来操纵数据。
2.2 通过现成的模型来使用视图
QListView和QTreeView类是两个最合适的,用来展示如何使用QFilesystemModel的视图,下面的例子将展示一个放置文件目录的树状列表,并在旁边的列表中显示相同的信息。视图中将高亮显示用户选中的项目。
我们设置了QFileSyetemModel为可以用的状态,并且创造了一些视图来显示文件目录,下面展示了最简单的使用一个模型的方法,下面使用main()函数来展示最简单的使用这个模型的方式
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
QSplitter *splitter = new QSplitter;
QFileSystemModel *model new QFileSystemModel;
model->setRootPath(QDir::currentPath())
到此的话,此模型就就从本地的文件系统的获取到数据了,你调用了函数 setRootPath() 来告诉模型要向视图来展示哪个驱动器
我们创造两个视图,这样的话我们就可以用两种不同的方式来展示模型了
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()));
这个视图被另一个窗口用同样的方式来构造,设置视图要显示的窗体也仅仅是调用函数setModel(),我们通过调用函数setRootIndex()来过滤模型提供的数据,从文件系统中传递当前模型的路径
本例中使用的函数index()是QFileSystemModel独有的,我们提供给这个函数一个路径,然后这个函数返回一个索引,模型的索引在Model Classes中被讨论
剩下的事情仅仅是用分割视图来显示视图,并且开启应用程序的事件循环
splitter->setWindowTitle("Two views onto the same file system model");
splitter->show();
return app.exec();
这一节中,我们忽略了如何处理选中的项目,我们将在章节Handling Selection in Item Views中介绍
3 模型类
在介绍如何操作数据之前,你会发现介绍模型/视图框架中的概念是挺重要的
3.1 基本概念
在模型/视图架构中,模型负责提供一个视图和代理可以访问数据的标准接口,这个标准接口是定义在类 QAbstractItemModel中的,你不需要关系数据是如何储存在数据结构中的,所有QAbstractItemModel的子类会将数据表示成具有层状的结构,视图通过约定来访问模型中的数据,但是它不仅限于信息展示的功能
模型同时通过信号和槽的方式告诉视图数据源的任何变化
本章节描述了一些基本的概念,它是其他组件访问模型类的核心,后面将讨论更多的概念
3.1.1 模型的索引
模型/视图介绍了数据的和显示分离的概念,可以通过模型索引来找到模型的所有的数据,视图和代理使用这些索引来请求数据并展示
结果就是,只有模型需要知道原始的数据,模型中的数据就我们后面再详细讨论。模型索引包含一个指向模型的指针,来确保在使用多个模型的时候不会产生混淆。
QAbstractItemModel *model = index.model();
模型索引提供了对数据片段的临时引用,而且你可以使用它来检索和修改数据。尽管模型会时不时地重组它内部的数据结构,模型索引也会失效,而且索引是不能被储存的,如果需要对信息片段进行长时间的引用,需要创造一个常模型指针,它将会随着模型的更新保持对该模型最新的引用,临时的模型索引是由类QModelIndex提供的,然后常模型指针是由类QPresistentModelIndex类提供的
为了获得对应一个数据项的模型索引,需要指定3个特性,一个行数,一个列数,以及父项模型的索引,下面的章节将描述这三个特性。
3.1.2 行和列
在最基本的形式中,模型可以作为一个简单的表来访问,其中的项目是由行和列数来放置的,这不意味着底层的数据是在一个数组结构中储存的,我们使用行数和列数仅仅是一个组件相互通信的约定,我们可以通过给定的行和列来索引项目中的信息,并且我们可以通过下面的方式来获取索引
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()来参考,这个我们后面再讨论
3.1.3 项目的父项
模型所提供的类表格的接口在使用表格或列表视图的时候是理想的,行号和列号直接索引的系统是视图显示项目的精确方式。然而,比如树状视图的结构要求模型提供更灵活的接口,结果就是,每一个项目有时候是另外一个表格项目的父对象,与树状列表中的一个顶层项目可以包含一个列表的形式非常类似
当对一个模型项目请求一个索引的时候,我们必须提供有关于项目父类的信息,在模型之外,唯一的引用模型数据的方式是通过模型索引,所以父模型一定按照如下方式给定
QModelIndex index = model->index(row, column, parant);
3.1.4 父项,行和列
上面的架构图中展示了树状列表的代表,其中每一个项目都由父项所引用,一个行数,一个列数
项目“A”和“C”表示为模型的顶层访问
QModelIndex indexA = model->index(0, 0, QModelIndex());
QModelIndex indexB = model->index(2, 1, QModelIndex());
项目“A”有很多子项目然后项目“B”是由如下的代码来访问的
QModelIndex indexB = model->index(1, 0, indexA);
3.1.5 项目规则
模型中的项目可以可以指定不同的规则,这样允许不同的数据的类型可以根据不同的情况来应用,举个例子,Qt::DisplayRole用于设置可以在视图中显示文本的字符串,典型的情况是,项目包含数据和一系列的规则。标准的规则在Qt::ItemDataRole中定义
我们可以通过向模型类传递特定索引的方式来向模型请求特定的数据,并且制定我们想要指定的规则
QVariant value = model->data(index, role);
项目向模型指示其所引用的数据数据类型,所以为每一个规则提供适当的信息是非常有必要的
章节 Creating New Models 详细的指定规则的用法
强烈建议项目的数据使用Qt::ItemDataRole中所定义的标准规则,通过给每一个数据施加合适的规则,模型可以向视图和代理提供项目如何向用户展示的提示,不同的视图类型可以自由的根据需要来选择使用或忽略这些信息,特殊的应用程序可以自定义附加的规则
3.1.6 总结
- 模型索引给视图和代理有关于模型项目的位置信息,无论数据结果是什么样子
- 项目是通过他们的行数和列数还有他们父项项目的模型索引来引用的
- 模型的索引是由其他组件发出请求的时候有模型构造的,其他组件比如视图和代理
- 如果一个有效的模型索引是使用函数index()来发出请求的,而且这个索引是指定其父项的,那么这个索引将引用父项目下面的一个子项目
- 如果模型的索引是无效的,那么使用函数Index()的返回值是该父项目的顶层项目
- 规则区分项目的不同类型的数据
3.2 使用模型索引
为了演示数据是如何在模型中被检索到的,我们使用模型索引,我们设置了一个QFileSystemModel,但是不设置视图,尽管没有展示模型的正常方式,这部分仅仅展示了如何处理模型索引
我们按照如下的方式构造了一个文件系统模型
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);
为了获取模型的索引,我们指定了行数,列数(第一列的列数为0),还有适当的所有我们想要的父项目的索引,储存在每个项目中的文本可以使用函数data()来查找,我们指定了模型索引和DisplayRole通过字符串的形式来获取项目的数据
QString text = model->data(index, Qt::DisplayRole).toString();
// Display the text in a widget.
}
上面的例子展示了从模型中检索数据的基本原则:
- 模型的规模可以通过函数rowCount()和columentCount()来获取,这些函数通常需要一个父模型的索引来指定
- 模型索引用来访问模型中的项目,行,列和父模型索引需要指定项目
- 如果需要访问模型的顶层项目,需要使用函数QModelIndex()来指定一个空的模型索引
- 项目们包含数据和不同的规则,为了获取指定规则的数据,模型索引和规则必须提供给模型
3.2.1 拓展阅读
新的模型可以通过重写QAbstractItemModel的标准接口函数来创造。这些会在章节 New Model section 中介绍
4 视图类
4.1 基本概念
在“视图/模型”架构中,视图从模型中获取项目的数据,然后展示给用户,展示数据的方式不需要像模型提供数据那样来实现,而可能和数据储存结构的理解完全不同。
内容和界面分离的实现是通过使用QAbstractItemModel提供的标准接口来实现的,标准的视图接口是由QAbstractItemView来提供的,常规的方法是使用模型索引来访问数据项目,视图典型的管理一个从模型中获取的数据的整体的布局,他们可能渲染个别的数据,或者使用代理来操作渲染和编辑特性
和显示数据类似,视图操纵项目之间的导航,项目选择等方面,视图也实现了基本的用户接口,比如环境菜单和拖拽等,一个视图可以提供对项目的默认的编辑工具,或者通过代理来提供定制的编辑器
视图可以不通过一个模型来构造,但是在视图显示之前必须提供有效的信息,视图通过使用可分别为每个视图维护或在多个视图之间共享的选项来跟踪用户选择的项。
一些视图,比如QTableView和QTreeView可以像项目一项显示头,他们同样是通过一个视图类QheaderView来实现的,头通常访问视图所使用的模型,它使用QAbstractItemModel::headerData()来检索数据,而且通常在标题的区域显示头信息,新的头可以从QHeaderView中创造子类,来提供更多指定的标题和视图
4.2 使用现成的视图
Qt提供了三个开箱即用的视图类可以展示模型中的数据,这是用于绝大多数的用户,QListView可以显示显示的模型,或者以图标的方式,QTreeView可以显示树状的多重列表,可以实现嵌套的结构。QTableView可以通过表格的形式来显示项目,比较像电子表格软件。
上面展示基本形式的视图够一般的应用程序使用了,他们提供了基本的编辑设备,然后可以根据指定的用户接口来定制化
4.2.1 使用模型
我们使用字符串列表模型来创造范例数据,我们设置了一些数据,然后构造了一个视图来显示模型的内容,它可以使用下面的代码来创建
int main(int argc, char *argv[])
{
QApplication app(argc, argv);
// Unindented for quoting purposes:
QStringList numbers;
numbers << "One" << "Two" << "Three" << "Four" << "Five";
QAbstractItemModel *model = new StringListModel(numbers);
我们注意到 StringListModel 是作为QAbstractItemModel来声明的,所以在模型中使用虚接口是可行的,而且还能够确保代码工作正常,即使我们使用不同的模型
由QListView提供的列表视图是足够展示字符串模型的,我们构建视图并且使用下面的代码来设置模型。
QListView *view = new QListView;
view->setModel(model);
视图通过下面的方式来展示
view->show();
return app.exec();
}
视图渲染了模型的内容,通过模型的接口来访问数据,当用户尝试去编辑他们的时候,视图使用默认的代理提供了编辑窗口
上面的图片展示了QListView是如何展示字符串列表模型的数据的,一旦模型是可编辑的,视图将会自动的允许列表中的每一个项目可使用默认的代理来编辑
4.1.2 一个模型使用多个视图
为一个模型提供多个视图是很简单的,只需要为每一个视图设置相同的模型,下面的代码我们创造了两个表格视图,每一个都使用我们在本例中创造的简单的模型。
QTableView *firstTableView = new QTableView;
QTableView *secondTableView = new QTableView;
firstTableView->setModel(model);
secondTableView->setModel(model);
在模型/视图架构使用信号槽意味着模型的改变可以发送到其所有应用的窗体里面,这样可以确保我们可以访问同样的数据源,而和用什么视图没有关系
上面的视图展示了应用相同模型的两个不同的视图,每一个都包含一些选中的项目,尽管模型的数据直接通过视图来显示,但是每个视图还是保持着它自己的选择的模型,通常来将这对很多应用程序是很有用的,但是也有很多应用程序需要两个视图共享选择模型
4.2 操作选中的项目
操作视图中的项目的机制是由类QItemSelectionModel类提供的,所有标准的视图默认情况下都会构造它自己所选择的模型,并用正常的方式来实现内部的互动,视图所使用的选择的模型可以通过函数selectionModel()来获取,一个替代可选择模型可以被函数setSelectionModel()来指定,当我们想为相同模型数据提供多个视图的时候,控制选中的模型的能力是很有用的。
通常来说,除非你想创建一个模型或视图的子类,你不需要直接操作你选中的内容,然而,选中模型的接口是可以访问的,如果需要的话,这部分将在章节 Handing Selections in Item Views 中介绍
4.2.1 共享视图中选中的区域
尽管默认情况下视图类提供他们自己选择的模型是很方便的,不过有的时候,当我们对一个模型使用多个视图的时候通常需要考虑到所有模型的数据和用户选择的部分可以在所有的模型中同步显示,尽管视图类允许他们内部所选择的模型可以更换,我们可以使用下面的代码来实现不同的视图之间所选择的区域共享
secondTableView->setSelectionModel(firstTableView->selectionModel());
这样的话两个视图就相当于操作同一个选中的模型了,这样可以保持两个数据和所选择的项目同步
上面的例子中描述了相同数据结构的模型在相同的视图类型中的展示方式,然而,如果使用两个不同类型的视图,选择的区域可能会有明显的不同。举个例子在表格视图中的相邻的选中区域,换做在树状视图中可能是连续的选中区域
5 代理类
5.1 介绍
和模型-视图-操作模型不同,模型/视图的初衷不会包含完全分离的组件来管理用户的组件。通常来说,视图负责向用户展示模型的数据,还需要处理用户的输入,为了允许在获取用户输入的过程中有一些灵活性,所以这些交互是由代理来提供的,这些组件提供了输入能力,同时也负责呈现个别的项目,标准的用来控制代理的接口在QAbstractItemDelegate类中定义。
代理希望可以有能力来渲染他们自己的内容,这部分通过重写paint()函数和sizeHint()来实现的,然而简单的,基于窗体的代理可以通过派生QItemDelegate,而不是通过QAbstractItemDelegate来实现,并利用这些函数的默认实现。
代理的编辑控件也可以通过通过使用窗体小部件来管理编辑过程,或者是通过事件直接驱动来实现,第一个实现的方法在这章中的后面来实现,它同时在范例 SpinBoxDelegate中展示
范例 Pixielator 展示了如何创造一个定制的代理可以执行表格视图的渲染
5.2 使用一个现成的代理
Qt提供的标准的视图使用QItemDelegate实例来提供编辑工具,默认的代理接口的实现是每个样式的标准实现:QListView,QTableView,QTreeView
所有的标准规则是通过标准视图的默认代理所操作的,这种方式将在章节QItemDelegate中实现的
视图使用的代理是由函数itemDelegate()返回的,函数setItemDelegate()允许你为标准视图安装一个定制的代理,而且在为一个定制的视图设置代理的时候,是有必要设置这个函数的
5.3 一个简单的代理
这里我们使用QSpinBox为代理提供编辑能力,它主要是提供给用户一个简单的用来显示数字的窗口,尽管我们的目的是设置一个定制的基于整数的表格模型,我们可以简单的使用QStandardItemModel来代替,因为使用自定义委托来显示数据项。我们构造了一个表格视图来展示模型的内容,然后这将使用定制的委托用于编辑。
我们从QStyleItemDelegate中派生出子类,因为我们不想去自定义显示的函数,然而我们仍然提供了来管理编辑窗体的函数
class SpinBoxDelegate : public QStyledItemDelegate
{
Q_OBJECT
public:
SpinBoxDelegate(QObject *parent = 0);
QWidget *createEditor(QWidget *parent, const QStyleOptionViewItem &option,
const QModelIndex &index) const override;
void setEditorData(QWidget *editor, const QModelIndex &index) const override;
void setModelData(QWidget *editor, QAbstractItemModel *model,
const QModelIndex &index) const override;
void updateEditorGeometry(QWidget *editor,
const QStyleOptionViewItem &option, const QModelIndex &index) const override;
};
不过在代理没有构造之前,我们没有任何编辑窗体,下面我们需要的就是在需要的时候构建编辑窗体
5.3.1 提供一个编辑器
在这个例子中,当表格窗体需要提供一个编辑器的时候,它要求代理提供一个合适的窗口小部件,函数createEditor()在任何需要设置合适的窗口部件的时候都会被调用:
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;
}
注意到现在我们不需要保持一个指向编辑窗体的指针因为视图在不需要它的时候负责销毁它。
我们安装了代理的默认事件过滤器,确保它提供用户期望的捷径,附加的捷径可以附加在编辑器中允许更细节的表现,这些将会在章节Editing Hints中说明
视图来确保编辑控件设置数据和格式的函数我们后面再定,我们可以依据视图提供的模型索引创造不同的编辑控件,如果我们有一列需要显示整数,有一列需要显示字符串,那么我们可以自定义他们,取决于我们需要编辑那一列,整数的使用QSpinBox,字符串的使用QLineEdit
模型必须提供一个用来复制模型数据到编辑控件中的函数,在这个例子中,我们读取在display role中的数据,因此设置spinbox中的值
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);
}
在这个例子中我们知道编辑窗体是一个SpinBox,但是我们可以为模型中的不同的数据类型提供不同的编辑控件,在这种情况下我们需要在访问成员函数前转换为相应的窗体类型
5.3.2 为模型提供数据
当我们在spinbox中完成了数值编辑后,视图将会询问代理来在模型中储存编辑好的值,通过调用函数setMoelData()
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);
}
因为视图管理委托的编辑控件,我们现在需要做的是根据编辑控件中的内容来更新模型,在这个例子中,我们确保spinbox是实时更新的,同时使用指定的索引来更新模型中的值
标准的QItemDelegate类在结束编辑后通过信号closeEditor()来告诉视图,视图确保编辑窗体是关闭的,而且被销毁,在这个例子中,我们仅仅提供了简单的编辑功能,所以我们不需要发射这个信号
所有的在数据上的操作的执行是通过QAbstractItemModel提供的接口来实现的,这确保委托基本上脱离他所操作的数据类型,但是为了使用特定的编辑控件,必须对数据类型做一些假设,在本例中,我们假设模型通常储存整型值,但是我们仍然可以使用为不同的模型使用这个代理,因为QVariant提供了意外数据的合理的默认值
5.3.3 更新编辑控件的轮廓
管理编辑控件的几何形状是代理的责任,几何形状必须在编辑控件创造的时候和在视图中的项目和尺寸变化的时候生成,幸运的是,在view option对象中视图提供了所有的几何尺寸的信息
void SpinBoxDelegate::updateEditorGeometry(QWidget *editor,
const QStyleOptionViewItem &option, const QModelIndex &/* index */) const
{
editor->setGeometry(option.rect);
}
在这种情况下,我们仅使用视图选项中的项目的矩形区域作为编辑控件的轮廓信息,代理渲染项目的时候不会直接使用项目的矩形框,而是使用一些其中的单元,它将编辑器和其他单元关联在一起
5.3.4 编辑提示
在编辑后,代理会为编辑过程的结果的其他组件提供一些提示,还会再任何随后的编辑过程中提供一些帮助提示,这个特性是通过发送信号closeEditor()和合适的提示来实现的,需要注意的是安装在spin box的默认的QItemDelegate事件过滤器将会被构建。
spin box需要重新调整,来使其更加友好,默认的事件过滤器是由QItemDelegate来提供的,如果用户敲下或者键,希望自己输入的内容被提交后,代理将会向模型提交数据,然后关闭spinbox,我们可以通过安装我们自己的事件过滤器在spinbox中来改变它的行为,还提供了一个正在编辑的提示来适应我们的需求。举个例子,我们希望发射带EditNextItem的提示信号closeEditor()来自动的跳转到下一个需要编辑的视图项目
另一个不需要提供事件过滤器的方式是提供我们自己的编辑窗体,也许为了方便可以派生QSPinBox的子类。这种方法可以在不增加代码的情况下给我们更多的控制编辑控件的行为的能力。如果你需要自定义一个标准Qt编辑器的行为的话通常简单的办法是在代理中安装一个事件过滤器
代理不会发射这些信号,但是他们不会集成在应用程序中,因为相比可以发射提示来支持常规的编辑相位的方法相比,这个方法比较鸡肋
6 在视图中操作选中的项目
6.1 概念
项目视图类中使用的选择模型提供了基于模型/视图体系结构功能的选择的一般描述,尽管操作选择的标准类对提供的项目视图来说是足够的,选择模型允许你创造指定选择的模型来满足你自己的模型视图的需求。
关于在视图中选中的项目的信息储存在一个QItemSelectionModel类的实例,这将位置模型的索引在一个单一的模型中,而且是独立于任何视图的,尽管可以有很多视图对应一个模型,但是在视图之间共享选中的区域是有可能的,允许应用程序用持续的方式来展示多个视图。
选择是由选中的区域组成的,有效的方法是记录模型的开始索引和结束索引所构成的选中区域。不连续的项目选择是通过多个选中区域来描述的。
选中是应用于一个模型的选中索引的集合,最近的项目选择应用是作为当前选中区域来识别的,有效的选中区域是可以被修改的,即使在应用程序通过确定的选中请求之后,这些我们后面再讨论
6.1.1 当前项目和选中的项目
在一个视图中,通常有两种状态-一个是当前的项目,一个是选中的项目,一个项目可以同时成为当前的项目和选中的项目,视图会确保这里经常类似键盘布局一样有一个当前的项目,比如,请求当前的项目
下面高亮的表格是当前项目和选中项目的区别
当前项目 | 选中的项目 |
---|---|
只能有一个当前项目当前项目 | 可以有多个选中的项目 |
当前的项目会通过键盘导航和鼠标点击而改变 | 项目的选择状态可以是设置或非设置,取决于有多少个事先定义的模型,比如单一选择,多重选择等等 |
当前的项目可以被编辑 | 当前项可以与锚一起使用,以指定应该选择或取消选择的范围 |
当前的项目可以通过焦点矩形类表示 | 选中项由选中区域来表示 |
当操作选中的时候,用QItemSelectionModel作为项目模型中在所有项目中选中状态的记录通常是很有用的,一旦视图设置好了模型,所收集的项目就可以被选中,放弃选中,或者他们选中的状态可以在不需要知道哪一个项目已经被选中的情况下控制选中的状态,所有选中的项目的索引可以在任何时候被查找,可以通过信号槽的机制来通知其他组件。
6.2 使用一个选中的模型
标准的视图类提供了大多数应用程序可以使用的默认选中模型,一个属于一个视图的模型可以通过视图的selectionModel()函数来获得,并使用函数setSelectionModel()来在很多视图中共享,所以新选中模型的构造函数通常来说是不需要的。
一个选中是由指定的模型和一对QItemSelection的模型索引来创造的,它使用索引来参考给定模型的项目,并作为左上和右下的的项目圈中来实现,这可以通过很多种方式来实现,每个都对当前选中的模型有不同的影响
6.2.1 选中的项目
为了证明一些选中项目的特殊特性,我们构造了一个定制的表格模型的实例,里面包含32个项目,然后在一个表格视图中打开这个模型
TableModel *model = new TableModel(8, 4, &app);
QTableView *table = new QTableView(0);
table->setModel(model);
QItemSelectionModel *selectionModel = table->selectionModel();
表格视图的默认选中模型是为了后面使用,我们不想再模型中修改任何项目,相反的是选择一些项目,然后显示在表格的左上角,为了做这些,我们需要根据左上角的项目和右下角的项目来关联模型的索引,如下所示
QModelIndex topLeft;
QModelIndex bottomRight;
topLeft = model->index(0, 0, QModelIndex());
bottomRight = model->index(5, 2, QModelIndex());
为了在模型中显示这些项目,并在表格中看到相应的更改,我们需要构造一个选中的对象,然后在选中的模型中应用:
QItemSelection selection(topLeft, bottomRight);
selectionModel->select(selection, QItemSelectionModel::Select);
使用由selection flags组合定义的命令将选择应用于选择模型。在这个情况中,flags的使用将记录选中模型的选中的对象,不管他们的选中的状态,现在结果如下所示
上面的选中的项目可以通过很多种由selection flags定义的操作方式来修改。由这种操作方式选择的选择区域,通常是比较复杂的架构,但是选择模型可以比较有效的表示它,在研究如何更新选择时,将描述如何使用不同的选择标志来操作所选的项。
6.2.2 读取选中的状态
储存在选中模型中的模型索引可以通过函数selectedIndexes()来读取,这个函数返回一个未排序的模型索引列表,我们可以通过下面的方式来遍历
QModelIndexList index = selectionModel->selectedIndexes();
QModelIndex index;
foreach(index, indexes) {
QString text = QString("(1%, 2%)").arg(index.row()).arg(index.column());
model->setData(index, text);
}
上面的代码中使用了Qt方便的foreach关键词来遍历,如果需要修改的话,由选择模型返回相应的索引。
选择模型会发射信号来表明选中区域的变化,他们通知其他组件有关整个选中区域的变化和项目模型中所聚焦的项目。我们可以连接信号selectionChanged()到一个槽函数上,当选中区域变化的时候,检查模型中的项目是选中状态还是非选中状态,这个槽函数通过两个QItemSelection对象来调用:一个包含一个索引的列表有关于新选中的项目,另一个包含被释放选中的项目。
下面的代码,我们提供了一个槽函数来接受selectionChanged()信号,然后在选中的项目中填充一个字符串,并清除没有释放选中项目的内容。
这部分代码需要注意,经过实战后发现需要在view中设置好model后view中的selectionmode才可以被连接到自定义的槽函数上。
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()信号的槽函数,并且使用这个信息提供了一个QMainWindow的状态栏的升级
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()));
}
对于这些信号,监视用户所做的选择是很简单的,但是我们也可以直接更新选择模型。
6.2.3 升级一个选中区域
选中区域的请求是由selection flags的混合提供的,定义在QItemSelectionModel::SelectionFlag中,每一个selection flag告诉选中的模型如何去升级它内部的关于选中区域的记录,或者当函数select()调用的时候,最常用的使用flag的的方式是 Select flag,它告诉了选中的模型记录指定项目是否是选中的,Toggle flag 告诉选中模型来切换指定项目的状态,来选中任何未选中的项目,和放弃选中任何选中的项目,Deselect falg 会放弃选中所有的指定的项目。
模型中的个体的项目的升级方式是通过创造一个项目的选中,然后将其应用在指定的项目中,在下面的代码中,我们应用了一个上面的table模型中的二次选中,使用 Toggle 命令来切换给定项目的选中状态。
QItemSelection toggleSelection;
topLeft = model->index(2, 1, QModelIndex());
bottomRight = model->index(7, 3, QModelIndex());
toggleSelection.select(topLeft, bottomRight);
selectionModel->select(toggleSelection, QItemSelectionModel::Toggle);
现在展示在table view中的操作的结果如下
默认情况下,选中命令仅操作指定了模型索引的个体项目,然而,用来操作选中区域的命令可以通过附加多个flag的方式来操作,下面的代码将展示如何使用Rows和Columns flags:
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 selection falgs可以使两个行和列都被选中,下面的图像显示了这两个选中的作用。
上面例子中执行的代码涉及到所有项目的选择,同样可以来清除选中区域,或者替换当前选中区域为新的
为了使用一个新的选中区域来替换当前的选中区域,通过将其他的flags和Curremt flag一起使用的方式。使用了这个falg的指令,通过调用函数select()来替换它当前的模型索引的集合。你可以通过和Clear flag命令一起使用,这样可以在你新添加新的选中区域的时候来清除所有的选中区域,这样做的效果是重置选择模型的模型索引集合。
6.2.4 选中模型中的所有的项目
为了选中模型中的所有的项目,需要创造一个可以选择模型的所有层的命令,我们通过使用top-left和bottom-right来检索所有的项目。
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()用来确定是某一项是否是某一项的父级。
7 创建新的模型
模型视图分离的功能允许可以利用现有的视图来创建模型,这个特性可以让我们使用标准的视图组件来适配不同的数据源,比如QListView,QTableView,QTreeView
QAbstractItemModel提供的接口是足够灵活,可以在层级结构中安排信息,允许数据可以按照一定的方式来插入,移除,修改,或者排序,同时也提供了拖拽的操作。
QAbstactListModel和QAbstractTableModel类提供了简单的非层次结构的接口,而且可以简单的使用一个简单的列表和表格指针。
在这一章节中,我们创造一个只读的模型,来探索基本的模型视图的原则,后面的章节中,我们采用了这个简单的模型,以便用户可以自己修改
更复杂的模型看Simple Tree Model范例
QAbstractItemModel子类的需求在ModelSubclassing Reference文档中
7.1 设计一个模型
我们为一个存在的数据结构创建了一个新的模型,这时候需要考虑需要给数据提供哪种接口,如果数据结构可以表示成列表或者表格,那么就可以创建QAbstractListModel的子类或者QAbstractTableModel的子类因为这些类可以默认实现很多功能。
然而,如果当前的数据结构仅仅可以理解成一个具有层次结构的数据结构,这就需要创建QAbstractItemModel的子类,这个特性在 Simple Tree Model的例子中。
在这个章节中,我们基于string列表实现了一个简单的模型,所以QAbstractListModel提供了一个理想的基类
无论数据结构如何,都可以使用标准的QAbstractItemModel API来指定模型,因为这个类对基本的数据结构有更底层的访问,这使得用数填充模型变得更加容易,也允许其他通用的模型视图组件使用标准的API来相互通信,下面描述的模型提供了一个定制的构造函数就是为了这个目的
7.2 只读示例模型
这里实现的模型是一个简单的,非结构化的,只读的数据模型,这个模型基于QStringListModel类,它有一个QStringList作为内部的数据源,并且实现仅仅需要的功能,为了使实现变得更简单,我们构建了QAbstractListModel的基类,因为它定义了列表的默认表现行为,并且它比QAbstractItemModel提供了更简单的接口。
在实现一个模型的时候,需要记住QAbstractItemModel不会储存任何的数据,它仅仅是为视图提供了一个访问数据的接口,一个最小的只读模型,仅仅需要实现少部分的函数,因为它有大多是接口的默认实现,下面的代码将声明这个类:
class StringListModel : public QAbstractListModel
{
Q_OBJECT
public:
StringListModel(const QStringList &strings, QObject *parent = nullptr)
: QAbstractListModel(parent), stringList(strings) {}
int rowCount(const QModelIndex &parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role) const override;
QVariant headerData(int section, Qt::Orientation orientation,
int role = Qt::DisplayRole) const override;
private:
QStringList stringList;
};
除了模型的构造函数之外,我们只需要实现两个函数:rowCount()用来返回模型的行数,row()函数根据指定的模型索引返回项目的数据
良好表现的模型还需要实现函数headerData(),用来给树状和列表视图提供他们头的显示。
如果是一个非层次的模型,我们不需要但系父子类之间的关系,如果你的模型是层次化的,我们需要实现index()和parent()函数
私有成员变量stringList存放字符串列表
7.2.1 模型的规模
我们希望模型的行数和字符串列表中的字符串的数量相同,所以我们需要实现函数rowCount()函数
int StringListModel::rowCount(const QModelIndex &parent) const
{
return stringList.count();
}
因为模型是非层次化的,我们可以安全的忽略掉父项的模型索引。默认情况下,从QAbstractListModel仅仅包含一列,所以我们不需要重写函数columnCount()。
7.2.2 模型的头和数据
对于视图中的项目,我们希望返回字符串列表中的字符串,函数data()负责根据模型索引来返回项目的数据
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());
else
return QVariant();
}
我们仅仅希望在模型的索引有效的情况下返回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 QStringLiteral("Column %1").arg(section);
else
return QStringLiteral("Row %1").arg(section);
}
同样的,我们在只有规则是我们希望的时候返回有效的QVariant,头的方向也需要根据存在的数据来返回
不是所有的视图中的项目都需要显示头,这样的话我们可能需要配置一些东西来显示他们。除此之外,建议你实现函数headerData()来提供有关模型提供的数据的相关的信息。
一个项目可以由多种规则,根据指定角色的不同给出不同的数据,我们模型中的项目只有一个规则,所以我们不考虑指定的角色来返回数据,然而,我们可以在其他规则中重用提供给DisplayRole的数据。比如ToolTipRole在视图中我们可以用来显示有关于项目的工具提示的信息。
7.3 一个可编辑的模型
在不可编辑的模型中展示了如何给用户展示简单的选择,但是在很多应用程序中,往往可编辑的模型是更常用的,我么可以通过改变函数data()来使项目可以编辑,还需要重写两个函数,flags()和setData(),下面是加在类中的函数声明:
7.3.1 使模型可编辑
无论模型是否可编辑,需要在创建编辑器前进行一个代理检查,模型必须让代理知道是否项目是可编辑的,我们通过为每一个项目返回其当前的flags来完成上面的判断,在这个例子中,我们使能所有flag来保证所有的项目都是可以可选和可编辑的
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, {role});
return true;
}
return false;
}
在这个模型中,与模型索引相关的在字符串列表中的项目被给定的值所替换,然而,在我们修改字符串列表之前,我们必须确认索引是有效的,项目是正确的类型,而且项目的规则是受支持的,按照惯例,我们坚持这个规则是EditRole,因为这是标准项目的委托所使用的规则,对于布尔型数据,无论如何,你可以使用Qt::CheckStateRole并且设置Qt::ItemsUserCheckable规则,checkbox是用来编辑数据的,模型中潜在的数据对于所有的规则是相同的,因此这个细节使使用标准组件来集成模型变得简单。
当数据被设置的时候,模型必须让视图知道哪些数据改变了,这是通过发射信号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());
else
return QVariant();
}
7.3.2 插入和移除行
在模型中插入和改变行数和列数是很重要的,在字符串列表模型中,只有改变行数才有意义,因此我们只实现插入和移除行的函数,类的声明如下:
bool insertRows(int position, int rows, const QModelIndex &index = QModelIndex()) override;
bool removeRows(int position, int rows, const QModelIndex &index = QModelIndex()) override;
尽管模型的行和列表中的字符串是相关的,函数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;
}
函数beginRemoveRows()通常在任何潜在的数据移除之前调用,并且指定了需要移除的第一行和最后一行,这允许其他组件访问数据,在行被移除之后,模型会发射信号endRemoveRows()来结束操作,并且让其他组件知道模型变化了。
7.3.3 下一步
我们可以显示模型提供的数据,或者其他的模型,使用QListView类来以竖状的形式来显示模型的项目,对于字符串列表模型,视图同样提供了一个默认的可以编辑项目的编辑器,我们研究了视图类中的标准视图提供的可能性
Model Subcalssing Reference文档尽可能详细的描述了QAbstractItemModel的子类的需求,并且提供了一个虚函数必须需要实现的函数的方针。
7.4 便利视图类
基于项目的视图窗体的名字反映了他们的用法,QListWidgets提供了一个项目列表,QTreeWidget展示了一个多重树形架构的内容,QTableWidget提供了一个一个列表,每一个类都继承了QAbstractItemView类的特性和方法,这个类提供了基本的表现的函数。
7.4.1 列表视图窗体
单一层级的项目的列表的展示可以使用QListWidget和QListWidgetsItems,列表视图窗体可以像其他窗体一样来构造:
QListWidget *listWidget = new QListWidget(this);
列表中的项目可以在其构造的时候直接添加到列表视图窗体中
new QListWidgetItem(tr("Sycamore"), listWidget);
new QListWidgetItem(tr("Chestnut"), listWidget);
new QListWidgetItem(tr("Mahogany"), listWidget);
也可以不使用构造函数:
QListWigdetItem *newItem = new QListWidgetItem;
newItem->setText(itemText);
listWidget->insertItem(row, newItem);
列表中的每一个项目可以显示为一个文字标签和一个图标,可以更改显示的字体和颜色来改变外观,工具提示,状态提示和“这是什么”帮助的配置都是很简单的,来确保列表可以更好的集成在应用程序中。
newItem->setToolTip(toolTipText);
newItem->setStatusTip(toolTipText);
newItem->setWhatsThis(whatsThisText);
默认情况下,列表中的项按照创建的顺序来排序,项目列表可以通过标准的Qt::SortOrder来排序,Qt::SortOrder提供了正序和逆序的排序方法
listWidget->sortItems(Qt::AscendingOrder);
listWidget->sortItems(Qt::DescendingOrder);
7.4.2 树状视图窗体
树状或者纵向列表是由QTreeWidget和QTreeWidgetItem类提供的,在树状视图窗体中的每一个项目可以有他们自己的子类,然后可以显示一些列的信息,树状视图窗体可以想其他窗体一样构建
QTreeWigdet *treeWdget = new QTreeWdget(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()函数来移除树形的顶层项目,但是移除低层次的项目需要调用他们父项目的takeTopLevelItem()函数,树形结构中的低层,需要用到父项目的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());
else
newItem = new QTreeWidgetItem(treeWidget, treeWidget->currentItem());
7.4.3 表格视图窗体
表格形式的项目比较像电子表格软件中的那样,通过QTableWidge和QTableWidgetItem来构造,这些提供了一个带头的可以滚动的表格视图窗体。
表格可以通过设置行数和列数来创造,或者这些可以被添加在一个不定尺寸的表格中,如果需要的话。
QtableWidget *tableWigdet;
tableWidget = new QTableWidget(12, 3, this);
项目也可以在表格之外构建,在添加到表格之前
QTableWidgetItem *newItem = new QTableWidgetItem(tr("%1").arg(pow(row, column + 1)));
tableWidget->setItem(row, column, newItem);
行头和列头也可以在表格之外构造,然后用他们作为头
QTableWidgetItem *valueHeaderItem = new QTableWidgetItem(tr("Values"));
tableWidget->setHorizontalHeaderItem(0, valuesHeaderItem);
需要注意的是表格视图中的第一行和第一列都为0。
7.4.5 通用的特性
每一个便利类中有一些基于项目的通用特性,可以用在每一个类的相同的接口中,我们下面展示的章节包含一些不同视图的例子,详细的看Model/View Classes中的描述。
7.4.5.1 项目隐藏
有时候在视图中隐藏一个项目比删除一个项目更有用,上面所有的窗体的项目都可以被隐藏然后再次显示,你可以通过调用函数isItemHidden()来确认是否一个项目是隐藏的,还可以通过调用函数setItemHidden()来隐藏一个项目。
尽管这个操作是基于项目的,同样的功能适用于上面的三个便利类
7.4.5.2 项目选中
项目选中的方式是由窗体的选中模式 QAbstractItemView::SelectionModel来控制的,其控制属性控制着是否用户可以选中一个或者多个项目和是否选中的区域是连续的,上面的三个便利类有相同的选中模式的工作方式
单一项目选中:
当用户需要从窗体中选择单一的项目的时候,默认的SingleSelection模式是最适合的,在这个模式下,当前项目和选中项目是相同的。
多重项目选中:
在这个模式中,用户可以对任何在窗体中的项目切换选中和不选中的状态,而不改变现存的选中的状态。
拓展选中模式:
窗体通常需要选中连续的项目,就像电子表格中的那样,需要ExtendedSelection模式,在这个模式中,可以通过鼠标和键盘的方式选中连续的项目区域,复杂的选项,涉及许多与小部件中其他选定项不相邻的项,如果修饰键使用的话也可以被创建。
如果用户不使用修饰键,已经选中的区域会被清空。
使用函数selectedItems()来读取项目的选中状态,还提供了一个可以遍历的项目列表,举个例子,通过下面的代码,我们可以看到我们选中区域的所有数值型的数据的和:
QList<QTableWidgetItem *> selected = tableWidget->selectedItem();
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++;
}
}
7.4.5.3 搜索
通常这是很有用的,有能力去找到项目视图窗体中的项目,或者开发者想将搜索作为一个给用户的服务,所有的三个便利的视图类提供了一个公共的函数findItems()让搜索变得尽可能简单
项目是通过文字来进行搜索的,这些文字包含从Qt::MatchFlags中选中的值指定的标准,通过函数findItem()我们可以得到一个符合条件的列表。
QtreeWidgetItem *item;
QList<QTreeWidgetItem *> found = treeWidget->findItems(itemText, Qt::MatchWildcard);
foreach (item, found) {
treeWidget->setItemSelected(item, true);
}
上面的代码在树状视图窗体中将会选中包含搜索的字符串的的项目,这个图案也可以被用在列表视图窗体或者表格视图窗体中。
8 在项目视图中使用拖拽
Qt的拖拽基础是被模型/视图架构所完全支持的,在列表,表格,和树状列表中,同时数据可以以MIME编码的形式导入导出。
标准的视图会自动的支持内部的拖拽,从哪里移动可以改变现实的顺序,默认情况下,这些视图不支持拖放,因为它们是为最简单、最常见的用途配置的。为了允许项目可以拖拽,视图的某些特性需要被使能,同时项目本身也需要支持被拖拽。
模型的需求中仅仅允许项目可以从视图中被导出,而不允许模型被拖进去,这样的模型比支持导入导出的模型要多
8.1 使用便利视图
在QListWidget,QtableWidget,QTreeWidget中使用的每一个项目的类型默认情况下被配置为使用不同的flags,打个比方,每一个QListWidgetItem或QTreeWidgetItem最初的时候都会使能可选中,可检查,还可以被用来拖放操作的源,每一个QTableWidgetItem也可以被编辑和用作拖放操作的目标。
尽管所有的标准的视图有一个或所有的flag来设置拖放,你通常需要在视图中设置不同的参数来用到内置拖放功能的优势。
- 如果需要使能项目拖拽功能,视图的dragEnabled参数设置为true
- 如果需要允许视图内部或外部的项目为所有的用户都提供放置功能,需要设置视图的viewport()的acceptDrops属性为true
- 为了展示给用户当前拖拽的项目在哪里可以被方式,需要设置视图的showDropIndicator属性,它提供给用户关于项目在视图中放置的位置的持续更新的信息
举个例子,我们可以使用下面的代码片段来在一个列表视图窗体中使能拖拽和方式功能
QListWidget *listWidget = new QListWidget(this);
listWidget->setSelectionMode(QAbstractItemView::SingleSelection);
listWidget->setDragEnabled(true);
listWidget->viewport()->setAcceptDrops(true);
listWidget->setDropIndicatorShown(true);
结果就是这个列表视图窗体允许项目可以在视图之间复制,并且也允许用户在包含相同数据类型的视图之间来回拖放数据,在所有的情况下,项目是复制而不是移动。
如果需要使能移动而不是复制,需要设置列表视图窗体为dragDropMode:
listWidget->setDrapDropMode(QAbstractItemView::InternalMove);
8.2 使用模型/视图类
在模型/视图类中使用拖拽和放置的功能和使用便利视图类似的,举个例子,QListView可以按照使用QListWidget的方式来设置
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中可以混合任何的值,模型需要写下来支持他们,举个例子,为了允许Qt::MoveAction可以在列表视同中适当的应用,模型需要提供一个QAbstractItemModel::removeRows()的重写,或者直接从基类中继承。
8.3.1 使能项目的拖拽和放置功能
模型告诉视图哪些项目可以被拖拽和放置,通过重写函数QAbstractitemModel::flags()。
举个例子,基于QAbstractListModel的模型提供了一个简单的列表,然后可以为每一个项目提供拖拽和放置功能,通过确保flags的返回值包含Qt::ItemDragEnabled和Qt::ItemIsDragEnabled值:
Qt::ItemFlags DragDropListModel::falgs(const QModelIndex &index) const
{
Qt::ItemFlags defaultFlags = QStringListModel::flags(index);
if (index.isValid()){
return Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled | defaultFlags;
} else {
return Qt::ItemIsDropEnabled | defaultFalgs;
}
}
需要注意的是项目可以保拖放至模型的顶层中,但是拖拽只对有效的项目起作用。
在上面的代码中,尽管模型是QStringListModel的派生类,我们通过调用它的重载函数falgs()来获取默认的flags的设置。
8.3.2 导出数据的编码
当项目从模型中通过拖拽和放置功能导出的时候,他们都会被编码成一个或多个IMME格式的编码格式,模型声明的MIME类型她们可以通过重写QAbstractItemModel::miniTypes()函数,然后返回标准的IMME类型的数据
举个例子,通过下面的重载函数代码可以实现模型仅提供文本类型的数据
QStringList DragDropListModel::mineTypes() const
{
QStringList types;
types << "application/vnd.text.list";
return types;
}
模型也一定按照广告声明的方式提供了编码数据的代码,这个特性是通过重写QAbstractItemModel::mimeData()函数实现的,这个韩式提供了一个QMimeData的对象,仅仅通过拖拽和放置操作。
下面的代码展示了有关给定的索引列表所关联的项目的数据是如何被编码成一个纯文本额数据,然后储存在QMineData对象中的
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.isVaild()) {
QString text = data(index, Qt::DisplayRole).toString();
stream << text;
}
}
mineData->setData("application/vnd.text.lest", encodedData);
return mimeData;
}
因为提供留给了这个函数模型多音列表,所以这个方法可以用在多层次和非多层次模型中。
需要注意的是定制的数据类型一定被声明为meta对象,并且stream操作一定要继承他们,详细的请看QMetaObject类。
8.3.3 在模型中插入一个放置的数据
给定模型操作放置数据的方式取决于他们的数据类型,比如list,table,或者tree。以及这种方式需要呈现给用户,通常来将,用于容纳删除数据的方法应该是最适合模型底层数据存储的方法。
不同的类型的数据类型的模型通过不同的方法操作放置的数据,列表和表格类型的数据仅仅提供了一个项目数据储存的扁平的数据架构,结果就是,当数据被拖放进一个存在的视图中的时候,他们将被插入到新的行(和列)中,或者他们可能通过给定的数据来替换项目中的内容,树状模型通常可以将包含新数据的子项添加到自己的基础数据储存中,这样的话,用户的行为更加可以预测。
放置模型的动作是通过重写函数QAbstractItemModel::dropMimeData()函数实现的,举个例子,一个可以操作字符串列表的模型提供了一个分别操作数据放置在存在的项目中和拖放到顶层模型中的方法。
模型可以禁止在指定的项目中放置数据,或者取决于要放置的数据本身,这是通过函数QAbstractItemMoel::canDropMimeData().
首先要确认的是操作是可以被操作的,以这种格式提供的数据是可以使用的,并且模型中放置的目的地是有效的。
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;
}
简单的一列的字符串模型在模型没有应用文本,或者给定的列数是无效的时候将会返回故障。
要插入到模型中的数据会根据是否放置在已存的项目中来区别对待,在这个简单的例子中,我们希望允许在项目将数据放置在两个项目中间。在第一个之前,或者在最后一个项目之后。
当放置的动作发生的时候,和父项关联的模型的索引也会变的失效,指示出放置生效的项目,否则这将会无效,指示drop发生在视图中与模型顶层对应的某个地方。
int beginRow;
if (row != -1) {
beginRow = row;
}
我们先检查给定的行号是否我们可以用它来插入到模型中,先不管是否父项索引是否是有效的。
else if (parent.isVaild()) {
beginRow = parent.row();
}
如果父项模型索引是有效的,放置发生在一个项目上。在最简单的列表模型中,我们找出项目的行数,并且用这个值插入到模型的顶层级别的要放置的项目中。
else {
beginRow = rowCount(QModelIndex());
}
当一个放置到视图中的任何地方的时候,并且行数是不可用的,我们就会将项目附加在模型的顶层级别中。
在层次化模型中,当放置发生在项目中时,最好是将作为项目的子项添加到模型中,在此处展示的简单例子中,模型仅有一个层级,索引这个特性是不可以用的。
8.3.4 对导入的数据进行解码
每一次对函数dropMimeData()的重写也都需要将数解码成模型的当下的数据格式。
对于简单的字符串列表模型,可以对拖入的数据解码成QStringList:
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()函数
9 代理模型
在模型视图架构中,一个模型提供的数据项目可以在不同的视图中共享,并且这些视图可以用不同的方式来展示相同的信息,自定义的视图和委托是对相同的模型提供不同显示方式的方法,然而,应用程序通常需要提供便利的视图提供相同数据的不同的版本,比如不同排序方式的项目列表。
尽管在视图的内置函数中执行排序和筛选的操作是很正常的,这个特性不允许多个视图共享这种很大开销的方式,另一种方式是在模型内部执行排序操作,这样的问题就是,每个视图都会根据最近的一次操作来组织显示结构。
为了解决这个问题,模型视图架构使用代理模型来管理每个视图或模型的信息,代理模型是一个类似于一个组件,其表现就像是视图透视中的一个原生的模型,并且代表视图访问原始模型中的数据,信号和槽的机制允许模型视图架构确保无论有多少代理模型在视图和源模型之间都可以保证所有的视图都可以变化。
9.1 使用代理模型
代理模型可以插入在已存的模型和任意数量的视图之间,Qt有一个标准的代理模型,QSortFilterProxyModel,它通常直接实例化使用,但是也可以通过派生子类的方式来实现更加定制化的少选和排序动作,QSortFilterProxyModel类可以按照如下的方式使用。
QSortFilterProxyModel *filterModel = new QSortFilterProxyModel(parent);
filterModel->setSourceModel(sringListModel);
QListView *filteredView = new QListView;
filteredView->setModel(filterModel);
一旦代理模型从QAbstractItemModel中继承,他就可以连接到任何类型的视图中,然后在不同的视图之间共享,他们也可以从其他的代理模型中进行信息获取。
QSortFilterProxyModel类是用来实例化的,然后可以在应用程序中直接使用,更多指定的代理模型可以通过创造子类,并且实现所需的比较操作。
9.2 定制化代理模型
通常来说,代理模型中所处理的类型需要遍历模型的原始数据源,要么需要遍历代理模型的不同的位置,在一些模型中,一些项目可能在代理模型中么有相关的位置,这些模型是筛选代理模型,视图通过代理模型的模型索引来访问项目,并且他们不包含原始模型的信息,或者原始模型的位置。
QSortFilterProxyModel在模型应用在视图前就使能了模型的筛选功能,并且也允许模型筛选后在设置在视图中。
9.2.1 定制筛选模型
QSortFilterProxyModel类提供了一个筛选模型,这个是比较常规的。并且可以用在各种比较常见的情况,对于高阶的用户,QSortFilterProxyModel可以派生子类,提供一个定制筛选的机制。
QSortFilterProxyModel的子类可以通过实现两个虚函数来实现,这个两个函数在调用代理模型的模型索引的时候被调用。
- filterAcceptsColumn()用来筛选源模型的指定的列。
- filterAcceptsRow()用来指定指定源模型的行。
默认的关于QSortFilterProxyModel的实现返回true来确保所有的项目都通过了视图。这些函数的重新实现应该返回false来过滤掉单独的行和列。
9.2.2 定制排序模型
QSortFilterProxyModel的实现是在源模型和代理模型中使用Qt内部的qStableSort()函数来设置遍历的,允许一个排序头,从而在不需要修改源模型中的结构。为了提供定制的排序表现,重写lessThan()函数来指定定制的比较。
10 模型派生子类的一些参考
模型的派生需要提供定义在QAbsractItemModel基类中的很多虚函数。需要重写的函数的数量取决于模型的类型,比如是列表,表格,树状。从QAbstractListModel和QAbstractTableModel中继承的模型可以利用一些默认的函数的实现,需要按照树状结构来显示的模型一定需要从类QAbstractItemModel中继承很多函数。
需重写的函数可以分为3组:
- Item data handing:项目的数据操作,所有的模型都需要重写这一类的函数,来允许视图和代理来查询模型尺寸,检查项目和检索数据。
- Navigation and index creation:导航和索引创建,层次化模型需要提供这些函数,这样的话视图可以调用树状视图的导航,并且获取项目的导航索引。
- Drag and drop support and MIME type handling:拖拽和放置支持,还有MIME类型的数据操作,模型需要重写这些函数,来控制内部的和外部的拖拽和放置操作,这些函数允许项目的数据用MIME的方式来描述,这样的话其他的组件和应用可以被理解。
10.1 项目数据操作
模型可以提供不同级别的数据访问的方式,他们可以是简单的只读组件,一些模型可能会支持改变大小的操作,或者允许项目被编辑。
10.2 只读访问
如果需要提供只读的模型的访问,下面的函数一定在子类中重写。
函数名 | 说明 |
---|---|
flags() | 这个函数是其他组件用来获取有关模型中的项目的信息的函数,在很多模型中,需要混合的falgs通常包含Qt::ItemIsEnabled和Qt::ItemIsSelectable |
data() | 这个函数用来给视图和代理提供项目的数据,通常来讲,模型仅仅需要给Qt::DisplayRole和其他应用程序指定的用户规则提供数据,但是这也是一个好做法,给Qt::ToolRole, Qt::AccessibleTextRole, 和Qt::AccessableDescriptionRole提供数据,可以看Qt::ItemDataRole的枚举类型文档来看每一个规则中所关联的类型 |
headerData() | 这个函数提供给视图用来显示他们的头的信息,信息仅由能够显示标题信息的视图检索 |
rowCount() | 这个函数提供了模型额行数 |
这些函数必须在所有的类型的模型中实现,包括列表模型,QAbstractListModel子类,还有表格模型,QAbstractTableModel子类。
还有一点,下面的函数一定在QAbstractTableModel和QAbstractItemModel中实现。
函数名 | 说明 |
---|---|
columnCount() | 提供模型中数量的列数,列表型模型不需要提供是因为已经在QAbstractListModel中实现了 |
10.2.1 可编辑的项目
可编辑的模型允许项目中的数据是可以编辑的,而且通常还允许行和列可以增加或删除,为了使能编辑,下面的函数必须要实现:
函数名 | 说明 |
---|---|
flags() | 这个函数用来返回每个项目的flags的结合,尤其是,这个函数的返回值必须包含flags Qt::ItemIsEditable 作为一个附加项,添加在只读模型中 |
setData() | 用指定的模型索引来修改项目中的数据,为了可以接收用户用输入接口的值,这个函数必须和flag Qt::ItemIsEditable进行关联,这个实现同样接收由Qt::ItemDataRole指定的其他的fflags,在改变项目中的数据之后,模型必须发射 dataChanged()信号,来通知给其他组件有关于模型的数据的变化 |
setHeaderData() | 用来修改行头和列头的信息,在改变项目的数据之后,模型必须发射headerDataChanged信号来通知其他组件有关系模型的数据的变化 |
10.2.2 可变规模的模型
所有类型的模型都偶支持插入和移除行,表格模型和层次话模型还需要支持插入和移除列,在规模变化前和变化后通知其他组件有关于该模型的变化是很重要的。这样的结果就是,如果允许模型可以变化大小,就需要重写下面的函数,但是重写必须需要确保调用适当的函数来通知附加的视图和委托
函数名 | 说明 |
---|---|
insertRows() | 此函数用来对所有的模型来添加或移除行,重写必须在插入行之前调用函数beginInsertRows()和在插入之后调用函数endInsertRows() |
removeRows() | 此函数用来对所有的模型来移除行,重写必须在移除行之前调用函数beginRemoveRows()和在移除之后调用函数endRemoveRows() |
insertColumns() | 使用这个函数来对所有的模型来添加列,重写必须在添加列之前调用函数beginInsertColumns()和在移除之后调用函数endRemoveColmns() |
removeColumns() | 使用这个函数来对所有的模型来移除列,重写必须在移除列之前调用函数beginRemoveCOlumns()和在移除之后调用函数endRemoveColumns() |
整体上来说,这些函数在操作成功后都需要返回true,然而,有时候存在一种情况是函数里面的部分操作是部分成功的,举个例子,如果少于指定数量的行数被插入,在这种情况下,模型需要返回false,以指示操作失败。
由这个函数所发射的信在函数重写中给了附加组件在任何附加组件变失效之前机会。在插入或移除操作的封装中和开始结束函数也允许模型来直接管理persistent model indexes。
正常情况下,开始和结束函数是有能力告诉其他组件有关于模型的隐藏组件的变化的,对于更复杂的模型的结构的改变,或许设计到内部重组,遍历数据,或者任何其他结构的改变,非常有必要的是执行下面的步骤:
- 发射layoutAboutToBeChanged()信号
- 根据相关的模型的结构类升级内部的数据
- 使用changePersistentIndexList()来升级持续的索引
- 发射信号layoutChanged()信号
此序列可用于任何结构更新,以代替更高级、更方便的受保护方法,举个例子,如果一个模型有两百万行,需要多所有原有的行进行移动,也就是100万个不连续的空间,这样应该使用beginRemoveRows()和endRemoveRow()100万次,这样必定是非常低效的,更好的方法是,这可以通过一次更新所有必要的持久索引的布局更改来通知。
10.2.3 模型数据的惰性填充
模型的惰性数据填充有效的允许关于模型的信息的请求直到视图延迟到真正需要的时候才发起。
一些模型需要从远程数据数据源中获取数据,这必将是比较耗费时间的操作,尽管模型模型咋尽可能的更新数据以跟上模型刷新的速度,对后续的请求限制返回的数据的数量通常是很有用的。
在层次模型中,找出一个给定项目对的子项目的数量是开销很大的操作。确保模型的函数rowCount()的实现尽可能的少次调用是很有用的,在一些情况,函数hasChildren()可以重写来提供一个低开销的操作来检查其是否有一个子项目,并且在QTreeView中,绘制父项适当的装饰。
无论函数hasChilren()返回true还是false,视图调用函数rowCount()来找出有多少子项来展示可能对于视图不是必须的。举个例子,QTreeView在父项没有展开的时候不知道其子项的数量。
如果知道了有多少项目有子项,函数hasChildren()的重写中无条件的返回true有时候是一种很有用的方式来获取的方式,这确保了每一个项目可以尽可能快的在模型数据初始化的时候保证每一个项目可以被随后展开子项。惟一的缺点是,在某些视图中,没有子项的项可能会显示不正确,直到用户试图查看不存在的子项为止。
10.3 创建导航和模型索引
层次化模型需要提供函数,来让树状类型的结构展示导航,这需要获取项目索引。
10.3.1 父项和子项
尽管提供给视图的架构通过潜在的数据结构被确定,那要每个模型的子类通过下面的函数的重写来创建它自己的模型索引。
函数名 | 介绍 |
---|---|
index() | 给定一个模型索引来作为父项目,这个函数允许视图和代理访问项目的子项,如果没有有效的子项 - 对应指定的行,列,父项模型索引,这个函数将会返回QModelIndex(),这是一个无效的模型索引 |
parent() | 这个函数根据任何给定的子项的父项提供一个模型索引,如果模型索引是关联的命的顶层,或者模型中没有有效的父项项目,这个函数一定会返回无效的模型的索引,这个索引是由空的构造函数QModelIndex()来构建的 |
上面所有的函数都使用createIndex()工厂函数来生成索引供其他组件使用,模型通常会向这个函数提供一些惟一的标识符,以确保稍后可以将模型索引与其对应的项重新关联。
10.4 拖拽和放置支持,还有MIME类型操作
模型/视图类支持拖拽和放置操作,其提供的默认的函数的行为适合很多应用程序,然而,也可以定制化在拖拽和放置过程中的编码的方式,无论是复制,还是移动。还定义了它是如何插入到一个存在的模型中的。
再说一句,便利类一定的表现应该可以适合现有的开发人员的需要,便利视图部分提供了此行为的概述。
10.4.1 MIME数据
默认情况下,内置的模型和视图使用内置的MIME类型(application/x-qabstractitemmodeldatalist)来传递模型索引的信息,这个指定的关于项目列表的数据,包含了每一个项目的行和列,还有每一个项目所支持的规则。
数据通过MIME类型来编码,可以通过调用函数QAbstractItemModel::mimeData()来获得,这个函数使用QModelIndexList来包含序列化的项目。
当在一个定制化模型中实现了拖拽和放置的支持后,通过重写下面的函数来这顶项目的数据导出格式:
函数名 | 函数介绍 |
---|---|
mimeData() | 这个函数可以被重写,来返回除了内置的MIME格式的默认数据格式 application/x-qabstractitemmodeldatalist 。子类可以从基类中得到默认的QMimeData对象,还可以使用附加对的格式添加数据。 |
对于很多模型而言,通过MIME类型的数据比如 text/plain 和 image/png 的方式来提供公共的项目的内容是很有用的,需要注意的是图像,颜色,还有HTML文档可以简单的添加在QMimeData对象中,通过使用函数QMimeData::setImageData(),QMimeData::setColorData()还有QMimeData::setHtml()函数。
10.4.2 接受放置数据
那个拖拽和放置的操作在视图中被执行的时候,隐含的模型会被查询来确定其所支持的操作类型和可以接收的MIME类型,这个信息是通过函数QAbstractItemModel::supportedDropActions()函数和QAbstractItemModel::mimeTypes()函数来执行的,模型不会重写由QAbstractItemModel所支持的实现和默认内部的MIME的项目类型。
当有一系列的数据被放置在视图中的时候,数据插入在当前的模型中是通过重写函数QAbstractItemModel::dropMimeData()来实现的,这个函数的默认实现不会重写模型的任何数据,相反,它将作为兄弟项或者子项的方式插入项目。
利用QAbstractItemModel的默认的实现来建立内置的MIME类型,新的模型必须重写下面的函数
函数名 | 函数介绍 |
---|---|
insertRows() insertColumns() | 这个函数会使用QAbstractItemModel::dropMimeData()自动的使能模型来自动的插入新的数据 |
setData() | 这个函数允许新的行和列来填充新项 |
setItemData() | 这个函数为填充新项目提供了更有效的支持 |
为了接受其他的数据格式,下面的函数一定需要重写
函数名 | 函数介绍 |
---|---|
supportedDropActions() | 这个函数返回放置动作的集合,指示模型可以接受的拖拽和放置操作的类型 |
mimeTypes() | 使用这个函数返回MIME类型的列表,这个列表是可以被模型解码的,通常来说,MIME类型可以支持模型的输入和其他组件用来解码的数据一致 |
dropMimeData() | 执行拖拽和放置操作的中数据传递的实际额解码,确定在模型中哪个位置来设置数据,和在哪里插入新行和新列,在子类中如何实现这个函数取决于每个模型的数据展示 |
如果函数dropMimeData()的重写通过插入或移除行或者列而改变了模型的规模,或者如果项目的数据被修改了,需要注意的是所有相关的信号需要被发射,在子类中简单的调用重载函数是非常有用的,比如setData(),insertRows()和insertColumns(),来确保模型的行为一致。
为了确保拖拽动作正常的工作,需要重写下面的函数
- removeRows()
- removeRow()
- removeColumns()
- removeColumn()
更多的有有关于项目视图的拖拽和放置的信息,请参考: Using drag and drop with item views.
10.4.3 便利视图
便利视图(QListWidget, QTableWidget 和 QTreeWidget)重写了默认的拖拽和放置函数,提供了不是那么灵活,但是很自然的表现,适用于大多数的应用程序。举个例子,尽管往QTableWidget中,往一个单元中放置数据是很平常的操作,比如替换原有的数据,这种情况下,模型将替换数据而不是新增行或者列,更多的关于便利视图中的拖拽和放置的信息,可以看章节 Using drag and drop with item views.
10.5 针对大量数据的性能优化
函数canFetchMore()检查是否有更多有效的数据并且根据情况返回true或者false,函数fetchMore()基于指定的父项获取数据,所有的函数可以混合在一起,举个例子,指定了QAbstractItemModel的数据库涉及到增长的数据,我们重写了函数canFetchMore()来判断是否有更多的数据需要获取,用函数fetchMore()来根据需要填充模型。
另一个例子将会动态的填充树状模型,我们重写了函数fetchMore(),来在模型展开子项的时候动态的加载数据。
如果你重写了函数fetchMore()来向模型中添加行,你需要调用beginInsertRows()函数和endInsertRows(),并且,函数canFetchMore()和fetchMore()一定需要被重写成他们默认的实现并返回false和不做任何事情。
11 模型/视图类
下面的类用在模型视图的设计模式是隐藏数据(模型中的数据)保持和显示和操作独立
类名 | 类简单描述 |
---|---|
QAbstractItemDelegate | 用来显示和编辑模型中的数据 |
QAbstractItemModel | 模型类项目的纯虚接口 |
QAbstractItemView | 项目视图类的基本实现 |
QAbstractListModel | 可以用来创建一维列表模型的纯虚类 |
QAbstractProxyModel | 代理项目模型的基类,可以做排序,筛选,或者其他的数据处理任务 |
QAbstractTableModel | 可以用来创建列表模型的纯虚模型类 |
QColumnView | 单列视图的模型/视图类的实现 |
QDataWidgetMapper | 模型到窗体的一部分数据的映射 |
QFileSystemModel | 本地文件系统的数据模型 |
QHeaderView | 项目视图的列头或者行头 |
QIdentityProxyModel | 代理其未修改的源模型 |
QItemDelegate | 显示和编辑模型的数据项设施 |
QItemEditorCreator | 可以实现不派生QItemEditorCreatorBase类的情况下创建item editor creator |
QItemEditorCreatorBase | 当需要实现新的item editor creators的时候用来继承的纯虚积类 |
QItemEditorFactory | 在视图和代理中编辑项目数据的窗体 |
QItemSection | 管理模型中的选中的项目 |
QItemSectionModel | 跟踪视图的选中的项目 |
QItemSectionRange | 管理模型中的选中项目的信息 |
QListView | 一个列表或图表视图 |
QListWidget | 基于项目列表的窗体 |
QListWidgetItem | 使用QListWidget视图的时候所使用的项目 |
QModelIndex | 用来定位数据模型的数据 |
QPersistenModelIndex | 用来定位数据模型的数据 |
QSortFilterProxyModel | 支持另一个模型和视图的数据的排序和筛选 |
QStandardItem | 用在QSStandardItemModel类中的项目 |
QStandardItemEditorCreator | 不需要从QItemEditorCreatorBase中派生子类也可以实现窗体注册 |
QStandardItemModel | 储存定制数据的通用模型 |
QStringListModel | 用来放置字符串到视图中的模型 |
QStyleItemDelegate | 显示和编辑模型中的项目数据的配置 |
QTableView | 表格视图的默认模型视图的实现 |
QTableWidget | 默认的基于项目的表格视图窗体 |
QTableWidgetItem | 用在QTableWidget类的项目 |
QTableWidgetSelectionRange | 在不使用模型索引和选择模型的情况下与模型中的选择交互的方式 |
QTreeView | 树状视图的默认的模型和视图的实现 |
QTreeWidget | 使用预先定义的树状模型的窗体视图 |
QTreeWidgetItem | 用在QTreeWidget便利类中的项目 |
QTreeWidgetitemIterator | QTreeWidget中的项目迭代器 |
12 相关的范例
- Dir View
- Spin Box Delegate
- Pixelator
- Simple Tree Model
- Chart