Thanks again so much @SGaist , @friedemannkleint and @mpergand for the help and patience. Just for the record, here are the details for a working example, based on the original https://doc.qt.io/qtforpython-6/examples/example_widgets_itemviews_editabletreemodel.html
======================
mainwindow.py:
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
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.QtGui import QFont
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.SelectItems)
self.view.setHorizontalScrollMode(QAbstractItemView.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)
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.EditRole)
if not model.headerData(column, Qt.Horizontal):
model.setHeaderData(column, Qt.Horizontal, "[No header]",
Qt.EditRole)
selection_model.setCurrentIndex(
model.index(0, 0, index), QItemSelectionModel.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.Horizontal, "[No header]",
Qt.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.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)
model: QAbstractItemModel = self.view.model()
newFont = QFont("Times New Roman", 20, QFont.Bold)
newFont.setItalic(True)
model.setData(current_index, newFont, role=Qt.FontRole)
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)
======================
treeitem.py:
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
class TreeItem:
def __init__(self, data: list, parent: 'TreeItem' = None):
self.item_data = data
self.item_font = [None] * len(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 font(self, column: int):
if column < 0 or column >= len(self.item_data):
return None
return self.item_font[column]
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_font(self, column: int, value):
if column < 0 or column >= len(self.item_data):
return False
self.item_font[column] = value
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
======================
treemodel.py:
# Copyright (C) 2022 The Qt Company Ltd.
# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
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.DisplayRole and role != Qt.EditRole and role != Qt.FontRole:
return None
item: TreeItem = self.get_item(index)
if role == Qt.FontRole:
return item.font(index.column())
return item.data(index.column())
def flags(self, index: QModelIndex) -> Qt.ItemFlags:
if not index.isValid():
return Qt.NoItemFlags
return Qt.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.DisplayRole):
if orientation == Qt.Horizontal and role == Qt.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.EditRole) and (role != Qt.FontRole):
return False
item: TreeItem = self.get_item(index)
if role == Qt.FontRole:
result: bool = item.set_font(index.column(), value)
else:
result: bool = item.set_data(index.column(), value)
if result:
self.dataChanged.emit(index, index, [Qt.DisplayRole, Qt.EditRole, Qt.FontRole])
return result
def setHeaderData(self, section: int, orientation: Qt.Orientation, value,
role: int = None) -> bool:
if role != Qt.EditRole or orientation != Qt.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)