QAbstractItemModel.dataChanged() for one row and two columns updates entire view
-
I have looked back at my QTreeView performance issues yet again and I'm seeing that if call
dataChanged()
for a single location it works fine and updates only that single location but as soon as I specify two columns it updates my entire view. I can individually update either one cell or the other or both in separate calls just fine.Is this expected? If not, are there any common causes you might suggest I look into?
-
Hi,
How are you determining the range you pass to the signal ?
-
When a message is received on the bus the corresponding message node is found and the columns are hardcoded (first snippet). The nodes (not model indexes) and columns are passed to
changed()
which converts them to model indexes including the specified columns. As the code is now (on a debugging branch) it updates only for a single message (id0x0CFFAB30
) and a single column (Columns.indexes.value
is 3 (out of 6 columns total)). Putting the+1
with the secondcolumn
(as commented) causes the entire view to update. Add another+1
to the firstcolumn
so they are again equal and it updates a single cell in column 4.https://github.com/altendky/st/blob/b69467b613a4a3bc14d3216c355ed2d82983bb47/epyqlib/txrx.py#L291
if msg.arbitration_id == 0x0CFFAB30: for column in [Columns.indexes.value]:#, Columns.indexes.dt, Columns.indexes.count]: self.changed.emit( message, column, message, column, # column+1 updates entire view [Qt.DisplayRole]) # for child in message.children: # self.changed.emit( # child, column, # child, column, # [Qt.DisplayRole])
@pyqtSlot(TreeNode, int, TreeNode, int, list) def changed(self, start_node, start_column, end_node, end_column, roles): start_index = self.index_from_node(start_node) start_row = start_index.row() start_parent = start_index.parent() start_index = self.index(start_row, start_column, start_parent) if end_node is start_node: end_row = start_row end_parent = start_parent else: end_index = self.index_from_node(end_node) end_row = end_index.row() end_parent = end_index.parent() end_index = self.index(end_row, end_column, end_parent) self.dataChanged.emit(start_index, end_index, roles)
-
I won't claim this is an SSCCE since I don't think I can call nearly 500 lines simple, but I think it is nearly an SCCE at least. Depends on Python3 and PyQt5 (available on PyPi). It creates a three-row/three-column model with view as well as some buttons to manually trigger changing of data and separate triggering of
dataChanged()
for one and two columns (only the second row in both cases).As in my full application the single update updates a single cell as expected but the double update updates all cells. In both the single and double column update the parents test equal and the rows and columns seem like what i would expect.
dataChanged() emitted Start:r1,c1 parent(<PyQt5.QtCore.QModelIndex object at 0x7fac9df157b8>) End :r1,c1 parent(<PyQt5.QtCore.QModelIndex object at 0x7fac9df15828>) Parents equal: True dataChanged() emitted Start:r1,c1 parent(<PyQt5.QtCore.QModelIndex object at 0x7fac9df157b8>) End :r1,c2 parent(<PyQt5.QtCore.QModelIndex object at 0x7fac9df15898>) Parents equal: True
https://gist.github.com/d5e1dc68fdf7cfe490efac83b5eb2434
#!/usr/bin/env python3 #TODO: """DocString if there is one""" import string import functools import random import sys from PyQt5.QtCore import (Qt, QAbstractItemModel, QVariant, QModelIndex, pyqtSignal, pyqtSlot, QSize) from PyQt5.QtWidgets import (QWidget, QApplication, QTreeView, QMainWindow, QHBoxLayout, QVBoxLayout, QPushButton) # See file COPYING in this source tree __copyright__ = 'Copyright 2016, EPC Power Corp.' __license__ = 'GPLv2+' class TreeNode: def __init__(self, tx=False, parent=None): self.last = None self.tx = tx self.tree_parent = None self.set_parent(parent) self.children = [] def set_parent(self, parent): self.tree_parent = parent if self.tree_parent is not None: self.tree_parent.append_child(self) def append_child(self, child): self.children.append(child) child.tree_parent = self def child_at_row(self, row): try: return self.children[row] except IndexError: return None def row_of_child(self, child): for i, item in enumerate(self.children): if item == child: return i return -1 def remove_child(self, row=None, child=None): if child is None: child = self.children[row] self.children.remove(child) return True def traverse(self, call_this, payload=None): for child in self.children: if len(child.children) == 0: call_this(child, payload) else: child.traverse(child, call_this, payload) def __len__(self): return len(self.children) unique_role = Qt.UserRole class PyQAbstractItemModel(QAbstractItemModel): root_changed = pyqtSignal(TreeNode) def __init__(self, root, checkbox_columns=None, editable_columns=None, alignment=None, parent=None): QAbstractItemModel.__init__(self, parent=parent) self.root = root self.checkbox_columns = checkbox_columns self.editable_columns = editable_columns if alignment is not None: self.alignment = alignment else: self.alignment = Qt.AlignTop | Qt.AlignLeft self.index_from_node_cache = {} self.role_functions = { Qt.DisplayRole: self.data_display, unique_role: self.data_unique, Qt.TextAlignmentRole: lambda index: int(self.alignment), Qt.CheckStateRole: self.data_check_state, Qt.EditRole: self.data_edit, Qt.SizeHintRole: self.data_size_hint } def headerData(self, section, orientation, role): if orientation == Qt.Horizontal and role == Qt.DisplayRole: return QVariant(self.headers[section]) return QVariant() def data_display(self, index): node = index.internalPointer() try: return node.fields[index.column()] except IndexError: return None def data_unique(self, index): return index.internalPointer().unique() def data_check_state(self, index): if self.checkbox_columns is not None: if self.checkbox_columns[index.column()]: node = index.internalPointer() try: return node.checked(index.column()) except AttributeError: return None def data_edit(self, index): node = index.internalPointer() try: get = node.get_human_value except AttributeError: value = node.fields[index.column()] else: try: value = get() except TypeError: value = '' if value is None: value = '' else: value = str(value) return value def data_size_hint(self, index): return QSize(50, 50) def data(self, index, role): if not index.isValid(): return None try: return self.role_functions[role](index=index) except KeyError: return None def flags(self, index): flags = QAbstractItemModel.flags(self, index) if not index.isValid(): return flags if self.editable_columns is not None: if self.editable_columns[index.column()]: flags |= Qt.ItemIsEditable if self.checkbox_columns is not None: if self.checkbox_columns[index.column()]: flags |= Qt.ItemIsUserCheckable return flags def index(self, row, column, parent): # TODO: commented out stuff ought to be good rather than # breaking stuff. # # http://stackoverflow.com/questions/26680168/pyqt-treeview-index-error-removing-last-row if not self.hasIndex(row, column, parent): return QModelIndex() # if not parent.isValid(): # return QModelIndex() # if row < 0 or column < 0: # return QModelIndex() node = self.node_from_index(parent) child = node.child_at_row(row) if child is None: return QModelIndex() return self.createIndex(row, column, child) def columnCount(self, parent): return len(self.headers) def rowCount(self, parent): # TODO: this seems pretty particular to my present model # "the second column should NOT have the same children # as the first column in a row" # https://github.com/bgr/PyQt5_modeltest/blob/62bc86edbad065097c4835ceb4eee5fa3754f527/modeltest.py#L222 # # then again, the Qt example does just this # http://doc.qt.io/qt-5/qtwidgets-itemviews-simpletreemodel-example.html if parent.column() > 0: return 0 node = self.node_from_index(parent) if node is None: return 0 return len(node) def parent(self, child): if not child.isValid(): return QModelIndex() node = self.node_from_index(child) if node is None: return QModelIndex() parent = node.tree_parent if parent in [None, self.root]: return QModelIndex() grandparent = parent.tree_parent if grandparent is None: return QModelIndex() row = grandparent.row_of_child(parent) assert row != - 1 return self.createIndex(row, 0, parent) def node_from_index(self, index): if index.isValid(): return index.internalPointer() else: return self.root def index_from_node(self, node): # TODO make up another role for identification? try: index = self.index_from_node_cache[node] except KeyError: if node is self.root: index = QModelIndex() else: index = self.match(self.index(0, 0, QModelIndex()), unique_role, node.unique(), 1, Qt.MatchRecursive)[0] self.index_from_node_cache[node] = index return index @pyqtSlot(TreeNode, int, TreeNode, int, list) def changed(self, start_node, start_column, end_node, end_column, roles): start_index = self.index_from_node(start_node) start_row = start_index.row() start_parent = start_index.parent() start_index = self.index(start_row, start_column, start_parent) if end_node is start_node: end_row = start_row end_parent = start_parent else: end_index = self.index_from_node(end_node) end_row = end_index.row() end_parent = end_index.parent() end_index = self.index(end_row, end_column, end_parent) self.dataChanged.emit(start_index, end_index, roles) print('dataChanged() emitted') for name, index in [('Start', start_index), ('End', end_index)]: print('{:5s}:r{},c{} parent({})'.format(name, index.row(), index.column(), index.parent())) print('Parents equal: {}'.format(start_index.parent() == end_index.parent())) @pyqtSlot(TreeNode, int, int) def begin_insert_rows(self, parent, start_row, end_row): self.beginInsertRows(self.index_from_node(parent), start_row, end_row) @pyqtSlot() def end_insert_rows(self): self.index_from_node_cache = {} self.endInsertRows() @pyqtSlot(TreeNode, int, int) def begin_remove_rows(self, parent, start_row, end_row): self.beginRemoveRows(self.index_from_node(parent), start_row, end_row) @pyqtSlot() def end_remove_rows(self): self.index_from_node_cache = {} self.endRemoveRows() @pyqtSlot() def set_root(self, root): self.beginResetModel() self.root = root self.endResetModel() self.root_changed.emit(root) class AbstractColumns: def __init__(self, **kwargs): for member in self._members: try: value = kwargs[member] except KeyError: value = None finally: setattr(self, member, value) object.__setattr__(self, '_length', len(self.__dict__)) invalid_parameters = set(kwargs.keys()) - set(self.__dict__.keys()) if len(invalid_parameters): raise ValueError('Invalid parameter{} passed: {}'.format( 's' if len(invalid_parameters) > 1 else '', ', '.join(invalid_parameters))) @classmethod def __len__(cls): return len(cls._members) @classmethod def indexes(cls): return cls(**dict(zip(cls._members, range(len(cls._members))))) @classmethod def fill(cls, value): return cls(**dict(zip(cls._members, [value] * len(cls._members)))) def __iter__(self): for i in range(len(self)): yield self[i] @functools.lru_cache(maxsize=None) def index_from_attribute(self, index): for attribute in self.__class__._members: if index == getattr(self.__class__.indexes, attribute): return attribute raise IndexError('column index out of range') def __getitem__(self, index): if index < 0: index += len(self) return getattr(self, self.index_from_attribute(index)) def __setitem__(self, index, value): if index < 0: index += len(self) return setattr(self, self.index_from_attribute(index), value) def __getattr__(self, name, value): if name in self._members: object.__getattr__(self, name, value) else: raise TypeError("Attempted to get attribute {}" .format(name)) def __setattr__(self, name, value): if name in self._members: object.__setattr__(self, name, value) else: raise TypeError("Attempted to set attribute {}" .format(name)) class Columns(AbstractColumns): _members = ['name', 'letter', 'number'] Columns.indexes = Columns.indexes() class Node(TreeNode): def __init__(self, name, letter='a', number=0): TreeNode.__init__(self) self.fields = Columns(name=name, letter=letter, number=number) self.possibilities = Columns(letter=string.ascii_letters.lower(), number=range(10)) def randomize(self, _): letters = set(self.possibilities.letter) letters.remove(self.fields.letter) self.fields.letter = random.choice(list(letters)) numbers = set(self.possibilities.number) numbers.remove(self.fields.number) self.fields.number = random.choice(list(numbers)) def unique(self): return object.__repr__(self) class Model(PyQAbstractItemModel): def __init__(self, root, parent=None): PyQAbstractItemModel.__init__(self, root=root, parent=parent) self.headers = Columns(name='Name', letter='Letter', number='Number') def data_clicked(self): self.root.traverse(call_this=Node.randomize) def single_clicked(self): node = self.root.children[1] self.changed(node, 1, node, 1, []) def double_clicked(self): node = self.root.children[1] self.changed(node, 1, node, 2, []) def main(): app = QApplication(sys.argv) root = Node(name='Root', letter='-', number=-1) a = Node(name='A', letter='a', number=1) b = Node(name='B', letter='b', number=2) c = Node(name='C', letter='c', number=3) root.append_child(a) root.append_child(b) root.append_child(c) model = Model(root=root) window = QWidget() vlayout = QVBoxLayout() window.setLayout(vlayout) tree_view = QTreeView() tree_view.setModel(model) vlayout.addWidget(tree_view) hlayout = QHBoxLayout() data = QPushButton() data.setText('Change Data') data.clicked.connect(model.data_clicked) hlayout.addWidget(data) single = QPushButton() single.setText('Update Single') single.clicked.connect(model.single_clicked) hlayout.addWidget(single) double = QPushButton() double.setText('Update Double') double.clicked.connect(model.double_clicked) hlayout.addWidget(double) vlayout.addLayout(hlayout) window.show() return app.exec_() if __name__ == '__main__': sys.exit(main())
-
I added a button to trigger
dataChanged()
for two rows but only one column. Same result, all are updated.dataChanged() emitted Start:r1,c1 parent(<PyQt5.QtCore.QModelIndex object at 0x7fbcb0084898>) End :r1,c1 parent(<PyQt5.QtCore.QModelIndex object at 0x7fbcb0084908>) Parents equal: True dataChanged() emitted Start:r1,c1 parent(<PyQt5.QtCore.QModelIndex object at 0x7fbcb0084898>) End :r1,c2 parent(<PyQt5.QtCore.QModelIndex object at 0x7fbcb0084978>) Parents equal: True dataChanged() emitted Start:r1,c1 parent(<PyQt5.QtCore.QModelIndex object at 0x7fbcb0084898>) End :r2,c1 parent(<PyQt5.QtCore.QModelIndex object at 0x7fbcb0084978>) Parents equal: True
https://gist.github.com/7a178479d2d469efb00d4c6915adbc86
#!/usr/bin/env python3 #TODO: """DocString if there is one""" import string import functools import random import sys from PyQt5.QtCore import (Qt, QAbstractItemModel, QVariant, QModelIndex, pyqtSignal, pyqtSlot, QSize) from PyQt5.QtWidgets import (QWidget, QApplication, QTreeView, QMainWindow, QHBoxLayout, QVBoxLayout, QPushButton) # See file COPYING in this source tree __copyright__ = 'Copyright 2016, EPC Power Corp.' __license__ = 'GPLv2+' class TreeNode: def __init__(self, tx=False, parent=None): self.last = None self.tx = tx self.tree_parent = None self.set_parent(parent) self.children = [] def set_parent(self, parent): self.tree_parent = parent if self.tree_parent is not None: self.tree_parent.append_child(self) def append_child(self, child): self.children.append(child) child.tree_parent = self def child_at_row(self, row): try: return self.children[row] except IndexError: return None def row_of_child(self, child): for i, item in enumerate(self.children): if item == child: return i return -1 def remove_child(self, row=None, child=None): if child is None: child = self.children[row] self.children.remove(child) return True def traverse(self, call_this, payload=None): for child in self.children: if len(child.children) == 0: call_this(child, payload) else: child.traverse(child, call_this, payload) def __len__(self): return len(self.children) unique_role = Qt.UserRole class PyQAbstractItemModel(QAbstractItemModel): root_changed = pyqtSignal(TreeNode) def __init__(self, root, checkbox_columns=None, editable_columns=None, alignment=None, parent=None): QAbstractItemModel.__init__(self, parent=parent) self.root = root self.checkbox_columns = checkbox_columns self.editable_columns = editable_columns if alignment is not None: self.alignment = alignment else: self.alignment = Qt.AlignTop | Qt.AlignLeft self.index_from_node_cache = {} self.role_functions = { Qt.DisplayRole: self.data_display, unique_role: self.data_unique, Qt.TextAlignmentRole: lambda index: int(self.alignment), Qt.CheckStateRole: self.data_check_state, Qt.EditRole: self.data_edit, Qt.SizeHintRole: self.data_size_hint } def headerData(self, section, orientation, role): if orientation == Qt.Horizontal and role == Qt.DisplayRole: return QVariant(self.headers[section]) return QVariant() def data_display(self, index): node = index.internalPointer() try: return node.fields[index.column()] except IndexError: return None def data_unique(self, index): return index.internalPointer().unique() def data_check_state(self, index): if self.checkbox_columns is not None: if self.checkbox_columns[index.column()]: node = index.internalPointer() try: return node.checked(index.column()) except AttributeError: return None def data_edit(self, index): node = index.internalPointer() try: get = node.get_human_value except AttributeError: value = node.fields[index.column()] else: try: value = get() except TypeError: value = '' if value is None: value = '' else: value = str(value) return value def data_size_hint(self, index): return QSize(50, 50) def data(self, index, role): if not index.isValid(): return None try: return self.role_functions[role](index=index) except KeyError: return None def flags(self, index): flags = QAbstractItemModel.flags(self, index) if not index.isValid(): return flags if self.editable_columns is not None: if self.editable_columns[index.column()]: flags |= Qt.ItemIsEditable if self.checkbox_columns is not None: if self.checkbox_columns[index.column()]: flags |= Qt.ItemIsUserCheckable return flags def index(self, row, column, parent): # TODO: commented out stuff ought to be good rather than # breaking stuff. # # http://stackoverflow.com/questions/26680168/pyqt-treeview-index-error-removing-last-row if not self.hasIndex(row, column, parent): return QModelIndex() # if not parent.isValid(): # return QModelIndex() # if row < 0 or column < 0: # return QModelIndex() node = self.node_from_index(parent) child = node.child_at_row(row) if child is None: return QModelIndex() return self.createIndex(row, column, child) def columnCount(self, parent): return len(self.headers) def rowCount(self, parent): # TODO: this seems pretty particular to my present model # "the second column should NOT have the same children # as the first column in a row" # https://github.com/bgr/PyQt5_modeltest/blob/62bc86edbad065097c4835ceb4eee5fa3754f527/modeltest.py#L222 # # then again, the Qt example does just this # http://doc.qt.io/qt-5/qtwidgets-itemviews-simpletreemodel-example.html if parent.column() > 0: return 0 node = self.node_from_index(parent) if node is None: return 0 return len(node) def parent(self, child): if not child.isValid(): return QModelIndex() node = self.node_from_index(child) if node is None: return QModelIndex() parent = node.tree_parent if parent in [None, self.root]: return QModelIndex() grandparent = parent.tree_parent if grandparent is None: return QModelIndex() row = grandparent.row_of_child(parent) assert row != - 1 return self.createIndex(row, 0, parent) def node_from_index(self, index): if index.isValid(): return index.internalPointer() else: return self.root def index_from_node(self, node): # TODO make up another role for identification? try: index = self.index_from_node_cache[node] except KeyError: if node is self.root: index = QModelIndex() else: index = self.match(self.index(0, 0, QModelIndex()), unique_role, node.unique(), 1, Qt.MatchRecursive)[0] self.index_from_node_cache[node] = index return index @pyqtSlot(TreeNode, int, TreeNode, int, list) def changed(self, start_node, start_column, end_node, end_column, roles): start_index = self.index_from_node(start_node) start_row = start_index.row() start_parent = start_index.parent() start_index = self.index(start_row, start_column, start_parent) if end_node is start_node: end_row = start_row end_parent = start_parent else: end_index = self.index_from_node(end_node) end_row = end_index.row() end_parent = end_index.parent() end_index = self.index(end_row, end_column, end_parent) self.dataChanged.emit(start_index, end_index, roles) print('dataChanged() emitted') for name, index in [('Start', start_index), ('End', end_index)]: print('{:5s}:r{},c{} parent({})'.format(name, index.row(), index.column(), index.parent())) print('Parents equal: {}'.format(start_index.parent() == end_index.parent())) @pyqtSlot(TreeNode, int, int) def begin_insert_rows(self, parent, start_row, end_row): self.beginInsertRows(self.index_from_node(parent), start_row, end_row) @pyqtSlot() def end_insert_rows(self): self.index_from_node_cache = {} self.endInsertRows() @pyqtSlot(TreeNode, int, int) def begin_remove_rows(self, parent, start_row, end_row): self.beginRemoveRows(self.index_from_node(parent), start_row, end_row) @pyqtSlot() def end_remove_rows(self): self.index_from_node_cache = {} self.endRemoveRows() @pyqtSlot() def set_root(self, root): self.beginResetModel() self.root = root self.endResetModel() self.root_changed.emit(root) class AbstractColumns: def __init__(self, **kwargs): for member in self._members: try: value = kwargs[member] except KeyError: value = None finally: setattr(self, member, value) object.__setattr__(self, '_length', len(self.__dict__)) invalid_parameters = set(kwargs.keys()) - set(self.__dict__.keys()) if len(invalid_parameters): raise ValueError('Invalid parameter{} passed: {}'.format( 's' if len(invalid_parameters) > 1 else '', ', '.join(invalid_parameters))) @classmethod def __len__(cls): return len(cls._members) @classmethod def indexes(cls): return cls(**dict(zip(cls._members, range(len(cls._members))))) @classmethod def fill(cls, value): return cls(**dict(zip(cls._members, [value] * len(cls._members)))) def __iter__(self): for i in range(len(self)): yield self[i] @functools.lru_cache(maxsize=None) def index_from_attribute(self, index): for attribute in self.__class__._members: if index == getattr(self.__class__.indexes, attribute): return attribute raise IndexError('column index out of range') def __getitem__(self, index): if index < 0: index += len(self) return getattr(self, self.index_from_attribute(index)) def __setitem__(self, index, value): if index < 0: index += len(self) return setattr(self, self.index_from_attribute(index), value) def __getattr__(self, name, value): if name in self._members: object.__getattr__(self, name, value) else: raise TypeError("Attempted to get attribute {}" .format(name)) def __setattr__(self, name, value): if name in self._members: object.__setattr__(self, name, value) else: raise TypeError("Attempted to set attribute {}" .format(name)) class Columns(AbstractColumns): _members = ['name', 'letter', 'number'] Columns.indexes = Columns.indexes() class Node(TreeNode): def __init__(self, name, letter='a', number=0): TreeNode.__init__(self) self.fields = Columns(name=name, letter=letter, number=number) self.possibilities = Columns(letter=string.ascii_letters.lower(), number=range(10)) def randomize(self, _): letters = set(self.possibilities.letter) letters.remove(self.fields.letter) self.fields.letter = random.choice(list(letters)) numbers = set(self.possibilities.number) numbers.remove(self.fields.number) self.fields.number = random.choice(list(numbers)) def unique(self): return object.__repr__(self) class Model(PyQAbstractItemModel): def __init__(self, root, parent=None): PyQAbstractItemModel.__init__(self, root=root, parent=parent) self.headers = Columns(name='Name', letter='Letter', number='Number') def data_clicked(self): self.root.traverse(call_this=Node.randomize) def single_clicked(self): node = self.root.children[1] self.changed(node, 1, node, 1, []) def double_clicked(self): node = self.root.children[1] self.changed(node, 1, node, 2, []) def double_row_clicked(self): node = self.root.children[1] other_node = self.root.children[2] self.changed(node, 1, other_node, 1, []) def main(): app = QApplication(sys.argv) root = Node(name='Root', letter='-', number=-1) a = Node(name='A', letter='a', number=1) b = Node(name='B', letter='b', number=2) c = Node(name='C', letter='c', number=3) root.append_child(a) root.append_child(b) root.append_child(c) model = Model(root=root) window = QWidget() vlayout = QVBoxLayout() window.setLayout(vlayout) tree_view = QTreeView() tree_view.setModel(model) vlayout.addWidget(tree_view) hlayout = QHBoxLayout() data = QPushButton() data.setText('Change Data') data.clicked.connect(model.data_clicked) hlayout.addWidget(data) single = QPushButton() single.setText('Update Single') single.clicked.connect(model.single_clicked) hlayout.addWidget(single) double = QPushButton() double.setText('Update Double Column') double.clicked.connect(model.double_clicked) hlayout.addWidget(double) double_row = QPushButton() double_row.setText('Update Double Row') double_row.clicked.connect(model.double_row_clicked) hlayout.addWidget(double_row) vlayout.addLayout(hlayout) window.show() return app.exec_() if __name__ == '__main__': sys.exit(main())
-
Indeed, it looks like something is not working as it should.
Can you reproduce this directly with C++ ?