【翻译】Qt模型视图框架介绍(长文)

一、概述

 Qt 包含一组项目视图类,这些类使用模型/视图框架来管理数据之间的关系以及数据呈现给用户的方式。

模型-视图-控制器 (MVC)设计模式,通常在构建用户界面时使用。

MVC 由三种对象组成。

  • Model 是应用程序对象
  • View 是 Model 的屏幕展示
  • Controller 定义了用户界面对用户输入的反应方式

MVC 将用户界面设计解耦以增加灵活性和重用性。

如果视图和控制器(V & C)对象组合在一起,结果就是模型/视图框架。结果是:

  • 这仍然将数据的存储方式与向用户呈现的方式分开,但提供了一个原则不变的更简单的框架。
  • 这种分离使得可以在几个不同的视图中显示相同的数据。可以自定义实现新的视图类型用来展示数据,而无需更改底层数据结构。

为了灵活处理用户输入,我们引入了委托的概念。在此框架中使用委托的优势在于,它允许自定义呈现和编辑数据项的方式。

1.1、模型

所有模型都基于 QAbstractItemModel 类。视图和委托使用此类的接口来访问数据。

数据本身不必存储在模型中,它可以保存在由单独的类、文件、数据库或某些其他应用程序组件提供的数据结构或存储库中。

QAbstractItemModel 提供了一个数据接口,该接口足够灵活,可以处理以表格、列表和树的形式表示数据的视图。但是,在为列表(1列n行)和类似表格(n行m列)的数据结构实现新模型时,QAbstractListModel QAbstractTableModel 类是更好的起点,因为它们提供了常用函数的适当默认实现。这些类中的每一个都可以被子类化以提供支持特殊类型列表和表格的模型。

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

  • QStringListModel 用于存储 QString 项的简单列表。
  • QStandardItemModel 管理更复杂的项目树结构,每个项目都可以包含任意数据。
  • QFileSystemModel 提供有关本地文件系统中文件和目录的信息。
  • QSqlQueryModelQSqlTableModel QSqlRelationalTableModel 用于使用模型/视图方式访问数据库。

如果这些标准模型不符合要求,可以将 QAbstractItemModel、QAbstractListModel 、QAbstractTableModel 子类化以创建自定义模型。

1.2、视图

Qt为不同类型的视图提供了完整的实现:

  • QListView 显示项目列表
  • QTableView 在表格中显示模型中的数据
  • QTreeView 在分层列表中显示模型数据项。

这些类中都基于 QAbstractItemView 抽象基类。虽然这些类是现成的实现,但它们也可以被子类化以提供自定义视图。

1.3、委托

QAbstractItemDelegate 是模型/视图框架中委托的抽象基类。

默认委托实现由 QStyledItemDelegate 提供,它被 Qt 的标准视图用作默认委托。

QStyledItemDelegate 和 QItemDelegate 是为视图中的项目绘制和提供编辑器的两个独立替代方案。

它们之间的区别在于 QStyledItemDelegate 使用当前样式来绘制其项目。因此建议在实现自定义委托时使用 QStyledItemDelegate 作为基类

1.4、便利类

许多便利类是从标准视图类派生出来的,不建议对它们进行子类化。

此类类的示例包括 QListWidget、QTreeWidget 、QTableWidget

这些类不如视图类灵活,并且不能与任意模型一起使用。建议优先使用模型/视图方法来处理项目视图中的数据,实在需要时才使用这些类。


二、使用模型和视图的示例

Qt 提供的两个标准模型 QStandardItemModel QFileSystemModel

  • QStandardItemModel 是一个多用途模型,可用于表示列表、表、树视图所需的各种不同数据结构。
  • QFileSystemModel 是一个模型,用于维护有关目录内容的信息。因此,它本身不保存任何数据项,而只是表示本地文件系统上的文件和目录。

QListView QTreeView 类是最适合与 QFileSystemModel 一起使用的视图。

int main(int argc, char *argv[])
{
    QApplication app(argc, argv);
    QSplitter *splitter = new QSplitter;

    QFileSystemModel *model = new QFileSystemModel;
    model->setRootPath(QDir::currentPath());

    QTreeView *tree = new QTreeView(splitter);
    tree->setModel(model);
    tree->setRootIndex(model->index(QDir::currentPath()));

    QListView *list = new QListView(splitter);
    list->setModel(model);
    list->setRootIndex(model->index(QDir::currentPath()));

    splitter->show();
    return app.exec();
}


