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

Qt 5.2 Model-View-Pattern: How to inform model object about changes in underlying data structure

本文关键字:模型 对象 通知 数据结构 变化 视图 模式 何通知 Qt      更新时间:2023-10-16

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

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

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

问题:

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

2) 假设1)已解决。在通过GUI触发更改的情况下,避免"双重"GUI更新的最佳方法是什么?例如:用户点击表格小部件中的单元格->表格小部件调用模型的setData->模型将更改推到后端->后端触发自己的"onUpdate"功能->模型重新读取完整的后端(尽管它已经知道更改)->GUI第二次更新

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

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

Matthias

模型语义

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

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

  • 结构更改需要围绕修改调用beginXxxendXxx。在调用beginXxx之前,不能修改任何结构。更改完结构后,请调用endXxxXxxInsertColumnsMoveColumnsRemoveColumnsInsertRowsMoveRowsRemoveRowsResetModel之一。

    如果更改影响了许多不连续的行/列,那么指示模型重置会更便宜,但要注意视图上的选择可能无法保存。

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

这也意味着非QObject类中的非常数模型几乎是无用的,当然,除非您使用observer或类似的模式来实现桥接功能。

中断更新循环

处理模型上更新循环的Qt惯用方法是利用项角色。你的模特如何诠释角色完全取决于你自己。QStringListModel实现的一个简单而有用的行为就是简单地将角色从setData调用转发到dataChanged,否则忽略该角色。

股票视图小部件仅对具有DisplayRoledataChanged作出反应。然而,当他们编辑数据时,他们用EditRole调用setData。这打破了循环。该方法既适用于查看小部件,也适用于Qt快速查看项目。

将数据插入排序模型

只要模型在排序完成时正确地发出更改信号,您就可以了。

操作顺序为:

  1. 视图添加一行并调用模型的insertRow方法。模型可以将该空行添加到基础容器中,也可以不添加。关键是现在必须保留空的行索引。

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

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

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

  5. 该模型调用beginMoveRows

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

  7. 该模型调用endMoveRows

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

所需的容器功能

您的DataContainer没有足够的功能使其工作,除非所有对它的访问都是通过模型完成的。如果您想直接访问容器,请使容器显式继承QAbstractXxxxModel,或者必须向容器添加通知系统。前者是一个更容易的选择。

您的核心问题归结为:在不实现模型通知API的某些变体的情况下,我是否可以拥有模型功能。显而易见的答案是:不,对不起,你不能——根据定义。要么有功能,要么没有。如果您不希望容器是QObject,那么您可以使用观察者模式来实现通知API,然后您将需要您的模型填充程序类。真的没有办法。

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

指数

QModelIndex最重要的文档不足方面是:它的实例只有在模型结构没有改变的情况下才有效。如果你的模型被传递了一个在结构变化之前生成的索引,你就可以自由地以未定义的方式行事(崩溃、发动核打击等等)。

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

来自方法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();

必须提供CCD_ 61和CCD_。

对于beginDeleteRows()beginMoveRows()来说是相同的。

当您有一个简单修改现有项中数据的方法时,该方法必须在末尾发出信号:dataChanged()

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

问题2的答案:

这取决于View类实现是否会"双重更新"。在视图中输入数据时,数据将通过模型中的一种编辑方法(insertRows()setData()等)发送到模型。这些方法的默认实现总是使用begin*()end*()方法,因此模型会发出适当的通知信号。所有视图都会监听这些信号,包括用于输入数据的信号,因此将执行"双重更新"。

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

我不确定Qt视图是否已经这样做了。要解决这个问题,需要有人对Qt内部有更多的了解,或者研究Qt源代码(我目前还没有)。如果有人知道这一点,请发表评论,我会更新答案。

我认为从模型中重新加载数据并没有那么糟糕——它保证了您看到的确实是模型中的值。可以避免编辑器和视图错误可能出现的问题。

问题3的答案:

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

但是,如果您进行部分刷新(使用我在答案1中描述的方法),则视图将为您跟踪所选内容。没什么好担心的。

最终评论:

如果您想编辑模型类之外的数据,您可以这样做。只需将begin*()end*()方法公开为公共API,这样编辑数据的其他代码就可以通知模型和视图有关更改。

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