Using multiple item delegates in QTableView columns
-
I've made two different QItemDelegate subclasses, one for presenting a combobox to edit values, the other to present a checkbox.
Problem is when using .tableView.setItemDelegateForColumn I can set a column to use one subclass OR the other, but I cannot set two different columns to a different delegate at the same time. When I try I just get a empty window that flashes on the screen then the program quits.
I've tried to string together 5 different .py files into one working file pasted below. Apologies in advance if I screwed this up:
import sys from PySide6.QtCore import (QCoreApplication, QDate, QDateTime, QLocale, QMetaObject, QObject, QPoint, QRect, QSize, QTime, QUrl, Qt, QAbstractTableModel, QModelIndex) from PySide6.QtGui import (QBrush, QColor, QConicalGradient, QCursor, QFont, QFontDatabase, QGradient, QIcon, QImage, QKeySequence, QLinearGradient, QPainter, QPalette, QPixmap, QRadialGradient, QTransform) from PySide6.QtWidgets import (QApplication, QHeaderView, QMainWindow, QMenuBar, QSizePolicy, QStatusBar, QTableView, QWidget, QItemDelegate) from PySide6 import QtWidgets class Ui_MainWindow(object): def setupUi(self, MainWindow): if not MainWindow.objectName(): MainWindow.setObjectName(u"MainWindow") MainWindow.resize(942, 787) self.centralwidget = QWidget(MainWindow) self.centralwidget.setObjectName(u"centralwidget") self.tableView = QTableView(self.centralwidget) self.tableView.setObjectName(u"tableView") self.tableView.setGeometry(QRect(70, 60, 781, 591)) MainWindow.setCentralWidget(self.centralwidget) self.menubar = QMenuBar(MainWindow) self.menubar.setObjectName(u"menubar") self.menubar.setGeometry(QRect(0, 0, 942, 22)) MainWindow.setMenuBar(self.menubar) self.statusbar = QStatusBar(MainWindow) self.statusbar.setObjectName(u"statusbar") MainWindow.setStatusBar(self.statusbar) self.retranslateUi(MainWindow) QMetaObject.connectSlotsByName(MainWindow) # setupUi def retranslateUi(self, MainWindow): MainWindow.setWindowTitle(QCoreApplication.translate("MainWindow", u"MainWindow", None)) # retranslateUi class ComboBoxDelegate(QItemDelegate): def __init__(self, parent=None): super(ComboBoxDelegate, self).__init__(parent) self.items = ['one', 'two', 'three'] def setItems(self, items): self.items = items def createEditor(self, widget, option, index): editor = QComboBox(widget) editor.addItems(self.items) return editor def setEditorData(self, editor, index): value = index.model().data(index, Qt.EditRole) if value: editor.setCurrentIndex(int(value)) def setModelData(self, editor, model, index): model.setData(index, editor.currentIndex(), Qt.EditRole) def updateEditorGeometry(self, editor, option, index): editor.setGeometry(option.rect) def paint(self, painter, option, index): text = self.items[index.row()] option.text = text QtWidgets.QApplication.style().drawControl(QtWidgets.QStyle.CE_ItemViewItem, option, painter) class CheckBoxDelegate(QtWidgets.QItemDelegate): """ A delegate that places a fully functioning QCheckBox cell of the column to which it's applied. """ def __init__(self, parent=None): QtWidgets.QItemDelegate.__init__(self, parent) def createEditor(self, parent, option, index): """ Important, otherwise an editor is created if the user clicks in this cell. """ return None def paint(self, painter, option, index): """ Paint a checkbox without the label. """ self.drawCheck(painter, option, option.rect, QtCore.Qt.Unchecked if int(index.data()) == 0 else QtCore.Qt.Checked) def editorEvent(self, event, model, option, index): ''' Change the data in the model and the state of the checkbox if the user presses the left mousebutton and this cell is editable. Otherwise do nothing. ''' if not int(index.flags() & QtCore.Qt.ItemIsEditable) > 0: return False if event.type() == QtCore.QEvent.MouseButtonRelease and event.button() == QtCore.Qt.LeftButton: # Change the checkbox-state self.setModelData(None, model, index) return True return False def setModelData (self, editor, model, index): ''' The user wanted to change the old state in the opposite. ''' model.setData(index, 1 if int(index.data()) == 0 else 0, QtCore.Qt.EditRole) class ExampleTableModel(QAbstractTableModel): def __init__(self, data): super(ExampleTableModel, self).__init__() self._data = data def rowCount(self, parent=QModelIndex()): return len(self._data) def columnCount(self, parent=QModelIndex()): try: return len(self._data[0]) except: return 0 def data(self, index, role=Qt.DisplayRole): if index.isValid(): if role == Qt.DisplayRole: val = self._data[index.row()][index.column()] return str(val) return None def headerData(self, section, orientation, role=Qt.DisplayRole): if role == Qt.DisplayRole: if orientation == Qt.Horizontal: return f'Col {section}' if orientation == Qt.Vertical: return f'Row {section}' def setData(self, index, value, role=Qt.EditRole): if index.isValid(): if role==Qt.EditRole: self._data[index.row()][index.column()] = value self.dataChanged.emit(index, index) return True return False def flags(self, index): if index.isValid(): return Qt.ItemIsEnabled | Qt.ItemIsSelectable | Qt.ItemIsEditable return Qt.ItemIsEnabled def insertRows(self, row, count, parent=QModelIndex()): self.beginInsertRows(QModelIndex(), row, row + count - 1) for n in range(count): self._data.insert(row + n, [None] * len(self._data[0])) self.endInsertRows() return True def insertColumns(self, column, count, parent=QModelIndex()): self.beginInsertColumns(QModelIndex(), column, column + count - 1) for r in range(len(self._data)): for c in range(count): self._data[r].insert(column + c, None) self.endInsertColumns() return True def removeRows(self, row, count, parent=QModelIndex()): self.beginRemoveRows(QModelIndex(), row, row + count - 1) del self._data[row:row+count] self.endRemoveRows() return True def removeColumns(self, column, count, parent=QModelIndex()): self.beginRemoveColumns(QModelIndex(), column, column + count - 1) for r in range(len(self._data)): del self._data[r][column:column+count] self.endRemoveColumns() return True class MainWindow(QMainWindow): def __init__(self): super(MainWindow, self).__init__() self.ui = Ui_MainWindow() self.ui.setupUi(self) self.model = ExampleTableModel([['0, 0', 0, 'one'], ['1, 0', 1, 'two'], ['2, 0', 1, 'three']]) d1 = CheckBoxDelegate() d2 = ComboBoxDelegate() # HERE IS THE PROBLEM - I can comment either one of the two following lines and the script runs as expected. # but I cannot run both lines: self.ui.tableView.setItemDelegateForColumn(1, d1) self.ui.tableView.setItemDelegateForColumn(2, d2) self.ui.tableView.setModel(self.model) if __name__ == '__main__': app = QApplication() win = MainWindow() win.show() sys.exit(app.exec())
-
Hi,
self.d1 = CheckBoxDelegate() self.d2 = ComboBoxDelegate() self.ui.tableView.setItemDelegateForColumn(1, self.d1) self.ui.tableView.setItemDelegateForColumn(2, self.d2)
You also need to add:
from PySide6 import QtCore from PySide6.QtWidget import QComboBox
-
Thank you @SGaist , that works!
Can you elaborate on what's going on here that the delegates need to be initialized as part of the instance and why worked previously with just one delegate but not with two?
The examples I had been working with just called something like self.ui.tableView.setItemDelegateForColumn(1, CheckBoxDelegate()) for example. I just so happened to assign them to d1 and d2 when I was screwing with it.
-
@BamboozledBaboon
It's not that it needs to be "initialized". This is a Python issue. By assigning theself.d2 = ComboBoxDelegate()
to a member variable @SGaist is tying its lifetime to the instance. With only a local variable Python destroys it at the end of your constructor code.The key here is at PySide6.QtWidgets.QAbstractItemView.setItemDelegateForColumn(column, delegate)
QAbstractItemView
does not take ownership of delegate.Note that not. This means you are still responsible for keeping the delegate alive.
As for your "one delegate works but not two", I suspect you will find that is "coincidence". Maybe the second one made the problem show up more.
-
@JonB said in Using multiple item delegates in QTableView columns:
As for your "one delegate works but not two", I suspect you will find that is "coincidence". Maybe the second one made the problem show up more.
It's not quite so random though. I can run either subclass individually without problems, or I could even run one instance of a delegate in multiple columns. The issue was when I tried to run the second subclass or a second instance of the same subclass.
-
@BamboozledBaboon
This may be due to the vagaries of Python and when it actually destroys unreferenced objects. I believe it is still not correct with even a single instance used once. If you attach a slot tod1.destroyed.connect()
that should indicate when it gets destroyed.