三、模型类

3.1、基本概念

在模型/视图框架中,模型提供了视图和委托用来访问数据的标准接口。标准接口由 QAbstractItemModel 类定义。

无论数据项如何存储在任何底层数据结构中,QAbstractItemModel 的所有子类都将数据表示为包含项表的分层结构。

视图使用此约定来访问模型中的数据项,但它们在向用户呈现此信息的方式方面不受限制。

3.2、模型索引

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

模型索引对象包含一个指向创建它们的模型的指针,这可以防止在处理多个模型时出现混淆。

 QAbstractItemModel *model = index.model();

模型索引提供对数据片段的临时引用,可用于通过模型检索或修改数据。 由于模型可能会不时重新组织其内部结构,因此模型索引可能会失效,不应存储。

如果需要对一条信息进行长期引用,则必须创建一个持久模型索引。这提供了对模型保持最新的信息的参考。

临时模型索引由 QModelIndex 类提供,持久模型索引由 QPersistentModelIndex 类提供。

要获得与数据项对应的模型索引,必须为模型指定三个属性:行号、列号、父项的模型索引。

3.3、行和列

最基本的形式中,模型可以作为一个简单的表来访问,其中的项目按其行号和列号定位。这并不意味着底层数据存储在数组结构中,使用行号和列号只是允许组件相互通信的约定。可以通过向模型指定其行号和列号来检索有关任何给定项目的信息,并且我们会收到表示该项目的索引:

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

如上图显示了一个基本表模型的表示,其中每个项目都由一对行号和列号定位。通过将相关的行号和列号传递给模型来获得引用数据项的模型索引。

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

模型中的顶级项目始终通过将 QModelIndex() 指定为其父项目来引用。

3.4、项的父项

上面是一种理想情况。树视图等结构要求模型向其中的项目公开更灵活的接口。即项目也可以是另一个项目表的父项,就像树视图中的顶级项目可以包含另一个项目列表一样。

当请求模型项的索引时,必须提供有关该项父项的信息。在模型之外,引用项的唯一方法是通过模型索引(父项信息包含在父项的引用中):

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

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

项目“A”和“C”表示为模型中的顶级兄弟:

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

项目“A”有许多子项。使用以下代码获得项目“B”的模型索引:

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

3.5、项目角色

模型中的项目可以为其他组件扮演各种角色,允许为不同情况提供不同类型的数据。

enum Qt::ItemDataRole:模型中的每个项目都有一组与之关联的数据元素,每个元素都有自己的角色。视图使用角色来向模型指示它需要哪种类型的数据。 自定义模型应该返回这些类型的数据。

  • Qt::DisplayRole:要以文本形式呈现的关键数据。 (QString)
  • Qt::DecorationRole:以图标的形式呈现为装饰的数据。(QColor、QIcon 或 QPixmap)
  • Qt::EditRole:适合在编辑器中编辑的形式的数据。 (QString)
  • Qt::ToolTipRole:显示在项目工具提示中的数据。 (QString)
  • Qt::StatusTipRole:状态栏中显示的数据。 (QString)
  • Qt::WhatsThisRole:为“这是什么?”中的项目显示的数据 模式。 (QString)
  • Qt::SizeHintRole:将提供给视图的项目的大小提示。 (QSize)

Qt::ItemDataRole 中定义的标准角色涵盖了项目数据的最常见用途。还可以为特定于应用程序的目的定义其他角色。

可以通过向模型传递项目对应的模型索引来向模型询问项目的数据,并通过指定角色来获取想要的数据类型:

 QVariant value = model->data(index, role);

3.6、模型索引使用示例

    QFileSystemModel *model = new QFileSystemModel;
    QFileSystemModel::connect(model, &QFileSystemModel::directoryLoaded, [model](const QString &directory)
    {
        QModelIndex parentIndex = model->index(directory);
        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();
            qDebug()<<text;
        }
    });
    model->setRootPath(QDir::currentPath());


四、视图类

