模型/视图编程

模型/视图编程简介

Qt包含一组项视图类,这些类使用模型/视图架构来管理数据与其呈现给用户的方式之间的关系。该架构引入的功能分离给予了开发人员更大的灵活性以自定义项的展示,并提供了标准模型接口,以便允许广泛的数据源与现有的项视图一起使用。在本文档中,我们简要介绍了模型/视图范式,概述了相关概念,并描述了项视图系统架构。架构中的每个组件都进行了说明,并给出了示例,展示了如何使用提供的类。

模型/视图架构

模型-视图-控制器(MVC)是起源于Smalltalk的设计模式,在构建用户界面时经常使用。在设计模式中,Gamma等写道

MVC包含三种类型的对象。模型是应用程序对象,视图是其屏幕展示,而控制器定义了用户界面对用户输入的反应方式。在MVC之前,用户界面设计往往将这些对象混为一谈。MVC将它们分离以增加灵活性和复用。

如果将视图和控制器对象结合,则得到模型/视图架构。这仍然将数据存储的方式与其呈现给用户的方式分开,但提供了一个基于相同原理的更简单框架。这种分离使得能够在多个不同的视图中显示相同的数据,并实现新的视图类型,而不需要改变底层的数据结构。为了允许灵活处理用户输入,我们引入了“代理”的概念。在这个框架中拥有代理的优点是它允许自定义数据的项渲染和编辑的方式。

模型/视图架构

模型与数据源通信,为架构中的其他组件提供一个接口。通信的性质取决于数据源的类型和模型的实现方式。

视图从模型获取模型索引;这些都是对数据项的引用。通过向模型提供模型索引,视图可以从数据源检索数据项。

在标准视图中,代理渲染数据项。当编辑项时,代理直接使用模型索引与模型通信。

通常,模型/视图类可以分为上面描述的三个组:模型、视图和代理。上述每个组件都由提供通用接口的抽象类定义,在某些情况下,也提供了一些特征的默认实现。抽象类旨在被子类化,以提供其他组件所需的所有功能集;这也允许编写专用组件。

模型、视图和代理之间通过信号和槽进行通信。

  • 来自模型的信号通知视图数据源中数据的变化。
  • 来自视图的信号提供有关用户与所显示项交互的信息。
  • 在编辑过程中,来自代理的信号用于向模型和视图告知编辑器的状态。

模型

所有项模型都是基于QAbstractItemModel类的。此类定义了一个接口,用于视图和代理访问数据。数据本身不需要存储在模型中;它可以通过单独的类、文件、数据库或其他应用程序组件提供的数据结构或存储库进行存储。

有关模型的基本概念在模型类章节中介绍。

QAbstractItemModel提供了一个足够灵活的数据接口,可以处理以表格、列表和树的形式表示数据的视图。然而,在实现新的列表和类似表的数据结构模型时,QAbstractListModelQAbstractTableModel类是更好的起点,因为它们提供了常用函数的适当默认实现。每个此类都可以被继承以提供支持特定类型的列表和表的模型。

模型继承过程在创建新模型章节中讨论。

Qt提供了一些现成的模型,可以用来处理数据项

如果这些标准模型不能满足您的需求,您可以通过继承QAbstractItemModelQAbstractListModelQAbstractTableModel来创建自己的自定义模型。

视图

为不同类型的视图提供了完整的实现:QListView显示一项列表,QTableView以表格形式显示来自模型的供数据,QTreeView以分层列表显示模型的数据项。每个此类都是基于QAbstractItemView抽象基类的。虽然这些类是现成可用的实现,但它们也可以被继承以提供定制视图。

视图类章节中检查了可用的视图。

代理

QAbstractItemDelegate 是模型/视图框架中代表的抽象基类。默认代表的实现由 QStyledItemDelegate 提供,Qt的标准视图默认使用它作为代表。然而,QStyledItemDelegateQItemDelegate 都是独立的可选方案,用于在视图中绘制项目并提供编辑器。它们之间的区别在于 QStyledItemDelegate 用来绘制项目的当前样式。因此我们建议在实现自定义代表或在处理Qt样式单时,使用 QStyledItemDelegate 作为基类。

代表在 代表类 部分进行了描述。

排序

在模型/视图架构中,排序有两种方法;选择哪种方法取决于你的底层模型。

如果你的模型是可排序列的,即它重写了 QAbstractItemModel::sort() 函数,那么 QTableViewQTreeView 都提供了API,允许你以编程方式排序模型数据。此外,你可以通过将 QHeaderView::sortIndicatorChanged() 信号连接到 QTableView::sortByColumn() 插槽或 QTreeView::sortByColumn() 插槽来启用交互式排序(即允许用户通过点击视图的表头来排序数据)。

如果你需要使用一个列表视图来呈现数据,或者你的模型没有所需的接口,我们可以使用代理模型将模型的结构转换后再在视图中呈现数据。这在 代理模型 部分有详细说明。

Convenience类

许多 方便 类是从标准视图类衍生出来的,为了使依靠Qt基于项的项视图和表格类的应用程序受益。它们并不打算被继承。

此类类的示例包括 QListWidgetQTreeWidgetQTableWidget

这些类比视图类更不灵活,并且不能与任意模型一起使用。我们建议除非你急需使用基于项的一组类,否则你应使用模型/视图方法处理项视图中的数据。

如果你想在保持基于项的接口的同时利用模型/视图方法提供的功能,考虑使用视图类,如 QListViewQTableViewQTreeView,与 QStandardItemModel 一起使用。

使用模型和视图

以下部分解释了如何在Qt中使用模型/视图模式。每个部分都包含了一个示例,之后有创建新组件的部分。

Qt中包含的两个模型

Qt提供的标准模型中有两个是 QStandardItemModelQFileSystemModelQStandardItemModel 是一个多用途模型,可以用来表示列表、表格和树视图需要的不同数据结构。此模型还包含了数据项。 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中,标准接口由QAbstractItemModel类定义。无论数据存储在底层数据结构中的方式如何,QAbstractItemModel的所有子类都将数据表示为一个包含项目表的分层结构。视图使用这个约定来访问模型中的数据项,但它们在如何向用户呈现这些信息的方式上不受限制。

模型还通过信号和槽机制通知任何附加视图有关数据更改。

本节描述了其他组件通过模型类访问数据时的基本概念。更高级的概念将在后面的章节中进行讨论。

模型索引

为了确保数据表示与访问方式的分离,引入了模型索引的概念。通过模型可以获取的每一项信息都由模型索引表示。视图和委托使用这些索引来请求要显示的数据项。

因此,只需模型知道如何获得数据,并且模型可以管理的类型可以实现相当通用的定义。模型索引包含创建它们的模型的指针,这在使用多个模型时的操作中可以防止混淆。

QAbstractItemModel *model = index.model();

模型索引提供了信息片段的临时引用,并且可以使用它们通过模型检索或修改数据。由于模型可能会定期重新组织其内部结构,模型索引可能会变得无效,因此不应存储。如果需要长期引用信息片段,必须创建一个持久模型索引。这提供了一个引用,模型可以保持信息更新。临时模型索引由QModelIndex类提供,持久模型索引由QPersistentModelIndex类提供。

要获取与数据项对应的模型索引,必须向模型指定三个属性:行号、列号和一个父项的模型索引。以下各节将详细描述和解释这些属性。

行和列

在最基本的形式中,模型可以作为一个简单表格访问,其中项通过其行和列号定位。这并不意味着底层的数据项以数组结构存储;使用行和列号只是组件之间通信的约定。我们可以通过指定行和列号从模型中检索任何给定项的信息,并收到表示该项的索引。

QModelIndex index = model->index(row, column, ...);

提供简单单一层级数据结构(如列表和表格)接口的模型不需要提供其他信息,但如上述代码所示,我们获取模型索引时需要提供更多信息。

行和列

该图显示了基本表格模型的表示,其中每个项通过行号和列号对进行定位。我们通过向模型传递相关的行和列号来获取指向数据项的模型索引。

QModelIndex indexA = model->index(0, 0, QModelIndex());
QModelIndex indexB = model->index(1, 1, QModelIndex());
QModelIndex indexC = model->index(2, 1, QModelIndex());

