Changing Font and background color of individual item in editable Treeview
-
Hi,
The most straightforward would be to modify your first if and expand its check, the rest is the same.
-
Hi,
The most straightforward would be to modify your first if and expand its check, the rest is the same.
Thanks @SGaist for helping out!
That doesn't seem to work. If use
model.setData(current_index, 'QWidget { font: bold italic large "Times New Roman"; font-size: 10pt }', role=Qt.FontRole)
inside the update_actions function of mainwindow.py, together with
if (role != Qt.EditRole) and (role != Qt.FontRole):
inside the setData function of treemodel.py, what happens is that it just replaces the text of the item by
'QWidget { font: bold italic large "Times New Roman"; font-size: 10pt }'
as if that was a normal edit action. I think this is because the editable tree version has a whole wrapper of the data itself around it...
-
It looks like you are trying to set a style sheet in the font role.
The FontRole takes a QFont object.
You can see here more information about the roles and what they expect.
Ah. good point. But it still does the same. If I use
newFont = QFont("Helvetica") model.setData(current_index, newFont, role=Qt.FontRole)
It just replaces the content of the item with the text "Helvetica"...
-
Ah. good point. But it still does the same. If I use
newFont = QFont("Helvetica") model.setData(current_index, newFont, role=Qt.FontRole)
It just replaces the content of the item with the text "Helvetica"...
@Stan-Pamela do you also have a custom data function ?
-
Ah. good point. But it still does the same. If I use
newFont = QFont("Helvetica") model.setData(current_index, newFont, role=Qt.FontRole)
It just replaces the content of the item with the text "Helvetica"...
@Stan-Pamela
You need to return the right data for the roles in data() not setData() -
@Stan-Pamela do you also have a custom data function ?
@SGaist yes, it's
def data(self, index: QModelIndex, role: int = None): if not index.isValid(): return None #if role == Qt.FontRole: # sorry please ignore! that was confusing... # return item.data(index.column()) if role != Qt.DisplayRole and role != Qt.EditRole: return None item: TreeItem = self.get_item(index) return item.data(index.column())
which then goes to treeitem.py -> data
def data(self, column: int): if column < 0 or column >= len(self.item_data): return None return self.item_data[column]
in other words, it's wrapping a TreeItem to get item_data[column]. What would I need to target to return the font? or the style or background?
-
@SGaist yes, it's
def data(self, index: QModelIndex, role: int = None): if not index.isValid(): return None #if role == Qt.FontRole: # sorry please ignore! that was confusing... # return item.data(index.column()) if role != Qt.DisplayRole and role != Qt.EditRole: return None item: TreeItem = self.get_item(index) return item.data(index.column())
which then goes to treeitem.py -> data
def data(self, column: int): if column < 0 or column >= len(self.item_data): return None return self.item_data[column]
in other words, it's wrapping a TreeItem to get item_data[column]. What would I need to target to return the font? or the style or background?
@Stan-Pamela you are not passing the role down to your item data call.
-
@Stan-Pamela you are not passing the role down to your item data call.
@SGaist
:)
I can only reply every 600sec apparently because I'm a new user. I think you need to give me a "reputation" to be able to interact faster? :)Anyway, really sorry about last post, that was confusing, I was trying to edit the data function and copy pasted something that wasn't working...
Still, these are the two functions that wrap around item_data
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) result: bool = item.set_data(index.column(), value) if result: self.dataChanged.emit(index, index, [Qt.DisplayRole, Qt.EditRole, Qt.FontRole]) return result
and
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
-
@SGaist
:)
I can only reply every 600sec apparently because I'm a new user. I think you need to give me a "reputation" to be able to interact faster? :)Anyway, really sorry about last post, that was confusing, I was trying to edit the data function and copy pasted something that wasn't working...
Still, these are the two functions that wrap around item_data
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) result: bool = item.set_data(index.column(), value) if result: self.dataChanged.emit(index, index, [Qt.DisplayRole, Qt.EditRole, Qt.FontRole]) return result
and
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
@Stan-Pamela that's another issue: you don't handle the role in your wrapper just the value.
-
@Stan-Pamela that's another issue: you don't handle the role in your wrapper just the value.
@SGaist OK, how do I handle the role?
Really sorry, I know this is probably so obvious to you all, but I just can't figure it out. It seems this treeview example is a corner case with all the wrappers, isn't it? Or it's just me?Still can't write more than every 600sec :( maybe just as well, ahah!
-
Ok, I see the issue. The example works well for just handling "data".
You need to modify your custom TreeItem to handle the role in addition to the value. As a basic version, you can use a dictionary where the key is the role and the item the value. Add a role parameter to the set_data and data methods so that you can properly set and retrieve these values.
-
Ok, I see the issue. The example works well for just handling "data".
You need to modify your custom TreeItem to handle the role in addition to the value. As a basic version, you can use a dictionary where the key is the role and the item the value. Add a role parameter to the set_data and data methods so that you can properly set and retrieve these values.
Thanks again. I'm still not sure how to do that...
Even for the "value" itself, could you explain how/where the final target TreeItem->item_data is set? This doesn't seem to be a standard Qt class/variable... I'm confused...At the very end of treeitem.py, in this editable tree example, there is this recursive function:
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
Is it this repr function that's used internally, in the backend of the Qt library, to display the text manually? Do I need to use a similar backend function for the role?
-
Thanks again. I'm still not sure how to do that...
Even for the "value" itself, could you explain how/where the final target TreeItem->item_data is set? This doesn't seem to be a standard Qt class/variable... I'm confused...At the very end of treeitem.py, in this editable tree example, there is this recursive function:
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
Is it this repr function that's used internally, in the backend of the Qt library, to display the text manually? Do I need to use a similar backend function for the role?
Basically:
def set_data(self, column: int, value, role): if column < 0 or column >= len(self.item_data): return False if column not in self.item_data.keys(): self.item_data[column] = {} self.item_data[column][role] = value return True
Don't forget to handle the EditRole and Display role properly.
-
Basically:
def set_data(self, column: int, value, role): if column < 0 or column >= len(self.item_data): return False if column not in self.item_data.keys(): self.item_data[column] = {} self.item_data[column][role] = value return True
Don't forget to handle the EditRole and Display role properly.
That doesn't work either. Or I'm not doing it properly(?)
Even ignoring the font role, and just trying to do the simplest thing possible: to use a dict like this for the text edit alone, and replace the original functionality, it doesn't work:def set_data(self, column: int, value): if column < 0 or column >= len(self.item_data): return False #self.item_data[column] = value self.item_data[column] = {Qt.EditRole: value}
-
That doesn't work either. Or I'm not doing it properly(?)
Even ignoring the font role, and just trying to do the simplest thing possible: to use a dict like this for the text edit alone, and replace the original functionality, it doesn't work:def set_data(self, column: int, value): if column < 0 or column >= len(self.item_data): return False #self.item_data[column] = value self.item_data[column] = {Qt.EditRole: value}
@Stan-Pamela did you adapt the data function in a similar fashion ?
-
@Stan-Pamela did you adapt the data function in a similar fashion ?
@SGaist, got it!
Thanks so much and sorry it took me so long to understand the trail of wrappers and how that feeds back into the main model. Awesome. I did it in a dirty way for now, but shall I clean up and post a zip of the code for consistency and closure of this forum thread? -
@SGaist, got it!
Thanks so much and sorry it took me so long to understand the trail of wrappers and how that feeds back into the main model. Awesome. I did it in a dirty way for now, but shall I clean up and post a zip of the code for consistency and closure of this forum thread?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)
-