Model/View Programming

引言

Qt包含了许多项目视图类,它们通过Model/View框架来管理数据和数据以何种方式呈现到用户之间的关系。由Model/View架构引入的功能分离赋予了开发者更大的灵活性定制化项目描述,提供了一种标准化的数据模型接口,允许使用现有的项目视图展示大量的数据源。在本文档中,我们简要介绍了Model/View范例,勾勒涉及到的概念,描述项目视图系统架构。本文档将会解释架构中的每个组件,并给出实例显示如何使用提供的类。

Model/View架构

模型-视图-控制器(MVC)是源自Smalltalk的一种设计模式,它常用于UI设计中。在《设计模式》中,Gamma写道:MVC由三类对象组成,Model是应用对象,View是屏幕展示,Controller定义了用户接口与用户输入的互动方式。在MVC产生前,用户接口(UI)设计倾向于把所有的对象混杂在一起。MVC将其解耦合,增加了灵活性和复用性。

如果把视图和控制器对象组合在一起,那么就产生了Model/View架构。这仍然把数据存储方式和数据呈现给用户的方式分离开来,而且基于同样的原理提供了一种更简单的框架。这种分离使得同样的数据可以有几种不同的视图,在需要新视图时,只需要实现新视图,而无需改变需要显示的数据结构。为了允许灵活处理用户输入,我们引入了代理的概念。在架构中使用代理的优势是它允许定制化数据项目被渲染和编辑的方式。

Model/View架构 模型负责与数据源通讯,为架构中其他组件提供了一种接口。通讯的本质依赖于数据源类型和模型实现的方式。 视图接受模型中模型索引,模型索引指向了数据项目,视图从数据源获取数据项目。在标准视图中,代理渲染数据项目。当项目被编辑时,代理使用模型索引与模型直接通信。
通常,Model/View类如上所述可以分为三组:模型,视图和代理。每个组件通过抽象类定义提供了公共接口。抽象类意味着可以被其他组件继承实现期望的所有功能,这也允许写特殊的组件。模型,视图和代理使用信号和槽机制进行交互:

  1. 来自模型的信号通知视图关于数据源的数据变化
  2. 来自视图的信号提供了用户与显示的项目的互动信息
  3. 来自代理的信号用于在编辑时通知模型和视图关于编辑器的状态

模型

所有项目模型都是基于QAbstractItemModel类。该类定义了视图和代理访问数据的一种接口。数据本身无需存储在模型中,它可以存储在数据结构中或者其他分开的类,文件,数据库提供的仓库或者其他应用组件。与模型相关的基本概念将在Model类章节进行描述。

QAbstractItemModel提供了一种面向数据的接口,它足够灵活,可以处理以表格,列表和树结构描述的视图。然而,当实现类列表和类树数据结构的新模型时,通过继承AbstractListModel和QAbstractTableModel类是更好的出发点,因为这两个类已经提供公共函数的默认实现。 这两个类都可以扩展子类提供支持特定类型列表和表格的模型。子类化模型的过程将会在Creating New Models章节进行讨论。

Qt提供了许多现成的模型来处理数据项目:

  1. QStringListModel用于存储一个简单的QString项目的列表
  2. QStandardItemModel 管理更复杂的项目的树结构,每个项目可以包含任一数据
  3. QFileSystemModel提供了本地文件系统中的文件和目录信息
  4. QSqlQueryModel, QSqlTableModel, and QSqlRelationalTableModel用于使用Model/View惯例访问数据库。

如果这些标准模型不能满足你的需求,那么可以子类化QAbstractItemModel, QAbstractListModel或QAbstractTableModel来创建你自己的客制化模型。

视图

下面提供了几种不同视图的完整实现:ListView显示项目的列表, QTableView 以表格形式显示模型的数据, QTreeView以层次式列表显示数据的模型项目。上述三个视图类都是继承自 QAbstractItemView抽象基类。尽管这些类即用型实现,但是它们也可以子类化提供客制化视图。在View类章节将会描述可得视图。

代理