模型中的顶层项总是通过指定QModelIndex()作为其父项来引用。这将在下一节中讨论。

项的父母

模型提供的类似表格项数据接口在用于表格或列表视图中的数据时非常理想;行和列号系统与视图显示项的方式完全对应。但是,如树视图这样的结构需要模型对内部项提供一个更灵活的接口。因此,每个项也可以是其他项表的父母,这与树视图中的顶级项包含其他项列表的方式非常相似。

在请求模型的索引时,我们必须提供有关项父母的某些信息。在模型外部,引用项的唯一方法是通过模型索引,因此必须提供父模型索引。

QModelIndex index = model->index(row, column, parent);
父母、行、列

该图显示了树模型的表示,其中每个项由父母、行号和列号引用。

项"A"和"C"在模型中表示为顶级兄弟。

QModelIndex indexA = model->index(0, 0, QModelIndex());
QModelIndex indexC = model->index(2, 1, QModelIndex());

获得项"B"的模型索引的代码如下

QModelIndex indexB = model->index(1, 0, indexA);

项角色

模型中的项目可以为其他组件执行各种角色,允许在不同情况下提供不同类型的数据。例如,Qt::DisplayRole用于访问可以显示为视图中的文本的字符串。通常,项目包含多个不同角色的数据,而标准角色由Qt::ItemDataRole定义。

我们可以通过传递对应项目的模型索引并指定角色来请求模型中的项目数据,以获取所需的数据类型。

QVariant value = model->data(index, role);
项目角色

角色指示模型正在引用哪种类型的数据。视图可以以不同的方式显示角色,因此为每个角色提供适当的信息非常重要。

创建新模型部分更详细地介绍了角色的某些特定用途。

最常见的项目数据用途由Qt::ItemDataRole中定义的标准角色涵盖。通过为每个角色提供适当的项目数据,模型可以向视图和代理提供有关如何向用户展示项目的提示。不同的视图类型可以根据需要自由解释或忽略这些信息。还可以为特定于应用程序的目的定义额外的角色。

总结

  • 模型索引以与任何底层数据结构无关的方式向视图和代理提供有关模型提供项的位置信息。
  • 项通过它们的行数和列数以及它们父项的模型索引来引用。
  • 模型索引由模型根据其他组件(如视图和代理)的请求构建。
  • 当使用index()请求索引并指定了父项的有效模型索引时,返回的索引指向模型中位于父项下方的项。获得的索引指向该项的子项。
  • 当使用index()请求索引时指定了父项的无效模型索引,返回的索引指向模型中的顶级项。
  • 角色区分与项相关联的不同类型的数据。

使用模型索引

为了演示如何通过使用模型索引从模型检索数据,我们在不使用视图的情况下设置了QFileSystemModel并显示了一个小部件中的文件和目录名称。尽管这并不显示使用模型的一种正常方式,但它演示了模型在处理模型索引时使用的约定。

QFileSystemModel的加载是异步的,以最大限度地减少系统资源的使用。我们必须注意这一点,当我们处理这个模型时。

我们按照以下方式构建文件系统模型:

    auto *model = new QFileSystemModel;

    auto onDirectoryLoaded = [model, layout, &window](const QString &directory) {
        QModelIndex parentIndex = model->index(directory);
        const int numRows = model->rowCount(parentIndex);
        for (int row = 0; row < numRows; ++row) {
            QModelIndex index = model->index(row, 0, parentIndex);

            QString text = model->data(index, Qt::DisplayRole).toString();
            // Display the text in a widget.
            auto *label = new QLabel(text, &window);
            layout->addWidget(label);
        }
    };

    QObject::connect(model, &QFileSystemModel::directoryLoaded, onDirectoryLoaded);
    model->setRootPath(QDir::currentPath());

在这种情况下,我们首先设置一个默认的QFileSystemModel。我们将它的信号directoryLoaded(QString)连接到一个lambda,在其中我们将使用该模型提供的特定index()实现来获取目录的父索引。

在lambda中,我们使用rowCount()函数确定模型中的行数。

为了简单起见,我们只对模型的第一列中的项感兴趣。我们依次检查每一行,获取每一行的第一个项的模型索引,并读取模型中存储的那个项的数据。

        for (int row = 0; row < numRows; ++row) {
            QModelIndex index = model->index(row, 0, parentIndex);

要获取模型索引,我们指定行号、列号(第一列列为零)以及所有想获取的项目的父级的适当模型索引。每个项目中存储的文本通过模型的 data() 函数检索。我们指定模型索引和 DisplayRole,以字符串形式获取项目数据。

            QString text = model->data(index, Qt::DisplayRole).toString();

        }

最后,我们设置 QFileSystemModel 的根路径,使其开始加载数据并触发 lambda 表达式。

上述示例展示了从模型中检索数据所用到的基本原理。

  • 可以使用 rowCount() 和 columnCount() 函数查找模型的尺寸。这些函数通常需要指定父级模型索引。
  • 模型索引用于访问模型中的项目。需要行、列和父级模型索引来指定项目。
  • 要访问模型中的顶级项目,将 QModelIndex() 作为父级索引指定为零。
  • 项目包含不同角色的数据。要获取特定角色的数据,必须向模型提供模型索引和角色。

更多信息

可以通过实现由 QAbstractItemModel 提供的标准接口来创建新的模型。在 创建新模型 部分,我们通过创建一个方便的、可用于存储字符串列表的模型来展示这一点。

视图类

概念

在模型/视图架构中,视图从模型中获取数据项并向用户展示。数据展示的方式不需要与模型提供的数据表示相似,甚至可能与存储数据项的数据结构完全不同。

通过使用 QAbstractItemModel 提供的标准模型接口,以及由 QAbstractItemView 提供的标准视图接口,实现内容和展示的分离。视图通常管理从模型获取的数据的整体布局。它们可以自己渲染数据项,或使用 代理类 来处理渲染和编辑功能。

除了展示数据,视图还处理项目之间的导航和某些项目选择方面。视图还实现基本的用户界面功能,如上下文菜单和拖放。视图可以为项目提供默认的编辑功能,或与 代理 协作以提供自定义的编辑器。

视图可以在没有模型的情况下构建,但在显示有用的信息之前必须提供模型。视图通过 选择 的方式跟踪用户选择的项目,每个视图可以选择单独维护选择,或者多个视图之间共享选择。

一些视图,如QTableViewQTreeView,同时显示表头和项目。这些也是通过一个视图类实现的,即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());

将第一个视图的选择模型赋给第二个视图。现在两个视图都操作同一个选择模型,保持数据和选中的项目同步。

在上面的例子中,使用了相同类型的两个视图来显示同一个模型的数据。然而,如果使用了不同类型的视图,每个视图中的选中项目可能表示得非常不同;例如,在表格视图中连续的选择可能在树视图中表示为一系列分散的突出显示项。

代理类

概念

与Model-View-Controller模式不同,模型/视图设计不包含用于管理与用户交互的完全独立的组件。通常,视图负责将模型数据呈现给用户,并处理用户输入。为了允许在获取这种输入方面有一定的灵活性,交互由代理执行。这些组件提供输入功能,并且在某些视图中还负责渲染单个项目。用于控制代理的标准接口定义在QAbstractItemDelegate类中。

预期代理能够通过实现paint()和sizeHint()函数来自行渲染其内容。然而,简单的基于小部件的代理可以继承QStyledItemDelegate而不是QAbstractItemDelegate,并利用这些函数的默认实现。

代理的编辑器可以是使用小部件管理编辑过程或直接处理事件来实现。第一种方法将在本节的后面部分介绍。

使用现有代理

Qt提供的标准视图使用QStyledItemDelegate的实例来提供编辑功能。该代理界面的默认实现以每个标准视图的常规样式渲染项:QListViewQTableViewQTreeView

所有标准角色都由标准视图使用的默认代理处理。这些角色是如何被解释的请参阅QStyledItemDelegate文档。

视图使用的委托通过 itemDelegate() 函数返回。函数 setItemDelegate() 允许你为标准视图安装自定义委托,而在设置自定义视图的委托时必须使用此函数。

一个简单的委托

