Editable Tree Model Example¶
A Python application that demonstrates the analogous example in C++ Editable Tree Model Example
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from __future__ import annotations
import sys
from PySide6.QtWidgets import QApplication
from mainwindow import MainWindow
if __name__ == '__main__':
app = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(app.exec())
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from __future__ import annotations
import sys
from pathlib import Path
from PySide6.QtCore import (QAbstractItemModel, QItemSelectionModel,
QModelIndex, Qt, Slot)
from PySide6.QtWidgets import (QAbstractItemView, QMainWindow, QTreeView,
QWidget)
from PySide6.QtTest import QAbstractItemModelTester
from treemodel import TreeModel
class MainWindow(QMainWindow):
def __init__(self, parent: QWidget = None):
super().__init__(parent)
self.resize(573, 468)
self.view = QTreeView()
self.view.setAlternatingRowColors(True)
self.view.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectItems)
self.view.setHorizontalScrollMode(QAbstractItemView.ScrollMode.ScrollPerPixel)
self.view.setAnimated(False)
self.view.setAllColumnsShowFocus(True)
self.setCentralWidget(self.view)
menubar = self.menuBar()
file_menu = menubar.addMenu("&File")
self.exit_action = file_menu.addAction("E&xit")
self.exit_action.setShortcut("Ctrl+Q")
self.exit_action.triggered.connect(self.close)
actions_menu = menubar.addMenu("&Actions")
actions_menu.triggered.connect(self.update_actions)
self.insert_row_action = actions_menu.addAction("Insert Row")
self.insert_row_action.setShortcut("Ctrl+I, R")
self.insert_row_action.triggered.connect(self.insert_row)
self.insert_column_action = actions_menu.addAction("Insert Column")
self.insert_column_action.setShortcut("Ctrl+I, C")
self.insert_column_action.triggered.connect(self.insert_column)
actions_menu.addSeparator()
self.remove_row_action = actions_menu.addAction("Remove Row")
self.remove_row_action.setShortcut("Ctrl+R, R")
self.remove_row_action.triggered.connect(self.remove_row)
self.remove_column_action = actions_menu.addAction("Remove Column")
self.remove_column_action.setShortcut("Ctrl+R, C")
self.remove_column_action.triggered.connect(self.remove_column)
actions_menu.addSeparator()
self.insert_child_action = actions_menu.addAction("Insert Child")
self.insert_child_action.setShortcut("Ctrl+N")
self.insert_child_action.triggered.connect(self.insert_child)
help_menu = menubar.addMenu("&Help")
about_qt_action = help_menu.addAction("About Qt", qApp.aboutQt) # noqa: F821
about_qt_action.setShortcut("F1")
self.setWindowTitle("Editable Tree Model")
headers = ["Title", "Description"]
file = Path(__file__).parent / "default.txt"
self.model = TreeModel(headers, file.read_text(), self)
if "-t" in sys.argv:
QAbstractItemModelTester(self.model, self)
self.view.setModel(self.model)
self.view.expandAll()
for column in range(self.model.columnCount()):
self.view.resizeColumnToContents(column)
selection_model = self.view.selectionModel()
selection_model.selectionChanged.connect(self.update_actions)
self.update_actions()
@Slot()
def insert_child(self) -> None:
selection_model = self.view.selectionModel()
index: QModelIndex = selection_model.currentIndex()
model: QAbstractItemModel = self.view.model()
if model.columnCount(index) == 0:
if not model.insertColumn(0, index):
return
if not model.insertRow(0, index):
return
for column in range(model.columnCount(index)):
child: QModelIndex = model.index(0, column, index)
model.setData(child, "[No data]", Qt.ItemDataRole.EditRole)
if not model.headerData(column, Qt.Orientation.Horizontal):
model.setHeaderData(column, Qt.Orientation.Horizontal, "[No header]",
Qt.ItemDataRole.EditRole)
selection_model.setCurrentIndex(
model.index(0, 0, index), QItemSelectionModel.SelectionFlag.ClearAndSelect
)
self.update_actions()
@Slot()
def insert_column(self) -> None:
model: QAbstractItemModel = self.view.model()
column: int = self.view.selectionModel().currentIndex().column()
changed: bool = model.insertColumn(column + 1)
if changed:
model.setHeaderData(column + 1, Qt.Orientation.Horizontal, "[No header]",
Qt.ItemDataRole.EditRole)
self.update_actions()
@Slot()
def insert_row(self) -> None:
index: QModelIndex = self.view.selectionModel().currentIndex()
model: QAbstractItemModel = self.view.model()
parent: QModelIndex = index.parent()
if not model.insertRow(index.row() + 1, parent):
return
self.update_actions()
for column in range(model.columnCount(parent)):
child: QModelIndex = model.index(index.row() + 1, column, parent)
model.setData(child, "[No data]", Qt.ItemDataRole.EditRole)
@Slot()
def remove_column(self) -> None:
model: QAbstractItemModel = self.view.model()
column: int = self.view.selectionModel().currentIndex().column()
if model.removeColumn(column):
self.update_actions()
@Slot()
def remove_row(self) -> None:
index: QModelIndex = self.view.selectionModel().currentIndex()
model: QAbstractItemModel = self.view.model()
if model.removeRow(index.row(), index.parent()):
self.update_actions()
@Slot()
def update_actions(self) -> None:
selection_model = self.view.selectionModel()
has_selection: bool = not selection_model.selection().isEmpty()
self.remove_row_action.setEnabled(has_selection)
self.remove_column_action.setEnabled(has_selection)
current_index = selection_model.currentIndex()
has_current: bool = current_index.isValid()
self.insert_row_action.setEnabled(has_current)
self.insert_column_action.setEnabled(has_current)
if has_current:
self.view.closePersistentEditor(current_index)
msg = f"Position: ({current_index.row()},{current_index.column()})"
if not current_index.parent().isValid():
msg += " in top level"
self.statusBar().showMessage(msg)
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from __future__ import annotations
class TreeItem:
def __init__(self, data: list, parent: 'TreeItem' = None):
self.item_data = data
self.parent_item = parent
self.child_items = []
def child(self, number: int) -> 'TreeItem':
if number < 0 or number >= len(self.child_items):
return None
return self.child_items[number]
def last_child(self):
return self.child_items[-1] if self.child_items else None
def child_count(self) -> int:
return len(self.child_items)
def child_number(self) -> int:
if self.parent_item:
return self.parent_item.child_items.index(self)
return 0
def column_count(self) -> int:
return len(self.item_data)
def data(self, column: int):
if column < 0 or column >= len(self.item_data):
return None
return self.item_data[column]
def insert_children(self, position: int, count: int, columns: int) -> bool:
if position < 0 or position > len(self.child_items):
return False
for row in range(count):
data = [None] * columns
item = TreeItem(data.copy(), self)
self.child_items.insert(position, item)
return True
def insert_columns(self, position: int, columns: int) -> bool:
if position < 0 or position > len(self.item_data):
return False
for column in range(columns):
self.item_data.insert(position, None)
for child in self.child_items:
child.insert_columns(position, columns)
return True
def parent(self):
return self.parent_item
def remove_children(self, position: int, count: int) -> bool:
if position < 0 or position + count > len(self.child_items):
return False
for row in range(count):
self.child_items.pop(position)
return True
def remove_columns(self, position: int, columns: int) -> bool:
if position < 0 or position + columns > len(self.item_data):
return False
for column in range(columns):
self.item_data.pop(position)
for child in self.child_items:
child.remove_columns(position, columns)
return True
def set_data(self, column: int, value):
if column < 0 or column >= len(self.item_data):
return False
self.item_data[column] = value
return True
def __repr__(self) -> str:
result = f"<treeitem.TreeItem at 0x{id(self):x}"
for d in self.item_data:
result += f' "{d}"' if d else " <None>"
result += f", {len(self.child_items)} children>"
return result
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
from __future__ import annotations
from PySide6.QtCore import QModelIndex, Qt, QAbstractItemModel
from treeitem import TreeItem
class TreeModel(QAbstractItemModel):
def __init__(self, headers: list, data: str, parent=None):
super().__init__(parent)
self.root_data = headers
self.root_item = TreeItem(self.root_data.copy())
self.setup_model_data(data.split("\n"), self.root_item)
def columnCount(self, parent: QModelIndex = None) -> int:
return self.root_item.column_count()
def data(self, index: QModelIndex, role: int = None):
if not index.isValid():
return None
if role != Qt.ItemDataRole.DisplayRole and role != Qt.ItemDataRole.EditRole:
return None
item: TreeItem = self.get_item(index)
return item.data(index.column())
def flags(self, index: QModelIndex) -> Qt.ItemFlags:
if not index.isValid():
return Qt.ItemFlag.NoItemFlags
return Qt.ItemFlag.ItemIsEditable | QAbstractItemModel.flags(self, index)
def get_item(self, index: QModelIndex = QModelIndex()) -> TreeItem:
if index.isValid():
item: TreeItem = index.internalPointer()
if item:
return item
return self.root_item
def headerData(self, section: int, orientation: Qt.Orientation,
role: int = Qt.ItemDataRole.DisplayRole):
if orientation == Qt.Orientation.Horizontal and role == Qt.ItemDataRole.DisplayRole:
return self.root_item.data(section)
return None
def index(self, row: int, column: int, parent: QModelIndex = QModelIndex()) -> QModelIndex:
if parent.isValid() and parent.column() != 0:
return QModelIndex()
parent_item: TreeItem = self.get_item(parent)
if not parent_item:
return QModelIndex()
child_item: TreeItem = parent_item.child(row)
if child_item:
return self.createIndex(row, column, child_item)
return QModelIndex()
def insertColumns(self, position: int, columns: int,
parent: QModelIndex = QModelIndex()) -> bool:
self.beginInsertColumns(parent, position, position + columns - 1)
success: bool = self.root_item.insert_columns(position, columns)
self.endInsertColumns()
return success
def insertRows(self, position: int, rows: int,
parent: QModelIndex = QModelIndex()) -> bool:
parent_item: TreeItem = self.get_item(parent)
if not parent_item:
return False
self.beginInsertRows(parent, position, position + rows - 1)
column_count = self.root_item.column_count()
success: bool = parent_item.insert_children(position, rows, column_count)
self.endInsertRows()
return success
def parent(self, index: QModelIndex = QModelIndex()) -> QModelIndex:
if not index.isValid():
return QModelIndex()
child_item: TreeItem = self.get_item(index)
if child_item:
parent_item: TreeItem = child_item.parent()
else:
parent_item = None
if parent_item == self.root_item or not parent_item:
return QModelIndex()
return self.createIndex(parent_item.child_number(), 0, parent_item)
def removeColumns(self, position: int, columns: int,
parent: QModelIndex = QModelIndex()) -> bool:
self.beginRemoveColumns(parent, position, position + columns - 1)
success: bool = self.root_item.remove_columns(position, columns)
self.endRemoveColumns()
if self.root_item.column_count() == 0:
self.removeRows(0, self.rowCount())
return success
def removeRows(self, position: int, rows: int,
parent: QModelIndex = QModelIndex()) -> bool:
parent_item: TreeItem = self.get_item(parent)
if not parent_item:
return False
self.beginRemoveRows(parent, position, position + rows - 1)
success: bool = parent_item.remove_children(position, rows)
self.endRemoveRows()
return success
def rowCount(self, parent: QModelIndex = QModelIndex()) -> int:
if parent.isValid() and parent.column() > 0:
return 0
parent_item: TreeItem = self.get_item(parent)
if not parent_item:
return 0
return parent_item.child_count()
def setData(self, index: QModelIndex, value, role: int) -> bool:
if role != Qt.ItemDataRole.EditRole:
return False
item: TreeItem = self.get_item(index)
result: bool = item.set_data(index.column(), value)
if result:
self.dataChanged.emit(index, index,
[Qt.ItemDataRole.DisplayRole, Qt.ItemDataRole.EditRole])
return result
def setHeaderData(self, section: int, orientation: Qt.Orientation, value,
role: int = None) -> bool:
if role != Qt.ItemDataRole.EditRole or orientation != Qt.Orientation.Horizontal:
return False
result: bool = self.root_item.set_data(section, value)
if result:
self.headerDataChanged.emit(orientation, section, section)
return result
def setup_model_data(self, lines: list, parent: TreeItem):
parents = [parent]
indentations = [0]
for line in lines:
line = line.rstrip()
if line and "\t" in line:
position = 0
while position < len(line):
if line[position] != " ":
break
position += 1
column_data = line[position:].split("\t")
column_data = [string for string in column_data if string]
if position > indentations[-1]:
if parents[-1].child_count() > 0:
parents.append(parents[-1].last_child())
indentations.append(position)
else:
while position < indentations[-1] and parents:
parents.pop()
indentations.pop()
parent: TreeItem = parents[-1]
col_count = self.root_item.column_count()
parent.insert_children(parent.child_count(), 1, col_count)
for column in range(len(column_data)):
child = parent.last_child()
child.set_data(column, column_data[column])
def _repr_recursion(self, item: TreeItem, indent: int = 0) -> str:
result = " " * indent + repr(item) + "\n"
for child in item.child_items:
result += self._repr_recursion(child, indent + 2)
return result
def __repr__(self) -> str:
return self._repr_recursion(self.root_item)
Getting Started How to familiarize yourself with Qt Designer
Launching Designer Running the Qt Designer application
The User Interface How to interact with Qt Designer
Designing a Component Creating a GUI for your application
Creating a Dialog How to create a dialog
Composing the Dialog Putting widgets into the dialog example
Creating a Layout Arranging widgets on a form
Signal and Slot Connections Making widget communicate with each other
Using a Component in Your Application Generating code from forms
The Direct Approach Using a form without any adjustments
The Single Inheritance Approach Subclassing a form's base class
The Multiple Inheritance Approach Subclassing the form itself
Automatic Connections Connecting widgets using a naming scheme
A Dialog Without Auto-Connect How to connect widgets without a naming scheme
A Dialog With Auto-Connect Using automatic connections
Form Editing Mode How to edit a form in Qt Designer
Managing Forms Loading and saving forms
Editing a Form Basic editing techniques
The Property Editor Changing widget properties
The Object Inspector Examining the hierarchy of objects on a form
Layouts Objects that arrange widgets on a form
Applying and Breaking Layouts Managing widgets in layouts
Horizontal and Vertical Layouts Standard row and column layouts
The Grid Layout Arranging widgets in a matrix
Previewing Forms Checking that the design works
Using Containers How to group widgets together
General Features Common container features
Frames QFrame
Group Boxes QGroupBox
Stacked Widgets QStackedWidget
Tab Widgets QTabWidget
Toolbox Widgets QToolBox
Connection Editing Mode Connecting widgets together with signals and slots
Connecting Objects Making connections in Qt Designer
Editing Connections Changing existing connections