QAbstractItemDelegate是Model/View架构中代理的抽象基类。QStyledItemDelegate提供了默认代理实现,同时也是Qt标准视图的默认代理。 然而,QStyledItemDelegate和QItemDelegate是独立可选方案。差异主要是QStyledItemDelegate使用当前style绘制数据项目。 因此,当实现客制化代理或者使用Qt Style sheets时,我们建议使用QStyledItemDelegate作为基类。代理将会在Delegate类章节进行描述。

模型索引

为了确保数据的描述与数据访问分离开来,引入了模型索引概念。通过模型获得的每条信息都是通过模型索引描述的。视图和代理使用这些索引请求显示数据项。因此,只要模型需要知道如何获取数据,那么需要定义模型管理的数据类型。模型索引包含一个创建它们模型的指针。当操作多个模型时,模型索引避免了混淆。

QAbstractItemModel *model = index.model();

模型索引提供了对信息的临时引用,可以用来通过模型获取或修改数据。由于模型有时可能重组它的数据结构,那么模型索引就会变得无效,不应该存储。如果需要数据的长期引用,那么必须创建一个一致性模型索引。一致性模型索引提供了模型保持更新的信息。临时模型索引由QModelIndex类提供,而一致性模式索引由QPersistentModelIndex类提供。

为了获得数据项对应的模型索引,必须指定模型的三个属性:行号,列号和父项的索引。下面涨价将会详细描述这些属性。

行和列

在最基础的表格中,一个模型可以按照简单表进行访问,在表中每个项通过行和列号进行定位,但是这并不是说真实的数据是按照矩阵结构存储的,行号和列号的使用仅仅是一种约定俗成的说法,允许组件彼此之间进行通讯。我们可能通过指定模型的行号和列号获取给定数据项的信息,我们可以通过如下代码获取描述数据项的一个索引:

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

可以提供对简单,单层级数据结构(诸如列表和表格)接口的模型不需要其它信息,所上述代码所示,当获取一个模型索引时,我们需要提供更多的信息。
在这里插入图片描述

数据项父节点

当在表格视图或列表视图中使用数据时,由模型提供数据项类表格的接口是理想化的。通过行号和列号能够准确映射视图显示数据项的方式。然而,对于树形视图的结构需要模型提供数据项更加灵活的接口。因此,每个数据项也可以成为其他数据项的父项,同样的方式,在树形视图中,顶层数据项可以包含另一个数据项列表。

当请求一个模型数据项的一个索引时,我们必须提供该数据项父项的一些信息。在模型之外,对数据项引用的唯一方式是通过一个模型索引,因此必须提供父项模型索引。

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

在这里插入图片描述

Item roles

模型中的数据项可以为其他组件执行各种角色,允许在不同场合提供不同类型的数据。比如,Qt::DisplayRole可以用来访问字符串,并在视图中显示为文本。通常,数据项包含的数据可以用于大量不同角色,标准的角色定义为Qt::ItemDataRole。

我们可以通过传递数据项对应的模型索引来从模型请求数据项,通过指定角色获取我们想要的数据类型:

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

在这里插入图片描述由Qt::ItemDataRole定义的标准角色可以满足数据项的大部分应用。通过为了每个角色提供合适的数据项,模型可以为视图和代理提供了数据项应该如何呈现给用户的线索。不同类型的视图可以自由解析或忽略要求的信息,它也可以根据特定应用目的定义额外角色。

总结

  1. 模型索引为视图和代理以一种与数据无关的方式提供了数据项在模型中的位置信息。
  2. 数据项可以通过它们的行,列和它们父项的模型索引来引用。数据项可以通过它们的行,列和它们父项的模型索引来引用。
  3. 模型索引可以应其他组件的要求由模型创建,比如视图和代理。
  4. 当使用index()请求一个索引时,如果为父项指定了一个有效的模型索引,那么返回的索引指向模型中父项下面的一个数据项。获得的索引指向了数据项的子项。
  5. 当使用index()请求索引时,如果为父项指定了一个无效模型索引,那么返回的索引指向模型的顶层数据项。
  6. 角色用于区分数据项的不同类型数据。

使用模型索引

为了演示如何使用模型索引从模型中获取数据,我们创建了QFileSystemModel(没有视图),在一个Widget中显示文件名。尽管这不会展示模型使用正常方式,但是它演示了处理模型索引时模型使用惯例。

