QAbstractItemModel.dataChanged() for one row and two columns updates entire view



  • Re: How do I debug excessive QAbstractItemModel::data() callbacks and general extreme inefficiency (PyQt5)

    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?


  • Lifetime Qt Champion

    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 (id 0x0CFFAB30) and a single column (Columns.indexes.value is 3 (out of 6 columns total)). Putting the +1 with the second column (as commented) causes the entire view to update. Add another +1 to the first column 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])
    

    https://github.com/altendky/st/blob/b69467b613a4a3bc14d3216c355ed2d82983bb47/epyqlib/pyqabstractitemmodel.py#L205

    @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())
    

  • Lifetime Qt Champion

    Indeed, it looks like something is not working as it should.

    Can you reproduce this directly with C++ ?


Log in to reply
 

Looks like your connection to Qt Forum was lost, please wait while we try to reconnect.