在模型/视图框架中,视图从模型中获取数据项并将它们呈现给用户。数据的呈现方式可能与用于存储数据项的底层数据结构完全不同。

内容和表示的分离是通过使用 QAbstractItemModel 提供的标准模型接口、QAbstractItemView 提供的标准视图接口以及使用以通用方式表示数据项的模型索引来实现的。

  • 视图通常管理从模型获得的数据的整体布局。他们可以自己渲染单个数据项,或者使用委托来处理渲染和编辑功能。
  • 除了呈现数据,视图还处理项目之间的导航以及项目选择等操作。
  • 视图还实现了基本的用户界面功能,例如上下文菜单和拖放。
  • 视图可以为项目提供默认的编辑工具,或者它可以与委托一起提供自定义编辑器。
  • 可以在没有模型的情况下构建视图,但必须提供模型才能显示有用的信息。

4.1、现有视图

Qt 提供了三个视图类,它们以大多数用户熟悉的方式呈现来自模型的数据。

  • QListView 可以将模型中的项目显示为简单列表。
  • QTreeView 将模型中的项目显示为列表的层次结构。
  • QTableView 以表格的形式呈现模型中的项目。

这些标准视图的默认行为对于大多数应用程序来说应该足够了。它们提供基本的编辑功能,并且可以进行定制以满足更专业的用户界面的需要。

4.2、处理项目的选择

处理视图中项目选择的机制由 QItemSelectionModel 类提供。默认情况下,所有标准视图都会构建自己的选择模型。 视图使用的选择模型可以通过 selectionModel() 函数获得,替换选择模型可以通过 setSelectionModel() 指定。

当希望在同一模型数据上提供多个一致的视图时,控制视图使用的选择模型的能力非常有用。

尽管默认情况下视图类提供自己的选择模型很方便,但是当在同一个模型上使用多个视图时,通常希望模型的数据和用户的选择在所有视图中一致显示。由于视图类允许替换其内部选择模型,因此可以使用以下行实现视图之间的统一选择:

     secondTableView->setSelectionModel(firstTableView->selectionModel());


五、委托类

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

委托通过实现paint() 和sizeHint() 函数来自己呈现他们的内容。简单的基于小部件的委托可以子类化 QStyledItemDelegate 而不是 QAbstractItemDelegate,并利用这些函数的默认实现。

委托的编辑器可以通过使用小部件来管理编辑过程或直接处理事件来实现。

5.1、使用现有委托

Qt 提供的标准视图使用 QStyledItemDelegate 的实例来提供编辑功能。委托接口的这个默认实现为标准视图(QListView、QTableView 、 QTreeView)以通常的样式呈现项目。

视图使用的委托由 itemDelegate() 函数返回。 setItemDelegate() 函数允许标准视图安装自定义委托。

5.2、一个简单的委托

Qt模型视图框架:自定义委托

六、视图中的选择操作

有关在视图中选择的项目的信息存储在 QItemSelectionModel 类的实例中。这在单个模型中维护项目的模型索引,并且独立于视图。

通过仅记录每个选定项目范围的开始和结束模型索引,可以有效地维护有关大量项目选择的信息。项目的非连续选择是通过使用多个选择范围来描述选择来构建的。

6.1、当前项和选中项

在一个视图中,总是有一个当前项和一个选中项。一个项目可以是当前项目并同时被选中。视图负责确保始终存在当前项,如按下方向键进行导航需要当前项。

当前项只能有一个。可以有多个选中项。

当前项由焦点矩形指示。选中项由选择矩形指示。

在选择操作时,QItemSelectionModel 对象记录模型中所有项的选择状态。设置选择模型之后就可以选择、取消选择项目集合。或者可以切换它们的选择状态,而无需知道哪些项目已被选中。 


七、视图便利类

  • QListWidget 提供单级项目列表
  • QTreeWidget 显示多级树结构
  • QTableWidget 提供单元格项目表

每个类都继承了 QAbstractItemView 类,该类实现了项目选择和标题管理的常见行为。


八、在视图中使用拖放

Qt模型视图框架:在视图中使用拖放


九、模型子类化参考

模型子类需要提供 QAbstractItemModel 基类中定义的许多虚函数的实现。需要实现的这些功能的数量取决于模型的类型——它是否为视图提供了简单的列表、表格或复杂的项目层次结构。