我们按照如下方法创建一个文件系统模型:

	QFileSystemModel *model = new QFileSystemModel;
    	QModelIndex parentIndex = model->index(QDir::currentPath());
    	int numRows = model->rowCount(parentIndex);

在这种情况下,我们创建一个默认QFileSystemModel,使用由模型提供index()方法获取父项索引。我们使用rowCount()函数计算模型中行数。

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

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

为了获取模型索引,我们指定了行号,列号(第一列索引为0)和我们需要数据项的父项模型索引。在每个数据项中存储的文本通过模型的data()函数获取。我们为每个数据项以字符串形式指定模型索引和DisplayRole。

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

上述示例演示了从模型中获取数据的基本原则:

  1. 模型的规模使用rowCount()和columnCount()获得,这些函数通常需要指定父项模型索引
  2. 在模型中使用模型索引访问数据项。行号,列号和父项模型索引需要用来指定数据项
  3. 为了访问模型中顶层数据项,指定一个null模型索引作为父项索引。
  4. 数据项包含不同角色的数据。为了获取特定角色的数据,必须提供模型索引和角色给模型

延伸阅读

新模型可以通过实现QAbstractItemModel提供的标准接口创建。在创建新模型章节,我们将通过创建一个传统即用型容纳字符串列表的模型来演示示例。

视图类

概念

在模型/视图架构中,视图从模型中获取数据项,然后把数据呈现给用户。数据呈现给用户的方式不需要与模型提供的数据描述类似,可以与存储数据项的数据结构完全不同。

通过使用QAbstractItemModel类和QAbstractItemView类提供的标准模型接口和模型索引(模型索引以一般方式描述数据项)可以实现内容和描述的分离。通常,视图管理从模型中获得的数据的整体布局。它们可以自己渲染单个数据项,或者使用代理处理数据项渲染和编辑特征。

与呈现数据一样,视图处理数据项之间导航和数据项选择的一些因素。视图也实现了基本的用户接口特征,比如上下文菜单,拖拽。视图可以为数据项提供默认编辑工具,或者它可以与代理合作提供一个客制化编辑器。

视图可以脱离模型创建,但是在视图显示有用信息前,必须提供合适的数据模型。视图跟踪用户通过选择用法选择的数据项,这可以为每个视图独立维护,也可以由多个视图共享。

一些视图,诸如QTableView和QTreeView,显示header和数据项。这些也可以通过视图类QHeaderView实现。Header通常访问像视图一样访问同一数据模型。它们使用QAbstractItemModel::headerData()函数从模型中获取数据。通常以标签形式显示header信息。新header可以子类化QHeaderView为视图提供更加特化的标签。

使用现成视图

Qt提供了现成的视图类呈现模型中的数据给用户。QListView可以显示数据项为一个简单的列表,或者经典的icon视图形式。QTreeView显示模型中的数据项为层次化列表。QTreeView以表的形式呈现数据项,调调有点儿像excel表格的样子。
在这里插入图片描述
上面展示的标准视图的默认行为对于大多数应用是足够了。它们提供了基本的编辑工具,可以客制化适应更加客制化的用户接口需求。

使用模型

我们以我们创建的字符串列表模型为例,并添加数据到模型,构建合适的视图显示模型的内容。这可以在单个函数中完成:

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

视图通过模型接口渲染模型的内容和访问数据。当用户试图编辑数据项时,视图使用默认代理提供一个编辑器widget。
在这里插入图片描述
上述图片显示了QListView如何描述字符串列表模型中的数据。由于模型是可编辑的,那么视图自动允许列表中的每个数据项使用默认代理编辑。

对一个模型使用多个视图

为同一个模型提供多个视图是一件简单的事情,就是为每个不同的视图添加相同的数据模型即可。在下面的代码中,我们创建了两个表视图,每个都使用我们创建的简单表模型为例:

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

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

在模型/视图架构中信号和槽的使用意味着模型的变化可以反向传递到关联视图,确保我们不用考虑使用的视图总是可以访问到同一数据。
在这里插入图片描述
上图显示了同一模型的两个不同视图,每个视图包含大量所选数据项。尽管模型的数据在两个视图中是一致的,但是每个视图维护着各自的内部选择模型。这在特定场合是有用的,但是,对于大部分应用,一般需要共享选择。