这里实现的委托使用了一个 QSpinBox 来提供编辑功能,主要适用于显示整数的模型。虽然我们为这个目的设置了一个自定义的基于整数的表格模型,但我们可以轻松地使用 QStandardItemModel 代替,因为自定义委托控制数据输入。我们构建一个表格视图来显示模型的内容,这将使用自定义委托进行编辑。

由于我们不希望编写自定义显示函数,所以我们从 QStyledItemDelegate 继承委托。然而,我们仍然需要提供管理编辑小部件的函数

class SpinBoxDelegate : public QStyledItemDelegate
{
    Q_OBJECT

public:
    SpinBoxDelegate(QObject *parent = nullptr);

    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;
};

SpinBoxDelegate::SpinBoxDelegate(QObject *parent)
    : QStyledItemDelegate(parent)
{
}

请注意,在构建委托时不会设置编辑小部件。我们只在小部件需要时才构建它。

提供编辑器

在此示例中,当表格视图需要提供编辑器时,它要求委托提供适合正在修改的项的编辑器小部件。该 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;
}

请注意,我们不需要保留编辑小部件的指针,因为视图负责在不再需要时销毁它。

我们将委托的默认事件过滤器安装到编辑器上,以确保它提供用户期望的标准编辑快捷方式。还可以向编辑器添加更多快捷方式以允许更复杂的操作;这些将在 编辑提示 部分中进行讨论。

视图通过调用我们稍后为这些目的定义的函数来确保编辑器的数据和几何位置设置正确。我们可以根据视图提供的模型索引创建不同的编辑器。例如,如果我们有一列整数和一列字符串,我们将根据正在编辑的列返回一个 QSpinBox 或一个 QLineEdit

委托必须提供一个函数将模型数据复制到编辑器中。在此示例中,我们读取存储在 显示角色 中的数据,并相应地设置计数器的值。

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);
}

在此示例中,我们知道编辑器小部件是一个计数器,但我们可以为模型中的不同类型的数据提供不同的编辑器。在这种情况下,在我们访问成员函数之前,我们需要将小部件转换为适当的类型。

将数据提交到模型

当用户完成在计数器中对值的编辑后,视图通过调用 setModelData() 函数来要求委托将编辑的值存储在模型中。

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);
}

由于视图管理委托的小部件,我们只需要使用提供的小部件内容更新模型。在这种情况下,我们确保计数器是最新的,并使用指定的索引通过函数更新模型中的值。

标准QStyledItemDelegate类通过发射closeEditor()信号来通知视图编辑完成后已结束。视图确保编辑器小部件被关闭并销毁。在这个例子中,我们只提供了简单的编辑功能,所以我们永远不需要发射此信号。

所有对数据操作的都是在QAbstractItemModel提供界面上进行的。这使得委托基本上独立于它操作的数据类型,但使用某些类型的编辑器小部件时,必须做一些假设。在这个例子中,我们假设模型始终包含整数值,但我们仍然可以使用此委托与不同类型的模型配合使用,因为QVariant为意外的数据提供了合理的默认值。

更新编辑器的几何形状

委托负责管理编辑器的几何形状。必须在创建编辑器时设置几何形状,并且当项目的大小或视图中的位置发生变化时。幸运的是,视图在视图选项对象内部提供了所有必要的几何信息。

void SpinBoxDelegate::updateEditorGeometry(QWidget *editor,
                                           const QStyleOptionViewItem &option,
                                           const QModelIndex &/* index */) const
{
    editor->setGeometry(option.rect);
}

在这种情况下,我们仅使用项目矩形中由视图选项提供的几何信息。渲染包含多个元素的项目的委托不会直接使用项目矩形。它会在项目中的其他元素上定位编辑器。

编辑提示

编辑完成后,委托应向其他组件提供有关编辑过程结果的建议,并提供建议以帮助随后的编辑操作。这是通过使用合适的提示发射closeEditor()信号来实现的。这是由我们在构造时安装在微调框上的默认QStyledItemDelegate事件过滤器来处理的。

可以调整微调框的行为,使其更加用户友好。在默认事件过滤器中,如果用户在微调框中按下Return键确认他们的选择,委托将值提交到模型并关闭微调框。我们可以通过在微调框上安装自定义事件过滤器来更改此行为,并提供适合我们需要的编辑提示;例如,我们可能发射带有closeEditor()的EditNextItem提示,以自动开始编辑视图中的下一个项。

另一种不需要使用事件过滤器的方法是提供我们自己的编辑器小部件,例如或许为了方便可以继承QSpinBox。这种替代方法将使我们能够更好地控制编辑器小部件的行为,但必须编写更多的代码。通常,如果您需要自定义标准Qt编辑器小部件的行为,则在委托中安装事件过滤器更简单。

委托不需要发射这些提示,但那些不发射提示的将不如那些发射支持常见编辑操作的提示的集成到应用程序中,并且它们的可用性会较低。

处理项目视图中的选择

概念

在项目视图类中使用的选择模型基于模型/视图架构的设施提供基于选择的通用描述。尽管用于处理选择的标准类对于提供的项目视图是足够的,但选择模型允许您创建适合您自己的项目模型和视图的需求的专用选择模型。

视图中选定项的信息存储在QItemSelectionModel类的实例中。这保留了单个模型中各项的模型索引,并且与任何视图无关。由于模型可以有多个视图,因此可以在视图之间共享选择,允许应用程序以一致的方式显示多个视图。

选择由选择范围组成。这些通过只记录每个选中项范围的起始和结束模型索引来高效地维护关于大量项目选择的信息。不连续的项目选择是通过使用多个选择范围来描述选择的。

选择应用于由选择模型持有的模型索引集合。最近应用的项目选择称为当前选择。通过使用某些类型的选择命令,即使在应用后也可以修改此选择的效果。这在本节后面进行讨论。

当前项和选定项

在视图中,始终存在一个当前项和一个选定项——两种独立的状态。一个项可以是当前项同时被选中。视图负责确保始终有一个当前项,例如,键盘导航需要当前项。

下表突出显示了当前项和选定项之间的差异。

当前项选定项
只能有一个当前项。可以有多个选定项。
当前项将通过键导航或鼠标按钮点击进行更改。在与项目交互时,将根据几个预定义模式(例如,单一选择、多选等)设置或清除项目选择状态。
如果按下编辑键F2或双击项(假设已启用编辑),则将编辑当前项。当前项可以与一个锚点一起使用来指定应选择或取消选择(或两者组合)的范围。
当前项通过焦点矩形表示。选定项通过选择矩形表示。

在操作选择时,通常将QItemSelectionModel视为记录项目模型中所有项的选择状态的记录。一旦设置了选择模型,就可以选择、取消选择项的集合,或切换其选择状态,而无需知道哪些项已经被选中。可以随时检索所有选中项的索引,并且可以通过信号和槽机制告知其他组件对选择模型的更改。

使用选择模型

标准视图类提供了在大多数应用程序中使用的默认选择模型。可以使用视图的selectionModel()函数获取属于一个视图的选择模型,并通过setSelectionModel()将其与许多视图共享,因此通常不需要构建新的选择模型。

通过指定模型和一个指向QItemSelection的模型索引对来创建选择。这使用索引来引用给定模型中的项,并将它们作为选中项块的左上角和右下角项进行解释。要将选择应用于模型中的项,需要将选择提交给选择模型;这可以通过多种方式实现,每种方式都会对选择模型中现有的选择产生不同的影响。

选择项目

为了演示选择的一些主要功能,我们构建了一个包含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);

使用由选择标志组合定义的命令将选择应用于选择模型。在这种情况下,使用的标志导致记录在选择对象中的项目被包括在选择模型中,无论其先前状态如何。视图显示了由此产生的选择。

可以通过定义选择标志的各种操作来修改项目选择。这些操作的结果可能具有复杂的结构,但由选择模型有效地表示。在检查如何更新选择时,将描述如何使用不同的选择标志来操作选择的项目。

读取选择状态

可以使用 selectedIndexes() 函数读取存储在选择模型中的模型索引。这个函数返回一个未排序的模型索引列表,只要我们知道它们对应的模型即可遍历

    const QModelIndexList indexes = selectionModel->selectedIndexes();

    for (const QModelIndex &index : indexes) {
        QString text = QString("(%1,%2)").arg(index.row()).arg(index.column());
        model->setData(index, text);
    }

