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])