警告
本节包含从C++自动翻译到Python的片段,可能包含错误。
模型/视图编程#
Qt的可扩展模型/视图架构指南。
模型/视图编程简介#
Qt包含一套使用模型/视图架构来管理数据与用户视图之间关系的项视图类。此架构引入的功能分离为开发人员提供了更大的灵活性,以自定义项目的展示,并提供了一种标准的模型接口,允许使用广泛的现有项视图数据源。在本文档中,我们简要介绍了模型/视图范式,概述了涉及的概念,并描述了项视图系统的架构。架构中的每个组件都进行了说明,并给出了示例,展示了如何使用提供的类。
模型/视图架构#
模型-视图-控制器(MVC)是一种起源于Smalltalk的设计模式,在构建用户界面时经常使用。在《设计模式》中,Gamma等人写道
MVC由三种类型的对象组成。模型是应用对象,视图是其屏幕表示,控制器定义用户界面对用户输入的反应方式。在MVC之前,用户界面设计倾向于将这些对象合在一起。MVC将它们解耦以增加灵活性和重用性。
如果将视图和控制器对象组合起来,结果就是一个模型/视图架构。这仍然将数据存储方式与其呈现给用户的方式分开,但提供一个基于相同原理的更简单的框架。这种分离使您能够以不同的视图展示相同的数据,并实现新的视图类型,而无需更改底层数据结构。为了允许灵活地处理用户输入,我们引入了“委托”的概念。在这个框架中拥有委托的优势在于,它允许自定义数据的渲染和编辑方式。
模型/视图架构
模型与数据源进行通信,为架构中的其他组件提供一个接口。通信的性质取决于数据源的类型以及模型的实现方式。
视图从模型获取模型索引;这些是数据项的引用。通过向模型提供模型索引,视图可以从数据源检索数据项。
在标准视图中,一个委托渲染数据项。当编辑一个项目时,委托直接使用模型索引与模型通信。
通常,模型/视图类可以分为上面描述的三组:模型、视图和代理。每个组件都由提供通用接口和在某些情况下提供功能默认实现的 抽象类 定义。抽象类是为了被继承以提供一个完整的功能集,以便其他组件使用;这也允许编写专门的组件。
模型、视图和代理之间通过 信号和槽 进行通信。
来自模型的信号会通知视图数据源数据的变化。
视图的信号提供关于用户与所显示项交互的信息。
代理的信号在编辑期间用于告知模型和视图编辑器的状态。
模型#
所有项模型都基于 QAbstractItemModel 类。这个类定义了一个接口,视图和代理用它来访问数据。数据本身不一定存储在模型中;它可以存储在由其他类、文件、数据库或其他应用程序组件提供的数据结构或存储库中。
围绕模型的基礎概念在 模型类 章节中介绍。
QAbstractItemModel 提供了一个通用的数据接口,足够灵活,可以处理以表格、列表和树的形式表示数据的视图。然而,当实现类似列表和表格的数据结构的新模型时,QAbstractListModel 和 QAbstractTableModel 类是更好的起点,因为它们提供了常见函数的适当默认实现。每个这些类都可以被继承,以提供支持特殊类型的列表和表的模型。
模型继承的过程在 创建新模型 章节中讨论。
Qt提供了可以用来处理数据项的现成模型
QStringListModel 用于存储简单的 QString 项列表。
QStandardItemModel 管理更复杂的项的树结构,每个都可以包含任意数据。
QFileSystemModel 提供有关本地文件系统中文件和目录的信息。
QSqlQueryModel、QSqlTableModel 和 QSqlRelationalTableModel 用于模型/视图约定访问数据库。
如果这些标准模型不能满足您的需求,则可以继承 QAbstractItemModel、QAbstractListModel 或 QAbstractTableModel 来创建您自己的自定义模型。
视图#
为不同类型的视图提供了完整的实现:QListView
显示项列表,QTableView
以表格形式显示模型数据,而 QTreeView
以层级列表显示模型数据项。每个此类都基于 QAbstractItemView
抽象基类。尽管这些类是可用于使用的实现,但它们也可以被继承以提供定制的视图。
在 视图类 章节中检查了可用的视图。
代理#
QAbstractItemDelegate
是模型/视图框架中代理的抽象基类。默认代理实现由 QStyledItemDelegate
提供,这也是 Qt 标准视图的默认代理。然而,QStyledItemDelegate
和 QItemDelegate
是绘制视图中的项和提供编辑器的独立替代方案。它们之间的区别在于 QStyledItemDelegate
使用当前样式来绘制其项。因此,我们建议在实现自定义代理或在处理 Qt 风格表单时将 QStyledItemDelegate
作为基类。
代理在 Delegate Classes 部分中有详细描述。
排序#
在模型/视图架构中,有两种处理排序的方法;选择哪种方法取决于您的底层模型。
如果您的模型是可排序的,即它重写了 QAbstractItemModel::sort() 函数,那么 QTableView
和 QTreeView
都提供了 API,允许您以编程方式对模型数据进行排序。此外,您可以通过将 sortIndicatorChanged()
信号连接到 sortByColumn()
槽或 sortByColumn()
槽,分别启用交互式排序(即允许用户通过单击视图的列标题来对数据进行排序)。
如果您的模型不具有所需的接口或您想使用列表视图来展示数据,另一种方法是使用代理模型来在向视图展示数据之前转换您的模型结构。这将在 Proxy Models 部分中详细说明。
方便的类#
存在许多派生自标准视图类的 方便的 类,以供依赖 Qt 的基于项的项视图和表格类的应用程序使用。它们不是旨在子类化的。
以下是一些此类类的示例:QListWidget
,QTreeWidget
,以及QTableWidget
。
这些类在灵活性上不如视图类,并且不能与任何模型一起使用。我们建议您在不是特别需要基于条目的类集时,使用模型/视图方法处理项目视图中的数据。
如果您希望在仍使用基于条目的接口的同时利用模型/视图方法提供的功能,请考虑使用视图类,例如QListView
,QTableView
,以及QTreeView
,并与QStandardItemModel一起使用。
使用模型和视图#
以下几节将解释如何在Qt中使用模型/视图模式。每个部分都包含一个示例,并随后是创建新组件的说明。
Qt中包含的两个模型#
Qt提供的标准模型中有两个:QStandardItemModel和QFileSystemModel。QStandardItemModel是一个多用途模型,可以用于表示列表、表格和树视图所需的各种不同数据结构。此模型还包含数据项。QFileSystemModel是维护目录内容信息的模型。因此,它本身不包含任何数据项,只是代表本地文件系统上的文件和目录。
QFileSystemModel提供了一个现成的模型供实验使用,并且可以轻松配置以使用现有数据。使用此模型,我们可以展示如何设置与现成视图一起使用的模型,并探讨如何使用模型索引操纵数据。
使用与现有模型一起的视图#
QListView
和QTreeView
类是与QFileSystemModel一起使用的最合适的视图。下面的示例显示了在列表视图中显示目录的相同信息旁边的树视图内容。这些视图共享用户的选择,以便在两个视图中突出显示所选条目。
我们设置一个QFileSystemModel,使其准备就绪以供使用,并创建一些视图来显示目录的内容。这展示了使用模型的最简单方法。模型的构建和使用是在单个main()
函数中执行的。
if __name__ == "__main__": app = QApplication([]) splitter = QSplitter() model = QFileSystemModel() model.setRootPath(QDir.currentPath())
模型配置为使用某个文件系统的数据。对setRootPath()的调用告诉模型向视图展示文件系统上的哪个驱动器。
我们创建两个视图,以便以两种不同的方式检查模型中包含的项目
tree = QTreeView(splitter) tree.setModel(model) tree.setRootIndex(model.index(QDir.currentPath())) list = 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() sys.exit(app.exec()) model = QFileSystemModel() model.setRootPath(QDir.currentPath()) model = QFileSystemModel() model.setRootPath(QDir.currentPath()) tree = QTreeView(splitter) tree = QTreeView(splitter) tree.setModel(model) tree = QTreeView(splitter) tree.setModel(model) tree.setRootIndex(model.index(QDir.currentPath())) list = QListView(splitter) list.setModel(model) list.setRootIndex(model.index(QDir.currentPath())) tree.setModel(model) tree.setRootIndex(model.index(QDir.currentPath()))
在上面的示例中,我们忽略了个项目选择的处理方法。这个问题在 处理项目视图中的选择 部分会更详细地讨论。
模型类#
在检查选择是如何处理之前,您可能发现查看模型/视图框架中使用的概念是有用的。
基本概念#
在模型/视图架构中,模型提供一个标准接口,视图和委托使用此接口来访问数据。在 Qt 中,标准接口是由 QAbstractItemModel 类定义的。无论数据以何种方式存储在任何底层数据结构中,所有的 QAbstractItemModel 的子类都将数据表示为包含项目表的分层结构。视图使用这种 约定 来访问模型中的数据项,但它们在将此信息呈现给用户的方式上不受限制。
模型还通过信号和槽机制通知所有附加视图关于数据更改的情况。
本节描述了一些基本概念,这些概念是其他组件通过模型类访问数据项的核心。更高级的概念将在后面的章节中讨论。
模型索引#
为了确保数据的表示与其访问方式保持分离,引入了 模型索引 的概念。每个可以通过模型获得的信息条目都由一个模型索引表示。视图和委托使用这些索引来请求要显示的数据项。
因此,只要求模型知道如何获取数据,并且由模型管理的数据类型可以相当一般地定义。模型索引包含一个指向创建它们的模型的指针,这有助于避免当处理多个模型时出现混淆。
model = index.model()
模型索引提供对信息片段的 临时 引用,可以使用它通过模型检索或修改数据。由于模型可能会不时重新组织其内部结构,模型索引可能会变得无效,并且 不应存储。如果需要长时间引用一个信息片段,必须创建一个 持久模型索引。这提供了一个对模型保持更新的信息的引用。临时模型索引由 QModelIndex 类提供,持久模型索引由 QPersistentModelIndex 类提供。
要获取与数据项对应的模型索引,必须向模型指定三个属性:行数、列数和父项的模型索引。以下部分将详细描述并解释这些属性。
行和列#
在最基本的形式中,可以根据行号和列号访问模型作为一个简单的表格。这不意味着底层的数据存储在数组结构中;使用行和列号只是一个约定,以便组件之间能够相互通信。我们可以通过向模型指定任意项目的行号和列号来检索关于该项目的任何信息,并从模型接收一个表示该项目的索引
index = model.index(row, column, ...)
提供简单单级数据结构如列表和表格接口的模型不需要提供其他信息,但是,如上述代码所示,在获取模型索引时需要提供更多信息。
行和列
图中展示了基本表格模型的一个表示,其中每个项通过行号和列号的配对来定位。我们通过向模型传递相关的行号和列号来获取一个指向数据项的模型索引。
indexA = model.index(0, 0, QModelIndex()) indexB = model.index(1, 1, QModelIndex()) indexC = model.index(2, 1, QModelIndex())模型的顶层项始终通过指定
QModelIndex()
作为它们的父项来引用。这一点在下一节中讨论。
项的父母#
当使用表格或列表视图中的数据时,模型提供的类似表格的项数据接口是理想的;行和列号系统与视图显示项的方式完全对应。然而,如树视图这样的结构需要模型向其内部的项暴露更灵活的接口。因此,每个项也可以是另一个项表的父项,就像树视图中的顶层项可以包含另一个项列表一样。
当请求模型的项索引时,我们必须提供有关该项父项的一些信息。在模型外部,唯一引用项的方法是通过模型索引,因此还必须提供父模型索引
index = model.index(row, column, parent)
父母、行和列
图中展示了树模型的一个表示,其中每个项通过一个父项、一个行号和一个列号来引用。
项“A”和“C”在模型中作为顶层兄弟项表示
indexA = model.index(0, 0, QModelIndex()) indexC = model.index(2, 1, QModelIndex())项“A”有多个子项。可以使用以下代码获取项“B”的模型索引
indexB = model.index(1, 0, indexA)
项作用域#
模型中的项可以为其他组件执行各种作用域,允许在不同的情境中提供不同类型的数据。例如,Qt::DisplayRole用于访问可以显示为视图中的文本的字符串。通常,项包含多个不同作用域的数据,标准作用域由Qt::ItemDataRole定义。
我们可以通过传递与项对应的模型索引以及指定一个作用域来请求模型中的项数据,以获得我们想要的数据类型
value = model.data(index, role)
项作用域
作用域指示模型正在引用哪种类型的数据。视图可以以不同方式显示作用域,因此为每个作用域提供适当的信息很重要。
创建新模型部分详细介绍了作用域的一些特定用途。
标准作用域定义在Qt::ItemDataRole中,通常用于项数据的常见用途。通过为每个作用域提供适当的项数据,模型可以为视图和代理提供关于如何向用户显示项的提示。不同类型的视图可以根据需要解释或忽略此信息。还可以定义用于特定应用程序目的的额外作用域。
总结#
模型索引以不依赖于任何底层数据结构的方式为视图和代理提供有关模型所提供项位置的信息。
元素通过它们的行列编号和父项的模型索引来引用。
模型索引是根据其他组件(如视图和代理)的请求由模型构建的。
当使用 index() 方法请求索引并指定了父项的有效模型索引时,返回的索引引用了模型中该父项下方的一个元素。获得的索引引用的是该元素的子元素。
当使用 index() 方法请求索引并指定了父项的无效模型索引时,返回的索引引用了模型中的顶层元素。
角色区分与元素关联的不同类型的数据。
使用模型索引#
为了演示如何使用模型索引从模型中检索数据,我们设置了一个没有视图的 QFileSystemModel,并在小部件中显示文件和目录的名称。尽管这并不是使用模型的一种正常方式,但它展示了模型在处理模型索引时使用的约定。
QFileSystemModel 的加载是异步的,以最小化系统资源的使用。在处理此模型时,我们必须考虑到这一点。
我们按照以下方式构建文件系统模型
class StringListModel(QAbstractListModel): def __init__(self, strings, parent=None): super().__init__(parent) self._strings = strings
在这种情况下,我们首先设置一个默认的 QFileSystemModel。我们将它的信号 directoryLoaded(QString)
连接到一个 lambda,在这个 lambda 中,我们将使用该模型提供的特定实现 of index() 来获取目录的父索引。
在 lambda 中,我们使用 rowCount() 函数确定模型中的行数。
为了简单起见,我们只关心模型的第一列中的元素。我们依次检查每一行,获取每一行的第一项的模型索引,并读取模型中存储该元素的数据。
def data(self, index, role): """Returns an appropriate value for the requested data. If the view requests an invalid index, an invalid variant is returned. Any valid index that corresponds to a string in the list causes that string to be returned.""" row = index.row() if not index.isValid() or row >= len(self._strings): return None if role != Qt.DisplayRole and role != Qt.EditRole: return None return self._strings[row]
要获取模型索引,我们指定行号、列号(对于第一列为零)以及我们想要的所有元素的父项的适当模型索引。使用模型的数据() 函数检索每个元素中存储的文本。我们指定模型索引和 DisplayRole 以获取以字符串形式表示的元素数据。
def headerData(self, section, orientation, role=Qt.DisplayRole): """Returns the appropriate header string depending on the orientation of the header and the section. If anything other than the display role is requested, we return an invalid variant.""" if role != Qt.DisplayRole: return None if orientation == Qt.Horizontal: return f"Column {section}" return f"Row {section}"
最后,我们设置 QFileSystemModel 的根路径,使其开始加载数据并触发 lambda。
上面的示例演示了从模型中检索数据所使用的基本原则。
可以使用 rowCount() 和 columnCount() 来找到模型的大小。这些函数通常需要指定一个父模型索引。
模型索引用于访问模型中的项目。需要行、列和父模型索引来指定项目。
要访问模型中的顶层元素,请使用
QModelIndex()
将父索引指定为空模型索引。项目包含不同角色(role)的数据。要获取特定角色的数据,模型必须提供模型索引和角色。
进一步阅读#
可以通过实现 QAbstractItemModel 提供的标准接口来创建新的模型。在 创建新模型 一节中,我们通过创建一个方便的、可用于存储字符串列表的模型来演示这一点。
视图类#
概念#
在模型/视图架构中,视图从模型中获取数据项并向用户展示。数据展示的方式无需与模型提供的数据表示相似,甚至可能与存储数据项的底层数据结构完全不同。
通过使用QAbstractItemModel提供的标准模型接口、QAbstractItemView提供的标准视图接口以及代表数据项的模型索引来实现在内容与表示之间的分离。视图通常管理从模型获取的数据的整体布局。它们可以自己渲染单个数据项,或者使用委托来处理渲染和编辑功能。
除了解示数据外,视图还处理项目之间的导航以及一些项目选择方面。视图还实现了基本用户界面功能,如上下文菜单和拖放。视图可以为项目提供默认的编辑功能,或者与委托一起工作以提供自定义编辑器。
视图可以在没有模型的情况下构造,但必须在显示有用的信息之前提供模型。视图通过使用选择来跟踪用户已选择的项目,这些选择可以保留在每个视图中供单独使用,或者在不同视图之间共享。
一些视图,如QTableView
和 QTreeView
,除了显示数据项外,还显示标题。这些也是通过视图类QHeaderView
实现的。标题通常访问包含它们的视图相同的模型。它们使用QAbstractItemModel::headerData()函数从模型检索数据,并且通常以标签的形式显示标题信息。可以基于QHeaderView
类创建新的标题来为视图提供更专业的标签。
使用现有视图#
Qt提供三个现成的视图类,用于以大多数用户熟悉的方式显示模型中的数据。QListView
可以将模型中的项显示为简单列表或经典图标视图。QTreeView
将模型中的项显示为列表层次结构,以紧凑的方式表示深层嵌套的结构。QTableView
以表格的形式显示模型中的项,类似于电子表格应用程序的布局。
上面显示的标准视图的默认行为对于大多数应用程序应该是足够的。它们提供基本编辑功能,并且可以根据更专门的用户界面需求进行自定义。
使用模型#
我们采用字符串列表模型,这是我们创建的示例模型,使用一些数据对其进行设置,并构建一个视图来显示模型的全部内容。所有这些操作都可以在单个函数内完成。
if __name__ == '__main__': app = QApplication(sys.argv) numbers = ["One", "Two", "Three", "Four", "Five"] model = StringListModel(numbers)
请注意,StringListModel
被声明为 QAbstractItemModel。这允许我们使用对模型的抽象接口,并确保即使我们用不同的模型替换字符串列表模型,代码仍然有效。
QListView
提供的列表视图对于展示字符串列表模型中的条目是足够的。我们构建视图,并使用以下行代码设置模型:
view = QListView() view.setModel(model)
视图以常规方式显示。
view.show() sys.exit(app.exec())
视图通过模型接口访问数据来呈现模型的全部内容。当用户尝试编辑一个条目时,视图使用默认的委托提供编辑小部件。
上面的图片展示了 QListView
如何在字符串列表模型中表示数据。由于模型是可编辑的,因此视图自动允许列表中的每个条目都可以使用默认委托进行编辑。
使用多个视图查看模型#
向同一模型提供多个视图只是将每个视图设置相同的模型的问题。在以下代码中,我们创建了两个表格视图,它们各自使用与示例中创建的相同简单表格模型:
firstTableView = QTableView() secondTableView = QTableView() firstTableView.setModel(model) secondTableView.setModel(model)
模型/视图架构中信号和槽的使用意味着对模型的更改可以传播到所有附加的视图,确保我们可以始终访问相同的数据,无论使用哪种视图。
上面的图片显示了两个对同一模型的不同视图,每个视图都包含一些选定的条目。尽管模型的数据在视图中始终一致地显示,但每个视图都保持自己的内部选择模型。这可能在某些情况下是有用的,但对于许多应用程序来说,一个共享的选择模型更为理想。
处理条目选择#
在视图中处理条目选择的机制由 QItemSelectionModel 类提供。所有标准视图默认都会构建自己的选择模型,并以常规方式与之交互。视图使用的选择模型可以通过 selectionModel()
函数获得,并可以使用 setSelectionModel()
指定一个替换选择模型。控制视图使用的选择模型的能力,当我们想为同一模型数据提供多个一致视图时是很有用的。
一般情况下,除非你正在子类化一个模型或视图,否则你不直接需要操作选择的内容。但是,如果需要,可以访问选择模型接口,这将在 处理项目视图中的选择 中进行探讨。
代理类#
概念#
与模型-视图-控制器模式不同,模型/视图设计不包括一个完全分离的组件来管理与用户的交互。通常,视图负责将模型数据展示给用户,并处理用户输入。为了在获取这种输入的方式上提供一些灵活性,交互是由代理来执行的。这些组件提供输入功能,并且在某些视图中还负责渲染单个项。控制代理的标准接口定义在QAbstractItemDelegate
类中。
预期代理能够通过实现 paint()
和 sizeHint()
函数来自行渲染其内容。但是,简单的基于小部件的代理可以继承 QStyledItemDelegate
而不是 QAbstractItemDelegate
,并利用这些函数的默认实现。
代理的编辑器可以通过使用小部件来管理编辑过程或直接处理事件来实现。
使用现有的委托#
Qt提供的标准视图使用 QStyledItemDelegate
的实例来提供编辑功能。这个委托接口的默认实现以每个标准视图的常规风格渲染项:QListView
,QTableView
,和 QTreeView
。
所有标准角色都由标准视图使用的新默认委托来处理。这些如何解释在QStyledItemDelegate
文档中有描述。
视图所使用的代表是由itemDelegate()
函数返回的。函数setItemDelegate()
允许您为标准视图安装自定义代表,并在设置自定义视图的代表时,需要使用此函数。
简单的代表#
此处实现的代表使用QSpinBox
提供编辑功能,主要用于与显示整数的模型一起使用。尽管我们为此目的设置了基于自定义整数的表格模型,但我们也可以轻松地使用QStandardItemModel,因为自定义代表控制数据输入。我们构建了一个表格视图来显示模型的内容,这将使用自定义代表进行编辑。
我们从QStyledItemDelegate
派生代表,因为我们不想编写自定义显示函数。但是,我们必须仍然提供管理编辑小部件的函数
class SpinBoxDelegate(QStyledItemDelegate): Q_OBJECT # public SpinBoxDelegate(QObject parent = None) QWidget createEditor(QWidget parent, QStyleOptionViewItem option, QModelIndex index) override def setEditorData(editor, index): def setModelData(editor, model,): QModelIndex index) override def updateEditorGeometry(editor, option,): QModelIndex index) override def __init__(self, parent): super().__init__(parent)
注意,在构建代表时没有设置任何编辑小部件。我们仅在需要时构建编辑小部件。
提供编辑器#
在此示例中,当表格视图需要提供编辑器时,它会要求代表提供适合正在修改的项的适当编辑器小部件。提供的createEditor()
函数包含代表设置合适小部件所需的一切。
QWidget SpinBoxDelegate.createEditor(QWidget parent, QStyleOptionViewItem /* option */, QModelIndex /* index */) editor = QSpinBox(parent) editor.setFrame(False) editor.setMinimum(0) editor.setMaximum(100) return editor
请注意,我们不需要保留编辑小部件的指针,因为视图负责在不再需要时销毁它。
我们在编辑器上安装代表默认事件过滤器,以确保提供用户期望的标准编辑快捷键。可以添加更多快捷键以允许更复杂的操作;这些在编辑提示部分中有讨论。
视图通过调用我们后来为此目的定义的函数,确保编辑器数据和几何形状设置正确。我们可以根据视图提供的模型索引创建不同的编辑器。例如,如果我们有一个整数列和一个字符串列,我们可以返回一个QSpinBox
或一个QLineEdit
,具体取决于正在编辑哪一列。
代表必须提供一个能将模型数据复制到编辑器的函数。在此示例中,我们读取存储在显示角色中的数据,并根据此设置计数器的值。
def setEditorData(self, editor,): QModelIndex index) value = index.model().data(index, Qt.EditRole).toInt() spinBox = QSpinBox(editor) spinBox.setValue(value)
在此示例中,我们知道编辑器小部件是一个计数器,但我们可以为模型中的不同类型的数据提供不同的编辑器,在这种情况下,在访问其成员函数之前,我们需要将小部件强制转换成适当的数据类型。
将数据提交到模型#
当用户完成对旋转框中的值进行编辑后,视图将通过调用 setModelData()
函数让委托将编辑后的值存储到模型中。
def setModelData(self, editor, model,): QModelIndex index) spinBox = QSpinBox(editor) spinBox.interpretText() value = spinBox.value() model.setData(index, value, Qt.EditRole)
因为视图管理委托的编辑器小部件,所以我们只需要用编辑器提供的内容更新模型。在这种情况下,我们确保旋转框是最新的,并用指定的索引更新模型中的值。
标准的 QStyledItemDelegate
类在完成编辑后,通过发射 closeEditor()
信号来通知视图。视图负责确保编辑器小部件被关闭并销毁。在这个例子中,我们只提供简单的编辑功能,因此我们永远不会发射此信号。
所有的数据操作都是通过 QAbstractItemModel 提供的接口执行的。这使得委托在处理不同类型的数据时大部分是独立的,但为了使用某些类型的编辑器小部件,必须做出一些假设。在这个例子中,我们假设模型总是包含整数值,但我们可以使用这个委托与不同类型的模型一起使用,因为 QVariant 为意外数据提供了合理的默认值。
更新编辑器的几何形状#
委托负责管理编辑器的几何形状。当创建编辑器时以及当项的大小或位置在视图中改变时必须设置几何形状。幸运的是,视图在 view option
对象内提供了所有必要的几何信息。
def updateEditorGeometry(self, editor,): QStyleOptionViewItem option, QModelIndex /* index */) editor.setGeometry(option.rect)
在这种情况下,我们只是使用视图选项在项矩形内提供的几何信息。用几个元素渲染项的委托不会直接使用项矩形。它会将编辑器定位在项中的其他元素相对于的位置。
编辑提示#
编辑完成后,委托应向其他组件提供有关编辑过程结果的信息,并提供将辅助任何后续编辑操作的建议。这通过发射带有适当提示的 closeEditor()
信号来实现。这由默认的 QStyledItemDelegate
事件过滤器来处理,我们在旋转框构造时安装了该过滤器。
可以对旋转框的行为进行调整,使其更具用户友好性。在默认的事件过滤器中,当用户在旋转框中按下Enter键以确认选择时,代理会将值提交到模型并关闭旋转框。我们可以通过在旋转框上安装自己的事件过滤器来更改此行为,并提供适合我们需求的编辑提示;例如,我们可以发送带有EditNextItem
提示的closeEditor()
以自动开始编辑视图中的下一个项目。
另一种不需要使用事件过滤器的方法是提供我们自己的编辑小部件,也许为了方便而继承自QSpinBox
。这种替代方法将以编写额外代码为代价,让我们能够更好地控制编辑小部件的行为。如果您需要自定义标准Qt编辑小部件的行为,通常在代理中安装事件过滤器会更简单。
代理无需发出这些提示,但那些未发出的将不如发出提示以支持常见编辑操作的那些集成到应用程序中,也不那么容易使用。
在项目视图中处理选择#
概念#
项目视图类中使用的选择模型基于模型/视图架构的设施提供了基于选择的通用描述。尽管用于操纵选择的标准类对于提供的项目视图是足够的,但选择模型允许您创建专化的选择模型来满足您自己的项目模型和视图的要求。
在视图中选定的项目信息存储在QItemSelectionModel类的实例中。这维护了单个模型中的项目索引,并且独立于任何视图。由于可以对模型有多个视图,因此可以在视图之间共享选择,允许应用程序以一致的方式显示多个视图。
选择由选择范围组成。这些通过只记录每个选择项目范围的起始和结束模型索引来有效地维护有关大型选择的项目信息。通过使用多个选择范围来描述选择,可以构建非连续选择的项目。
选择应用于由选择模型持有的模型索引集合。最近应用的项目选择被认为是当前选择。即使应用之后,也可以通过使用某些类型的 选择命令来修改此选择的效果。这些将在本节的后面进行讨论。
当前项目和选定项目#
在视图中,始终有一个当前项目和选定项目 - 两种独立的状态。一个项目可以同时是当前项目和选定项目。视图负责确保始终有一个当前项目,例如,键盘导航需要当前项目。
下表突出显示了当前项目和选定项目之间的差异。
当前项目
选定项目
只有一个当前项目。
可以有多个选定项目。
当前项目将通过按键导航或鼠标按钮点击来更改。
项目的选中状态根据多个预定义模式(例如单选、多选等)设置或取消选中,取决于用户与项目交互的方式。
如果按下编辑键F2或双击项(假定编辑功能已开启),当前项将被编辑。
当前项可以与锚点一起使用,以指定应该选中或取消选中的范围(或两者的组合)。
当前项由焦点矩形指示。
选中项由选择矩形指示。
在操作选择时,通常将QItemSelectionModel视为一个记录了项目模型中所有项的选中状态的记录。一旦设置了选择模型,就可以选择、取消选择项集合,或无需知道哪些项已经被选中就可以切换其选中状态。任何时候都可以检索所有选中项的索引,并且可以通过信号和槽机制通知其他组件有关选择模型的变化。
使用选择模型#
标准的视图类提供了默认的选择模型,可以用于大多数应用程序。可以使用视图的selectionModel()
函数获取属于一个视图的选择模型,并用setSelectionModel()
与多个视图共享,因此通常不需要构建新的选择模型。
通过指定模型和一对指向QItemSelection的模型索引来创建一个选择。这使用指数来引用给定模型中的项,并将它们解释为选定项块中的左上角和右下角项。要将选择应用于模型中的项,需要将选择提交给选择模型;这可以通过多种方式实现,每种方式都会对选择模型中现有的选择产生不同的影响。
选择项#
为了演示一些选择的主要功能,我们构建了一个具有总数32个项的自定义表模型实例,并打开一个表视图来显示其数据
model = TableModel(8, 4, app) table = QTableView(0) table.setModel(model) selectionModel = table.selectionModel()
检索表视图的默认选择模型以备后用。我们不对模型中的任何项进行修改,而是选择一些视图将在表顶部左侧显示的项。为此,我们需要检索对应于要选中区域左上角和右下角项的模型索引
topLeft = QModelIndex() bottomRight = QModelIndex() topLeft = model.index(0, 0, QModelIndex()) bottomRight = model.index(5, 2, QModelIndex())
为了在模型中选择这些项,并查看表视图中相应的变化,我们需要构造一个选择对象并将其应用于选择模型
selection = QItemSelection(topLeft, bottomRight) selectionModel.select(selection, QItemSelectionModel.Select)
使用由选择标志组合定义的命令将选择应用于选择模型。在这种情况下,所用的标志会导致记录在选择对象中的项被包括在选择模型中,而不管它们之前的状态如何。结果是视图显示出的选择。
可以使用由选择标志定义的各种操作修改项的选择。这些操作产生的选择可能有复杂的结构,但它通过选择模型有效地表示。在更新选择时,将描述如何使用不同的选择标志来操作选中项。
读取选择状态#
选择模型中存储的模型索引可以使用selectedIndexes()函数进行读取。这个函数返回一个未排序的模型索引列表,我们可以遍历这个列表,只要我们知道它们对应哪个模型即可。
indexes = selectionModel.selectedIndexes() for index in indexes: text = QString("(%1,%2)").arg(index.row()).arg(index.column()) model.setData(index, text)
上述代码使用基于范围的for循环迭代并修改选择模型返回的索引对应的项。
选择模型会发出信号来指示选择的变化。这些信号会通知其他组件关于整个选择以及当前项目模型中聚焦项的变化。我们可以将selectionChanged()信号连接到一个槽,当选择发生变化时,检查模型中选中和未选中的项。该槽使用两个QItemSelection对象调用:一个包含对应新选中项的索引列表;另一个包含对应新未选中项的索引。
在以下代码中,我们提供了一个接收selectionChanged()信号的槽,它使用提供的字符串填写选中的项,并清除未选中项的内容。
def updateSelection(self, selected,): QItemSelection deselected) items = selected.indexes() for index in items: text = QString("(%1,%2)").arg(index.row()).arg(index.column()) model.setData(index, text) items = deselected.indexes() for index in items: model.setData(index, QString())
我们可以通过将currentChanged()信号连接到一个槽,并使用带有两个模型索引的调用信息来跟踪当前聚焦的项。这些索引对应于之前聚焦的项和当前聚焦的项。
在以下代码中,我们提供了一个接收currentChanged()信号的槽,并使用提供的信息更新QMainWindow
的状态栏。
def changeCurrent(self, current,): 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命令反转指定项的选中状态,将第二个项的选择应用于上述表模型。
toggleSelection = QItemSelection() 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标志的使用。
columnSelection = QItemSelection() topLeft = model.index(0, 1, QModelIndex()) bottomRight = model.index(0, 2, QModelIndex()) columnSelection.select(topLeft, bottomRight) selectionModel.select(columnSelection, QItemSelectionModel.Select | QItemSelectionModel.Columns) rowSelection = QItemSelection() topLeft = model.index(0, 0, QModelIndex()) bottomRight = model.index(1, 0, QModelIndex()) rowSelection.select(topLeft, bottomRight) selectionModel.select(rowSelection, QItemSelectionModel.Select | QItemSelectionModel.Rows)
尽管只向选择模型提供了四个索引,但使用Columns和Rows选择标志意味着选中了两列和两行。以下图像显示了这两个选择的结果。
对示例模型执行的所有命令都涉及到在模型中累积项的选择。我们还可以清除选择,或者用新的选择替换当前选择。
要将当前选择替换为新的选择,请将其他选择标志与当前标志组合使用。使用此标志的命令指示选择模型将其当前模型索引集合替换为在select()调用中指定的索引集合。要在开始添加新的选择之前清除所有选择,请将其他选择标志与清除标志组合使用。这将重置选择模型模型索引集合。
选择模型中的所有项目#
要选择模型中的所有项目,需要在模型每个级别上创建一个选择,使其覆盖该级别的所有项目。我们通过获取与给定父索引对应的最左上角和最右下角项目的索引来完成此操作
topLeft = model.index(0, 0, parent) bottomRight = model.index(model.rowCount(parent)-1, model.columnCount(parent)-1, parent)
使用这些索引和模型构建选择。然后在选择模型中选择相应的项目
selection = QItemSelection(topLeft, bottomRight) selectionModel.select(selection, QItemSelectionModel.Select)
这需要在模型的所有级别上执行。对于顶级项目,我们会像往常一样定义父索引
parent = QModelIndex()
对于层次结构模型,使用hasChildren()函数确定任何给定的项目是否是另一个级别项目的父项。
创建新的模型#
模型/视图组件之间功能分离,这使我们可以创建能够利用现有视图的模型。这种做法允许我们使用标准的图形用户界面组件(例如QListView
、QTableView
和QTreeView
)呈现来自各种来源的数据。
QAbstractItemModel类提供一个接口,足够灵活,以支持在层次结构中排列信息的数据源,允许数据以某种方式插入、删除、修改或排序。它还提供了对拖放操作的支持。
QAbstractListModel和QAbstractTableModel类为简单的非层次结构数据结构接口提供支持,作为简单的列表和表格模型的起点更容易使用。
在本节中,我们创建一个简单的只读模型,以探索模型/视图架构的基本原则。在本节的后面部分,我们将这个简单的模型修改为允许用户修改项目。
有关更复杂模型的示例,请参阅Simple Tree Model
示例。
QAbstractItemModel子类的需求在Model Subclassing Reference文档中描述得更详细。
设计模型#
为现有数据结构创建新的模型时,需要考虑应使用哪种类型的模型以提供对数据的接口。如果数据结构可以表示为项目的列表或表格,可以子类化QAbstractListModel或QAbstractTableModel,因为这些类为许多函数提供了合适的默认实现。
但是,如果底层数据结构只能通过层次结构树结构表示,则需要子类化QAbstractItemModel。在Simple Tree Model
示例中采取了这种做法。
在本节,我们实现了一个基于字符串列表的简单模型,因此QAbstractListModel提供了一个理想的基类来构建。
无论底层的数据结构形式如何,通常来说,在特定的模型中补充标准的QAbstractItemModel API是一个好主意,它允许更自然地访问底层的数据结构。这使得在模型中填充数据变得更容易,同时仍然允许其他通用的模型/视图组件使用标准API与它互动。下面描述的模型提供了一个用于此目的的自定义构造函数。
一个只读示例模型#
这里实现的模型是基于标准QStringListModel类的简单、非分层、只读数据模型。它有一个QStringList作为其内部数据源,并仅实现了使其成为功能模型的必要部分。为了简化实现,我们通过继承QAbstractListModel,因为这为列表模型定义了合理的默认行为,并且它提供了一个比QAbstractItemModel类更简单的接口。
实现模型时,重要的一点是要记住,QAbstractItemModel本身不存储任何数据,它只是提供了一个视图用来访问数据的接口。对于一个最小的只读模型,只需要实现少数几个函数,因为大多数接口都有默认实现。类声明如下
class StringListModel(QAbstractListModel): def __init__(self, strings, parent=None): ... def rowCount(self, parent=QModelIndex()): ... def data(self, index, role): ... def headerData(self, section, orientation, role=Qt.DisplayRole): ... def flags(self, index): ... def setData(self, index, value, role=Qt.EditRole): ... def insertRows(self, position, rows, parent): ... def removeRows(self, position, rows, parent): ... role = Qt.DisplayRole) override() # private stringList = QStringList()
除了模型的构造函数之外,我们只需要实现两个函数:rowCount()返回模型中的行数,data()返回与指定的模型索引相对应的数据项。
表现良好的模型还应该实现headerData(),为树形和表格视图提供在标题中显示的内容。
请注意,这是一个非分层模型,因此我们不必担心父子关系。如果我们的模型是分层的,我们也必须实现index()和parent()函数。
字符串列表存储在内部的stringList
私有成员变量中。
模型尺寸#
我们希望模型中的行数与字符串列表中的字符串数量相同。我们以此为思路来实现rowCount()函数
class StringListModel(QAbstractListModel): def __init__(self, strings, parent=None): super().__init__(parent) self._strings = strings
由于模型是非分层的,我们可以安全地忽略与父项对应的模型索引。默认情况下,从QAbstractListModel派生的模型只包含一个列,所以我们不需要重新实现columnCount()函数。
模型标题和数据#
对于视图中的项,我们希望返回字符串列表中的字符串。data()函数负责返回与索引参数相对应的数据项。
def data(self, QModelIndex index, int role): if not 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()函数来提供有关标题的信息
def headerData(self, section, orientation, role=Qt.DisplayRole): """Returns the appropriate header string depending on the orientation of the header and the section. If anything other than the display role is requested, we return an invalid variant.""" if role != Qt.DisplayRole: return None if orientation == Qt.Horizontal: return f"Column {section}" return f"Row {section}"
同样,只有当我们支持的角色时,我们才返回一个有效的QVariant。在确定返回的确切数据时,还会考虑到标题的方向。
并非所有视图都显示包含项目数据的标题,即使显示了,也可能被配置为隐藏。然而,建议您实现headerData()函数来提供模型提供的数据的相关信息。
一个项可以有几个角色,根据指定的角色提供不同的数据。我们模型中的项只有一个角色,即DisplayRole,所以我们返回与指定的角色无关的数据项。然而,我们可以在DisplayRole提供的数据在其他角色中进行重复使用,如在ToolTipRole中,视图可以使用此角色在工具提示中显示有关项目的信息。
可编辑的模型#
只读模型展示了如何向用户提供简单选择,但对于许多应用程序而言,一个可编辑的列表模型更有用。我们可以通过更改用于只读的data()函数以及实现两个额外函数:flags()和setData()来修改只读模型,使项可编辑。以下函数声明被添加到类定义中
Qt.ItemFlags flags(QModelIndex index) override bool setData(QModelIndex index, QVariant value, role = Qt.EditRole) override()
使模型可编辑#
在创建编辑器之前,委托检查项是否可编辑。模型必须让委托知道其项是可编辑的。我们通过为模型中每个项返回正确的标志来实现这一点;在这种情况下,我们启用所有项,并使它们可选择和可编辑
def flags(self, index): """Returns an appropriate value for the item's flags. Valid items are enabled, selectable, and editable.""" if not index.isValid(): return Qt.ItemIsEnabled return super().flags(index) | Qt.ItemIsEditable
请注意,我们不必了解委托如何执行实际的编辑过程。我们只需提供一种方法供委托在模型中设置数据。这是通过setData()函数实现的
def setData(self, index, value, role=Qt.EditRole): """Changes an item in the string list, but only if the following conditions are met: # The index supplied is valid. # The index corresponds to an item to be shown in a view. # The role associated with editing text is specified. The dataChanged() signal is emitted if the item is changed.""" if index.isValid() and role == Qt.EditRole: self._strings[index.row()] = value self.dataChanged.emit(index, index, {role}) return True return False
在此模型中,对应于模型索引的字符串列表中的项被所提供的值替换。然而,在我们可以修改字符串列表之前,我们必须确保索引有效,项是正确的类型,并且角色被支持。按照惯例,我们坚持认为角色是EditRole,因为这是标准项委托使用的角色。对于布尔值,然而,您可以使用Qt::CheckStateRole并设置Qt::ItemIsUserCheckable标志;在这种情况下,将使用复选框来编辑值。在这个模型的所有角色中,底层数据都是相同的,所以这个细节只是使其更容易与标准组件集成。
当数据被设置后,模型必须让视图知道某些数据已更改。这是通过发射dataChanged()信号实现的。因为只有一个数据项已更改,因此在信号中指定项的范围仅限于一个模型索引。
而且,需要更改data()函数以添加Qt::EditRole测试
def data(self, index, role): """Returns an appropriate value for the requested data. If the view requests an invalid index, an invalid variant is returned. Any valid index that corresponds to a string in the list causes that string to be returned.""" row = index.row() if not index.isValid() or row >= len(self._strings): return None if role != Qt.DisplayRole and role != Qt.EditRole: return None return self._strings[row]
插入和删除行#
可以更改模型中的行和列的数量。在字符串列表模型中,只改变行数是有意义的,所以我们只重新实现了插入和删除行的函数。这些在类定义中声明
bool insertRows(int position, int rows, QModelIndex index = QModelIndex()) override bool removeRows(int position, int rows, QModelIndex index = QModelIndex()) override
在此模型中,行对应于列表中的字符串,所以insertRows()函数在指定位置之前将插入一些空字符串到字符串列表中。插入字符串的数量相当于指定的行数。
父索引通常用于确定在模型中应添加行的位置。在这种情况下,我们只有一个字符串列表的顶级列表,所以我们只需在列表中插入空字符串。
def insertRows(self, position, rows, parent): """Inserts a number of rows into the model at the specified position.""" self.beginInsertRows(QModelIndex(), position, position + rows - 1) for row in range(rows): self._strings.insert(position, "") self.endInsertRows() return True <Code snippet "stringlistmodel/model.cpp:7" not found>
模型首先调用beginInsertRows()函数来通知其他组件行数即将更改。函数指定了要插入的第一行和最后一行的新行号以及它们父项的模型索引。在更改字符串列表后,它调用endInsertRows()来完成操作并通知其他组件模型的维度已更改,返回true以指示成功。
删除模型中的行函数也很容易编写。要删除的行通过位置和给定的行数指定。为了简化实现,我们忽略了父索引,直接从字符串列表中删除相应的项。
def removeRows(self, position, rows, parent): """Removes a number of rows from the model at the specified position.""" self.beginRemoveRows(QModelIndex(), position, position + rows - 1) for row in range(rows): del self._strings[position] self.endRemoveRows() return True <Code snippet "stringlistmodel/model.cpp:9" not found>
beginRemoveRows() 函数总是在删除任何底层数据之前被调用,并指定要删除的第一行和最后一行。这样可以让其他组件在数据变得不可用之前访问它。删除行之后,模型将发出endRemoveRows()以完成操作,并让其他组件知道模型的尺寸已更改。
下一步操作#
我们可以使用QListView
类来显示由模型提供的数据或任何其他模型数据,以垂直列表的形式呈现模型的项。对于字符串列表模型,此视图还提供了一个默认编辑器,以便可以操作这些项。我们查看在视图类中可用的标准视图类提供的可能性。
模型子类化参考文档详细讨论了QAbstractItemModel子类的需求,并提供了一个指南,说明了必须实现哪些虚拟函数才能在不同类型的模型中启用各种功能。
项目视图便利类#
基于项目的部件名称反映了它们的用途:QListWidget
提供项目列表,QTreeWidget
显示多级树结构,而QTableWidget
提供单元格项的表格。每个类都继承自QAbstractItemView
类,它实现了选中和头部管理的通用行为。
列表部件#
通常使用QListWidget
和多个QListWidgetItem
显示单级项目列表。列表部件的构建方式与其他部件相同
listWidget = QListWidget(self)
在构建时,可以直接将项目添加到列表部件中
QListWidgetItem(tr("Sycamore"), listWidget) QListWidgetItem(tr("Chestnut"), listWidget) QListWidgetItem(tr("Mahogany"), listWidget)
也可以在不带父列表部件的情况下构建,并在稍后添加到列表中
newItem = QListWidgetItem() newItem.setText(itemText) listWidget.insertItem(row, newItem)
列表中的每个项目都可以显示文本标签和图标。渲染文本时使用的颜色和字体可以更改,为项目提供定制的外观。工具提示、状态提示和“这是什么?”帮助等都容易配置,以确保列表适当集成到应用程序中。
newItem.setToolTip(toolTipText) newItem.setStatusTip(toolTipText) newItem.setWhatsThis(whatsThisText)
默认情况下,列表中的项目以它们创建的顺序呈现。根据Qt::SortOrder中给出的标准,可以对项目列表进行排序,以生成按正序或倒序字母顺序排序的项目列表。
listWidget.sortItems(Qt.AscendingOrder) listWidget.sortItems(Qt.DescendingOrder)
树部件#
QTreeWidget
和QTreeWidgetItem
类提供项目树或分层列表。树部件中的每个项目都可以有自己的子项目,并可以显示多个信息列。树部件的创建方法与任何其他部件相同
treeWidget = QTreeWidget(self)
在将条目添加到树小部件之前,必须设置列数。例如,我们可以定义两列,并为每列创建一个标题以在顶部提供标签
treeWidget.setColumnCount(2) headers = QStringList() headers << tr("Subject") << tr("Default") treeWidget.setHeaderLabels(headers)
设置每个节点的标签的最简单方法是提供一个字符串列表。对于更复杂的标题,您可以创建一个树项目,按照自己的意愿进行装饰,并将其用作树小部件的标题。
在树小部件中,顶层项目是以树小部件作为其父小部件构建的。它们可以按任意顺序插入,或者通过在构建每个项目时指定前一个项目,确保它们按特定顺序列出
cities = QTreeWidgetItem(treeWidget) cities.setText(0, tr("Cities")) osloItem = QTreeWidgetItem(cities) osloItem.setText(0, tr("Oslo")) osloItem.setText(1, tr("Yes")) planets = QTreeWidgetItem(treeWidget, cities)
树小部件对待顶层项目与处理树中较深处的其他项目略有不同。可以通过调用树小部件的 takeTopLevelItem()
函数从树的第一层移除条目,但通过调用较低级别条目的父项目 takeChild()
函数移除这些条目的子项。使用 insertTopLevelItem()
函数将项目插入树的第一层。在较低的层次结构中,使用父项目的 insertChild()
函数。
要轻松地在树的第一层和较低的层次间移动项目,只需检查项目是否是顶层项目,此信息由每个项的 parent()
函数提供。例如,我们可以无论其位置如何移除树小部件中的当前项
parent = currentItem.parent() index = int() if parent: index = parent.indexOfChild(treeWidget.currentItem()) del parent.takeChild(index) else: index = treeWidget.indexOfTopLevelItem(treeWidget.currentItem()) del treeWidget.takeTopLevelItem(index)
在树小部件的其他位置插入项目遵循相同的模式
parent = currentItem.parent() newItem = QTreeWidgetItem() if parent: newItem = QTreeWidgetItem(parent, treeWidget.currentItem()) else: newItem = QTreeWidgetItem(treeWidget, treeWidget.currentItem())
表格小部件#
类似于在电子表格应用程序中找到的项的表格是由 QTableWidget
和 QTableWidgetItem
构建的。它们提供了一个带有标题和项目的可滚动表格小部件,可以在其中使用。
表可以以固定数量的行和列创建,或者根据需要将它们添加到未设置大小的表中。
tableWidget = QTableWidget() tableWidget = QTableWidget(12, 3, self)
在将项目添加到表格的所需位置之前,在表格外部构建这些项目
newItem = QTableWidgetItem(tr("%1").arg(() pow(row, column+1))) tableWidget.setItem(row, column, newItem)
可以通过在表格外部构建项目并将其用作标题来向表格添加水平和垂直标题
valuesHeaderItem = QTableWidgetItem(tr("Values")) tableWidget.setHorizontalHeaderItem(0, valuesHeaderItem)
请注意,表格中的行和列从零开始。
常见特性#
有多个基于项目的特性是可以通过在每个类中相同的接口在所有便利类之间通用的。我们在以下章节中展示了这些特性,并提供了一些不同小部件的示例。请查看每个小部件的 模型/视图类 列表,以获取有关每个函数使用的更多详细信息。
选择#
项目的选择方式由小部件的选择模式(SelectionMode
)控制。此属性控制用户可以选中一个或多个项目,在现场选择中,是否必须是连续的项目范围。选择模式对上述所有小部件的工作方式相同。
单个项目选择:当用户需要从一个小部件中选择单个项目时,默认的
SingleSelection
模式最为合适。在此模式下,当前项目和选中项目相同。多项选择:在此模式下,用户可以切换小部件中任何项目的选择状态,而不会改变现有的选择,就像非排他性复选框可以独立切换一样。
扩展选择:对于经常需要选择许多相邻项的控件,例如电子表格中找到的控件,需要
ExtendedSelection
模式。在此模式中,可以使用鼠标和键盘选择小部件中连续项目范围。如果使用修饰键,还可以创建涉及许多不与控件中其他选中项相邻的项目的高级选择。如果用户在不使用修饰键的情况下选择项目,现有的选择将被清除。
使用 selectedItems()
函数读取小部件中的选中项目,提供一个可以迭代的有关项目列表。例如,我们可以使用以下代码找到选中列表内所有数字值的总和
> selected = tableWidget.selectedItems() number = 0 total = 0 for item in selected: ok = bool() value = item.text().toDouble(ok) if ok and not item.text().isEmpty(): total += value number = number + 1
注意,对于单选模式,当前项将在选择中。在多选和扩展选择模式下,当前项可能不在选择中,这取决于用户形成选择的方式。
搜索#
通常能够在项目查看小部件中找到项目很方便,无论是作为开发者还是作为向用户提供的服务。所有三个项目查看便利类都提供了一个共同的 findItems()
函数,使这一切尽可能一致和简单。
根据 Qt::MatchFlags 选择的值指定的条件,根据所含文本搜索项目。我们可以通过 findItems()
函数获取匹配项目列表
> found = treeWidget.findItems( itemText, Qt.MatchWildcard) for item in found: item.setSelected(True) # Show the item->text(0) for each item.
上面的代码将在包含搜索字符串给定文本的树控件中选择项。此模式也可用于列表和小部件。
在项目视图中使用拖放#
Qt 的拖放基础设施由模型/视图框架完全支持。列表、表格和树中的项目可以在视图中拖动,并且可以将数据作为 MIME 编码的数据导入和导出。
标准视图自动支持内部拖放,即通过移动项目以改变它们显示的顺序。默认情况下,这些视图不支持拖放,因为它们是为最简单、最常用的用途配置的。要允许项目拖动,必须启用视图的一些属性,并且项目本身必须允许拖放发生。
仅允许从视图中导出项目且不允许向其中拖入数据的模型需求比完整拖放模型的要少。
有关在新模型中启用拖放支持的信息,请参阅模型子类化参考。
使用便利视图#
与QListWidget
, QTableWidget
, 和 QTreeWidget
一起使用的每种项目类型默认使用不同的标志集进行配置。例如,每个QListWidgetItem
或 QTreeWidgetItem
默认启用、可勾选、可选中,并可以用作拖放操作的数据源;每个QTableWidgetItem
也可用于编辑,并作为拖放操作的目标。
尽管所有标准项目都有一个或两个拖放标志被设置,但通常您需要设置视图中的一些属性来利用内置的拖放支持。
要启用项目拖动,请将视图的
dragEnabled
属性设置为true
。要允许用户在视图中内嵌或外嵌项目,请将视图的
viewport()
的acceptDrops
属性设置为true
。要显示用户如果放下当前拖动的项目将放置的位置,请设置视图的
showDropIndicator
属性。这为用户提供了关于项目在视图内放置的持续更新信息。
例如,我们可以通过以下代码行来在列表视图中启用拖放:
listWidget = QListWidget(self) listWidget.setSelectionMode(QAbstractItemView.SingleSelection) listWidget.setDragEnabled(True) listWidget.viewport().setAcceptDrops(True) listWidget.setDropIndicatorShown(True)
结果是一个列表视图,允许项目在视图内复制,甚至允许用户将项目拖动到包含相同类型数据的视图中。在两种情况下,项目都是复制而不是移动。
要启用用户在视图中移动项目,我们必须设置列表视图的dragDropMode
。
listWidget.setDragDropMode(QAbstractItemView.InternalMove)
使用模型/视图类#
为拖放设置视图遵循与便捷视图相同的模式。例如,一个 QListView
可以用与 QListWidget
相同的方式设置
listView = QListView(self) listView.setSelectionMode(QAbstractItemView.ExtendedSelection) listView.setDragEnabled(True) listView.setAcceptDrops(True) listView.setDropIndicatorShown(True)
由于视图显示的数据受模型控制,因此所使用的模型也必须支持拖放操作。模型支持的操作可以通过重新实现 QAbstractItemModel::supportedDropActions() 函数来指定。例如,使用以下代码启用复制和移动操作
def supportedDropActions(self): return Qt.CopyAction | Qt.MoveAction
尽管可以给出 Qt::DropActions 值的任何组合,但模型需要编写代码以支持它们。例如,要允许 Qt::MoveAction 正确使用在列表模型中,模型必须提供 QAbstractItemModel::removeRows() 的实现,无论是直接提供还是从其基类继承实现。
为项目启用拖放#
模型通过重新实现 QAbstractItemModel::flags() 函数以提供合适的标志来告知视图哪些项目可以拖动,以及哪些将接受拖放。
例如,一个基于 QAbstractListModel 的简单列表模型可以通过确保返回的标志包含 Qt::ItemIsDragEnabled 和 Qt::ItemIsDropEnabled 值来为每个项目启用拖放
def flags(self, index): default_flags = super().flags(index) if index.isValid(): return Qt.ItemIsDragEnabled | Qt.ItemIsDropEnabled | default_flags return Qt.ItemIsDropEnabled | default_flags
注意,项目可以放下到模型的顶层,但拖动仅对有效项目启用。
在上面的代码中,由于模型是从 QStringListModel 继承而来的,我们通过调用它的 flags() 函数的实现来获取默认的标志集。
导出数据的编码#
当将数据项从模型在拖放操作中导出时,它们被编码到一个或多个 MIME 类型的适当格式。模型通过重新实现 QAbstractItemModel::mimeTypes() 函数,返回标准 MIME 类型的列表来声明它们可以提供的 MIME 类型。
例如,仅提供纯文本的模型将提供以下实现
def mimeTypes(self): return ["application/vnd.text.list"]
模型还必须提供代码以将数据编码成广告的格式。这是通过重新实现 QAbstractItemModel::mimeData() 函数来提供一个 QMimeData 对象来实现的,就像在其他任何拖放操作中一样。
以下代码演示了如何将每个数据项(对应给定的索引列表)编码为纯文本并存储在 QMimeData 对象中。
def mimeData(self, indexes): mime_data = QMimeData() encoded_data = QByteArray() stream = QDataStream(encoded_data, QIODevice.WriteOnly) for index in indexes: if index.isValid(): text = self.data(index, Qt.DisplayRole) stream.writeQString(text) mime_data.setData("application/vnd.text.list", encoded_data) return mime_data
由于提供了模型索引列表到函数中,这种方法足够通用,可以在层次和非层次模型中使用。
注意,必须将自定义数据类型声明为元对象,并且必须为它们实现流运算符。有关详细信息,请参阅 QMetaObject 类描述。
将拖放数据插入模型#
任何给定模型处理拖放数据的方式不仅取决于其类型(列表、表格或树),还取决于其内容将如何向用户展示。通常,用于适应拖放数据的方法应该是最适合模型底层数据存储的方法。
不同类型的模型在处理丢失的数据采用的策略不同。列表和表格模型只提供一种扁平结构,其中数据项被存储。因此,当数据被拖放到视图中的现有项目时,它们可能会插入新的行(和列),或者使用提供的一些数据覆盖模型中项目的内容。树模型通常能够将其底层数据存储中添加包含新数据的孩子项目,因此在用户看来表现得更加可预测。
模型通过重新实现 QAbstractItemModel::dropMimeData() 来处理丢失的数据。例如,处理字符串简单列表的模型可以提供一个实现,它可以单独处理已存在项目的数据,以及拖放到模型顶层的数据(即拖放到无效项目)。
模型可以通过重新实现 QAbstractItemModel::canDropMimeData() 来禁止在特定的项目上丢载数据,或者根据丢载数据来。
模型首先必须确保操作应该被执行,提供的数据是可用的格式,且在模型内的目的地是有效的。
def canDropMimeData(self, data, action, row, column, parent): if not data.hasFormat("application/vnd.text.list"): return False if column > 0: return False return True def dropMimeData(self, data, action, row, column, parent): if not self.canDropMimeData(data, action, row, column, parent): return False if action == Qt.IgnoreAction: return True
一个简单的单列字符串列表模型可能会在提供的数据不是纯文本或指定的列号无效时指示失败。
要插入到模型中的数据是否被插入到现有项目取决于是否将其拖到现有项目上。在这个简单示例中,我们希望允许在现有项目之间、在列表的第一个项目之前以及最后一个项目之后进行丢载。
当发生丢载时,对应的父项目模型索引要么是有效的,表示丢载发生在项目上,要么是无效的,表示丢载发生在视图中的某个地方(例如,模型的顶层)。
begin_row = 0 if row != -1: begin_row = row
我们最初检查提供的行号,看看能否使用它将项目插入到模型中,无论父索引是否有效。
elif parent.isValid(): begin_row = parent.row()
如果父模型索引是有效的,这意味着在项目上发生了丢载。在这个简单的列表模型中,我们找到项目的行号,并使用该值将丢失的项目插入到模型的顶层。
else: begin_row = self.rowCount(QModelIndex())
当在视图的另一个地方发生丢载,且行号不可用,我们将项目附加到模型的顶层。
在层次模型中,当在项目上发生丢载时,将新项目作为该项目的子项插入到模型中会更好。在下面这个简单示例中,模型只有一个层级,所以这种方法不适用。
导入数据的解码#
dropMimeData() 的每个实现也必须解码数据和将其插入到模型的基础数据结构中。
对于简单的字符串列表模型,编码的项目可以被解码并流式传输到 QStringList
encoded_data = data.data("application/vnd.text.list") stream = QDataStream(encoded_data, QIODevice.ReadOnly) new_items = [] while not stream.atEnd(): new_items.append(stream.readQString())
字符串然后可以插入到基础数据存储中。为了保持一致性,这可以通过模型自己的接口完成
self.insertRows(begin_row, len(new_items), QModelIndex()) for text in new_items: idx = self.index(begin_row, 0, QModelIndex()) self.setData(idx, text) begin_row += 1 return True
请注意,模型通常需要提供QAbstractItemModel::insertRows() 和 QAbstractItemModel::setData() 函数的实现。
代理模型#
在模型/视图框架中,单个模型提供的数据可以由任何数量的视图共享,每个视图都可能以完全不同的方式表示相同的信息。自定义视图和代理是提供同一数据的不同表示的有效方法。然而,应用程序通常需要提供对相同数据处理的传统视图,如不同排序的视图。
虽然将排序和过滤操作作为视图的内部函数执行似乎很合适,但这种方法不允许多个视图共享这类可能昂贵的操作结果。采用另一种方法,在模型本身中进行排序,会导致类似的问题,即每个视图都需要显示根据最新处理操作组织的数据项。
为了解决这个问题,模型/视图框架使用代理模型来管理模型和视图之间传递的信息。代理模型是充当普通模型从视图的角度看,并代表该视图从源模型中访问数据的组件。模型/视图框架使用的信号和槽确保无论在视图和源模型之间放置多少个代理模型,每个视图都适当地进行更新。
使用代理模型#
代理模型可以插入到现有模型和任意数量的视图之间。Qt提供了一个标准代理模型QSortFilterProxyModel,通常直接实例化并使用,但也可以扩展以提供自定义的过滤和排序行为。以下是如何使用QSortFilterProxyModel类:
filterModel = QSortFilterProxyModel(parent) filterModel.setSourceModel(stringListModel) filteredView = QListView() filteredView.setModel(filterModel)
由于代理模型继承自QAbstractItemModel,它们可以连接到任何类型的视图,并且可以在视图之间共享。它们也可以用于以管道排列处理来自其他代理模型的信息。
QSortFilterProxyModel类旨在在应用程序中直接实例化和使用。可以通过扩展此类并实现所需的比较操作来创建更多专门的代理模型。
自定义代理模型#
通常,代理模型中使用的处理类型涉及将每个数据项从源模型中的原始位置映射到代理模型中的另一个位置。在某些模型中,一些项目可能在代理模型中没有对应的项;这些模型是《过滤》代理模型。视图使用代理模型提供的模型索引访问项目,这些索引不包含有关源模型或原始项在模型中位置的任何信息。
QSortFilterProxyModel允许在将数据提供给视图之前从源模型中过滤数据,并允许将源模型的 内容以预先排序的数据的形式提供给视图。
自定义过滤模型#
QSortFilterProxyModel类提供的过滤模型相当灵活,可以用于各种常见情况。对于高级用户,可以扩展QSortFilterProxyModel,提供实现自定义过滤器的机制。
QSortFilterProxyModel的子类可以重新实现两个虚函数,这些函数在请求或使用代理模型中的模型索引时调用
filterAcceptsColumn()用于从源模型的一部分过滤特定列。
filterAcceptsRow()用于从源模型的一部分过滤特定行。
QSortFilterProxyModel中上述函数的默认实现返回true,以确保所有项都传递到视图中;这些函数的重新实现应返回false以过滤出单个行和列。
自定义排序模型#
QSortFilterProxyModel实例使用std::stable_sort()函数在源模型项和代理模型项之间设置映射,允许向视图公开排序后的项层次结构,而无需修改源模型的结构。要提供自定义排序行为,重新实现lessThan()函数以执行自定义比较。
模型子类化参考#
模型子类需要提供实现基类 QAbstractItemModel 中定义的大多数虚函数。需要实现的这些函数的数量取决于模型类型 - 模型是提供简单列表、表格,还是复杂的项目层次结构给视图。继承自 QAbstractListModel 和 QAbstractTableModel 的模型可以利用这些类提供的默认函数实现。必须提供 QAbstractItemModel 中许多虚函数实现的模型,以暴露树状结构中的项目数据。
在模型子类中需要实现的函数可以分为三类
处理项目数据:所有模型都需要实现函数,以使视图和委托能够查询模型的尺寸,检查项目,并检索数据。
导航和索引创建:层次结构模型需要提供视图可以调用的函数,以在它们暴露的树形结构中进行导航,并获取项目索引。
拖放支持和 MIME 类型处理:模型继承的函数控制内部和外部拖放操作的方式。这些函数允许使用 MIME 类型来描述数据项,其他组件和应用程序可以理解这些类型。
处理项目数据#
模型可以向它们提供的数据提供不同级别的访问:它们可以是简单的只读组件,某些模型可能支持调整大小操作,而其他模型可能允许编辑项目。
只读访问#
为了提供对模型提供的数据的只读访问,必须在模型的子类中实现以下函数
flags()
由其他组件使用,以获取有关模型提供的每个项目的信息。在许多模型中,标志的组合应包括 Qt::ItemIsEnabled 和 Qt::ItemIsSelectable。
data()
用于向视图和委托提供项数据。通常,模型只需要提供 Qt::DisplayRole 和任何应用程序特定的用户角色,但也推荐提供 Qt::ToolTipRole、Qt::AccessibleTextRole 和 Qt::AccessibleDescriptionRole 的数据。请参阅 Qt::ItemDataRole 枚举的文档,以获取有关与每个角色关联的类型的详细信息。
headerData()
为视图提供信息,以便在它们的标题中显示。只有能够显示标题信息的视图会检索这些信息。
rowCount()
提供模型公开的数据行数。
包括列表模型(QAbstractListModel 子类)和表格模型(QAbstractTableModel 子类)在内的所有类型的模型都必须实现这四个函数。
此外,以下函数 必须 在 QAbstractTableModel 和 QAbstractItemModel 的直接子类中实现
columnCount()
提供模型公开的数据列数。列表模型不提供此函数,因为 QAbstractListModel 已实现了它。
可编辑的项目#
可编辑的模型允许修改数据项,并且可能还提供允许插入和删除行和列的函数。为了启用编辑,以下函数必须正确实现
flags()
必须为每个项返回适当的标志组合。特别是,此函数返回的值必须包含 Qt::ItemIsEditable,除此外还包括应用于只读模型中的项的值。
setData()
用于修改与指定模型索引关联的数据项。为了能够接受用户输入,这种函数必须处理与Qt::EditRole关联的数据。实现还可以接受与Qt::ItemDataRole指定的许多不同类型的角色关联的数据。在更改数据项之后,模型必须发出dataChanged()信号来通知其他组件已更改。
setHeaderData()
用于修改水平和垂直表头信息。在更改数据项后,模型必须发出headerDataChanged()信号来通知其他组件更改。
可调整大小的模型#
所有类型的模型都可以支持行和列的插入和删除。表模型和层次模型也可以支持列和行的插入和删除。在改变发生之前和之后,通知其他组件关于模型尺寸的改变是很重要的。因此,可以实现以下函数来允许模型调整大小,但是实现必须确保调用适当的函数来通知附件的视图和代理。
insertRows()
用于向所有类型的模型添加新的行和数据项。实现必须在任何基本数据结构中插入新行之前调用beginInsertRows(),并且立即调用endInsertRows()。
removeRows()
用于从所有类型的模型中删除行和数据项。实现必须在任何基本数据结构中删除行之前调用beginRemoveRows(),并且立即调用endRemoveRows()。
insertColumns()
用于向表模型和层次模型添加新的列和数据项。实现必须在任何基本数据结构中插入新列之前调用beginInsertColumns(),并且立即调用endInsertColumns()。
removeColumns()
用于从表模型和层次模型中删除列和数据项。实现必须在任何基本数据结构中删除列之前调用beginRemoveColumns(),并且立即调用endRemoveColumns()。
通常,这些函数应在操作成功时返回true。然而,可能有情况,操作只部分成功;例如,如果插入的行数少于指定数目。在这种情况下,模型应该返回false来指示失败,以便任何附加的组件能夠处理这种情况。
在调整大小API实现中调用的函数发出的信号给附加组件提供了在数据变得不可用之前采取行动的机会。使用begin和end函数封装插入和删除操作,也能让模型正确地管理持久模型索引。
通常,begin和end函数有足够的能力通知其他组件模型的基本结构的变化。对于更复杂的模型结构的变化,可能包括内部重组、数据排序或其他结构的变更,有必要执行以下顺序:
发出layoutAboutToBeChanged()信号
更新表示模型结构的内部数据。
使用changePersistentIndexList()更新持久索引。
发出layoutChanged()信号。
这个序列可以用来替换更高层次和方便的保护方法进行任何结构更新。例如,如果一个有两百万行的模型需要删除所有奇数行,即一百万个由一个元素组成的非连续的范围。理论上可以调用一次beginRemoveRows和endRemoveRows,但这显然是不高效的。相反,可以将其作为一个单一的布局更改信号,一次更新所有必要的持久索引。
模型数据的懒加载#
模型数据惰性加载可以有效地推迟对模型信息的请求,直到视图实际上需要时才予以处理。
一些模型需要从远程源获取数据,或者必须执行耗时操作以获取关于数据组织方式的有关信息。由于视图通常请求尽可能多的信息以便准确显示模型数据,因此限制返回给它们的信息量可以减少对数据进行非必要跟进请求。
在层级模型中,找到给定项的孩子数量可能是一项昂贵的操作。在这种情况下,确保只有当必要时才调用模型 rowCount() 方法是很有用的。在这种情况下,可以重新实现 hasChildren() 函数,为视图提供一个低成本的检查孩子存在性的方法,并在 QTreeView
中为父项绘制适当的装饰。
无论重新实现 has_children() 返回 true 或 false,视图可能没有必要调用 rowCount() 来找出有多少孩子。例如,如果父项未被展开以显示它们,则 QTreeView
不需要知道有多少孩子。
如果已知将有多个项目具有孩子,则无条件返回 true 重新实现 has_children() 有时是一个有用的方法。这确保了可以在后续检查孩子,同时尽可能地加快模型数据的初始加载。唯一的缺点是,在用户尝试查看不存在的孩子项之前,可能某些视图中没有孩子的项显示不正确。
父项和子项#
由于向视图公开的结构由底层数据结构确定,因此每个模型子类都负责通过提供以下函数的实现来创建自己的模型索引:
index()
对于父项的模型索引,此函数允许视图和委托访问该项的孩子。如果无法找到与指定的行、列和父模型索引对应的有效的孩子项,则函数必须返回 QModelIndex(),这是一个无效的模型索引。
parent()
为任何给定孩子项提供对应于其父项的模型索引。如果指定的模型索引对应于模型中的顶级项,或者如果模型中没有有效的父项,则函数必须返回一个使用空的 QModelIndex() 构造函数创建的无效模型索引。
上述两个函数都使用 createIndex() 工厂函数来生成其他组件使用的索引。模型通常向此函数提供一些唯一标识符,以确保模型索引可以稍后与其对应项重新关联。
拖放支持和多模式类型处理#
模型/视图类支持拖放操作,提供了对于许多应用程序足够的默认行为。然而,也可以自定义拖放操作中对项的编码方式,它们是默认复制还是移动,以及它们如何插入到现有模型中。
此外,便捷视图类实现了特定行为,这些行为应该与现有开发者预期的行为紧密一致。《便捷视图》部分概述了这种行为。
MIME数据#
默认情况下,内置的模型和视图使用内部MIME类型(application/x-qabstractitemmodeldatalist
)来传递有关模型索引的信息。这指定了项目列表的数据,包含每个项目的行和列号,以及每个项目支持的角色信息。
使用此MIME类型编码的数据可以通过调用QAbstractItemModel::mimeData()并传入包含要序列化的项目的QModelIndexList来获取。
在实现自定义模型的拖放支持时,可以通过重新实现以下函数来以一种特殊格式导出数据项
mimeData()
此函数可以重新实现以返回除默认的
application/x-qabstractitemmodeldatalist
内部MIME类型之外的数据格式。子类可以从基类获取默认的QMimeData对象,并以额外的格式向其中添加数据。
对于许多模型,提供以MIME类型如text/plain
和image/png
表示的常用格式的项目内容是有用的。请注意,可以使用QMimeData::setImageData()、QMimeData::setColorData()和QMimeData::setHtml()函数轻松地将图像、颜色和HTML文档添加到QMimeData对象中。
接受拖放数据#
当在视图中执行拖放操作时,将查询底层模型以确定它支持的操作类型和它可以接受的MIME类型。这些信息由QAbstractItemModel::supportedDropActions()和QAbstractItemModel::mimeTypes()函数提供。未重写QAbstractItemModel提供实现的模型支持复制操作和默认内部MIME类型。
当序列化项目数据被拖放到视图中时,数据将使用其在QAbstractItemModel::dropMimeData()中的实现插入到当前模型中。此函数的默认实现永远不会覆盖模型中的任何数据;相反,它会试图将数据项作为项的兄弟项插入,或者作为该项的子项。
要利用QAbstractItemModel对内置MIME类型的默认实现,新模型必须提供以下函数的重实现
insertRows()
这些函数使模型能够自动使用QAbstractItemModel::dropMimeData()提供的现有实现插入新数据。
insertColumns()
setData()
允许填充新行和新列的内容。
setItemData()
此函数提供了更高效的新填充新项目的方法。
要接受其他形式的数据,必须重写以下函数
supportedDropActions()
用于返回一组拖放操作,表示模型接受的拖放操作类型。
mimeTypes()
用于返回模型可以解码和处理的一组MIME类型。一般来说,模型支持的输入MIME类型与它编码数据以便外部组件使用时相同。
dropMimeData()
执行通过拖放操作传载数据的实际解码,确定在模型中放置位置,并在必要时插入新的行和列。此函数在子类中的实现取决于每个模型公开的数据的需求。
如果dropMimeData()函数的实现通过添加或删除行或列修改了模型的尺寸,或者如果数据项被修改,必须注意确保发出所有相关的信号。调用子类中其他函数的重实现(如setData()、insertRows()和insertColumns()),以确保模型的行为一致是有用的。
为了确保拖放操作正常工作,重要的是要重写以下从模型中删除数据的函数
removeRows()
removeRow()
removeColumns()
removeColumn()
有关关于项目视图的拖放操作更多信息,请参阅使用项目视图实现拖放。
便捷视图#
便捷视图(QListWidget
、QTableWidget
和QTreeWidget
)覆盖了默认的拖放功能,以提供更自然但灵活性较低的行为,这适用于许多应用程序。例如,由于在QTableWidget
单元格中放置数据更为常见,以及用传输中的数据替换现有内容,底层模型将设置目标项的数据而不是在模型中插入新行和列。有关便捷视图的拖放更多信息,您可以参阅使用项目视图实现拖放。
大量数据性能优化#
canFetchMore()函数检查父项是否还有更多数据可用,并根据情况返回正确或错误的值。fetchMore()函数根据指定的父项获取数据。这两个函数可以结合起来使用,例如在涉及增量数据的数据库查询中填充QAbstractItemModel。我们重写canFetchMore()以指示是否有更多数据要获取,并重写fetchMore()以按要求填充模型。
另一个例子是动态填充的树形模型,在展开树形模型的分支时重写fetchMore()函数。
如果您的fetchMore()重写添加了行到模型,您需要调用beginInsertRows()和endInsertRows()。此外,canFetchMore()和fetchMore()都必须重写,因为它们的默认实现返回false并且不执行任何操作。
模型/视图类#
这些类使用模型/视图设计模式,其中底层数据(模型中)与用户展示和操作数据的方式(视图中)保持独立。
QAbstractItemDelegate类用于显示和编辑来自模型的数据项。
QAbstractItemView类为项目视图类提供基本功能。
QColumnView类提供了一个列视图的模型/视图实现。
QDataWidgetMapper类提供数据模型一部分与小部件之间的映射。
QHeaderView类为项目视图提供标题行或列。
QItemDelegate类为从模型获取的数据项提供显示和编辑功能。
QItemEditorFactory类为视图和委托中编辑项数据提供小部件。
QItemEditorCreatorBase类提供了一个抽象基类,在新项编辑创建器实现时必须子类化。
QItemEditorCreator
QItemEditorCreator类使得在不子类化QItemEditorCreatorBase的情况下创建项编辑创建器基类成为可能。
QStandardItemEditorCreator
QStandardItemEditorCreator类提供了注册小部件而不必子类化QItemEditorCreatorBase的可能性。
QListView类为模型提供了列表或图标视图。
QListWidgetItem类为与QListWidget项视图类一起使用的项提供了。
QListWidget类提供了一个基于项的列表小部件。
QStyledItemDelegate类为从模型获取的数据项提供显示和编辑功能。
QTableView类提供了表格视图的默认模型/视图实现。
QTableWidgetSelectionRange类提供了一种与模型选择交互的方式,而不需要使用模型索引和选择模型。
QTableWidgetItem类为与QTableWidget类一起使用的项提供了。
QTableWidget类提供了一个具有默认模型的基于项的表格视图。
QTreeView类为树视图提供了默认模型/视图实现。
QTreeWidgetItem类为与QTreeWidget便利类一起使用的项提供了。
QTreeWidget类提供了一个使用预定义树模型的树视图。
QTreeWidgetItemIterator类为迭代QTreeWidget实例中的项目提供了一种方式。