上述代码使用基于范围的for循环遍历并修改由选择模型返回的索引对应的项目。

选择模型发出信号以指示选择的变化。这些通知其他组件关于整个选择以及项目模型中当前焦点项目的更改。我们可以将 selectionChanged() 信号连接到一个槽,并检查当选择更改时在模型中选择或取消选择的项目。该槽与两个 QItemSelection 对象一起调用:一个包含对应新选择的索引列表;另一个包含对应新取消选择的索引。

在以下代码中,我们提供了一个接收 selectionChanged() 信号、将字符串填充到选择的项目中,并清除取消选择的项目内容的槽。

void MainWindow::updateSelection(const QItemSelection &selected,
    const QItemSelection &deselected)
{
    QModelIndexList items = selected.indexes();

    for (const QModelIndex &index : std::as_const(items)) {
        QString text = QString("(%1,%2)").arg(index.row()).arg(index.column());
        model->setData(index, text);
    }

    items = deselected.indexes();

    for (const QModelIndex &index : std::as_const(items)) {
        model->setData(index, QString());
}

我们可以通过将 currentChanged() 信号连接到一个槽来跟踪当前聚焦的项目,该槽调用时带有两个模型索引。这些对应于以前聚焦的项目,和目前聚焦的项目。

在以下代码中,我们提供了一个接收 currentChanged() 信号,并使用提供的信息更新QMainWindow状态条的槽。

void MainWindow::changeCurrent(const QModelIndex &current,
    const QModelIndex &previous)
{
    statusBar()->showMessage(
        tr("Moved from (%1,%2) to (%3,%4)")
            .arg(previous.row()).arg(previous.column())
            .arg(current.row()).arg(current.column()));
}

使用这些信号监测用户所做的选择非常简单,但我们也可以直接更新选择模型。

更新选择

选择命令由选择标志的组合提供,这些标志由QItemSelectionModel::SelectionFlag定义。每个选择标志告诉选择模型,当调用任一select()函数时,如何更新其内部选择项的记录。最常用的标志是Select标志,它指示选择模型将指定的项记录为选中。 当Toggle标志发挥作用时,将导致选择模型反转指定项的状态,选择任何给定未选中的项,取消选择任何当前选中的项。《a href="qitemselectionmodel.html#SelectionFlag-enum" translate="no">Deselect标志取消所有指定项的选择。

通过创建项选择并将它们应用于选择模型来更新选择模型中的单个项。在以下代码中,我们使用Toggle命令将上表所示的表型单中的定制项选择反转到选择状态。

    QItemSelection toggleSelection;

    topLeft = model->index(2, 1, QModelIndex());
    bottomRight = model->index(7, 3, QModelIndex());
    toggleSelection.select(topLeft, bottomRight);

    selectionModel->select(toggleSelection, QItemSelectionModel::Toggle);

此操作的结果在表格视图中显示,为可视化了我们所实现的结果提供了一种方便的方法

默认情况下,选择命令仅对由模型索引指定的单个项起作用。但是,用来描述选择命令的标志可以与额外的标志组合起来以改变整个行和列。例如,如果您只使用一个索引调用select(),但带有SelectRows的选择组合,则将选中包含该引用项的整个行。以下代码演示了RowsColumns标志的使用

    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);

尽管只向选择模型提供了四个索引,但使用ColumnsRows选择标志意味着选中了两列和两行。以下图像显示了这两个选择的成果

对示例模型执行的命令都涉及在模型中累计选择项。当然,也可以清除选择,或者用新的选择替换当前的选择。

要用新的选择替换当前选择,将其他选择标志与Current标志组合起来。使用此标志的命令指示选择模型用select()调用中指定的索引替换其当前模型索引集合。为了在开始添加之前清除所有选择,将其他选择标志与Clear标志组合起来。这会将选择模型的模型索引集合重置为零。

选择模型中的所有项

为了在模型中选择所有项,有必要为模型中的每一层创建一个选择,以确保该层中所有项都被选中。我们通过检索一个给定父索引对应的左上角和右下角项的索引来做到这一点

    QModelIndex topLeft = model->index(0, 0, parent);
    QModelIndex bottomRight = model->index(model->rowCount(parent)-1,
        model->columnCount(parent)-1, parent);

使用这些索引和模型构建选择。然后选择模型中选择相应的项

    QItemSelection selection(topLeft, bottomRight);
    selectionModel->select(selection, QItemSelectionModel::Select);

这对模型中的所有层都需要执行。对于顶级项,我们以通常的方式定义父索引

对于层级模型,使用hasChildren()函数来确定任何给定项是否是另一个项目层的父项。

创建新的模型

模型/视图组件之间的功能分离使得可以创建能够利用现有视图功能的模型。这种方法允许我们使用标准图形用户界面组件,如QListViewQTableViewQTreeView来展示来自多种来源的数据。

QAbstractItemModel类提供了一个足够灵活的接口来支持以分层结构排列信息的源数据,从而允许数据以某种方式插入、删除、修改或排序。它还提供了对拖放操作的支持。

QAbstractListModelQAbstractTableModel类为简单非分层数据结构的接口提供支持,并且作为简单列表和表格模型的起点更容易使用。

在本节中,我们创建了一个简单的只读模型来探讨模型/视图体系结构的基本原理。在本节的后面部分,我们将这个简单的模型修改为允许用户修改项。

关于更复杂模型的例子,请参阅简单树模型示例。

QAbstractItemModel子类的需求在模型子类化参考文件中描述得更为详细。

设计模型

在为现有的数据结构创建新模型时,考虑选择哪种类型的模型来提供数据接口非常重要。如果数据结构可以表示为项目列表或表格,由于这些类提供了许多函数的合适默认实现,因此可以子类化QAbstractListModelQAbstractTableModel

然而,如果底层数据结构只能用分层树结构表示,则必须子类化QAbstractItemModel。这种方法在简单树模型示例中采用。

在本节中,我们将基于字符串列表实现一个简单的模型,因此QAbstractListModel提供了一个理想的基础类。

不管底层数据结构是什么形式,通常在一个专门模型中补充标准的QAbstractItemModel API,使用一个能更自然地访问底层数据结构的API是一个好主意。这使得填充模型容易,同时仍允许其他通用模型/视图组件使用标准API与之交互。下面的模型提供了一个用于此目的的定制构造函数。

一个只读示例模型

这里实现的模型是一个基于标准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() 返回模型中的行数和 data() 返回与指定模型索引相对应的数据项。

表现良好的模型还实现了 headerData() 以提供给树视图和表格视图一些显示在标题中的内容。

请注意,这是一个非层次模型,所以我们不必担心父子关系。如果我们的模型是分层的,我们还需要实现 index() 和 parent() 函数。

字符串列表存储在内部的私有成员变量 stringList 中。

模型的维度

我们希望模型的行数与字符串列表中的字符串数相同。我们根据这一点实现了rowCount() 函数

int StringListModel::rowCount(const QModelIndex &parent) const
{
    return stringList.count();
}

由于模型是非分层的,我们可以安全地忽略与父项对应的模型索引。默认情况下,从 QAbstractListModel 派生的模型只包含一列,因此我们不需要重新实现 columnCount() 函数。

模型标题和数据

对于视图中的项,我们希望返回字符串列表中的字符串。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

某些视图,如 QTreeViewQTableView,能够显示标题以及项目数据。如果我们的模型在带有标题的视图中显示,我们希望标题显示行和列号。我们可以通过子类化 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,因此无论指定的角色是什么,我们都返回项的数据。然而,我们可以在其他角色中重复使用为DisplayRole提供的数据,例如视图可以用于在工具提示中显示有关项信息的ToolTipRole

可编辑模型

只读模型展示了如何向用户展示简单的选择,但对于许多应用来说,可编辑列表模型更有用。我们可以通过修改为只读实现的数据()函数,以及实现另外两个函数flags()和setData()来修改只读模型,以便使条目可编辑。以下函数声明被添加到类定义中

    Qt::ItemFlags flags(const QModelIndex &index) const override;
    bool setData(const QModelIndex &index, const QVariant &value,
                 int role = Qt::EditRole) override;

