Qt Model View 编程

Qt Model View 编程

模型视图编程简介

Qt包含一组使用模型/视图结构的类,可以用来管理数据并呈现给用户。这种体系结构引入的分离使开发人员更灵活地定制项目,并且提供了一个标准模型的接口,以允许广泛范围的数据源被使用到到现有的视图中。

模型 - 视图 - 控制器(MVC)是一种设计模式,由三类对象组成:表示数据的模型(Model)、表示用户界面的视图(View)和定义用户界面上的操作控制(Controller)。

Model/View 结构

Model-View-Controller(MVC), 是从Smalltalk发展而来的一种设计模式,常被用于构建用户界面。经典设计模式的著作中有这样的描述:

MVC 由三种对象组成。Model是应用程序对象,View是它的屏幕表示,Controller定义了用户界面如何对用户输入进行响应。
在MVC之前,用户界面设计倾向于三者揉合在一起,MVC对它们进行了解耦,提高了灵活性与重用性。

假如把 View 与 Controller 结合在一起,结果就是Model/View结构。这个结构依然是把数据存储与数据表示进行了分离,它与MVC都基于同样的思想,但它更简单一些。这种分离使得在几个不同的view上显示同一个数据成为可能,也可以重新实现新的View,而不必改变底层的数据结构。为了更灵活的对用户输入进行处理,引入了delegate这个概念。它的好处是,数据项的渲染与编程可以进行定制。 在这里插入图片描述
模型与数据源进行通信,在这个体系结构中为其它组件提供了一个接口。通信的性质依赖于数据源的类型以及模型的实现方式。
视图从模型中得到模型索引,这些都引用到数据项。通过为模型提供模型索引,视图可以从数据源中检索数据项。
在标准的视图里,委托呈现数据项目。当一个项目被编辑,委托与模型直接利用模型索引进行通信。

模型/视图/委托通信

模型、视图、委托使用信号和槽相互通信:

  • 模型(Model)的信号:通知视图(View)数据源中的数据发送改变。
  • 视图(View)的信号:提供了关于用户交互显示的项目信息。
  • 委托(Delegate)的信号:当编辑时告诉模型和视图编辑器的状态。

Model(模型)

所有项目模型都基于 QAbstractItemModel 类。 此类定义一个接口,供视图和委托用于访问数据。 数据本身不必存储在模型中。 它可以保存在由单独的类,文件,数据库或某些其他应用程序组件提供的数据结构或存储库中。

有关模型的基本概念在“模型类”部分中介绍。

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

在“创建新模型”一节中讨论了模型的子类化过程。

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

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

如果这些标准模型不能满足要求,则可以继承化QAbstractItemModel、QAbstractListModel或QAbstractTableModel来创建自定义模型。

View(视图)

提供了完整的为实现各种不同的视图:QListView显示项目列表,QTableView显示表中模型的数据,QTreeView在分层列表中显示数据的模型项。每个类是基于QAbstractItemView抽象基类。尽管这些类是现成的实现,但也可以将它们子类化以提供自定义视图。

Delegate(委托)

QAbstractItemDelegate在模型/视图框架中代表抽象的基类。默认的委托实现由 QStyledItemDelegate 提供,这被Qt的标准视图用作默认的委托。然而,QStyledItemDelegate和QItemDelegate独立替代绘画,且为视图项提供编辑器。
它们之间的区别在于QStyledItemDelegate使用当前样式来绘制项目。因此,在实现自定义委托或使用Qt样式表时,建议将QStyledItemDelegate用作基类。

Sorting (排序)

在模型/视图架构中,有两种方法可以进行排序:选择哪种方法取决于您的基础模型。

如果您的模型是可排序的,即重新实现了QAbstractItemModel :: sort()函数,则QTableView 和 QTreeView 都将提供一个API,该API允许您以编程方式对模型数据进行排序。 此外,可以启用交互式排序(即允许用户将数据通过单击视图的标题进行排序),由QHeaderView::sortIndicatorChanged()信号分别连接到QTableView:: sortByColumn()槽或QTreeView::sortByColumn()槽。

另一种方法,如果模型没有所需的接口,或者如果想使用一个列表视图来显示数据,使用代理模型呈现数据视图之前应转换模型的结构。

方便的类

从标准视图类派生出许多便利类,以使依赖Qt基于项目的项目视图和表类的应用程序受益。它们不打算被子类化。

此类的示例包括QListWidget,QTreeWidget和QTableWidget。

这些类比视图类灵活性差,且不能与任意的模型使用。建议使用模型/视图的方法来处理在项目视图中的数据,除非强烈需要一个基于项目的类。

如果想利用模型/视图提供的特性方法,同时使用一个基于项目的接口,可以考虑使用视图类,例如:QListView、QTableView、QTreeView与QStandardItemModel。

