1

I'm trying to implement tree-like structure using nested dicts to store my data and display it using QTreeView. The crucial part for me is to track changes made to my data in either nested dict structure or QTreeView. I tried subclassing QAbstractItemModel, but couldn't figure out how to sync changes made in QTreeView with my nested dict structure, the only thing that worked was repopulating the entire tree on every change... Is it possible to implement something like this without rebuilding a tree every time the changes are made?

The desired behaviour is something like this: Change data in nested dict –> changes are automatically displayed in QTreeView.

I've found similar question, but with QTreeWidget instead: QTreeWidget to Mirror python Dictionary

However, this approach requires tree repopulation on every change as well...

Nested dict (tree data) looks something like this:

root_node = {
            node1: { leaf1: data, leaf2: data}
            node2: { node3: { leaf3: data, leaf4: data }}
       }

UPD: added the code I've got so far:

from PyQt5.QtCore import QModelIndex, Qt, QAbstractItemModel


class TreeNode(object):
    def __init__(self, data, parent=None):
        self.parent_node = parent
        self.node_data = data  # it's also []
        self.child_nodes = []

    def child(self, row):
        return self.child_nodes[row]

    def child_count(self):
        return len(self.child_nodes)

    def child_number(self):
        if self.parent_node is not None:
            return self.parent_node.child_nodes.index(self)
        return 0

    def column_count(self):
        return len(self.node_data)

    def data(self, column):
        return self.node_data[column]

    def insert_children(self, position, count, columns):
        if position < 0 or position > len(self.child_nodes):
            return False

        for row in range(count):
            data = [None for v in range(columns)]
            node = TreeNode(data, self)
            self.child_nodes.insert(position, node)

        return True

    def append_child_by_node(self, node):
        node.parent_node = self
        self.child_nodes.append(node)

    def append_child_by_data(self, data):
        self.child_nodes.append(TreeNode(data, self))

    def insert_columns(self, position, columns):
        if position < 0 or position > len(self.node_data):
            return False

        for column in range(columns):
            self.node_data.insert(position, None)

        for child in self.child_nodes:
            child.insert_columns(position, columns)

        return True

    def parent(self):
        return self.parent_node

    def remove_children(self, position, count):
        if position < 0 or position + count > len(self.child_nodes):
            return False

        for row in range(count):
            self.child_nodes.pop(position)

        return True

    def remove_columns(self, position, columns):
        if position < 0 or position + columns > len(self.node_data):
            return False

        for column in range(columns):
            self.node_data.pop(position)

        for child in self.child_nodes:
            child.remove_columns(position, columns)

        return True

    def set_data(self, column, value):
        if column < 0 or column >= len(self.node_data):
            return False

        self.node_data[column] = value

        return True


class TreeModel(QAbstractItemModel):
    def __init__(self, parent=None, nested_dict=dict):
        super(TreeModel, self).__init__(parent)
        self.root_node = TreeNode(["Name", "Type"])
        self.update_tree(self.root_node, nested_dict)

        print(self.root_node.child_count())

    def columnCount(self, parent=QModelIndex()):
        return self.root_node.column_count()

    def data(self, index, role):
        if not index.isValid():
            return None

        if role != Qt.DisplayRole and role != Qt.EditRole:
            return None

        node = self.get_node(index)
        return node.data(index.column())

def flags(self, index):
    if not index.isValid():
        return 0

    return Qt.ItemIsEnabled | Qt.ItemIsSelectable

def get_node(self, index):
    if index.isValid():
        node = index.internalPointer()
        if node:
            return node

    return self.root_node

def headerData(self, section, orientation, role=Qt.DisplayRole):
    if orientation == Qt.Horizontal and role == Qt.DisplayRole:
        return self.root_node.data(section)

    return None

def index(self, row, column, parent=QModelIndex()):
    if parent.isValid() and parent.column() != 0:
        return QModelIndex()

    parent_node = self.get_node(parent)
    child_node = parent_node.child(row)
    if child_node:
        return self.createIndex(row, column, child_node)
    else:
        return QModelIndex()

def insertColumns(self, position, columns, parent=QModelIndex()):
    self.beginInsertColumns(parent, position, position + columns - 1)
    success = self.root_node.insert_columns(position, columns)
    self.endInsertColumns()

    return success

def insertRows(self, position, rows, parent=QModelIndex()):
    parent_node = self.get_node(parent)
    self.beginInsertRows(parent, position, position + rows - 1)
    success = parent_node.insert_children(position, rows, self.root_node.column_count())
    self.endInsertRows()

    return success

def parent(self, index):
    if not index.isValid():
        return QModelIndex()

    child_node = self.get_node(index)
    parent_node = child_node.parent()

    if parent_node == self.root_node:
        return QModelIndex()

    return self.createIndex(parent_node.child_number(), 0, parent_node)

def removeColumns(self, position, columns, parent=QModelIndex()):
    self.beginRemoveColumns(parent, position, position + columns - 1)
    success = self.root_node.remove_columns(position, columns)
    self.endRemoveColumns()

    if self.root_node.column_count() == 0:
        self.removeRows(0, self.rowCount())

    return success

def removeRows(self, position, rows, parent=QModelIndex()):
    parent_node = self.get_node(parent)

    self.beginRemoveRows(parent, position, position + rows - 1)
    success = parent_node.remove_children(position, rows)
    self.endRemoveRows()

    return success

def rowCount(self, parent=QModelIndex()):
    parent_node = self.get_node(parent)

    return parent_node.child_count()

def setData(self, index, value, role=Qt.EditRole):
    if role != Qt.EditRole:
        return False

    node = self.get_node(index)
    result = node.set_data(index.column(), value)

    if result:
        self.dataChanged.emit(index, index)

    return result

def setHeaderData(self, section, orientation, value, role=Qt.EditRole):
    if role != Qt.EditRole or orientation != Qt.Horizontal:
        return False

    result = self.root_node.set_data(section, value)
    if result:
        self.headerDataChanged.emit(orientation, section, section)

    return result

def update_tree(self, parent, nested_dict):
    self.layoutChanged.emit()
    parent.child_nodes.clear()
    for k, v in nested_dict.items():
        if isinstance(v, dict):
            parent.append_child_by_data([k[0] if isinstance(k, tuple) else k, None])
            self.update_tree(parent.child(parent.child_count() - 1), v)
        else:
            parent.append_child_by_data([k, None])
asymmetriq
  • 195
  • 1
  • 8
  • As with any abstract model implementation, you need to call the methods related to row/column insertion and deletion (`beginInsertRows`, etc). If you provide a [minimal, reproducible example](https://stackoverflow.com/help/minimal-reproducible-example) we might be able to put you on the right track. – musicamante Mar 19 '21 at 10:33
  • Consider that there's no way of "automatically" update the structure using a basic dictionary. I see two possible solutions: 1) subclass `dict` (but be aware that you should be very careful, as it's easy to forget to implement all required methods) in order to *notify* the model about changes and ensure that each nested value is of the same class; 2) use the dict only to initialize the data and always use the model to manipulate its contents while keeping the dict updated. – musicamante Mar 19 '21 at 15:19

0 Answers0