使模型可编辑

在创建编辑器之前,委托会检查项是否可编辑。模型必须让委托知道其项是可编辑的。通过为模型中的每个项返回正确的标识,我们做到这一点;在这种情况下,我们启用所有项,并使它们可选择和可编辑

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::ItemIsUserCheckable标志;然后使用复选框来编辑值。此模型中所有角色的底层数据是相同的,因此这个细节只使得模型与标准组件的集成更简单。

数据设置后,模型必须让视图知道某些数据已更改。这是通过发射dataChanged()信号来实现的。由于只有一个数据项已更改,因此信号中指定的数据项范围仅限于一个模型索引。

的data()函数需要更改以添加Qt::EditRole测试

QVariant StringListModel::data(const QModelIndex &index, int role) const
{
    if (!index.isValid())
        return QVariant();

    if (index.row() >= stringList.size())
        return QVariant();

    if (role == Qt::DisplayRole || role == Qt::EditRole)
        return stringList.at(index.row());
    else
        return QVariant();
}

插入和删除行

可以更改模型中的行数和列数。在字符串列表模型中,仅更改行数是有意义的,因此我们只重新实现插入和删除行的函数。这些函数声明在类定义中

    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()以完成操作,并通知其他组件模型的尺寸已更改。

下一步

我们可以使用QListView类来显示由该模型或其他模型提供的数据,以显示模型项的垂直列表形式。对于字符串列表模型,此视图还提供了一个默认编辑器,以便可以操作项。我们探讨了标准视图类在View Classes中提供的可能性。

Model Subclassing Reference》文档详细讨论了QAbstractItemModel子类的要求,并提供了一组必须实现的虚拟函数的指南,以在各类模型中启用各种功能。

项目视图便捷类

基于项目的控件命名反映了它们的使用:QListWidget提供项目列表,QTreeWidget显示多级树结构,而QTableWidget提供单元格项表格。每个类都继承了QAbstractItemView类的行为,该类实现了项目选择和标题管理的基本行为。

列表控件

通常使用QListWidget和一些QListWidgetItem来显示单级项目列表。与任何其他小控件一样构造列表控件。

    QListWidget *listWidget = new QListWidget(this);

在创建时可以将项目直接添加到列表控件中

    new QListWidgetItem(tr("Sycamore"), listWidget);
    new QListWidgetItem(tr("Chestnut"), listWidget);
    new QListWidgetItem(tr("Mahogany"), listWidget);

它们也可以在没有父列表控件的情况下构造,并在稍后添加到列表中

    QListWidgetItem *newItem = new QListWidgetItem;
    newItem->setText(itemText);
    listWidget->insertItem(row, newItem);

列表中的每个项目都可以显示文本标签和图标。可以更改用于渲染文本的颜色和字体,以提供项的定制外观。提示、状态提示和“这是什么?”帮助都可以轻松配置,以确保列表正确集成到应用程序中。

    newItem->setToolTip(toolTipText);
    newItem->setStatusTip(toolTipText);
    newItem->setWhatsThis(whatsThisText);

默认情况下,列表中的项按创建顺序显示。可以根据Qt::SortOrder中给出的标准对项列表进行排序,以生成按正序或逆序字母顺序排序的项列表

    listWidget->sortItems(Qt::AscendingOrder);
    listWidget->sortItems(Qt::DescendingOrder);

树控件

QTreeWidgetQTreeWidgetItem类提供了项树或层次列表。树控件中的每个项目都可以有自己的子项目,并可以显示多个信息列。与任何其他小控件一样创建树控件

    QTreeWidget *treeWidget = new QTreeWidget(this);

在向树控件添加项目之前,必须设置列数。例如,我们可以定义两个列,并创建一个标题,提供每个列顶部的标签

    treeWidget->setColumnCount(2);
    QStringList headers;
    headers << tr("Subject") << tr("Default");
    treeWidget->setHeaderLabels(headers);

为每个部分设置标签的最简单方法是为它们提供一个字符串列表。对于更复杂的标题,可以构造一个树项,按要求装饰它,并使用它作为树控件的标题。

树小工具中的顶级项是用树小工具作为其父小工具构建的。它们可以以任意顺序插入,或者在构建每个项时指定上一个项以确保它们按特定顺序列出。

    QTreeWidgetItem *cities = new QTreeWidgetItem(treeWidget);
    cities->setText(0, tr("Cities"));
    QTreeWidgetItem *osloItem = new QTreeWidgetItem(cities);
    osloItem->setText(0, tr("Oslo"));
    osloItem->setText(1, tr("Yes"));

    QTreeWidgetItem *planets = new QTreeWidgetItem(treeWidget, cities);

树小工具在处理顶级项时与其他树内部更深的项略有不同。可以通过调用树小工具的takeTopLevelItem()函数从树的顶级移除项,但通过调用父项的takeChild()函数移除较低级别的项。使用insertTopLevelItem()函数在树的顶级插入项。在树中的较低级别,使用父项的insertChild()函数。

在树中顶级和较低级别之间移动项很容易。我们只需检查项是否是顶级项,每个项的parent()函数提供此信息。例如,我们可以从树小工具中删除当前项,无论其位置如何。

    QTreeWidgetItem *parent = currentItem->parent();
    int index;

    if (parent) {
        index = parent->indexOfChild(treeWidget->currentItem());
        delete parent->takeChild(index);
    } else {
        index = treeWidget->indexOfTopLevelItem(treeWidget->currentItem());
        delete treeWidget->takeTopLevelItem(index);
    }

在树小工具中的其他地方插入项遵循相同的模式。

    QTreeWidgetItem *parent = currentItem->parent();
    QTreeWidgetItem *newItem;
    if (parent)
        newItem = new QTreeWidgetItem(parent, treeWidget->currentItem());
    else
        newItem = new QTreeWidgetItem(treeWidget, treeWidget->currentItem());

表格小工具

类似于电子表格应用中找到的项的表格是用QTableWidgetQTableWidgetItem构建的。这些提供带有标头的滚动表格小工具,可以在其中使用它。

可以有固定行数和列数的表格,或者根据需要将其添加到未测量表格中。

    QTableWidget *tableWidget;
    tableWidget = new QTableWidget(12, 3, this);

项在添加到表格所需位置之前在表格外构建。

    QTableWidgetItem *newItem = new QTableWidgetItem(tr("%1").arg(
        pow(row, column+1)));
    tableWidget->setItem(row, column, newItem);

可以通过在表格外构建项并将其用作标头来添加水平和垂直标头到表格。

    QTableWidgetItem *valuesHeaderItem = new QTableWidgetItem(tr("Values"));
    tableWidget->setHorizontalHeaderItem(0, valuesHeaderItem);

注意,表格的行和列从零开始。

常见功能

许多方便类之间有一些基于项的常见功能,这些功能在各个类中通过相同的接口提供。我们在以下部分中提供了这些功能,以及不同小工具的示例。查看小工具的模型/视图类列表,获取有关每个函数使用的更多详细信息。

隐藏项

有时隐藏项在项视图小工具中而不是删除它们是有用的。可以隐藏所有上述小工具的项并稍后再次显示它们。您可以通过调用isItemHidden()函数来确定项是否隐藏,并且可以使用setItemHidden()来隐藏项。

由于此操作是基于项的,因此对于所有三个方便类,都提供了相同的函数。

选择

项的选择方式受小工具的选择模式(QAbstractItemView::SelectionMode)控制。此属性控制用户是否可以选中一项或多项,以及在多项选择中,是否必须是一系列连续的项。选择模式与上述所有小工具以相同的方式工作。

单项选择:当用户需要从小工具中中选择单一项时,最合适的是默认的SingleSelection模式。在此模式下,当前项和选择项是相同的。

多项选择:在这种模式下,用户可以切换组件中任何项的选择状态,而不会改变现有选择,就像独立切换的非排他性复选框一样。

扩展选择:需要同时选择许多相邻项的组件(如电子表格中的组件)需要使用ExtendedSelection模式。在这种模式下,可以使用鼠标和键盘选择组件中项的连续范围。如果使用修饰键,还可以创建涉及许多不相邻项的复杂选择。