使用模型和视图

以下各节说明如何在Qt中使用模型/视图模式。每个部分都包含一个示例,然后是显示如何创建新组件的部分。

Qt包含两个模型

Qt提供的两个标准模型是 QStandardItemModel 和 QFileSystemModel。QStandardItemModel是一个多功能模型,可用于表示列表,表和树视图所需的各种不同的数据结构。该模型还保存数据项。QFileSystemModel是用于维护有关目录内容的信息的模型。它本身不保存任何数据项,而仅表示本地归档系统上的文件和目录。

QFileSystemModel提供了一个现成的模型进行试验,并且可以轻松配置为使用现有数据。使用该模型,我们可以展示如何建立一个可用于现成视图的模型,并探索如何使用模型索引(index)来操纵数据。

在现有模型中使用视图

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());
    
	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->setWindowTitle("Two views onto the same file system model");
    splitter->show();
    return app.exec();
}

上面的例子中,我们忽略提及如何处理选择的项目。下面将详细讲述在视图中处理所选的项目。

模型类

在检查如何处理选择之前,您可能会发现检查模型/视图框架中使用的概念很有用。

基本概念

在模型/视图体系结构中,模型提供了视图和委托用来访问数据的标准接口。在Qt中,标准接口由QAbstractItemModel类定义。无论数据项如何存储在任何基础数据结构中,QAbstractItemModel的所有子类都将数据表示为包含项表的层次结构。视图使用此约定来访问模型中的数据项,但是它们向用户呈现此信息的方式不受限制。
在这里插入图片描述
模型还通过信号和插槽机制通知所有附加视图有关数据更改的信息。

Model indexes (模型索引)

为确保数据被分开被访问,模型索引的概念被引入。可以通过模型索引来获得每条信息。视图与委托使用这些索引来请求显示的数据项。

因此,模型只需要知道如何获取数据,并通过模型管理的数据的类型可以被相当普遍定义。型号索引包含一个指向创建它们的模型的指针,在处理多个模型时可以防止混乱。

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

Item role (项目角色)

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

我们可以通过模型索引传递给相应的项目向模型请求项目数据,并通过指定一个角色来获取想要的数据类型,如下:

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

在这里插入图片描述
数据类型被称为模型的角色指示器。视图可以以不同的方式显示角色,因此,为每个角色提供相应的信息非常重要。

Qt :: ItemDataRole中定义的标准角色涵盖了项目数据的最常见用法。通过为每个角色提供适当的项目数据,模型可以向视图和委托提供有关如何将项目呈现给用户的提示。不同类型的视图可以根据需要自由解释或忽略此信息。也可以为特定于应用程序的目的定义其他角色。

使用模型索引

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

我们通过以下方式构造文件系统模型:

QFileSystemModel *model = new QFileSystemModel;
connect(model, &QFileSystemModel::directoryLoaded, [model](const QString &directory) {
    QModelIndex parentIndex = model->index(directory);
    int numRows = model->rowCount(parentIndex);
});
model->setRootPath(QDir::currentPath);

在这种情况下,我们首先设置默认的QFileSystemModel。我们将其连接到lambda,在其中我们将使用该模型提供的index()的特定实现来获取父索引。在lambda中,我们使用rowCount()函数计算模型中的行数。最后,我们设置QFileSystemModel的根路径,以便它开始加载数据并触发lambda。

为简单起见,我们仅对模型第一栏中的项目感兴趣。我们依次检查每一行,获取每一行中第一项的模型索引,然后读取模型中为该项存储的数据。

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

为了获得一个模型索引,我们指定了行号、列号(第一列为零),以及所有我们想要的项目的父项目的模型索引。每个项目中的文本检索可以使用模型的data()函数来获取。我们指定了模型索引和Qt::DisplayRole来获取数据项中的字符串。

		QString text = model->data(index, Qt::DisplayRole).toString();
        // Display the text in a widget.
    }

上面的示例演示了用于从模型检索数据的基本原理:

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

进一步阅读

可以通过实现 QAbstractItemModel 提供的标准接口来 创建新模型。在“创建新模型”部分,我们通过创建一个方便的随时可用的模型来保存字符串列表来演示这一点。

View Classes 视图类

概念

在模型/视图体系结构中,视图从模型中获取数据项并将它们呈现给用户。数据的呈现方式不必类似于模型提供的数据的表示,而且可能与用于存储数据项的底层数据结构完全不同。

通过使用QAbstractItemModel提供的标准模型接口,QAbstractItemView提供的标准视图接口以及使用以一般方式表示数据项的模型索引来实现内容和表示的分离。视图通常管理从模型获得的数据的整体布局。 他们可以自己渲染单个数据项,或使用委托来处理渲染和编辑功能。

除了显示数据外,视图还可以处理项目之间的导航以及项目选择的某些方面。这些视图还实现了基本的用户界面功能,例如上下文菜单和拖放功能。视图可以提供项目的默认编辑功能,也可以与委托一起使用以提供自定义编辑器。

可以在没有模型的情况下构造视图,但是必须提供模型才能显示有用的信息。视图通过使用选择来跟踪用户选择的项目,这些选择可以为每个视图单独维护,或者在多个视图之间共享。

一些视图,例如 QTableView 和 QTreeView,显示标题和项目。这些也由视图类QHeaderView实现。标头通常与包含它们的视图访问同一模型。他们使用QAbstractItemModel :: headerData()函数从模型中检索数据,并且通常以标签形式显示标头信息。可以从QHeaderView类继承新的标头,以为视图提供更专业的标签。

使用现有视图

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

  • QListView可以将模型中的项目显示为简单列表,也可以经典图标视图的形式显示。
  • QTreeView将模型中的项目显示为列表的层次结构,从而以紧凑的方式表示深层嵌套的结构。
  • QTableView以表格的形式显示来自模型的项目,非常类似于电子表格应用程序的布局。
    在这里插入图片描述

使用模型

我们以创建的 字符串列表模型 作为示例模型,使用一些数据对其进行设置,并构造一个视图以显示模型的内容。所有这些都可以在一个函数中完成:

int main(int argc, char *argv[])
{
    QApplication app(argc, argv);

	// Unindented for quoting purposes:
	QStringList numbers;
	numbers << "One" << "Two" << "Three" << "Four" << "Five";
	
	QAbstractItemModel *model = new StringListModel(numbers);

请注意,将StringListModel声明为QAbstractItemModel。这使我们能够使用模型的抽象接口,并确保即使我们将字符串列表模型替换为其他模型,代码仍然可以正常工作。

QListView提供的列表视图足以在字符串列表模型中显示项目。我们构造视图,并使用以下代码行建立模型:

	QListView *view = new QListView;
	view->setModel(model);

该视图以正常方式显示:

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

该视图呈现模型的内容,并通过模型的界面访问数据。当用户尝试编辑项目时,视图将使用默认委托来提供编辑器小部件。
在这里插入图片描述

使用模型的多个视图

一个模型可以为多个视图所使用。在下面的代码中,我们创建两个表视图,使用的均是创建好的同一个模型。

    QTableView *firstTableView = new QTableView;
    QTableView *secondTableView = new QTableView;

    firstTableView->setModel(model);
    secondTableView->setModel(model);

在模型/视图框架中使用信号和槽是指更改模型可以传递给所有相连的视图,以确保始终可以访问相同的数据,而不管所使用的视图。
在这里插入图片描述

上图显示了同一模型的两个不同视图,每个视图包含许多选定项。尽管在视图中始终显示来自模型的数据,但每个视图都维护自己的内部选择模型。在某些情况下这可能很有用,但是对于许多应用程序来说,需要一个共享的选择模型。

在视图之间共享选择

虽然视图类提供自己的默认选择模型很方便,但当我们使用多个视图到同一个模型时,通常需要所有的模型数据和用户的选择在所有视图显示一致。由于视图类允许其内部选择模型进行更换,那么可以使用如下方式实现视图之间的统一:

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

在这里插入图片描述

第二个视图给出了第一个视图的选择模型。这两种视图现在在同一个选择模型进行操作,保持了数据和所选项目的同步。

Delegate Classes 委托类

基本概念

不同于模型 - 视图 - 控制器模式,模型/视图设计不包括用于管理与用户交互的一个完全独立的组件。一般情况,视图负责将模型数据呈现给用户以及处理用户输入。为了输入更加具有灵活性,则由委托来执行交互。这些组件提供输入功能,且在一些视图中还负责渲染个别项目。控制委托的标准接口在QAbstractItemDelegate类中定义。

委托能够通过实现的paint()和sizeHint()函数来展示它们的内容。但是,基于简单窗口小部件的委托可以继承QStyledItemDelegate而不是QAbstractItemDelegate的子类,并利用这些函数的默认实现。

委托编辑器可以通过使用小工具来管理编辑过程或直接处理事件来实现。 第一种方法将在本节后面介绍,并在Spin Box Delegate示例中显示。

该Pixelator示例演示如何创建一个自定义委托执行专业渲染表视图。

使用现有的委托

Qt提供的标准视图中使用 QStyledItemDelegate 提供编辑功能。委托接口的默认实现以一贯风格来呈现项目为每个标准视图:QListView、QTableView、QTreeView。

所有标准角色由所使用的标准视图中的默认委托处理。

视图使用委托是由 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;
};

请注意,构造委托时未设置任何编辑器小部件。我们仅在需要时构造一个编辑器小部件。

提供编辑

