更新QTableView当数据发生变化时,请完全更新。

huangapple go评论58阅读模式
英文:

Update a QTableView entirely when data has changed

问题

我有一个使用自定义QAbstractTableModelQTableView,当底层数据发生变化时,我想要更新整个表格视图。

请注意,数据变化可以是任何内容:

  • 插入行
  • 删除行
  • 更改现有行
  • 以上所有情况

我找到的所有现有解决方案都涉及使用insertRows / deleteRows,但据我所知,这将需要我对我的数据进行一些记录,以确切找出哪些现有行发生了更改,以及在现有行之间添加了哪些行。

是否有一种方法只是告诉视图整个模型可能已经发生了变化,它应该更新所有内容?

这是一个代码示例,演示了我的问题:

import sys
from PyQt5 import QtCore, QtWidgets
from PyQt5.QtCore import Qt


class MyTableModel(QtCore.QAbstractTableModel):
    def __init__(self):
        super().__init__()
        self._data = [
            ("spam", "spam", "spam", "spam"),
            ("spam", "spam", "spam", "spam"),
            ("spam", "spam", "spam", "spam"),
        ]

    def headerData(self, section: int, orientation: Qt.Orientation, role=Qt.DisplayRole):
        if role == Qt.DisplayRole and orientation == Qt.Horizontal:
            return f"Foo {section + 1}"

    def data(self, index, role=Qt.DisplayRole):
        if role == Qt.DisplayRole:
            return self._data[index.row()][index.column()]

    def rowCount(self, parent=QtCore.QModelIndex()):
        return len(self._data)

    def columnCount(self, parent=QtCore.QModelIndex()):
        return 4

    def refresh_data(self):
        self._data.insert(0, ("ham", "ham", "ham", "ham"))
        topLeft = self.createIndex(0, 0)
        bottomRight = self.createIndex(self.rowCount(), self.columnCount())
        self.dataChanged.emit(topLeft, bottomRight)


class MainWindow(QtWidgets.QMainWindow):
    def __init__(self):
        super().__init__()

        widget = QtWidgets.QWidget()
        layout = QtWidgets.QVBoxLayout()
        widget.setLayout(layout)
        self.setCentralWidget(widget)

        self.table = QtWidgets.QTableView()
        layout.addWidget(self.table)

        self.model = MyTableModel()
        self.table.setModel(self.model)

        update_data_button = QtWidgets.QPushButton("Update data")
        update_data_button.clicked.connect(self.model.refresh_data)
        layout.addWidget(update_data_button)


app = QtWidgets.QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec_()

当我点击update_data_button时,我看到新数据插入到开头,但视图中的行数未更新,因此最后的行被丢弃。

更新QTableView当数据发生变化时,请完全更新。

更新QTableView当数据发生变化时,请完全更新。

将来,我想实现更细粒度的数据更新,根据需要插入/删除行,但我还没有到达那一步。我必须假设我的数据源可以在数据的任何地方更改任何单元格并在任何地方添加/删除行,而不容易找到更改发生的地方(除非保留数据的副本并遍历旧数据结构和新数据结构以找到差异)。

英文:

I have a QTableView using a custom QAbstractTableModel, and I would like to update the entire table view when the underlying data has change.

Note that a data change can be anything:

  • rows inserted
  • rows deleted
  • existing rows changed
  • all of the above

All existing solutions I found involve using insertRows / deleteRows, but AFAIK that would require me to do some bookkeeping of my data to find out exactly which existing rows changed, and which rows where added in between existing rows.

Is there a way to just tell the view that the entire model may have changed and that it should update everything?

Here is a code example to demonstrate my problem:

import sys
from PyQt5 import QtCore, QtWidgets
from PyQt5.QtCore import Qt
class MyTableModel(QtCore.QAbstractTableModel):
def __init__(self):
super().__init__()
self._data = [
("spam", "spam", "spam", "spam"),
("spam", "spam", "spam", "spam"),
("spam", "spam", "spam", "spam"),
]
def headerData(self, section: int, orientation: Qt.Orientation, role=Qt.DisplayRole):
if role == Qt.DisplayRole and orientation == Qt.Horizontal:
return f"Foo {section + 1}"
def data(self, index, role=Qt.DisplayRole):
if role == Qt.DisplayRole:
return self._data[index.row()][index.column()]
def rowCount(self, parent=QtCore.QModelIndex()):
return len(self._data)
def columnCount(self, parent=QtCore.QModelIndex()):
return 4
def refresh_data(self):
self._data.insert(0, ("ham", "ham", "ham", "ham"))
topLeft = self.createIndex(0, 0)
bottomRight = self.createIndex(self.rowCount(), self.columnCount())
self.dataChanged.emit(topLeft, bottomRight)
class MainWindow(QtWidgets.QMainWindow):
def __init__(self):
super().__init__()
widget = QtWidgets.QWidget()
layout = QtWidgets.QVBoxLayout()
widget.setLayout(layout)
self.setCentralWidget(widget)
self.table = QtWidgets.QTableView()
layout.addWidget(self.table)
self.model = MyTableModel()
self.table.setModel(self.model)
update_data_button = QtWidgets.QPushButton("Update data")
update_data_button.clicked.connect(self.model.refresh_data)
layout.addWidget(update_data_button)
app = QtWidgets.QApplication(sys.argv)
window = MainWindow()
window.show()
app.exec_()