如果在没有使用修饰键的情况下选择项,现有选择将被清除。

使用selectedItems()函数可以读取组件中的选定项,提供一个可以遍历的相关项列表。例如,我们可以使用以下代码找到选定项列表中所有数值的总和

    const QList<QTableWidgetItem *> selected = tableWidget->selectedItems();
    int number = 0;
    double total = 0;

    for (QTableWidgetItem *item : selected) {
        bool ok;
        double value = item->text().toDouble(&ok);

        if (ok && !item->text().isEmpty()) {
            total += value;
            number++;
        }
    }

注意,对于单选模式,当前项将包含在选择中。在多选和扩展选择模式下,当前项可能不在选择中,这取决于用户形成选择的方式。

搜索

作为一个开发者或作为一个服务来向用户展示,能够找到项视图组件内的项通常是非常有用的。所有三个项视图便利类都提供了一个通用的findItems()函数,以使其尽可能一致和简单。

根据从Qt::MatchFlags中选择的一组值指定的标准搜索包含文本的项。我们可以使用findItems()函数获取匹配项的列表

    const QList<QTreeWidgetItem *> found = treeWidget->findItems(
        itemText, Qt::MatchWildcard);

    for (QTreeWidgetItem *item : found) {
        item->setSelected(true);
        // Show the item->text(0) for each item.
    }

上述代码会在树组件中选择包含搜索字符串中给定文本的项。此模式也可以在列表和表格组件中使用。

使用拖放与项视图

Qt的拖放基础设施完全由模型/视图框架支持。列表、表格和树中的项可以在视图中拖动,数据可以作为MIME编码的数据导入和导出。

标准视图自动支持内部拖放,其中项被移动以改变其显示顺序。默认情况下,不启用这些视图的拖放,因为它们已配置为最简单、最常用的用途。要允许拖动项,需要启用视口的某些属性,并且项本身也必须允许执行拖放。

不允许数据拖入但允许从视图中导出项的模型的要求比完全启用的拖放模型的要求要少。

有关在新模型中启用拖放支持的更多信息,请参阅模型子类化参考

使用便利视图

QListWidgetQTableWidgetQTreeWidget一起使用的每种类型的项都默认配置有不同的一组标志。例如,每个QListWidgetItemQTreeWidgetItem最初被启用、可勾选、可选择,并可以用作拖放操作的源;每个QTableWidgetItem也可以被编辑并用作拖放操作的目标。

尽管所有标准项都设置了用于拖放的一个或两个标志,但你通常需要设置视图本身的各种属性,以便利用内置的拖放支持。

  • 要启用项目拖放,将视图的 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->setDragDropMode(QAbstractItemView::InternalMove);

使用模型/视图类

设置视图以支持拖放的设置方式与便利视图使用的模式相同。例如,可以将 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() 的实现,无论是直接提供还是从其基类继承实现。

启用项的拖放

通过重新实现 QAbstractItemModel::flags() 函数以提供合适的标志,模型向视图指示哪些项可以拖放,以及哪些将接受放下操作。

例如,一个基于 QAbstractListModel 的简单列表模型可以确保返回的标志包含 Qt::ItemIsDragEnabledQt::ItemIsDropEnabled 值,以此使每个项都能启用拖放。

Qt::ItemFlags DragDropListModel::flags(const QModelIndex &index) const
{
    Qt::ItemFlags defaultFlags = QStringListModel::flags(index);

    if (index.isValid())
        return Qt::ItemIsDragEnabled | Qt::ItemIsDropEnabled | defaultFlags;
    else
        return Qt::ItemIsDropEnabled | defaultFlags;
}

注意,项可以放入模型的顶层,但只有有效的项才能进行拖放。

在上面的代码中,由于模型是从 QStringListModel 派生的,我们通过调用其 flags() 函数的实现来获得一组默认标志。

编码导出的数据

当数据项在拖放操作中从一个模型导出时,它们会被编码成与一个或多个 MIME 类型对应的适当格式。模型通过重新实现 QAbstractItemModel::mimeTypes() 函数来声明它们可以使用哪些 MIME 类型提供项,并返回一系列标准 MIME 类型。

例如,一个只提供纯文本的模型会提供以下实现

QStringList DragDropListModel::mimeTypes() const
{
    QStringList types;
    types << "application/vnd.text.list";
    return types;
}

模型还必须提供编码数据的代码,以提供规定的格式。这是通过重新实现 QAbstractItemModel::mimeData() 函数来提供 QMimeData 对象来实现的,就像在其他任何拖放操作中一样。

以下代码显示了如何将每个数据项(对应于给定的索引列表)编码为纯文本,并存储在 QMimeData 对象中。

QMimeData *DragDropListModel::mimeData(const QModelIndexList &indexes) const
{
    QMimeData *mimeData = new QMimeData;
    QByteArray encodedData;

    QDataStream stream(&encodedData, QIODevice::WriteOnly);

    for (const QModelIndex &index : indexes) {
        if (index.isValid()) {
            QString text = data(index, Qt::DisplayRole).toString();
            stream << text;
        }
    }

    mimeData->setData("application/vnd.text.list", encodedData);
    return mimeData;
}

由于函数提供了模型索引的列表,因此这种方法足够通用,可以用于层次结构和非层次结构模型。

注意,必须将自定义数据类型声明为 元对象,并且必须为它们实现流操作。有关详细信息,请参阅 QMetaObject 类描述。

将拖放数据插入到模型中

特定模型处理拖放数据的方式取决于其类型(列表、表格或树)以及其内容可能如何呈现给用户。通常,采用以适应拖放数据的策略应该是最适合模型底层数据存储的策略。

不同类型的模型通常以不同的方式处理拖放数据。列表和表格模型只提供扁平结构,其中数据项被存储。因此,当数据在视图中的现有项上放置时,它们可能插入新的行(和列),或者它们可能使用所提供的一些数据覆盖模型中项的内容。树模型通常能够将包含新数据的子项添加到其底层数据存储中,并且因此在使用者看来表现得更加可预测。

拖放数据由模型对 QAbstractItemModel::dropMimeData() 的重新实现来处理。例如,一个处理简单字符串列表的模型可以提供一个单独处理放置在模型顶层(即无效项)上的数据和放置在现有项上的数据的实现。

模型可以通过重新实现 QAbstractItemModel::canDropMimeData() 来禁止在某些项上放置或根据拖放数据执行此操作。

模型首先必须确保应该对该操作进行操作,提供的数据格式可以使用,以及其在模型中的目标是有效的。

bool DragDropListModel::canDropMimeData(const QMimeData *data,
    Qt::DropAction action, int row, int column, const QModelIndex &parent) const
{
    Q_UNUSED(action);
    Q_UNUSED(row);
    Q_UNUSED(parent);

    if (!data->hasFormat("application/vnd.text.list"))
        return false;

    if (column > 0)
        return false;

    return true;
}
bool DragDropListModel::dropMimeData(const QMimeData *data,
    Qt::DropAction action, int row, int column, const QModelIndex &parent)
{
    if (!canDropMimeData(data, action, row, column, parent))
        return false;

    if (action == Qt::IgnoreAction)
        return true;

一个简单的一列字符串列表模型在提供的数据不是纯文本或指定的拖放列号无效时可以指示失败。

要插入模型的数据根据它是放置在现有项上还是不是而以不同的方式处理。在这个简单的例子中,我们想允许项在现有项之间、列表的第一项之前和最后一项之后进行放置。

当发生拖放时,对应于父项的模型索引将是有效的,表示拖放在项上发生,或者它将无效,表示在视图的顶层(即在模型的首级)发生拖放。

    int beginRow;

    if (row != -1)
        beginRow = row;

我们最初检查提供的行号以确定是否可以使用它来在模型中插入项,而不管父索引是否有效。

    else if (parent.isValid())
        beginRow = parent.row();

如果父模型索引有效,则表示在项上发生拖放。在这个简单的列表模型中,我们找出项的行号并使用该值将已放置的项插入模型的顶层。

    else
        beginRow = rowCount(QModelIndex());

当在视图的其他地方发生拖放且行号不可用时,我们将项追加到模型的顶层。

在层次模型中,当一个项目发生删除时,最好是将其插入的新项目作为该项目的子项插入到模型中。在下面的简单示例中,模型只有一级,因此这种方法不适用。

解码导入的数据

每个实现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());
    for (const QString &text : std::as_const(newItems)) {
        QModelIndex idx = index(beginRow, 0, QModelIndex());
        setData(idx, text);
        beginRow++;
    }

    return true;
}