在这个例子中,当表视图需要提供一个编辑器时,它将要求委托提供一个编辑器部件适用于修改该项目。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,这取决于哪一列正在被编辑。

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

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

在这个例子中,我们知道编辑器部件(ediotr)是一个QSpinBox,但可以为模型中不同类型的数据提供不同的编辑器,在这种情况下,则需要在访问它的成员函数之前设置部件为适当的类型。

提交数据到模型

当用户完成在QSpinBox 中的值编辑后,视图通过调用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);
}

由于视图管理委托的编辑器小部件,因此我们只需要使用提供的编辑器的内容来更新模型。在这种情况下,我们确保QSpinBox 是最新的,并使用指定的索引将模型包含的值更新。

标准QStyledItemDelegate类通过发出closeEditor()信号在完成编辑后通知视图。该视图确保编辑器小部件已关闭并销毁。在此示例中,我们仅提供简单的编辑工具,因此我们不需要发出此信号。

所有的数据操作通过 QAbstractItemModel 提供的接口执行。这使得委托在很大程度上不受其处理的数据类型的影响,但为了使用某些类型的编辑器部件,则必须做出一些假设。在这个例子中,我们假设模型总是包含整数值,但我们仍然在不同类型的模型中使用此委托,因为的QVariant为意想不到的数据提供了合理的默认值。

更新编辑器的几何形状

管理编辑器的几何形状是委托的责任。当编辑器被创建,或者当项目视图的的位置、大小在视图中改变时,几何形状必须被设置。幸运的是,视图提供了视图选项物体内部所有必要的几何信息。

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

这种情况下,我们仅在项目区域中使用view选项提供的位置信息。使用一些元素展现项目的委托不会直接使用该项目矩形。根据这个项目中的其他元素来设定编辑器的位置。

创建新模型

通过模型/视图组件之间功能的分离,允许创建可以利用现有视图的模型。这种方法使我们能够使用标准的图形用户界面组件(例如QListView,QTableView和QTreeView)从各种来源显示数据。

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

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

在本节中,我们将创建一个简单的只读模型,以探索模型/视图架构的基本原理。在本节的稍后部分,我们将修改此简单模型,以便用户可以修改项目。

有关更复杂模型的示例,请参见“简单树模型”示例。

QAbstractItemModel子类的要求在“模型子类参考”文档中有更详细的描述。

设计模型

为现有数据结构创建新模型时,重要的是要考虑使用哪种类型的模型来提供数据接口。如果数据结构可以表示为项目列表或表,则可以将QAbstractListModel或QAbstractTableModel子类化,因为这些类为许多功能提供了合适的默认实现。

但是,如果基础数据结构只能由分层树结构表示,则必须对QAbstractItemModel进行子类化。在简单树模型示例中采用了这种方法。

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

无论基础数据结构采用什么形式,通常最好在专用模型中补充标准QAbstractItemModel 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()函数负责返回与index参数对应的数据项:

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

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

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

仅当提供的模型索引有效,行号在字符串列表中的项目范围内并且请求的角色是我们支持的角色时,我们才返回有效的QVariant。

一些视图(例如QTreeView和QTableView)能够显示标题以及项目数据。如果我们的模型显示在带有标题的视图中,我们希望标题显示行号和列号。我们可以通过将headerData()函数子类化来提供有关标题的信息:

QVariant StringListModel::headerData(int section, Qt::Orientation orientation,
                                     int role) const
{
    if (role != Qt::DisplayRole)
        return QVariant();

    if (orientation == Qt::Horizontal)
        return QStringLiteral("Column %1").arg(section);
    else
        return QStringLiteral("Row %1").arg(section);
}

同样,仅当角色是我们支持的角色时,我们才返回有效的QVariant。在确定返回的确切数据时,还将考虑标头的方向。

并非所有视图都显示带有项目数据的标题,并且可以将那些标题配置为隐藏它们。尽管如此,建议您实现headerData()函数以提供有关模型提供的数据的相关信息。

一个项目可以具有多个角色,根据指定的角色给出不同的数据。我们模型中的项目只有一个角色DisplayRole,因此无论指定的角色如何,我们都将返回项目的数据。但是,我们可以在其他角色中重用为DisplayRole提供的数据,例如ToolTipRole,视图可用来在工具提示中显示有关项目的信息。

可编辑的模型

只读模型显示了如何将简单的选择呈现给用户,但是对于许多应用程序而言,可编辑列表模型更加有用。我们可以通过更改为只读实现的 data()函数,以及实现两个额外的函数:flags()和setData(),来修改只读模型以使项目可编辑。以下函数声明已添加到类定义中:

	//flags
	Qt::ItemFlags flags(const QModelIndex &index) const override;
	//setData
    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()以完成操作,并让其他组件知道模型的尺寸已更改。

  • 1
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值