处理数据项选择

在视图中处理数据项选择的机制由QItemSelectionModel类提供。所有标准视图按照默认构建了各自的选择模型,按照正常方式交互。

视图使用的选择模型可以通过selectionModel()函数获得,替代选择模型可以通过setSelectionModel()函数获得。当我们想要对同一个模型数据提供多个一致视图时,视图用于控制选择模型的能力就显得十分有用。通常除非你子类化一个模型或者视图,否则,你都不需要直接操作选择的内容。然而,如果需要,可以访问选择模型的接口,这在数据项视图的处理选择章节进行探索。

在多个视图中共享选择

尽管视图类默认提供了自己的选择模型是方便的,但是当我们对同一数据使用多个视图时,通常期望模型的数据与用户的选择在所有视图中是一致的。由于视图类允许内部选择模型被替代,那么我们可以使用如下代码段达到视图间的一致选择:

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

第二个视图指定了第一个视图的选择模型。现在两个视图操作同一个选择模型,保持数据与所选的数据项是同步的。
在这里插入图片描述
在上述示例中,两个同类型视图用于显示同一模型数据。然而,如果使用两个不同类型的视图,那么所选数据项可以在每个视图中描述的大不相同。比如,在表视图中连续选择可以在树形视图中被描述为零碎的高亮数据项集合。

代理类

概念

不同于MVC模式,模型/视图设计并不包含完全分离的组件来管理与用户交互。通常,视图负责呈现数据到用户和处理用户输入。为了允许输入获取的灵活性,一般由代理执行交互。这些组件提供了输入能力,在一些场景中也负责渲染单个数据项。控制代理的标准接口由QAbstractItemDelegate类定义。

一般情况下,期望代理能够通过实现paint()和sizeHint()函数来渲染各自的内容。然而,简单的基于widget的代理可以子类化QItemDelegate而不是QAbstractItemDelegate,充分利用这些函数的默认实现。

代理的编辑器可以通过使用widget管理编辑过程或者通过直接处理事件来实现。第一种方法在本章节进行描述,它也会在代理示例中展示。

Pixelator示例展示了如何创建一个客制化代理来对表视图进行特化渲染。

使用现成代理