请注意,模型通常需要提供QAbstractItemModel::insertRows()和QAbstractItemModel::setData()函数的实现。

代理模型

在模型/视图框架中,单个模型提供的数据可以被多个视图共享,每个视图都可能以完全不同的方式表示相同的信息。自定义视图和委托是提供对相同数据的根本不同表示的有效方法。然而,应用程序通常需要提供对相同数据处理的常规视图,例如对项目列表的不同排序视图。

虽然将排序和过滤操作作为视图的内部功能执行似乎很合适,但这种方法不允许多个视图共享此类可能昂贵的操作的结果。作为替代方法,在模型本身中进行排序的方法导致了一个类似的问题,即每个视图都必须显示根据最新处理操作组织的数据项。

为了解决这个问题,模型/视图框架使用代理模型来管理各个模型和视图之间提供的信息。代理模型是从视图的角度看像普通模型的组件,代表该视图从源模型中获取数据。模型/视图框架使用的信号和槽确保无论有多少代理模型放置在视图和源模型之间,每个视图都会相应地更新。

使用代理模型

代理模型可以插入到现有模型和任意数量的视图之间。Qt提供了一个标准的代理模型QSortFilterProxyModel,它通常被实例化和直接使用,但也可以被继承来提供自定义的过滤和排序行为。可以使用以下方式使用QSortFilterProxyModel

    QSortFilterProxyModel *filterModel = new QSortFilterProxyModel(parent);
    filterModel->setSourceModel(stringListModel);

    QListView *filteredView = new QListView;
    filteredView->setModel(filterModel);

由于代理模型继承了QAbstractItemModel,它们可以连接到任何类型的视图,并且可以在视图之间共享。它们还可以用于在管道安排中处理从其他代理模型获得的信息。

QSortFilterProxyModel类被设计为可以直接在应用程序中实例化和使用的。更多专业的代理模型可以通过继承此类并实现所需的比较操作来创建。

定制代理模型

一般情况下,代理模式中使用的处理类型包括将源模型中每一项数据从其原始位置映射到代理模型中的不同位置。在某些模型中,某些项目可能没有对应的位置在代理模型中;这些模型是过滤代理模型。视图通过代理模型提供的数据索引来访问项目,这些索引不包含有关源模型或在该模型中原始项目位置的信息。

QSortFilterProxyModel允许将源模型中的数据在提供给视图之前进行过滤,并允许将源模型的内容作为预排序数据提供给视图。

自定义过滤模型

QSortFilterProxyModel类提供了一种相当灵活的过滤模型,可用于各种常见情况。对于高级用户,QSortFilterProxyModel可以被子类化,提供一种机制来实现自定义过滤器的实现。

QSortFilterProxyModel的子类可以重新实现两个虚拟函数,这两个函数在从代理模型请求或使用模型索引时调用。

QSortFilterProxyModel中上述函数的默认实现返回true,以确保所有条目都传递给视图;这些函数的重新实现应该返回false以过滤掉单个行和列。

自定义排序模型

QSortFilterProxyModel实例使用std::stable_sort()函数设置源模型中的条目与代理模型中的条目之间的映射,从而使视图在不修改源模型结构的情况下公开排序后的条目层次结构。为了提供自定义排序行为,重新实现lessThan()函数以执行自定义比较。

模型子类化参考

模型子类需要提供在QAbstractItemModel基类中定义的许多虚拟函数的实现。需要实现的这些函数的数量取决于模型类型——它是否为视图提供简单列表、表格或复杂的项目层次结构。从QAbstractListModelQAbstractTableModel继承的模型可以利用这些类提供的函数的默认实现。在树形结构中公开数据项的模型必须实现QAbstractItemModel中的许多虚拟函数。

需要实现模型子类的函数可以分为三组

  • 条目数据处理:所有模型需要实现函数,以便视图和代理可以查询模型的维度、检查条目和检索数据。
  • 导航和索引创建:层次模型需要提供视图可以调用以导航它们公开的树形结构并获得项目模型索引的函数。
  • 拖放支持和MIME类型处理:模型继承了控制内部和外部拖放操作方式的函数。这些函数允许以其他组件和应用软件可以理解的方式描述数据项的MIME类型。

项数据处理

模型可以提供不同级别的数据访问:它们可以是简单的只读组件,某些模型可能支持尺寸调整操作,而其他模型可能允许编辑项。

只读访问

要为模型提供的数据提供只读访问,模型子类必须实现以下功能必须

标志()由其他组件用于获取有关模型提供的每个项目的信息。在许多模型中,标志的组合应包括 Qt::ItemIsEnabledQt::ItemIsSelectable
数据()用于向视图和代理提供项数据。通常,模型只需为 Qt::DisplayRole 和任何特定于应用程序的用户角色提供数据,但也建议为 Qt::ToolTipRoleQt::AccessibleTextRoleQt::AccessibleDescriptionRole 提供数据。有关与每个角色关联的类型的更多信息,请参阅 Qt::ItemDataRole 枚举文档。
表头数据()为视图提供在标题中显示的信息。该信息仅由可以显示标题信息的视图检索。
行数()提供模型公开的数据行数。

这四个功能必须实现所有类型的模型中,包括列表模型(QAbstractListModel 子类)和表模型(QAbstractTableModel 子类)。

另外,以下功能必须QAbstractTableModelQAbstractItemModel 的直接子类中实现

列数()提供模型公开的数据列数。列表模型不提供此功能,因为它已在 QAbstractListModel 中实现。

可编辑项

可编辑模型允许修改数据项,并且可能还提供允许插入和删除行和列的功能。要启用编辑,必须正确实现以下功能

标志()必须返回每个项目的适当标志组合。特别是,此函数返回的值必须包括除了在只读模型中应用于项的值之外,还必须包括 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函数能够通知其他组件关于模型底层结构的变化。对于可能涉及内部重组、数据排序或任何其他结构变化的模型更复杂的变化,有必要执行以下序列:

此序列可用于任何结构更新,而不是更高级和方便的保护方法。例如,如果包含两千万行的模型需要删除所有奇数行,即100万个不相邻的1元素范围。可能使用beginRemoveRows和endRemoveRows 100万字,但这显然效率低下。相反,可以将其作为一个单个布局更改来发出信号,同时立即更新所有必要的持久索引。

模型数据的延迟填充

模型数据的延迟填充有效地允许将获取模型信息的请求推迟到视图实际需要的时候。

一些模型需要从远程来源获取数据,或者必须执行耗时的操作来获取有关数据组织方式的信息。由于视图通常请求尽可能多的信息来准确显示模型数据,因此有时限制返回给它们的信息量很有用,这样可以减少对数据的不必要后续请求。

在层级模型中,找到给定项的子项数目是一个昂贵的操作,在这种模型中,确保只有当需要时才调用模型的rowCount()实现是有用的。在这种情况下,可以重新实现hasChildren()函数,为视图提供一种经济的方法来检查子项的存在,并在QTreeView的情况下,为它们的父项绘制相应的装饰。

无论hasChildren()的重实现返回true还是false,视图可能没有必要调用rowCount()来确定有多少子项存在。例如,如果父项没有被展开以显示它们,QTreeView没有必要知道有多少子项。

如果已知许多项将具有子项,有时无条件地将hasChildren()重实现为总是返回true是采取的一种有用方法。这确保了在尽可能快地初始化模型数据时,可以稍后检查每个项的子项。唯一的缺点是,在没有子项的情况下,某些视图可能会在用户尝试查看不存在的子项之前看起来不正确。

层级模型需要提供视图可以调用的函数来导航它们公开的树结构,并获取项的模型索引。

父项和子项

由于向视图公开的结构是由底层数据结构确定的,因此每个模型子类都必须通过提供以下函数的实现来创建自己的模型索引

