Qt 5.2模型 - 视图 - 模式:如何通知模型对象底层数据结构的变化

时间:2014-01-31 11:15:41

标签: c++ qt user-interface model-view-controller model

我有一个类用于永久存储某些项目,这些项目以表格方式组织。这个类与Qt完全无关,来自不同的库。让我们在本问题的其余部分调用此类DataContainer。它提供了与std-c ++兼容的迭代器来访问和操作内容。

我需要通过Qt GUI显示和修改数据。我的想法是创建一个继承自DataContainerQtAdaptor的类QAbstractTableModel并存储指向DataContainer对象的指针。 DataContainerQtAdaptor充当DataContainer对象的适配器,我Qt应用程序内部的所有操作都是通过此适配器完成的。然后我使用QTableView小部件来显示信息。

不幸的是,线程/进程可能会更改DataContainer。 (例如,将DataContainer视为封装数据库连接的某个C ++类,并且该数据库可能被其他人更改。)

问题:

1)假设我每次更改DataContainer对象的内部结构时都会调用一个函数。必须调用QAbstractTableModel的正确函数是什么,以通知模型的潜在变化?我需要像#34;亲爱的模型,你的持久存储后端改变了。请更新自己并向每个附加的视图发出信号,以反映此更改"。

2)让我们说1)解决了。什么是避免" double"如果通过GUI触发更改,GUI更新?例如:用户点击表格小部件中的单元格 - >表格小部件调用模型的setData - > model将更改推送到后端 - >后端触发自己的" onUpdate"功能 - > model重新读取完整的后端(虽然它已经知道了变化) - > GUI第二次更新

3)用户应该能够通过GUI插入新的行/列并将数据放入其中。但是这个位置是由这个数据决定的,因为后端会保持数据的排序。因此,我有以下问题:用户决定在结尾创建一个新行,并将新数据推送到后端。重新读取后端/模型时,此数据通常不在最后位置,但已插入中间某处,所有其他数据已向前移动。我是否保留了表视图小部件的所有属性,例如"选择一个单元格"同步?

我相信,所有这些问题必须有一些简单的标准解决方案,因为它与QFileSystemModel的工作方式相同。用户选择文件,而其他一些进程创建新文件。新文件显示在视图中,所有后续行向前移动。选择也向前发展。

的Matthias

2 个答案:

答案 0 :(得分:6)

模型语义

首先,您必须确保QAbstractItemModel不能处于不一致状态。这意味着在对基础数据进行某些更改之前,必须在模型上触发一些信号。

结构更改和数据更改之间存在根本区别。结构更改是要添加或删除的模型的行/列。数据更改仅影响现有数据项的值。

  • 结构更改需要围绕修改调用beginXxxendXxx。在调用beginXxx之前,无法修改任何结构。当您完成更改结构后,请致电endXxxXxx之一是:InsertColumnsMoveColumnsRemoveColumnsInsertRowsMoveRowsRemoveRowsResetModel

    如果更改影响了许多不连续的行/列,则表明模型重置的成本会更低 - 但要小心,视图上的选择可能无法生存。

  • 保持结构完整性的数据更改仅需要在修改基础数据后发送 。这意味着,在查询模型的对象收到dataChanged之前,对data的调用可能会返回新值时会有一个时间窗口。

这也意味着非常量模型在非dataChanged类中几乎无用,除非您使用观察者或类似模式实现桥接功能。

打破更新循环

在模型上处理更新循环的Qt-idiomatic方法是利用项目角色。完全取决于您的模型如何解释角色。 QObject实施的简单有用的行为只是将角色从QStringListModel调用转发到setData,否则忽略该角色。

股票视图小工具仅对dataChanged dataChanged作出反应。然而,当他们编辑数据时,他们会使用DisplayRolesetData。这打破了循环。该方法适用于查看窗口小部件和Qt快速查看项目。

将数据插入已排序的模型

只要模型在排序完成后正确发出变化信号,您就可以了。