When I click the update_data_button, I see how the new data is inserted at the beginning, but the number of rows in the view is not updated, so the last rows are just dropped.

更新QTableView当数据发生变化时,请完全更新。

更新QTableView当数据发生变化时,请完全更新。

In the future I want to implement a more fine grained updating of my data, by inserting/deleting rows as needed, but I'm not there yet. I have to assume that my data source can change any cell anywhere in the data and add/remove rows anywhere, without being able to easily find where the change happened (short of having to keep a copy of the data and iterate through both the old and new data structures to find the differences).

答案1

得分: 2

dataChanged 不适合这个目的:它只告诉任何连接的视图当前 已知 项目的数据已更改。然后,项目视图会假定模型的大小和层次结构仍然相同,无法意识到行或列的数量增加或减少。事实上,在模型大小已更改时仅调用 dataChanged 可能会导致引发异常。

基本上,这就像通知列表项中的值已更改一样:假设列表仍然具有相同的项目数。

解决这个问题的常见方法是发出 layoutChanged 信号,但这并不太合适:布局 的概念通常表示基本结构(行数和列数)保持不变。只有在实际布局发生更改时才应使用该信号:例如,当模型已经排序时。

虽然 Qt 项目视图通常能够“解决”这个方法,但这不一致也不可靠。视图具有故障安全系统,当模型与它们“先前”知道的内容不一致时,它们会自动忽略某些方面,但信任它们至少只是一种合理的猜测,而在形式上是一种不可靠的假设。

每当模型发生根本性变化(行/列大小和项目数据无效)时,更合适的解决方案是依赖“模型重置”概念。

基本解决方案只是发出 modelReset 信号,但由于项目视图还考虑持久索引及其选择模型,更正确的方法是正确调用 QAbstractItemModel 提供的 begin/end 函数。在这种情况下,顺序是:

  1. 调用 beginResetModel()
  2. 更改底层数据;
  3. 调用 endResetModel()

使用上述函数还将为您准备好进一步更改模型:每当添加、删除或移动行或列时,您需要调用适当的 begin*() 函数,然后应用所需的更改,最后调用相关的 end*() 函数。

这确保了连接的视图保持其状态得到正确更新,考虑到索引顺序、选择,甚至使用对于大型或复杂模型必不可少的一些重要性能优化。

英文:

The dataChanged is not appropriate for this purpose: it only tells any connected view that the data of the currently known items has changed. The item view will then assume that the model size and hierarchy is still the same and cannot realize that there are less or more rows or columns. In fact, just calling dataChanged when the model size has changed could potentially result in a raised exception.

Basically speaking, it's just like notifying that a value in a list item has been changed: the assumption is that the list still has the same item count.

A common way to work around this is to emit the layoutChanged signal, but that's not very appropriate: the concept of layout normally indicates that the basic structure (row and column count) remains unchanged. That signal should only be used when the actual layout has changed: for instance, when the model has been sorted.

While Qt item views are normally able to "work around" this approach, it's not consistent nor reliable. Views have fail safe systems that automatically ignore some aspects when the model is not consistent with what they "previously" knew, but trusting them is just an educated guess at least, and, formally, an unreliable assumption.

Whenever the model radically changes (row/column size and item data are invalidated), the more appropriate solution is to rely on the "model reset" concept.

The basic solution is to just emit the modelReset signal, but since item views also consider persistent indexes and their selection model, a more correct way is to properly call the begin/end functions QAbstractItemModel provides. In this case, the order is:

  1. call beginResetModel();
  2. change the underlying data;
  3. call endResetModel()

Using the functions above will also prepare you to further changes in the model: whenever rows or columns are added, removed or moved, you need to call the appropriate begin*() function, then apply the required changes, and finally call the related end*() function.

This ensures that the connected views keep their status properly updated, considering index order, selections, and even using some important performance optimization that are necessary for large or complex models.

huangapple
  • 本文由 发表于 2023年7月3日 17:20:03
  • 转载请务必保留本文链接:https://go.coder-hub.com/76603422.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定