Qt 5.2 Model-View-Pattern:如何通知模型对象底层数据结构的变化

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

I have a class used for permanent storage of some item that are organized in a table-like manner. This class is totally unrelated to Qt and comes from a different library. Lets call this class DataContainer for the rest of this question. It provides std-c++ compatible iterators to access and manipulate the content.

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

I need to display and modify that data through a Qt GUI. My idea was to create a class DataContainerQtAdaptor that inherits from QAbstractTableModel and stores a pointer to the DataContainer object. The DataContainerQtAdaptor serves as an adaptor to the DataContainer object and all manipulation from inside of my Qt app is done through this adaptor. Then I use a QTableView widget to display the information.

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

Unfortunately the DataContainer might be changed by threads/processes. (For example think of DataContainer as some C++ class that encapsulates a database connection and that database might be changed by someone else.)

问题:

1) 假设我有一个函数,每次 DataContainer 对象的内部结构发生变化时都会调用该函数.必须调用 QAbstractTableModel 来通知模型底层更改的正确函数是什么?我需要类似亲爱的模型,您的持久存储后端已更改.请更新自己并向每个附加视图发出信号以反映此更改".

1) Assume I have a function that is called everytime the internal structur of the DataContainer object has been changed. What is the correct function of the QAbstractTableModel that must be called to inform the model of the underlying change? I need something like "Dear Model, your persistent storage backend changed. Please, update yourself and emit a signal to every attached view in order to reflect this change".

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

2) Lets say 1) is solved. What is the best way to avoid a "double" GUI update in case the change was triggered through the GUI? E.g: User clicks on a cell in the table widget -> table widget calls setData of the model -> model pushes change to backend -> backend triggers its own "onUpdate" function -> model re-reads complete backend (although it already knows the change) -> GUI is updated a second time

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

3) The user should be able to insert new rows/columns through the GUI and put data into it. But the position is detemined by this data, because the backend keeps the data sorted. Hence, I have the following problem: The user decides to create a new row at the end and the new data is pushed to the backend. When the backend/model is re-read this data is normally not at the last position, but has been inserted somewhere in the middle and all other data has been moved forward. Ho do I keep all the properties of the the table view widget like "selection of a cell" in sync?

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

I believe, there must be some simple standard solution to all these question, because it is the same way as QFileSystemModel works. The user selects a file and some other process creates a new file. The new file is displayed in the view and all subsequent rows move forward. The selection moves forward, too.

马蒂亚斯

推荐答案

模型语义

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

Model Semantics

First of all, you must ensure that the QAbstractItemModel cannot be in an inconsistent state. This means that there are some signals that must be fired on the model before certain changes to the underlying data are done.

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

There is a fundamental difference between changes to structure and changes to data. Structure changes are the rows/columns of the model being added or removed. Data changes affect the value of existing data items only.

  • 结构更改需要在修改前后调用 beginXxxendXxx.在调用 beginXxx 之前,您不能修改任何结构.完成结构更改后,调用 endXxx.Xxx 是以下之一:InsertColumnsMoveColumnsRemoveColumnsInsertRowsMoveRowsRemoveRowsResetModel.

  • Structural changes require calling beginXxx and endXxx around the modification. You cannot modify any structure before calling beginXxx. When you're done changing the structure, call endXxx. Xxx is one of: InsertColumns, MoveColumns, RemoveColumns, InsertRows, MoveRows, RemoveRows, ResetModel.

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

If the changes affect many discontiguous rows/columns, it's cheaper to indicate a model reset - but be wary that selections on the views might not survive it.

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

Data changes that keep the structure intact merely require that dataChanged is sent after the underlying data was modified. This means that there is a window of time when a call to data might return a new value before dataChanged is received by the object that queries the model.

这也意味着非QObject 类中的非常量模型几乎没有用,除非您当然使用观察者或类似模式实现桥接功能.

This also implies that non-constant models are almost useless from non-QObject classes, unless of course you implement bridge functionality using observer or similar patterns.

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

The Qt-idiomatic way of dealing with update loops on the model is by leveraging the item roles. It's entirely up to you how your model interprets the roles. A simple and useful behavior implemented by QStringListModel is simply to forward the role from the setData call to dataChanged, otherwise ignoring the role.

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

The stock view widgets react only to dataChanged with the DisplayRole. Yet, when they edit the data, they call setData with the EditRole. This breaks the loop. The approach is applicable both to view widgets and to Qt Quick view items.

只要模型在排序完成时正确地发出变化信号,你就会没事.

As long as the model properly emits the change signals when the sorting is done, you'll be fine.

操作顺序为:

  1. 视图添加一行并调用模型的insertRow 方法.该模型可以将此空行添加到底层容器中,也可以不添加.关键是必须暂时保留空行索引.

  1. The view adds a row and calls model's insertRow method. The model can either add this empty row to the underlying container or not. The key is that the empty row index must be kept for now.

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

The editing starts on an item in the row. The view state changes to Editing.

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

Editing is done on the item. The view exits the editing state, and sets the data on the model.

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

The model determines the final position of the item, based on its contents.

模型调用beginMoveRows.

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

The model changes the container by inserting the item at the correct location.

模型调用endMoveRows.

此时,一切都如您所愿.如果在移动之前获得焦点,则视图可以自动跟随移动的项目.默认情况下,编辑的项目具有焦点,因此效果很好.

At this point, everything is as you expect it to be. The views can automatically follow the moved item if it was focused prior to being moved. The edited items are focused by default, so that works fine.

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

Your DataContainer doesn't have enough functionality to make it work unless all access to it were to be done through the model. If you want to access the container directly, either make the container explicitly inherit QAbstractXxxxModel, or you'll have to add a notification system to the container. The former is an easier option.

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

Your core question reduces to: can I have model functionality without implementing some variant of the model notification API. The obvious answer is: no, sorry, you can't - by definition. Either the functionality is there, or it isn't. You can implement the notification API using an observer pattern if you don't want the container to be a QObject - then you'll need your model shim class. There's really no way around it.

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

The QFileSystemModel gets notified by the filesystem about individual directory entries that have changed. Your container has to do the same - and this amounts to providing a dataChanged signal, in some shape or form. If the model has items that get moved around or added/removed - its structure changes - it has to emit the xxxAboutToBeYyy and xxxYyy signals, by calling the relevant beginZzz and endZzz methods.

QModelIndex 最重要的未记录方面是:它的实例仅在模型结构没有改变时才有效.如果您的模型传递了在结构更改之前生成的索引,您就可以自由地以未定义的方式行事(崩溃、发动核打击等).

The most important underdocumented aspect of QModelIndex is: its instances are only valid for as long as the model's structure hasn't changed. If your model is passed an index that was generated prior to a structure change, you're free to behave in an undefined way (crash, launch a nuclear strike, whatever).

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

The whole reason for the existence of QModelIndex::internalPointer() is your use case of having an underlying, complex-indexed data container. Your implementation of the model's createIndex method must generate index instances that store references to the DataContainer's indices in some form. If those indices fit in a pointer, you don't need to allocate the data on the heap. If you need to allocate the container index storage on the heap, you must retain a pointer to this data and delete it any time the container's structure changes. You're free to do it, since nobody is supposed to use the index instance after a structure change.

相关文章