从 QAbstractListModel 和 QAbstractTableModel 继承的模型可以利用这些类提供的函数的默认实现。以树状结构公开数据项的模型必须为 QAbstractItemModel 中的许多虚函数提供实现。

9.1、项目数据处理

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

9.2、只读访问

要提供对模型提供的数据的只读访问,必须在模型的子类中实现以下函数:

  • flags():被其他组件用来获取模型提供的每个项目的信息。
  • data():用于向视图和委托提供项目数据。通常,模型只需要为 Qt::DisplayRole 和任何特定于应用程序的用户角色提供数据,但为 Qt::ToolTipRole、Qt::AccessibleTextRole 、Qt::AccessibleDescriptionRole 提供数据也是一种很好的做法。
  • headerData():为视图提供要在其标题中显示的信息。
  • rowCount():提供模型暴露的数据行数。

这四个函数必须在所有类型的模型中实现。

此外,必须在 QAbstractTableModel QAbstractItemModel 的直接子类中实现以下函数:

  • columnCount():提供模型暴露的数据列数。列表模型不提供此功能,因为它已在 QAbstractListModel 中实现。

9.2、可编辑项目

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

  • flags():必须为每个项目返回适当的标志组合。 特别是,此函数返回的值还必须包括 Qt::ItemIsEditable
  • setData():用于修改与指定模型索引关联的数据项。为了能够接受由用户界面元素提供的用户输入,该函数必须处理与 Qt::EditRole 相关的数据。 该实现还可以接受与 Qt::ItemDataRole 指定的许多不同类型的角色相关联的数据。 更改数据项后,模型必须发出 dataChanged() 信号以通知其他组件更改。
  • setHeaderData():用于修改水平和垂直标题信息。更改数据项后,模型必须发出 headerDataChanged() 信号以通知其他组件更改。

9.3、可调整大小的模型

所有类型的模型都可以支持行的插入和删除。表模型和层次模型也可以支持列的插入和删除。在模型尺寸发生更改之前和之后通知其他组件非常重要。可以实现以下函数以允许调整模型大小:

  • insertRows():用于向所有类型的模型添加新行和数据项。在将新行插入任何底层数据结构之前,实现必须调用 beginInsertRows(),然后立即调用 endInsertRows()。
  • removeRows():用于从所有类型的模型中删除行及其包含的数据项。在从任何底层数据结构中删除行之前,实现必须调用 beginRemoveRows(),然后立即调用 endRemoveRows()。
  • insertColumns():用于向表模型和层次模型添加新的列和数据项。在将新列插入任何底层数据结构之前,实现必须调用 beginInsertColumns(),然后立即调用 endInsertColumns()。
  • removeColumns():用于从表模型和层次模型中删除列及其包含的数据项。在从任何底层数据结构中删除列之前,实现必须调用 beginRemoveColumns(),然后立即调用 endRemoveColumns()。

通常,如果操作成功,这些函数应该返回 true。但是,可能存在操作仅部分成功的情况;例如,如果可以插入少于指定的行数。在这种情况下,模型应返回 false 。

在调整大小 API 的实现中调用的函数发出的信号使附加组件在数据变得不可用之前进行必要的操作。插入和删除操作与开始和结束函数的封装还使模型能够正确管理持久模型索引。

通常,begin 和 end 函数能够通知其他组件有关模型底层结构的更改。然而此方法有局限性。例如,如果一个 200 万行的模型需要删除所有奇数行,可以使用 beginRemoveRows() 和 endRemoveRows() 100 万次,但这显然是低效的。

对于模型结构的更复杂更改,可能涉及内部重组、数据排序或任何其他结构更改,要避免低效需要进行以下操作:

  • 发出 layoutAboutToBeChanged() 信号。
  • 更新代表模型结构的内部数据。
  • 使用 changePersistentIndexList() 更新持久索引。
  • 发出 layoutChanged() 信号。

此方法可用于任何结构更新。

9.4、模型数据的惰性填充

模型数据的惰性填充有效地允许延迟对模型信息的请求,直到视图实际需要它。

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

