目录
The Model/View Classes:模型/视图类:
另请参阅 Item Views Puzzle Example.
QT Model/View 编程(模型/视图编程)简介
Qt包含一组项视图类,这些类使用模型/视图体系结构来管理数据和数据呈现给用户的方式之间的关系。该体系结构引入的功能分离为开发人员提供了更大的灵活性来定制项的表示,并提供了一个标准模型接口,以允许在现有项视图中使用各种数据源。在本文中,我们简要介绍了模型/视图范式,概述了所涉及的概念,并描述了项目视图系统的体系结构。解释了体系结构中的每个组件,并给出了示例,说明如何使用提供的类。
模型/视图体系结构
模型视图控制器(MVC)是一种源于Smalltalk的设计模式,通常用于构建用户界面。在设计模式中,Gamma等人写道:
MVC由三种对象组成。模型是应用程序对象,视图是其屏幕显示,控制器定义用户界面对用户输入的反应方式。在MVC之前,用户界面设计倾向于将这些对象组合在一起。MVC将它们解耦,以增加灵活性和重用性。
备注:通俗的讲,M就是数据的源,比如数据库。M根据数据库的数据结构定制成数据模型。V就是怎样显示数据,比如HTML所展现的视图,相当于V怎样把数据模型展示给用户。C代表的是控制层,介于M和V中间,来支持用户的交互。
如果视图和控制器对象结合在一起,结果就是模型/视图架构。这仍然将数据的存储方式与呈现给用户的方式区分开来,但提供了一个基于相同原则的更简单的框架。这种分离使得可以在几个不同的视图中显示相同的数据,并实现新类型的视图,而无需更改底层数据结构。为了灵活处理用户输入,我们引入了委托的概念。在此框架中拥有委托的优点是它允许自定义呈现和编辑数据项的方式。
QT中把控制层C叫做委托。这样更容易理解。但QT中的委托C描述更贴近现实中的开发。
模型/视图架构 模型与数据源通信,为架构中的其他组件提供接口。通信的性质取决于数据源的类型和模型的实现方式。 视图从模型中获取模型索引;这些是对数据项的引用。通过向模型提供模型索引,视图可以从数据源检索数据项。 |
通常,模型/视图类可以分为上述三组:模型、视图和委托。这些组件中的每一个都由抽象类定义,这些抽象类提供公共接口,在某些情况下还提供功能的默认实现。抽象类意味着子类化,以便提供其他组件所期望的全套功能;这也允许编写专门的组件。
模型、视图和委托使用信号和槽相互通信:
- 来自模型的信号通知视图有关数据源持有的数据的更改。
- 来自视图的信号提供有关用户与正在显示的项目的交互的信息。
- 来自委托的信号在编辑期间用于告诉模型和视图有关编辑器的状态。
Models模型:
所有项目模型都基于QabstracteModel类。此类定义了视图和委托用来访问数据的接口。数据本身不必存储在模型中;它可以保存在由单独的类、文件、数据库或其他应用程序组件提供的数据结构或存储库中。
围绕模型的基本概念在模型类一节中介绍。
QabstracteModel提供了一个数据接口,该接口足够灵活,可以处理以表、列表和树的形式表示数据的视图。然而,当为列表和表类数据结构实现新模型时,QAbstractListModel和QAbstract TableModel类是更好的起点,因为它们提供了公共函数的适当默认实现。这些类中的每一个都可以子类化,以提供支持特定类型列表和表的模型。
在创建新模型一节中讨论了模型子类化的过程。
Qt提供了一些现成的模型,可用于处理数据项:
- QStringListModel用于存储QString项的简单列表。
- QStandardItemModel管理更复杂的项目树结构,每个项目可以包含任意数据。
- QFileSystemModel提供有关本地文件系统中文件和目录的信息。
- QSqlQueryModel、QSqlTableModel和QSqlRelationalTableModel用于使用模型/视图约定访问数据库。
如果这些标准模型不满足您的需求,您可以子类QAbstractItemModel、QAbstract ListModel或QAbstratchTableModel来创建您自己的自定义模型。
Views视图:
为不同类型的视图提供了完整的实现:QListView显示项目列表,QTableView在表中显示模型中的数据,QTreeView在分层列表中显示模型数据项。这些类中的每一个都基于QAbstractItemView抽象基类。尽管这些类已准备好使用实现,但它们也可以被子类化以提供定制视图。
在“View Classes 视图类”一节中查看可用视图。
Delegates委托:
QAbstractItemDelegate是模型/视图框架中委托的抽象基类。默认委托实现由QStyledItemDelegate提供,Qt的标准视图将其用作默认委托。但是,QStyledItemDelegate和QItemDelegate是独立的替代方案,可以为视图中的项目绘制和提供编辑器。它们之间的区别在于,QStyledItemDelegate使用当前样式绘制其项。因此,我们建议在实现自定义委托或使用Qt样式表时使用QStyledItemDelegate作为基类。
委托在委托类Delegate Classes一节中进行了描述。
排序
在模型/视图架构中有两种处理排序的方法;选择哪种方法取决于您的基础模型。
如果您的模型是可排序的,即,如果它重新实现QAbstractItemModelsort()函数,则QTableView和QTreeView都提供了一个API,允许您以编程方式对模型数据进行排序。此外,通过将QHeaderView::sortIndicatorChanged()信号分别连接到QTableView::sortByColumn()槽或QTreeView::sortByColumm()槽,您可以启用交互式排序(即允许用户通过单击视图的标题对数据进行排序)。
如果您的模型没有所需的接口,或者您希望使用列表视图来显示数据,另一种方法是在视图中显示数据之前使用委托模型来转换模型的结构。委托模型一节详细介绍了这一点。
便利类
许多便利类从标准视图类派生而来,以利于依赖Qt基于项的项视图和表类的应用程序。它们不打算被子类化。
此类类的示例包括QListWidget、QTreeWidget和QTableWidget。
这些类不如视图类灵活,不能用于任意模型。我们建议您使用模型/视图方法来处理项视图中的数据,除非您非常需要一组基于项的类。
如果您希望利用模型/视图方法提供的功能,同时仍然使用基于项的接口,请考虑使用视图类,如QListView、QTableView和QTreeView与QStandardItemModel。
使用模型和视图
以下各节解释如何在Qt中使用模型/视图模式。每一节都包括一个示例,后面是一节介绍如何创建新组件。
Qt 中包含的两个模型
Qt提供的两个标准模型是QStandardItemModel和QFileSystemModel。QStandardItemModel是一个多用途模型,可用于表示列表、表和树视图所需的各种不同数据结构。该模型还保存数据项。QFileSystemModel是一个维护目录内容信息的模型。因此,它本身不保存任何数据项,而只是表示本地归档系统上的文件和目录。
QFileSystemModel提供了一个现成的模型来进行实验,并且可以轻松地配置为使用现有数据。使用此模型,我们可以展示如何设置模型以与现成视图一起使用,并探索如何使用模型索引操作数据。
在现有模型中使用视图
QListView和QTreeView类是最适合与QFileSystemModel一起使用的视图。下面的示例在树视图中显示目录的内容,在列表视图中的相同信息旁边。视图共享用户的选择,以便在两个视图中突出显示所选项目。
我们设置了一个QFileSystemModel,以便它可以使用,并创建一些视图来显示目录的内容。这显示了使用模型的最简单方法。模型的构造和使用是在单个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是唯一的;我们为它提供一个目录,它返回一个模型索引。模型索引在模型类中讨论。
该函数的其余部分只显示拆分器小部件中的视图,并运行应用程序的事件循环:
splitter->setWindowTitle("Two views onto the same file system model");
splitter->show();
return app.exec();
在上面的示例中,我们忽略了如何处理项目的选择。在有关处理项目视图中的选择的部分中,将更详细地介绍此主题。
模型类
在检查如何处理选择之前,您可能会发现检查模型/视图框架中使用的概念很有用。
基本概念
在模型/视图体系结构中,模型提供了一个标准接口,视图和委托使用该接口访问数据。在Qt中,标准接口由QabstracteModel类定义。无论数据项如何存储在任何底层数据结构中,QabstracteModel的所有子类都将数据表示为包含项表的层次结构。视图使用此约定访问模型中的数据项,但它们向用户呈现此信息的方式不受限制。
模型还通过信号和时隙机制通知任何附加视图有关数据的更改。
本节描述了一些基本概念,这些概念对于其他组件通过模型类访问数据项的方式至关重要。更高级的概念将在后面的章节中讨论。
模型索引
为了确保数据的表示与访问方式保持分离,引入了模型索引的概念。可以通过模型获得的每一条信息由模型索引表示。视图和委托使用这些索引请求显示数据项。
因此,只有模型需要知道如何获取数据,并且模型管理的数据类型可以相当一般地定义。模型索引包含一个指向创建它们的模型的指针,这可以防止在使用多个模型时出现混淆。
QAbstractItemModel *model = index.model();
模型索引提供对信息片段的临时引用,可用于通过模型检索或修改数据。由于模型可能会不时重新组织其内部结构,因此模型索引可能会变得无效,不应存储。如果需要对一条信息进行长期引用,则必须创建持久模型索引。这为模型保持最新的信息提供了参考。临时模型索引由QModelIndex类提供,持久模型索引由QPersistentModelIndex类提供。
要获得与数据项对应的模型索引,必须为模型指定三个属性:行号、列号和父项的模型索引。以下各节详细描述和解释了这些属性。
行和列
在其最基本的形式中,模型可以作为一个简单的表来访问,其中的项通过行和列编号来定位。这并不意味着底层数据段存储在阵列结构中;使用行和列编号只是允许组件相互通信的约定。我们可以通过在模型中指定行和列编号来检索任何给定项的信息,并接收表示该项的索引:
QModelIndex index = model->index(row, column, ...);
为简单的单级数据结构(如列表和表)提供接口的模型不需要提供任何其他信息,但正如上面的代码所示,我们需要在获得模型索引时提供更多信息。
行和列 该图显示了一个基本表模型的表示,其中每个项由一对行和列编号定位。我们通过向模型传递相关的行和列编号来获得引用数据项的模型索引。 通过指定QModelIndex()作为其父项,始终引用模型中的顶级项。这将在下一节中讨论。 |
项目的父项
当使用表或列表视图中的数据时,模型提供的项目数据的类似表的界面是理想的;行和列编号系统精确映射到视图显示项目的方式。然而,树视图等结构要求模型向其中的项公开更灵活的接口。因此,每个项也可以是另一个项表的父项,这与树视图中的顶级项可以包含另一个项目列表的方式非常相似。
当请求模型项的索引时,我们必须提供有关该项父项的一些信息。在模型之外,引用项的唯一方法是通过模型索引,因此还必须给出父模型索引:
QModelIndex index = model->index(row, column, parent);
父级、行和列 该图显示了树模型的表示,其中每个项都由父项、行号和列号引用。项目“A”和“C”在模型中表示为顶级同级: 项目“A”有多个子项。项目“B”的模型索引由以下代码获得: |
Item roles项角色;数据元素显示方式
模型中的项可以为其他组件执行各种角色,允许为不同情况提供不同类型的数据。例如,Qt::DisplayRole用于访问可以在视图中显示为文本的字符串。通常,项包含许多不同角色的数据,标准角色由Qt::ItemDataRole定义。
我们可以通过向模型传递与该项对应的模型索引,并通过指定角色来获得我们想要的数据类型,从而向模型请求该项的数据:
QVariant value = model->data(index, role);
Item roles:项角色;数据元素显示方式 角色向模型指示所引用的数据类型。视图可以以不同的方式显示角色,因此为每个角色提供适当的信息非常重要。 “创建新模型”部分更详细地介绍了角色的一些特定用途。 |
Qt::ItemDataRole中定义的标准角色涵盖了项数据的最常见用途。通过为每个角色提供适当的项目数据,模型可以向视图和委托人提供关于如何向用户显示项目的提示。不同类型的视图可以根据需要自由解释或忽略这些信息。还可以为特定于应用程序的目的定义其他角色。
摘要,概述:
- 模型索引以独立于任何底层数据结构的方式提供关于模型提供的项的位置的视图和委托信息。
- 项目通过其行和列编号以及其父项目的模型索引来引用。
- 模型索引由模型根据其他组件(如视图和委托)的请求构建。
- 如果在使用index()请求索引时为父项指定了有效的模型索引,则返回的索引将引用模型中该父项下的项。获得的索引引用该项的子项。
- 如果在使用index()请求索引时为父项指定了无效的模型索引,则返回的索引将引用模型中的顶级项。
- 角色区分与项目关联的不同类型的数据。
使用模型索引:
为了演示如何使用模型索引从模型中检索数据,我们设置了一个没有视图的QFileSystemModel,并在小部件中显示文件和目录的名称。虽然这并没有显示使用模型的正常方式,但它演示了模型在处理模型索引时使用的约定。
QFileSystemModel加载是异步的,以最小化系统资源使用。在处理这个模型时,我们必须考虑到这一点。
我们以以下方式构建文件系统模型:
QFileSystemModel *model = new QFileSystemModel;
connect(model, &QFileSystemModel::directoryLoaded, [model](const QString &directory) {
QModelIndex parentIndex = model->index(directory);
int numRows = model->rowCount(parentIndex);
});
model->setRootPath(QDir::currentPath);
在本例中,我们首先设置默认的QFileSystemModel。我们将它连接到一个lambda,在该模型中,我们将使用该模型提供的index()的特定实现来获得父索引。在lambda中,我们使用rowCount()函数计算模型中的行数。最后,我们设置QFileSystemModel的根路径,以便它开始加载数据并触发lambda。
为了简单起见,我们只对模型第一列中的项目感兴趣。我们依次检查每一行,获得每一行中第一项的模型索引,并读取模型中为该项存储的数据。
for (int row = 0; row < numRows; ++row) {
QModelIndex index = model->index(row, 0, parentIndex);
为了获得模型索引,我们指定行编号、列编号(第一列为零)以及我们想要的所有项的父项的适当模型索引。使用模型的data()函数检索每个项中存储的文本。我们指定模型索引和DisplayRole,以获取字符串形式的项数据。
QString text = model->data(index, Qt::DisplayRole).toString();
// Display the text in a widget.
}
上述示例演示了用于从模型中检索数据的基本原理:
- 可以使用rowCount()和columnCount()找到模型的维度。这些函数通常需要指定父模型索引。
- 模型索引用于访问模型中的项。需要行、列和父模型索引来指定项。
- 要访问模型中的顶级项,请使用QModelIndex()指定空模型索引作为父索引。
- 项目包含不同角色的数据。要获取特定角色的数据,必须向模型提供模型索引和角色。
延伸阅读
可以通过实现QAbstracteModel提供的标准接口来创建新模型。在创建新模型一节中,我们通过创建一个方便的现成模型来演示这一点,该模型用于保存字符串列表。
视图类:
概念:
在模型/视图体系结构中,视图从模型中获取数据项并将其呈现给用户。数据的呈现方式不必类似于模型提供的数据的表示,并且可以与用于存储数据项的底层数据结构完全不同。
通过使用QAbstractItemModel提供的标准模型接口、QAbstractionItemView提供的标准视图接口以及以通用方式表示数据项的模型索引,实现了内容和表示的分离。视图通常管理从模型获得的数据的总体布局。它们可以自己渲染单个数据项,也可以使用委托处理渲染和编辑功能。
除了显示数据,视图还处理项目之间的导航以及项目选择的某些方面。视图还实现了基本的用户界面功能,如上下文菜单和拖放。视图可以为项提供默认编辑工具,也可以与委托一起使用以提供自定义编辑器。
可以在没有模型的情况下构建视图,但必须先提供模型,然后才能显示有用的信息。视图跟踪用户通过使用选项选择的项目,这些选项可以为每个视图单独维护,也可以在多个视图之间共享。
某些视图(如QTableView和QTreeView)显示标题和项。这些也由视图类QHeaderView实现。标题通常访问与包含它们的视图相同的模型。它们使用QAbstractItemModel::headerData()函数从模型中检索数据,通常以标签的形式显示标题信息。新标题可以从QHeaderView类子类化,为视图提供更专门的标签。
使用现有视图
Qt提供了三个现成的视图类,它们以大多数用户熟悉的方式显示模型中的数据。QListView可以以简单列表或经典图标视图的形式显示模型中的项目。QTreeView将模型中的项显示为列表层次结构,允许以紧凑的方式表示深度嵌套的结构。QTableView以表格的形式显示模型中的项目,非常类似于电子表格应用程序的布局。
上面显示的标准视图的默认行为对于大多数应用程序来说应该足够了。它们提供了基本的编辑工具,可以进行定制以满足更专业的用户界面的需要。
使用模型
我们将创建的字符串列表模型作为示例模型,使用一些数据进行设置,并构造一个视图来显示模型的内容。这可以在单个功能中执行:
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如何表示字符串列表模型中的数据。由于模型是可编辑的,因此视图自动允许使用默认代理编辑列表中的每个项目。
使用模型的多个视图
在同一模型上提供多个视图只是为每个视图设置相同的模型。在下面的代码中,我们创建了两个表视图,每个视图都使用我们为本示例创建的相同的简单表模型:
QTableView *firstTableView = new QTableView;
QTableView *secondTableView = new QTableView;
firstTableView->setModel(model);
secondTableView->setModel(model);
在模型/视图体系结构中使用信号和插槽意味着对模型的更改可以传播到所有附加视图,从而确保无论使用哪个视图,我们都可以访问相同的数据。
上图显示了同一模型上的两个不同视图,每个视图包含多个选定项目。尽管来自模型的数据在视图中显示一致,但每个视图都维护其自己的内部选择模型。这在某些情况下可能有用,但对于许多应用程序,共享选择模型是可取的。
处理项目的选择
QItemSelectionModel类提供了处理视图中项目选择的机制。默认情况下,所有标准视图都构建自己的选择模型,并以正常方式与它们交互。视图使用的选择模型可以通过selectionModel()函数获得,替换选择模型可以用setSelectionModel()指定。当我们希望在同一模型数据上提供多个一致的视图时,控制视图使用的选择模型的能力非常有用。
通常,除非要对模型或视图进行子类化,否则不需要直接操作选择的内容。但是,如果需要,可以访问选择模型的接口,这将在处理项目视图中的选择时进行探讨。
在视图之间共享选择
尽管默认情况下视图类提供自己的选择模型是方便的,但当我们在同一模型上使用多个视图时,通常希望模型数据和用户选择在所有视图中一致显示。由于视图类允许替换其内部选择模型,我们可以通过以下行实现视图之间的统一选择:
secondTableView->setSelectionModel(firstTableView->selectionModel());
第二视图给出了第一视图的选择模型。现在,两个视图都在同一选择模型上操作,保持数据和所选项目同步。
在上面显示的示例中,两个相同类型的视图用于显示相同模型的数据。然而,如果使用两种不同类型的视图,则所选项目在每个视图中的表示可能非常不同;例如,表视图中的连续选择可以表示为树视图中突出显示项的片段集。
委托类
概念:
与模型-视图-控制器模式不同,模型/视图设计不包括用于管理与用户交互的完全独立的组件。通常,视图负责向用户呈现模型数据,并处理用户输入。为了在获取输入的方式上具有一定的灵活性,交互由委托执行。这些组件提供输入功能,还负责在某些视图中呈现单个项。控制委托的标准接口在QAbstractItemDelegate类中定义。
委托应该能够通过实现paint()和sizeHint()函数来呈现其内容。但是,简单的基于小部件的委托可以子类化QStyledItemDelegate而不是qAbstractItemDelege,并利用这些函数的默认实现。
委托的编辑器可以通过使用小部件来管理编辑过程或直接处理事件来实现。第一种方法将在本节后面介绍,并在数字显示框代理示例中显示。
Pixelator示例显示了如何创建一个自定义委托,该委托为表视图执行专门的渲染。
使用现有委托类
Qt提供的标准视图使用QStyledItemDelegate实例提供编辑工具。委托接口的默认实现以每个标准视图的常规样式呈现项:QListView、QTableView和QTreeView。
所有标准角色都由标准视图使用的默认委托处理。QStyledItemDelegate文档中描述了这些的解释方式。
视图使用的委托由itemDelegate()函数返回。setItemDelegate()函数允许您为标准视图安装自定义委托,在为自定义视图设置委托时,必须使用此函数。
委托实例:
这里实现的委托使用QSpinBox提供编辑工具,主要用于显示整数的模型。尽管我们为此设置了一个自定义的基于整数的表模型,但我们可以很容易地使用QStandardItemModel,因为自定义委托控制数据输入。我们构造一个表视图来显示模型的内容,这将使用自定义委托进行编辑。
我们从QStyledItemDelegate子类化委托,因为我们不想编写自定义显示函数。然而,我们仍然必须提供管理编辑器小部件的功能: