在检查如何处理选择之前,您可能会发现检查模型/视图框架中使用的概念很有用。
基本概念
在模型/视图架构中,模型提供了一个标准接口,用于视图和委托访问数据。在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()作为它们的父项来引用。这将在下一节中讨论。
项目(items)的父元素
当在表或列表视图中使用数据时,模型提供的类表接口是理想的。行和列的编号系统精确地映射到视图显示项目的方式。然而,像树视图这样的结构要求模型向其中的项公开更灵活的接口。因此,每个元素项也可以是另一个元素表的父元素,就像树视图中的顶层元素项可以包含另一个元素表一样。
在为模型元素请求索引时,必须提供元素父元素的一些信息。在模型外部,只有通过模型索引才能引用元素,因此还必须提供一个父模型索引:
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);
项目角色
模型中的项可以为其他组件执行不同的角色,从而允许为不同的情况提供不同类型的数据。例如,Qt::DisplayRole用于访问可以在视图中显示为文本的字符串。通常,项包含许多不同角色的数据,标准角色由****Qt::ItemDataRole****定义。
我们可以向模型请求项目的数据,方法是将项目对应的模型索引传递给模型,并指定一个角色来获得我们想要的数据类型:
QVariant value = model->data(index, role);
项目角色
角色指示模型引用的数据类型。视图可以以不同的方式显示角色,因此为每个角色提供适当的信息很重要。
创建新模型部分更详细地介绍了角色的一些特定用途。
项目数据的最常见用途是由Qt::ItemDataRole中定义的标准角色覆盖的。通过为每个角色提供适当的项目数据,模型可以向视图和委托提供提示,说明项目应该如何呈现给用户。不同类型的视图可以根据需要自由地解释或忽略此信息。还可以为特定于应用程序的目的定义其他角色
总结
- 模型索引以一种独立于任何底层数据结构的方式向视图和委托提供关于模型所提供的项目位置的信息。
- 元素项通过行号和列号以及父元素项的model索引进行引用。
- 模型索引是由模型根据其他组件(如视图和委托)的请求构建的。
- 如果在使用index()方法请求索引时为父元素指定了有效的模型索引,则返回的索引指向模型中父元素下面的元素。获得的索引指向该项的一个子项。
- 如果在使用index()方法请求索引时,为父元素指定了无效的模型索引,则返回的索引指向模型中的顶层元素。
- 角色区分与项相关联的不同类型的数据。
使用模型索引
为了演示如何使用模型索引从模型中检索数据,我们设置了一个没有视图的QFileSystemModel,并在一个小部件中显示文件和目录的名称。虽然这不是使用模型的正常方式,但它展示了模型在处理模型索引时使用的约定。
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()函数检索存储在每个条目中的文本。我们指定模型索引和DisplayRole以字符串的形式获取项目的数据。
QString text = model->data(index, Qt::DisplayRole).toString();
// Display the text in a widget.
}
上面的例子演示了从模型中检索数据的基本原则:
- 使用rowCount()和columnCount()可以得到模型的维度。这些函数通常需要指定一个父模型索引。
- 模型索引用于访问模型中的项。指定项目需要行、列和父模型索引。
- 要访问模型中的顶层元素,可以用QModelIndex()指定一个空的模型索引作为父索引。
- 项目包含不同角色的数据。要获取特定角色的数据,必须向模型提供模型索引和角色。
创建新模型
模型/视图组件之间的功能分离,允许创建可以利用现有视图的模型。这种方法允许我们使用标准的图形用户界面组件(如QListView、QTableView和QTreeView)来表示来自各种来源的数据。
QAbstractItemModel类提供了一个足够灵活的接口来支持在层次结构中安排信息的数据源,允许数据被插入、删除、修改或以某种方式排序。它还支持拖放操作。
QAbstractListModel和qabstractttablemodel类提供了对更简单的非层次数据结构的接口支持,并且更容易作为简单列表和表模型的起点使用。
在本节中,我们创建一个简单的只读模型来探索模型/视图架构的基本原理。在本节的后面,我们将修改这个简单的模型,让用户可以修改物品。
有关更复杂模型的示例,请参见简单树模型示例。
QAbstractItemModel子类的需求在模型子类化参考文档中有更详细的描述。
设计模型
在为现有数据结构创建新模型时,重要的是要考虑使用哪种类型的模型来为数据提供接口。如果数据结构可以表示为一个列表或项目表,那么您可以子类化QAbstractListModel或qabstractttablemodel,因为这些类为许多函数提供了合适的默认实现。
然而,如果底层数据结构只能用层次树结构表示,则有必要将QAbstractItemModel子类化。在简单的树模型示例中采用了这种方法。
在本节中,我们基于字符串列表实现了一个简单的模型,因此QAbstractListModel提供了一个理想的基类来进行构建。
无论底层数据结构采用何种形式,在特殊模型中使用允许更自然地访问底层数据结构的API来补充标准QAbstractItemModel API通常是一个好主意。这使得用数据填充模型变得更容易,但仍然允许其他通用模型/视图组件使用标准API与之交互。下面描述的模型为此提供了一个自定义构造函数。
只读的示例模型
这里实现的模型是一个简单的、非层次的、只读的数据模型,基于标准的QStringListModel类。它有一个QStringList作为它的内部数据源,并且只实现了建立一个有效模型所需的功能。为了让实现更容易,我们创建了一个子类QAbstractListModel,因为它为列表模型定义了合理的默认行为,而且它公开了一个比qabstracttemmodel类更简单的接口。
当实现一个模型时,重要的是要记住,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()。在类定义中添加了下列函数声明:
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;
}
插入和删除行
可以更改模型中的行数和列数。在字符串列表模型中,只改变行数是有意义的,所以我们只重新实现插入和删除行的函数。这些在类定义中声明:
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()函数通知其他组件行数即将改变。该函数指定要插入的第一行和最后一行的行号,以及它们父项的model索引。修改字符串列表后,它调用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类以垂直列表的形式显示该模型或任何其他模型提供的数据。对于string list模型,这个视图还提供了一个默认编辑器,以便对项目进行操作。我们在视图类中检查标准视图类可用的可能性。
模型子类化参考文档更详细地讨论了QAbstractItemModel子类的需求,并提供了在不同类型的模型中启用各种特性必须实现的虚函数的指南。
代理模型
在模型/视图框架中,单个模型提供的数据项可以被任意多个视图共享,而且每个视图都可能以完全不同的方式表示相同的信息。自定义视图和委托是为相同数据提供完全不同表示的有效方法。然而,应用程序通常需要为相同数据的处理版本提供常规视图,例如对一组项目进行不同排序的视图。
尽管将排序和过滤操作作为视图的内部函数似乎是合适的,但这种方法不允许多个视图共享这种潜在代价高昂的操作的结果。另一种方法涉及在模型内部进行排序,会导致类似的问题,即每个视图都必须显示根据最近的处理操作组织的数据项。
为了解决这个问题,模型/视图框架使用代理模型来管理各个模型和视图之间提供的信息。从视图的角度来看,代理模型的行为类似于普通模型,并代表该视图从源模型中访问数据。模型/视图框架使用的信号和槽确保每个视图都被适当地更新,无论在其自身和源模型之间放置了多少代理模型。
使用代理模型
代理模型可以插入到现有模型和任意数量的视图之间。Qt提供了一个标准的代理模型,QSortFilterProxyModel,它通常被实例化并直接使用,但也可以被子类化来提供自定义的过滤和排序行为。QSortFilterProxyModel类可以按如下方式使用:
QSortFilterProxyModel *filterModel = new QSortFilterProxyModel(parent);
filterModel->setSourceModel(stringListModel);
QListView *filteredView = new QListView;
filteredView->setModel(filterModel);
由于代理模型继承自QAbstractItemModel,所以它们可以连接到任何类型的视图,并且可以在视图之间共享。它们还可以用于处理以流水线方式从其他代理模型获得的信息。
QSortFilterProxyModel类被设计为可以实例化并直接在应用程序中使用。通过继承这些类并实现所需的比较操作,可以创建更专用的代理模型。
自定义代理模型
通常,代理模型中使用的处理类型涉及将每个数据项从源模型中的原始位置映射到代理模型中的不同位置。在一些模型中,一些项目可能在代理模型中没有对应的位置;这些模型过滤代理模型。视图使用代理模型提供的模型索引访问项,这些索引不包含关于源模型或模型中原始项的位置的信息。
QSortFilterProxyModel允许源模型中的数据在提供给视图之前进行过滤,还允许源模型的内容作为预排序数据提供给视图。
自定义过滤模型
QSortFilterProxyModel类提供了一个相当通用的过滤模型,可以在各种常见情况下使用。对于高级用户,QSortFilterProxyModel可以被子类化,提供一种机制来实现自定义过滤器。
QSortFilterProxyModel的子类可以重新实现两个虚函数,每当请求或使用代理模型中的模型索引时,都会调用它们:
- filterAcceptsColumn()用于从源模型中筛选特定的列。
- filterAcceptsRow()用于从源模型中筛选特定的行。
- 在QSortFilterProxyModel中,上述函数的默认实现返回true,以确保所有项都传递给视图;这些函数的重新实现应该返回false来过滤单独的行和列。
自定义排序模型
QSortFilterProxyModel实例使用std::stable_sort()函数在源模型中的项和代理模型中的项之间建立映射关系,这样就可以在不修改源模型结构的情况下将有序层次的项暴露给视图。要提供自定义的排序行为,请重新实现lessThan()函数来执行自定义的比较。
模型子类化引用
模型子类需要提供QAbstractItemModel基类中定义的许多虚函数的实现。需要实现这些函数的数量取决于模型的类型——它是为视图提供简单的列表、表,还是复杂的项目层次结构。继承QAbstractListModel和qabstractttablemodel的模型可以利用这些类提供的函数的默认实现。以树状结构公开数据项的模型必须为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()信号。
这个序列可以用于任何结构更新,而不是更高级、更方便的保护方法。例如,如果一个有200万行数据的模型需要删除所有奇数行,那么就有100万个离散区间,每个区间包含1个元素。我们可以使用100万次beginRemoveRows和endRemoveRows,但这显然是低效的。相反,这可以表示为一次布局更改,一次性更新所有必要的持久索引。
模型数据的惰性填充
模型数据的惰性填充有效地允许将对模型信息的请求延迟到视图实际需要的时候。
有些模型需要从远程来源获取数据,或者必须执行耗时的操作来获取有关数据组织方式的信息。为了准确地显示模型数据,视图通常会请求尽可能多的信息,因此限制返回给视图的信息量可以减少不必要的后续数据请求。
在分层模型中,查找给定项的子元素的数量是一个昂贵的操作,因此确保模型的rowCount()实现仅在必要时调用是很有用的。在这种情况下,可以重新实现hasChildren()函数,以提供一种廉价的方式让视图检查子元素是否存在,在QTreeView中,为父元素绘制适当的装饰。
无论重新实现的hasChildren()返回true还是false,视图都不需要调用rowCount()来确定有多少个子节点。例如,如果父项还没有展开,QTreeView不需要知道有多少个子项。
如果知道很多项都有子项,那么重新实现hasChildren()无条件地返回true有时是一种有用的方法。这确保了在尽可能快地初始化模型数据的同时,可以在以后检查每个项目的子项。唯一的缺点是没有子项的项可能在某些视图中显示不正确,直到用户尝试查看不存在的子项。
导航和模型索引创建
层次模型需要提供视图可以调用的函数,以导航它们公开的树状结构,并获取项目的模型索引。
父母和孩子
由于暴露给视图的结构由底层数据结构决定,因此每个模型子类都需要实现以下函数来创建自己的模型索引。
index() | 给定一个父项的model索引,这个函数允许视图和代理访问该项的子项。如果没有找到与指定的行、列和父模型索引对应的有效子项,则函数必须返回QModelIndex(),这是一个无效的模型索引。 |
parent() | 提供与任何给定子项的父元素对应的模型索引。如果指定的模型索引对应于模型中的顶层元素,或者模型中没有有效的父元素,那么函数就必须返回一个无效的模型索引,这个索引是用空的QModelIndex()构造函数创建的。 |
上面的两个函数都使用createIndex()工厂函数来为其他组件生成索引。模型通常会为该函数提供一些唯一标识符,以确保稍后可以将模型索引与其对应的项重新关联起来。
拖放支持和MIME类型处理
model/view类支持拖放操作,为许多应用程序提供了足够的默认行为。然而,也可以自定义项目在拖放操作中编码的方式,无论它们是默认复制还是移动,以及它们如何插入到现有的模型中。
此外,便利视图类实现的特化行为应该与现有开发人员所期望的密切相关。便利视图部分提供了这种行为的概述。
MIME数据
默认情况下,内置的模型和视图使用内部的MIME类型(application/x-qabstractitemmodeldatalist)来传递关于模型索引的信息。这指定了项目列表的数据,包含每个项目的行号和列号,以及每个项目支持的角色信息。
使用这种MIME类型编码的数据可以通过调用QAbstractItemModel::mimeData()来获得,QModelIndexList包含要序列化的项。
当在自定义模型中实现拖放支持时,可以通过重新实现以下函数以特殊格式导出数据项:
mimeData() | 这个函数可以被重新实现,以application/x-qabstractitemmodeldatalist内部的MIME类型之外的格式返回数据。子类可以从基类获得默认的QMimeData对象,并以其他格式向其添加数据。 |
对于许多模型来说,以MIME类型表示的通用格式(如文本/纯文本和图像/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类型与编码数据供外部组件使用时可以使用的MIME类型相同。 |
dropMimeData() | 执行通过拖放操作传输的数据的实际解码,确定将在模型中的何处设置它,并在必要时插入新的行和列。在子类中如何实现这个函数取决于每个模型公开的数据的需求。 |
如果dropMimeData()函数的实现通过插入或删除行或列改变了模型的维度,或者修改了数据项,那么就必须小心确保发出了所有相关信号。简单地调用子类中其他函数的重新实现,比如setData()、insertRows()和insertColumns(),可以确保模型的行为一致。
为了确保拖动操作正常工作,重新实现以下从模型中删除数据的函数很重要:
- removeRows ()
- removeRow ()
- removeColumns ()
- removeColumn ()
有关在项目视图中拖放的更多信息,请参阅在项目视图中使用拖放。
针对大量数据的性能优化
canFetchMore()函数检查父节点是否有更多可用的数据,并相应地返回true或false。fetchMore()函数根据指定的父对象获取数据。这两个函数可以结合使用,例如,在涉及增量数据的数据库查询中,填充一个QAbstractItemModel。我们重新实现了canFetchMore()来表示是否还有更多数据需要获取,并根据需要实现了fetchMore()来填充模型。
另一个例子是动态填充的树模型,当树模型中的一个分支被扩展时,我们重新实现fetchMore()。
如果你重新实现了fetchMore()方法,将行添加到模型中,你需要调用beginInsertRows()和endInsertRows()方法。另外,canFetchMore()和fetchMore()都必须重新实现,因为它们的默认实现返回false,什么也不做。
Model/View Programming | Qt Widgets 5.15.17