When developing a GUI (graphical user interface) application, I found that lot of people posted questions in Stack Overflow to seek help on how to manipulate QTreeview Widget. However, few have accepted answers. This post will present some examples to show the solutions.
Populate a QTreeview with a dictionary using QStandardItemModel
To turn a dictionary into a tree using QTreeview widget, the key point is to have correct mapping relationship between parent item and child item through ‘unique_id’ and ‘parent_id’. Value of key ‘unique_id’ represents identifier of each item. While value of ‘parent_id’ for an item represents ‘unique_id’ of this item’s parent item.
import sys from collections import deque from PyQt5.QtWidgets import * from PyQt5.QtGui import * from PyQt5.QtCore import * class view(QWidget): def __init__(self, data): super(view, self).__init__() self.tree = QTreeView(self) layout = QVBoxLayout(self) layout.addWidget(self.tree) self.model = QStandardItemModel() self.model.setHorizontalHeaderLabels(['Name', 'Height', 'Weight']) self.tree.header().setDefaultSectionSize(180) self.tree.setModel(self.model) self.importData(data) self.tree.expandAll() def importData(self, data, root=None): self.model.setRowCount(0) if root is None: root = self.model.invisibleRootItem() seen = {} # List of QStandardItem values = deque(data) while values: value = values.popleft() if value['unique_id'] == 1: parent = root else: pid = value['parent_id'] if pid not in seen: values.append(value) continue parent = seen[pid] unique_id = value['unique_id'] parent.appendRow([ QStandardItem(value['short_name']), QStandardItem(value['height']), QStandardItem(value['weight']) ]) seen[unique_id] = parent.child(parent.rowCount() - 1) if __name__ == '__main__': data = [ {'unique_id': 1, 'parent_id': 0, 'short_name': '', 'height': ' ', 'weight': ' '}, {'unique_id': 2, 'parent_id': 1, 'short_name': 'Class 1', 'height': ' ', 'weight': ' '}, {'unique_id': 3, 'parent_id': 2, 'short_name': 'Lucy', 'height': '162', 'weight': '50'}, {'unique_id': 4, 'parent_id': 2, 'short_name': 'Joe', 'height': '175', 'weight': '65'}, {'unique_id': 5, 'parent_id': 1, 'short_name': 'Class 2', 'height': ' ', 'weight': ' '}, {'unique_id': 6, 'parent_id': 5, 'short_name': 'Lily', 'height': '170', 'weight': '55'}, {'unique_id': 7, 'parent_id': 5, 'short_name': 'Tom', 'height': '180', 'weight': '75'}, {'unique_id': 8, 'parent_id': 1, 'short_name': 'Class 3', 'height': ' ', 'weight': ' '}, {'unique_id': 9, 'parent_id': 8, 'short_name': 'Jack', 'height': '178', 'weight': '80'}, {'unique_id': 10, 'parent_id': 8, 'short_name': 'Tim', 'height': '172', 'weight': '60'} ] app = QApplication(sys.argv) view = view(data) view.setGeometry(300, 100, 600, 300) view.setWindowTitle('QTreeview Example') view.show() sys.exit(app.exec_())
In this example, parent item of 2nd, 5th and 8th items are the 1st item and therefore ‘Class 1’, ‘Class 2’ and ‘Class 3’ are the child items of QTreeview’s root item. ‘parent_id’ for 3rd and 4th item equals to 2 and thus they are child items of ‘Class 1’.
Transverse QTreeview node and save QTreeview into dictionary
Child items for a specific item can be extracted by row index and column index of this item. Let’s take root item from above figure as an example, it has three rows of child items and each row consists of three items. When row index equals 0 and column index equals 0, we will get first item of first row and retrieved data will be ‘Class 1’. When row index equals 0 and column index equals 1, we will get null.
The same approach can be used to transverse child items of ‘Class 1’. When row index is 1 and column index is 1, we will get 175. Row index of 1 and column index of 2 will lead to 3rd item of second row and the value of this item is 65.
When coming to a new level of item, row index and column index will start from 0 again. We cannot use common for loop to retrieve data of tree again. In this case, recursive traversal should be used.
import sys from collections import deque from PyQt5.QtWidgets import * from PyQt5.QtGui import * from PyQt5.QtCore import * class view(QWidget): def __init__(self, data): super(view, self).__init__() self.tree = QTreeView(self) layout = QVBoxLayout(self) layout.addWidget(self.tree) self.model = QStandardItemModel() self.model.setHorizontalHeaderLabels(['Name', 'Height', 'Weight']) self.tree.header().setDefaultSectionSize(180) self.tree.setModel(self.model) self.importData(data) self.tree.expandAll() tree_list = self.transverse_tree() print('tree_list saved from QTreeview:') for row in tree_list: print(row) # Function to save populate treeview with a dictionary def importData(self, data, root=None): self.model.setRowCount(0) if root is None: root = self.model.invisibleRootItem() seen = {} # List of QStandardItem values = deque(data) while values: value = values.popleft() if value['unique_id'] == 1: parent = root else: pid = value['parent_id'] if pid not in seen: values.append(value) continue parent = seen[pid] unique_id = value['unique_id'] parent.appendRow([ QStandardItem(value['short_name']), QStandardItem(value['height']), QStandardItem(value['weight']) ]) seen[unique_id] = parent.child(parent.rowCount() - 1) # Function to transverse treeview and derive tree_list def transverse_tree(self): tree_list = [] for i in range(self.model.rowCount()): item = self.model.item(i) level = 0 self.GetItem(item, level, tree_list) return tree_list def GetItem(self, item, level, tree_list): if item != None: if item.hasChildren(): level = level + 1 short_name = ' ' height = ' ' weight = ' ' id = 0 for i in range(item.rowCount()): id = id + 1 for j in reversed([0, 1, 2]): childitem = item.child(i, j) if childitem != None: if j == 0: short_name = childitem.data(0) else: short_name = short_name if j == 1: height = childitem.data(0) else: height = height if j == 2: weight = childitem.data(0) else: weight = weight if j == 0: dic = {} dic['level'] = level dic['id'] = id dic['short_name'] = short_name dic['height'] = height dic['weight'] = weight tree_list.append(dic) self.GetItem(childitem, level, tree_list) return tree_list if __name__ == '__main__': data = [ {'unique_id': 1, 'parent_id': 0, 'short_name': '', 'height': ' ', 'weight': ' '}, {'unique_id': 2, 'parent_id': 1, 'short_name': 'Class 1', 'height': ' ', 'weight': ' '}, {'unique_id': 3, 'parent_id': 2, 'short_name': 'Lucy', 'height': '162', 'weight': '50'}, {'unique_id': 4, 'parent_id': 2, 'short_name': 'Joe', 'height': '175', 'weight': '65'}, {'unique_id': 5, 'parent_id': 1, 'short_name': 'Class 2', 'height': ' ', 'weight': ' '}, {'unique_id': 6, 'parent_id': 5, 'short_name': 'Lily', 'height': '170', 'weight': '55'}, {'unique_id': 7, 'parent_id': 5, 'short_name': 'Tom', 'height': '180', 'weight': '75'}, {'unique_id': 8, 'parent_id': 1, 'short_name': 'Class 3', 'height': ' ', 'weight': ' '}, {'unique_id': 9, 'parent_id': 8, 'short_name': 'Jack', 'height': '178', 'weight': '80'}, {'unique_id': 10, 'parent_id': 8, 'short_name': 'Tim', 'height': '172', 'weight': '60'} ] app = QApplication(sys.argv) view = view(data) view.setGeometry(300, 100, 600, 300) view.setWindowTitle('QTreeview Example') view.show() sys.exit(app.exec_())
Here are the data extracted from above tree using recursive traversal.
Right-click Menu for QTreeview to add child item, insert item above, insert item down and delete item
AppendRow method can be used to create child items while InsertRow method can be used to create sibling items.
import sys from collections import deque from PyQt5.QtWidgets import * from PyQt5.QtGui import * from PyQt5.QtCore import * from functools import partial class view(QWidget): def __init__(self, data): super(view, self).__init__() self.tree = QTreeView(self) self.tree.setContextMenuPolicy(Qt.CustomContextMenu) self.tree.customContextMenuRequested.connect(self.openMenu) layout = QVBoxLayout(self) layout.addWidget(self.tree) self.model = QStandardItemModel() self.model.setHorizontalHeaderLabels(['Name', 'Height', 'Weight']) self.tree.header().setDefaultSectionSize(180) self.tree.setModel(self.model) self.importData(data) self.tree.expandAll() # Function to save populate treeview with a dictionary def importData(self, data, root=None): self.model.setRowCount(0) if root is None: root = self.model.invisibleRootItem() seen = {} # List of QStandardItem values = deque(data) while values: value = values.popleft() if value['unique_id'] == 1: parent = root else: pid = value['parent_id'] if pid not in seen: values.append(value) continue parent = seen[pid] unique_id = value['unique_id'] parent.appendRow([ QStandardItem(value['short_name']), QStandardItem(value['height']), QStandardItem(value['weight']) ]) seen[unique_id] = parent.child(parent.rowCount() - 1) # Function to add right click menu to treeview item def openMenu(self, position): indexes = self.sender().selectedIndexes() mdlIdx = self.tree.indexAt(position) if not mdlIdx.isValid(): return item = self.model.itemFromIndex(mdlIdx) if len(indexes) > 0: level = 0 index = indexes[0] while index.parent().isValid(): index = index.parent() level += 1 else: level = 0 right_click_menu = QMenu() act_add = right_click_menu.addAction(self.tr("Add Child Item")) act_add.triggered.connect(partial(self.TreeItem_Add, level, mdlIdx)) if item.parent() != None: insert_up = right_click_menu.addAction(self.tr("Insert Item Above")) insert_up.triggered.connect(partial(self.TreeItem_InsertUp, level, mdlIdx)) insert_down = right_click_menu.addAction(self.tr("Insert Item Below")) insert_down.triggered.connect(partial(self.TreeItem_InsertDown, level, mdlIdx)) act_del = right_click_menu.addAction(self.tr("Delete Item")) act_del.triggered.connect(partial(self.TreeItem_Delete, item)) right_click_menu.exec_(self.sender().viewport().mapToGlobal(position)) # # Function to add child item to treeview item def TreeItem_Add(self, level, mdlIdx): temp_key = QStandardItem('xx') temp_value1 = QStandardItem('xx') temp_value2 = QStandardItem('xx') self.model.itemFromIndex(mdlIdx).appendRow([temp_key, temp_value1, temp_value2]) self.tree.expandAll() # Function to Insert sibling item above to treeview item def TreeItem_InsertUp(self, level, mdlIdx): level = level - 1 current_row = self.model.itemFromIndex(mdlIdx).row() temp_key = QStandardItem('xx') temp_value1 = QStandardItem('xx') temp_value2 = QStandardItem('xx') self.model.itemFromIndex(mdlIdx).parent().insertRow(current_row, [temp_key, temp_value1, temp_value2]) self.tree.expandToDepth(1 + level) # Function to Insert sibling item above to treeview item def TreeItem_InsertDown(self, level, mdlIdx): level = level - 1 temp_key = QStandardItem('xx') temp_value1 = QStandardItem('xx') temp_value2 = QStandardItem('xx') current_row = self.model.itemFromIndex(mdlIdx).row() self.model.itemFromIndex(mdlIdx).parent().insertRow(current_row + 1, [temp_key, temp_value1, temp_value2]) self.tree.expandToDepth(1 + level) # Function to Delete item def TreeItem_Delete(self, item): item.parent().removeRow(item.row()) if __name__ == '__main__': data = [ {'unique_id': 1, 'parent_id': 0, 'short_name': '', 'height': ' ', 'weight': ' '}, {'unique_id': 2, 'parent_id': 1, 'short_name': 'Class 1', 'height': ' ', 'weight': ' '}, {'unique_id': 3, 'parent_id': 2, 'short_name': 'Lucy', 'height': '162', 'weight': '50'}, {'unique_id': 4, 'parent_id': 2, 'short_name': 'Joe', 'height': '175', 'weight': '65'}, {'unique_id': 5, 'parent_id': 1, 'short_name': 'Class 2', 'height': ' ', 'weight': ' '}, {'unique_id': 6, 'parent_id': 5, 'short_name': 'Lily', 'height': '170', 'weight': '55'}, {'unique_id': 7, 'parent_id': 5, 'short_name': 'Tom', 'height': '180', 'weight': '75'}, {'unique_id': 8, 'parent_id': 1, 'short_name': 'Class 3', 'height': ' ', 'weight': ' '}, {'unique_id': 9, 'parent_id': 8, 'short_name': 'Jack', 'height': '178', 'weight': '80'}, {'unique_id': 10, 'parent_id': 8, 'short_name': 'Tim', 'height': '172', 'weight': '60'} ] app = QApplication(sys.argv) view = view(data) view.setGeometry(300, 100, 600, 300) view.setWindowTitle('QTreeview Example') view.show() sys.exit(app.exec_())
Add styles into QTreeview
There is no method to set style for a whole row and the only way is to customize item one by one. Thus, recursive traversal introduced should be used here.
import sys from collections import deque from PyQt5.QtWidgets import * from PyQt5.QtGui import * from PyQt5.QtCore import * from functools import partial class view(QWidget): def __init__(self, data): super(view, self).__init__() self.tree = QTreeView(self) layout = QVBoxLayout(self) layout.addWidget(self.tree) self.model = QStandardItemModel() self.model.setHorizontalHeaderLabels(['Name', 'Height', 'Weight']) self.tree.header().setDefaultSectionSize(180) self.tree.setModel(self.model) self.importData(data) self.tree.expandAll() # Function to save populate treeview with a dictionary def importData(self, data, root=None): self.model.setRowCount(0) if root is None: root = self.model.invisibleRootItem() seen = {} # List of QStandardItem values = deque(data) while values: value = values.popleft() if value['unique_id'] == 1: parent = root else: pid = value['parent_id'] if pid not in seen: values.append(value) continue parent = seen[pid] unique_id = value['unique_id'] parent.appendRow([ QStandardItem(value['short_name']), QStandardItem(value['height']), QStandardItem(value['weight']) ]) seen[unique_id] = parent.child(parent.rowCount() - 1) # Add style to tree rows item = self.model.item(0) for i in range(item.rowCount()): for j in range(7): childitem = item.child(i, j) if childitem != None: childitem.setBackground(QColor(225, 225, 225)) childitem.setSizeHint(QSize(30, 25)) childitem.setTextAlignment(Qt.AlignBottom) childitem.setFont(QFont("Times New Roman", weight=QFont.Bold)) if __name__ == '__main__': data = [ {'unique_id': 1, 'parent_id': 0, 'short_name': '', 'height': ' ', 'weight': ' '}, {'unique_id': 2, 'parent_id': 1, 'short_name': 'Class 1', 'height': ' ', 'weight': ' '}, {'unique_id': 3, 'parent_id': 2, 'short_name': 'Lucy', 'height': '162', 'weight': '50'}, {'unique_id': 4, 'parent_id': 2, 'short_name': 'Joe', 'height': '175', 'weight': '65'}, {'unique_id': 5, 'parent_id': 1, 'short_name': 'Class 2', 'height': ' ', 'weight': ' '}, {'unique_id': 6, 'parent_id': 5, 'short_name': 'Lily', 'height': '170', 'weight': '55'}, {'unique_id': 7, 'parent_id': 5, 'short_name': 'Tom', 'height': '180', 'weight': '75'}, {'unique_id': 8, 'parent_id': 1, 'short_name': 'Class 3', 'height': ' ', 'weight': ' '}, {'unique_id': 9, 'parent_id': 8, 'short_name': 'Jack', 'height': '178', 'weight': '80'}, {'unique_id': 10, 'parent_id': 8, 'short_name': 'Tim', 'height': '172', 'weight': '60'} ] app = QApplication(sys.argv) view = view(data) view.setGeometry(300, 100, 600, 300) view.setWindowTitle('QTreeview Example') view.show() sys.exit(app.exec_())