操作顺序为:

  1. 视图会添加一行并调用模型的EditRole方法。模型可以将此空行添加到底层容器中。关键是现在必须保留空行索引。

  2. 编辑从行中的项目开始。视图状态更改为insertRow

  3. 对项目进行编辑。视图退出编辑状态,并在模型上设置数据。

  4. 模型根据其内容确定项目的最终位置。

  5. 该模型调用Editing

  6. 模型通过将项目插入正确的位置来更改容器。

  7. 该模型调用beginMoveRows

  8. 此时,一切都如你所愿。如果移动的项目在移动之前被聚焦,则视图可以自动跟随移动的项目。默认情况下,编辑的项目会被聚焦,因此工作正常。

    所需的容器功能

    您的endMoveRows没有足够的功能使其正常工作除非通过该模型完成对它的所有访问。如果要直接访问容器,请使容器明确继承DataContainer,否则您必须向容器添加通知系统。前者是一个更容易的选择。

    您的核心问题简化为:我是否可以在不实现模型通知API的某些变体的情况下拥有模型功能。显而易见的答案是:不,对不起,你不能 - 按照定义。功能是否存在,或者它不是。如果您不希望容器为QAbstractXxxxModel,则可以使用观察者模式实现通知API - 然后您需要模型填充类。真的没办法解决它。

    文件系统会通知QObject有关已更改的各个目录条目。你的容器也必须这样做 - 这相当于提供某种形状或形式的QFileSystemModel信号。如果模型包含移动或添加/删除的项目 - 其结构发生更改 - 则必须通过调用相关dataChanged和{{1}来发出xxxAboutToBeYyyxxxYyy信号方法。

    指数

    beginZzz最重要的未记录方面是:只要模型的结构没有改变,它的实例才有效。如果您的模型传递了一个在结构更改之前生成的索引,那么您可以自由地以不确定的方式行事(崩溃,发动核打击,无论如何)。

    endZzz存在的全部原因是您拥有一个基础的,复杂索引的数据容器的用例。您对模型QModelIndex方法的实现必须生成以某种形式存储对QModelIndex::internalPointer()索引的引用的索引实例。如果这些索引适合指针,则不需要在堆上分配数据。如果需要在堆上分配容器索引存储,则必须保留指向此数据的指针,并在容器的结构发生更改时将其删除。您可以自由地执行此操作,因为在结构更改后没有人应该使用索引实例。

答案 1 :(得分:2)

从方法bool QAbstractItemModel::insertRows(int row, int count, const QModelIndex & parent = QModelIndex())的文档:

  

如果您实施自己的模型,则可以重新实现此功能   你想支持插入。或者,您可以提供   自己的API用于更改数据。在任何一种情况下,您都需要打电话   beginInsertRows()和endInsertRows()通知其他组件   模特已经改变了。

同样适用于removeRows()moveRows()(他们拥有自己的begin*()end*()方法。为了修改现有项目的数据,有一个dataChanged()信号。

以下是它的结果(问题1的回答):

实现自己的插入/删除/修改数据的方法,其中每个方法必须如下所示:

beginInsertRows(parentIndex, beginRow, endRow);
// code that modifies underlying data
endInsertRows();
必须提供

beginRowendRow以告知插入行的位置以及行数(endRow-beginRow)。

对于beginDeleteRows()beginMoveRows(),它们是相同的。

当你有一个简单修改现有项目数据的方法时,这个方法必须在最后发出信号:dataChanged()

如果您对数据进行了大量更改,有时在执行此大修改的方法中调用beginResetModel()endResetModel()会更简单。它将导致所有视图刷新其中的所有数据。

回答问题2:

如果它将“双重更新”,这取决于View类的实现。在视图中输入数据时,数据将通过模型中的一种编辑方法(insertRows()setData()等)发送到模型。这些方法的默认实现始终使用begin*()end*()方法,因此模型会发出正确的通知信号。所有视图都会侦听这些信号,包括用于输入数据的信号,因此将执行“双重更新”。

定义此行为的唯一方法是继承View并重新实现其受保护的插槽(如dataChanged()等),以避免在检测到此视图提供的值时进行更新。

我不确定Qt视图是否已经这样做了。对此的回答需要有更多受过Qt内部教育的人,或者查看Qt源代码(目前我还没有)。如果有人知道这一点,请发表评论,我会更新答案。

我认为从模型重新加载数据并不是那么糟糕 - 它保证你所看到的确实是模型的价值。您可以避免编辑器和View错误可能出现的问题。

回答问题3:

当您重新加载整个模型时,没有简单的方法来跟踪选择。在这种情况下,您需要询问view->selectionModel()当前选择并尝试在重新加载后恢复它。

但是,如果您进行部分刷新(使用我在答案1中描述的方法),则视图将为您跟踪选择。无需担心。

最终评论:

如果要编辑模型类外部的数据,可以执行此操作。只需将begin*()end*()方法公开为公共API,因此编辑数据的其他代码可以通知模型和视图有关更改。

虽然可以做到,但这不是一个好习惯。它可能会导致错误,因为在修改数据的任何地方都很容易忘记调用通知。如果您必须调用模型API来通知有关更改,为什么还没有移动所有编辑代码来模拟模型并公开编辑API?