在分层模型中,查找给定项的子项数量是一项昂贵的操作,确保仅在必要时调用模型的 rowCount() 实现很有用。在这种情况下,可以重新实现 QAbstractItemModel hasChildren() 函数,为视图提供一种低廉的方式来检查子项的存在。

例如,如果父项没有展开显示它们,QTreeView 就可以不需要知道有多少个子项。如果知道许多项目将有子项,则重新实现 hasChildren() 以无条件返回 true 有时是一种有用的方法。这确保了以后可以检查每个项目的子项。缺点是没有子项的项目可能会在某些视图中显示不正确。

9.5、索引的父项和子项

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

由于暴露给视图的结构是由底层数据结构决定的,每个模型子类通过提供以下函数的实现来创建自己的模型索引:

  • index():给定父项的模型索引,此函数允许视图和委托访问该项的子项。如果找不到有效的子项 - 对应于指定的行、列和父模型索引,则该函数必须返回 QModelIndex()(一个无效的模型索引)
  • parent():提供对应于任何给定子项的父项的模型索引。如果指定的模型索引对应于模型中的顶级项,或者模型中没有有效的父项,则该函数必须返回 QModelIndex()(一个无效的模型索引)

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

9.6、拖放支持和 MIME 类型处理

模型/视图类支持拖放操作,提供足以满足许多应用程序的默认行为。也可以自定义项目在拖放操作期间的编码方式,默认情况下是复制还是移动,以及如何将它们插入到现有模型中。

9.6.1、MIME 数据

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

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

在自定义模型中实现拖放支持时,可以通过重新实现以下功能以特殊格式导出数据项:

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

9.6.2、接受拖入的数据

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

当项目数据被拖放到视图上时,数据将使用其 QAbstractItemModel::dropMimeData() 的实现插入到当前模型中。这个函数的默认实现是尝试将数据项插入为项的兄弟项或项的子项。

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

  • insertRows()
  • insertColumns()
  • setData():允许用项目填充新的行和列
  • setItemData():为填充新项目提供了更有效的支持

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

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

9.6.3、便利视图

便利视图(QListWidget、QTableWidget 、QTreeWidget)覆盖了默认的拖放功能,以提供不太灵活但更自然的行为。例如,由于在 QTableWidget 中将数据放入单元格中更为常见,将现有内容替换为正在传输的数据,底层模型将设置目标项目的数据,而不是在模型中插入新的行和列。


十、类列表

  • 17
    点赞
  • 45
    收藏
    觉得还不错? 一键收藏
  • 4
    评论
本课程详细、全面地介绍Qt 开发中的各个技术细节,并且额外赠送在嵌入式端编写Qt程序的技巧。整个课程涵盖知识点非常多,知识模块囊括 Qt-Core 组件、QWidgets、多媒体、网络、绘图、数据库,超过200个 C++ 类的分析和使用,学完之后将拥有 Qt 图形界面开发的非常坚实的功底。 每个知识点不仅仅会通过视频讲解清楚,并且会配以精心安排的实验和作业,用来保证学习过程中切实掌握核心技术和概念,通过实验来巩固,通过实验来检验,实验与作业的目的是发现问题,发现技术盲点,通过答疑和沟通夯实技术技能。注意:本套视频教程来源于线下的实体班级,因此视频中有少量场景对话和学生问答,对此比较介意的亲们谨慎购买。注意:本套视频教程包含大量课堂源码,包含对应每个知识点的精心编排的作业。由于CSDN官方规定在课程介绍中不能出现作者的联系方式,因此在这里无法直接给出QQ答疑号,视频中的源码、资料和作业文档链接统一在购买后从CSDN平台跟我沟通,我会及时回复跟进。注意:本套视频教程包含全套10套作业题,覆盖所有视频知识点,循序渐进,各个击破,作业总纲如下:下面是部分作业题目展示,每道题都有知识点说明,是检验学习效果的一大利器:(部分作业展示,为了防止盗图盗题对题干做了模糊处理)(部分作业展示,为了防止盗图盗题对题干做了模糊处理)(部分作业展示,为了防止盗图盗题对题干做了模糊处理)(部分作业展示,为了防止盗图盗题对题干做了模糊处理)(部分作业展示,为了防止盗图盗题对题干做了模糊处理)…… ……

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值