Qt提供的标准视图使用QItemDelegate的实例来提供编辑工具。代理接口的默认实现对每个标准视图(QListView, QTableView和QTreeView(以常规的方式渲染数据项。

所有的标准角色由标准视图的默认代理处理。这些标准角色的解析在QItemDelegate文档中有详细描述。

使用所用代理由itemDelegate()函数返回。setItemDelegate()函数允许你为标准视图安装一个客制化代理。当为客制化视图设置代理时,使用setItemDelegate()函数是必要的。

简单代理

在这里实现的代理使用QSpinBox提供一个编辑工具,主要旨在显示整数。尽管我们出于这个目的安装了一个基于整数的客制化模型,而不是使用QStandardItemModel,由于客制化代理控制数据进入。我们构建表视图来显示模型的内容,这将使用客制化代理用于编辑。
在这里插入图片描述
因为我们不需要定制显示功能,所以我们从QStyledItemDelegate子类化代理。然而,我们仍然必须提供管理编辑器widget的函数:

class SpinBoxDelegate : public QStyledItemDelegate
{
    Q_OBJECT

public:
    SpinBoxDelegate(QObject *parent = 0);

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

注:当构建代理时,无需安装任何编辑器widget。只有在有需求时,我们才构建一个编辑器widget。

提供编辑器

在这个示例中,当表视图需要提供一个编辑器时,它请求代理提供一个编辑器widget,编辑器widget要适合数据项修改。任何代理需要的东西都应该提供createEditor()函数,以便安装合适的widget:

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

注意:我们不需要保持一个指针指向编辑器widget,因为视图有责任在编辑器不需要时摧毁它。

我们安装在编辑器上安装代理的默认事件过滤器,确保它能够提供用户期望的标准编辑快捷方式。添加额外的快捷方式到编辑器允许更加复杂的行为,这些将在Editing Hints章节进行详述。

视图通过调用我们后续定义的函数确保编辑器的数据和几何结构正确设置。我们可以根据视图提供的模型索引创建不同的编辑器。比如,如果我们一列整数和一列字符串,我们可以根据正在被编辑的列内容返回QSpinBox或QLineEdit。

代理必须提供函数拷贝模型数据到编辑器。在这个示例中,我们读取显示角色中存储的数据,然后在spin box中设置相应的值。

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

在这个示例中,我们知道编辑器widget是spin box,但是我们会针对模型中数据类型的不同提供不同的编辑器,在这种场合中,我们需要在它的成员函数前将widget转换为合适类型。

提交数据到模型

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

由于视图管理代理的编辑器widget,所以我们仅仅需要把编辑器提供的内容更新到模型中。在这种情况下,我们确保spin box是最新的,并使用spin box的值更新到模型。

标准QItemDelegate类通过发射closeEditor()信号通知视图代理何时完成了编辑。视图确保编辑器widget被关闭和摧毁。在本例中,我们仅仅提供简单的编辑工具,因此,我们从来不需要发射这个信号。

所有对数据的操作都是通过QAbstractItemModel提供的接口执行。这使得代理在大部分情况下可以独立于其操作的数据,但是必须做一些假设目的是使用特定类型的编辑器widget。在本例中,我们假定模型总是容纳整数值,但是我们仍然可以使用这个代理处理不同类型的模型,因为QVariant为了意料之外数据提供了合理的默认值。

更新编辑器几何结构

代理有责任管理编辑器的几何结构。当编辑器被创建时或当视图中数据项的尺寸或者位置发生变化时,必须设置几何结构。幸运的是,视图在一个视图选项对象中提供了所有的必要的几何信息。

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

在这种情况下,我们仅仅使用数据项矩形中视图选项提供的几何信息。当代理渲染带有多个元素的数据项时,代理不会直接使用数据项矩形。它设置编辑器到数据项中其他元素的位置关系。

编辑线索

一旦编辑完毕后,代理应该提供线索给其他组件关于编辑过程的结果,提供线索有利于随后的编辑操作。这可以发射带有合适线索的closeEditor()信号来实现。默认QItemDelegate事件过滤器需要特别注意这一点儿。

Spin Box的行为需要调整使得让用户更加友好。在QItemDelegate提供的默认事件过滤器中,如果用户按下返回到确认spin box的选择时,代理把值指派给模型,然后关闭spin box。我们可以通过安装我们各自事件过滤器到Spin Box上来改变这种行为,提供适合我们需要的编辑线索;比如,我们可以发射带有QItemDelegate线索来自动开始编辑视图中的下一个数据项。

另外一种不需要使用事件过滤器的方法是提供我们自己的编辑器widget,当然子类化QSpinBox比较方便。可选方案可以让我们更好的控制编辑器widget的表现方式,当然了代价就是需要多写一些代码。如果需要定制标准Qt编辑器widget的行为,通常在代理安装一个事件过滤器是比较容易的。

代理不得不发射这些线索,但是那些不发射线索的代理很少会集成到应用中,也不如那些发射线索支持公共编辑动作的代理好用。

数据项视图中选择处理

概念

在数据项视图类中所用的选择模型会对基于模型/视图架构的选择提供一个通用的描述。尽管标准操作选择的标准类对于提供的数据项视图已经足够,但是选择模型允许你创建专门的选择模型来适应你自己数据项模式和视图的要求。

视图中所选数据项的信息存储在QItemSelectionModel类的实例中。在单个模型中,这为数据项维护模型索引,与其他视图无关。由于一个模型可能有多个视图,那么在多个视图间共享选择是可能的,多个视图允许应用一致地将选择显示在多个视图上。

选择由选择范围组成。这些通过仅仅记录每个被选数据项的范围的起始和终止模型索引,就足以维护大量被选数据项的信息了。非连续选择的数据项通过使用多个选择范围创建。

选择应用于选择模型持有的模型索引集合。最新选择的数据项也被称之为当前选择。

创建新模型

数据项视图的拖拽

Proxy Models

官网原始文档链接

Qt官网模型/视图编程

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值