index()给定父项的模型索引,此函数允许视图和委托访问该项的子项。如果没有找到对应于指定行、列和父模型索引的有效子项,则该函数必须返回一个无效的模型索引QModelIndex(),它是一个无效的模型索引。
parent()为任何给定子项提供对应于其父项的模型索引。如果指定的模型索引对应于模型的顶层项,或者如果模型中没有有效的父项,则该函数必须返回由空QModelIndex()构造函数创建的无效模型索引。

上述两个函数都使用createIndex()工厂函数来为其他组件生成索引。模型向该函数提供一些唯一标识符是正常的,以确保模型索引可以在以后与其对应的项重新关联。

拖放支持和MIME类型处理

模型/视图类支持拖放操作,提供了对于许多应用程序来说足够好的默认行为。然而,也有可能自定义拖放操作过程中项的编码方式,以及它们是默认复制还是移动,以及如何将它们插入现有模型。

此外,我们为方便视图实现的类添加了特殊的行为,这些行为应该与新开发者预期的非常相似。有关这种行为的概述,请参阅“方便视图”部分。

MIME 数据

默认情况下,内置的模型和视图使用一个内部 MIME 类型(application/x-qabstractitemmodeldatalist)来传递有关模型索引的信息。它指定了项目列表的数据,包括每个项目的行和列号以及每个项目支持的角色信息。

使用此 MIME 类型编码的数据可以通过调用QAbstractItemModel::mimeData()函数,并传递一个包含要序列化的项目的QModelIndexList,来获得。

在实现自定义模型的拖放支持时,可以通过重新实现以下函数来以特定格式导出数据项

mimeData()此函数可以重新实现以返回除了默认的内部 MIME 类型application/x-qabstractitemmodeldatalist之外的其他格式的数据。

子类可以从基类获取默认的QMimeData对象,并向其添加更多格式的数据。

对于许多模型,提供由 MIME 类型如text/plainimage/png表示的通用格式的内容是有用的。请注意,图像、颜色和 HTML 文档可以通过QMimeData::setImageData()、QMimeData::setColorData()和QMimeData::setHtml()函数轻松地添加到QMimeData对象中。

接收拖放的数据

当拖放操作在视图上执行时,底层模型会查询它支持的操作类型和它可以接受的 MIME 类型。此信息由QAbstractItemModel::supportedDropActions()和QAbstractItemModel::mimeTypes()函数提供。未重写由QAbstractItemModel提供的实现的自定义模型支持复制操作和项目的默认内部 MIME 类型。

当序列化项数据被拖放到视图上时,数据将使用其QAbstractItemModel::dropMimeData()的实现插入到当前模型中。此函数的默认实现永远不会覆盖模型中的任何数据;相反,它试图将数据项作为项的兄弟项或子项插入。

要利用QAbstractItemModel的默认内置 MIME 类型实现,新模型必须提供以下函数的实现

insertRows()这些函数允许模型使用由QAbstractItemModel::dropMimeData()提供的现有实现自动插入新数据。
insertColumns()
setData()允许向新行和新列填充项。
setItemData()此函数提供了向新项填充数据的高效支持。

要接受其他形式的数据,这些函数必须被重新实现

supportedDropActions()返回一个组合的拖放操作,指示模型可以接受哪些拖放操作类型。
mimeTypes()返回模型可以解码和处理的一系列 MIME 类型列表。通常,模型支持的用于输入的 MIME 类型与它使用编码数据为外部组件提供使用的 MIME 类型相同。
dropMimeData()执行通过拖放操作传输数据的实际解码,确定它将在模型中的哪个位置设置,并在必要时插入新的行和列。

如果dropMimeData()函数的实现通过插入或删除行和列更改了模型的维度,或者如果数据项发生了更改,必须注意确保所有相关信号都被发射。简单地调用子类中的其他函数的实现,例如setData(),insertRows()和insertColumns(),可能有助于确保模型的行为保持一致。

为了确保拖放操作能正常工作,重要的是要重新实现以下从模型中删除数据的函数:

有关使用项视图进行拖放操作的信息,请参阅使用项视图进行拖放操作

便捷视图

便捷视图(QListWidgetQTableWidgetQTreeWidget)覆盖了默认的拖放功能,以提供不太灵活但更符合许多应用的天然行为。例如,由于将数据拖放到QTableWidget的单元格中更为常见,用传输中的数据替换现有内容,因此底层模型将设置目标项的数据而不是在模型中插入新的行和列。有关便捷视图中的拖放操作的更多信息,请参阅使用项视图进行拖放操作

大量数据处理性能优化

canFetchMore()函数检查父项是否还有更多数据可用,并相应地返回真或假。 fetchMore()函数根据指定的父项检索数据。这两个函数可以组合使用,例如,在涉及增量数据的数据库查询中涉及QAbstractItemModel。我们重新实现canFetchMore()以指示是否有更多数据要检索,并重新实现fetchMore()以填充模型。

另一个例子是动态填充的树型模型,当树型模型的分支展开时,我们会重新实现fetchMore()。

如果您的fetchMore()重新实现添加行到模型中,您需要调用beginInsertRows()和endInsertRows()。同时,也需要重新实现canFetchMore()和fetchMore(),因为它们的默认实现返回假且不执行任何操作。

模型/视图类

这些类使用模型/视图设计模式,在这种模式下,底层数据(模型)与用户表示和操作数据的方式(视图)是分开的。

QAbstractItemDelegate

用于显示和编辑模型中的数据项

QAbstractItemModel

项模型类的抽象接口

QAbstractItemView

项目视图类的功能基础

QAbstractListModel

一个抽象模型,可以被继承以创建一维列表模型

QAbstractProxyModel

用于代理项目模型的基础类,可以对数据进行排序、过滤或其他数据处理任务

QAbstractTableModel

一个可以被继承以创建表格模型的抽象模型

QColumnView

列视图的模型/视图实现

QConcatenateTablesProxyModel

代理多个源模型,合并它们的行

QDataWidgetMapper

数据模型的一部分和控件之间的映射

QFileSystemModel

本地文件系统的数据模型

QHeaderView

项目视图的标题行或列

QIdentityProxyModel

以其未修改的形态代理源模型

QItemDelegate

用于显示和编辑来自模型的数据项的设施

QItemEditorCreator

在不继承QItemEditorCreatorBase的情况下创建项目编辑器基础类

QItemEditorCreatorBase

必须继承以实施新项目编辑器的抽象基类

QItemEditorFactory

用于在视图和代理中编辑项目数据的控件

QItemSelection

管理模型中选定项的信息

QItemSelectionModel

跟踪视图的选定项

QItemSelectionRange

管理模型中选定项范围的信息

QListView

模型上的列表或图标视图

QListWidget

基于项的列表控件

QListWidgetItem

用于QListWidget项视图类的项

QModelIndex

用于在数据模型中定位数据

QModelRoleData

持有角色及其关联的数据

QModelRoleDataSpan

在QModelRoleData对象上的范围

QPersistentModelIndex

用于在数据模型中定位数据

QSortFilterProxyModel

支持在另一个模型和视图之间传递数据时排序和过滤数据

QStandardItem

用于QStandardItemModel类的项

QStandardItemEditorCreator

无需继承QItemEditorCreatorBase即可注册控件

QStandardItemModel

存储自定义数据的通用模型

QStringListModel

为视图提供字符串的模型

QStyledItemDelegate

用于显示和编辑来自模型的数据项的设施

QTableView

表格视图的默认模型/视图实现

QTableWidget

带默认模型的基于项的表格视图

QTableWidgetItem

用于QTableWidget类的项

QTableWidgetSelectionRange

在不使用模型索引和选择模型的情况下 interacting with selection in a model 的一种方式

QTreeView

树视图的默认模型/视图实现

QTreeWidget

使用预定义树模型的树视图

QTreeWidgetItem

用于QTreeWidget便捷类的项

QTreeWidgetItemIterator

遍历QTreeWidget实例中的项的方法

© 2024 Qt公司。其中包含的文档贡献是各自所有者的版权。所提供的文档是根据自由软件基金会发布的GNU自由文档许可证版本1.3许可的。Qt和相应标志是芬兰和/或其他国家的Qt公司的商标。所有其他商标均为其各自所有者的财产。