QStyledItemDelegate QSS implementation (PySide2)
-
Hello,
I've been struggling to get this working correctly in PySide2, but it feels like I'm 90% of the way there.
There's a few issues that I'm unable to find enough resources on to solve by myself; I would really appreciate any help from more experienced users.Thanks
Goal
I've created a combobox delegate, which works as expected.
I also want to draw the appearance of a QComboBox (including the associated QSS) when the user hovers over an item.
On hover, the delegate should look identical to a QComboBox.Problem
After some trial and error, I've managed to figure out how to achieve this, but unfortunately I needed to create an instance of QComboBox and it's also missing foreground/text color.
Questions
- How do I draw the combobox (with its QSS), without creating an instance of the QComboBox within the delegate? (see comment "Q1." in example).
- How do I apply the foreground/text color? (see comment "Q2." in example).
- Looking at the source for QStyledItemDelegate::paint, is there an equivalent of
QStyledItemDelegatePrivate::widget(option)
in PySide2? - Is there a better way to achieve what I'm attempting here? (draw combobox on hover)
Working snippet attached below.Example
The last column has the delegate assigned to it.
When the user hovers over an item, it should appear the same as the QComboBox I've included above.
However, as you can see it's currently missing the foreground/text color, but everything else appears to be working fine.Snippet
import sys from PySide2 import QtCore, QtGui, QtWidgets QSS = """ QComboBox { background: rgb(125, 125, 125); color: cyan; border: 2px solid transparent; } QComboBox QAbstractItemView { background: rgb(125, 125, 125); border: 2px solid cyan; } """ TEST_DATA = ["AAA", "BBB", "CCC", "DDD"] class ExampleComboBoxDelegate(QtWidgets.QStyledItemDelegate): def __init__(self, parent: QtWidgets.QAbstractItemView): super().__init__(parent=parent) self.parent().setMouseTracking(True) def createEditor(self, parent, option, index) -> QtWidgets.QWidget: editor = QtWidgets.QComboBox(parent=parent) editor.addItems(TEST_DATA) return editor def setEditorData(self, editor, index): data = str(index.data(QtCore.Qt.DisplayRole)) item_index = editor.findText(data) if item_index >= 0: editor.setCurrentIndex(item_index) def setModelData(self, editor: QtWidgets.QWidget, model, index): data = editor.currentText() model.setData(index, data) # No QSS. def paint(self, painter, option, index): painter.save() hover_opt = QtWidgets.QStyleOptionComboBox() hover_opt.rect = option.rect hover_opt.currentText = str(index.data(QtCore.Qt.DisplayRole)) self.initStyleOption(option, index) # On hover, draw QComboBox. if option.state & QtWidgets.QStyle.State_MouseOver: style = option.widget.style() if option.widget else QtWidgets.QApplication.style() style.drawComplexControl(QtWidgets.QStyle.CC_ComboBox, hover_opt, painter) painter.restore() # With QSS. def paint(self, painter, option, index): # Q1. How do I completely eliminate this step? cbox = QtWidgets.QComboBox(parent=self.parent()) cbox.blockSignals(True) cbox.setVisible(False) # Paint. painter.save() self.initStyleOption(option, index) hover_opt = QtWidgets.QStyleOptionComboBox() hover_opt.initFrom(cbox) hover_opt.rect = option.rect hover_opt.currentText = str(index.data(QtCore.Qt.DisplayRole)) style = cbox.style() or QtWidgets.QApplication.style() # On hover, draw QComboBox with QSS. if option.state & QtWidgets.QStyle.State_MouseOver: style.drawComplexControl(QtWidgets.QStyle.CC_ComboBox, hover_opt, painter, cbox) # Draw text. # Q2. How do I apply the foreground/text color for this step from the QSS? style.drawControl(QtWidgets.QStyle.CE_ComboBoxLabel, hover_opt, painter, cbox) painter.restore() class Example(QtWidgets.QWidget): def __init__(self, parent: QtWidgets.QWidget = None): super().__init__(parent=parent) self.setStyleSheet(QSS) self.setMinimumSize(500, 500) self.__setup() self.__populate() def __setup(self): # Base. main_layout = QtWidgets.QVBoxLayout() self.setLayout(main_layout) # Styled combobox. lyt = QtWidgets.QHBoxLayout() self.__lbl = QtWidgets.QLabel("This is what the delegate should look like: ", self) self.__cbox = QtWidgets.QComboBox(self) self.__cbox.addItems(TEST_DATA) lyt.addWidget(self.__lbl) lyt.addWidget(self.__cbox) main_layout.addLayout(lyt) # Table. self.__model = QtGui.QStandardItemModel() self.__table = QtWidgets.QTableView(self) self.__table.setModel(self.__model) main_layout.addWidget(self.__table) def __populate(self): # Populate with test data. for index in range(0, 10): items = [] for item_data in TEST_DATA: item = QtGui.QStandardItem(item_data) items.append(item) self.__model.appendRow(items) # Delegate. self.__table.setItemDelegateForColumn(3, ExampleComboBoxDelegate(parent=self.__table)) if __name__ == '__main__': app = QtWidgets.QApplication(sys.argv) window = Example() window.show() sys.exit(app.exec_())
-
Leaving my working solution here.
How do I draw the combobox (with its QSS), without creating an instance of the QComboBox within the delegate? (see comment "Q1." in example).
This remains unresolved. It's not a big deal.
How do I apply the foreground/text color? (see comment "Q2." in example).
I apparently created this "bug" in my example, it did not exist in my working code and is no longer an issue.
If the dummy widget is created inpaint
, Qt ignores the foreground/text palette.
Moving it to__init__
resolves the issue.This doesn't make any sense to me, but it is what it is. 🤷♂️
Solution
My cursor is hovering over row 3, col 4.
Respects the item's background/foreground roles as well:
Snippet
import sys from PySide2 import QtCore, QtGui, QtWidgets QSS = """ QComboBox { background: rgb(125, 125, 125); color: cyan; border: 2px solid transparent; } QComboBox QAbstractItemView { background: rgb(125, 125, 125); border: 2px solid cyan; } """ TEST_DATA = ["AAA", "BBB", "CCC", "DDD"] class ExampleComboBoxDelegate(QtWidgets.QStyledItemDelegate): def __init__(self, parent: QtWidgets.QAbstractItemView): super().__init__(parent=parent) self.parent().setMouseTracking(True) # Q1. How do I completely eliminate this step? self.__proxy_cbox = QtWidgets.QComboBox(parent=self.parent()) self.__proxy_cbox.blockSignals(True) self.__proxy_cbox.setVisible(False) def createEditor(self, parent, option, index) -> QtWidgets.QWidget: editor = QtWidgets.QComboBox(parent=parent) editor.addItems(TEST_DATA) return editor def setEditorData(self, editor, index): data = str(index.data(QtCore.Qt.DisplayRole)) item_index = editor.findText(data) if item_index >= 0: editor.setCurrentIndex(item_index) def setModelData(self, editor: QtWidgets.QWidget, model, index): data = editor.currentText() model.setData(index, data) # With QSS. def paint(self, painter, option, index): # Paint. painter.save() self.initStyleOption(option, index) widget_option = QtWidgets.QStyleOptionComboBox() widget_option.initFrom(self.__proxy_cbox) # Inherit style from QComboBox. widget_option.rect = option.rect widget_option.currentText = str(index.data(QtCore.Qt.DisplayRole)) style = self.__proxy_cbox.style() or QtWidgets.QApplication.style() # Hover: Draw QComboBox control without widget implementation. if option.state & QtWidgets.QStyle.State_MouseOver: style.drawComplexControl(QtWidgets.QStyle.CC_ComboBox, widget_option, painter, self.__proxy_cbox) style.drawControl(QtWidgets.QStyle.CE_ComboBoxLabel, widget_option, painter, self.__proxy_cbox) # Draw standard control. else: style.drawControl(QtWidgets.QStyle.CE_ItemViewItem, option, painter, self.__proxy_cbox) painter.restore() class Example(QtWidgets.QWidget): def __init__(self, parent: QtWidgets.QWidget = None): super().__init__(parent=parent) self.setStyleSheet(QSS) self.setMinimumSize(500, 500) self.__setup() self.__populate() def __setup(self): # Base. main_layout = QtWidgets.QVBoxLayout() self.setLayout(main_layout) # Styled combobox. lyt = QtWidgets.QHBoxLayout() self.__lbl = QtWidgets.QLabel("This is what the delegate should look like: ", self) self.__cbox = QtWidgets.QComboBox(self) self.__cbox.addItems(TEST_DATA) lyt.addWidget(self.__lbl) lyt.addWidget(self.__cbox) main_layout.addLayout(lyt) # Table. self.__model = QtGui.QStandardItemModel() self.__table = QtWidgets.QTableView(self) self.__table.setModel(self.__model) main_layout.addWidget(self.__table) def __populate(self): # Populate with test data. for index in range(0, 10): items = [] for item_data in TEST_DATA: item = QtGui.QStandardItem(item_data) # item.setBackground(QtGui.QColor(255, 0, 0)) items.append(item) self.__model.appendRow(items) # Delegate. self.__table.setItemDelegateForColumn(3, ExampleComboBoxDelegate(parent=self.__table)) if __name__ == '__main__': app = QtWidgets.QApplication(sys.argv) window = Example() window.show() sys.exit(app.exec_())
Thanks for the help @JonB
-
@QtLand
I don't know the answers to a lot of this, but I think I do to two:Looking at the source for QStyledItemDelegate::paint, is there an equivalent of
QStyledItemDelegatePrivate::widget(option)
in PySide2?No, because anything with
...Private
is internal and not exposed to you/the outside world from the C++ definition. PySide is not going to allow you to access it.How do I draw the combobox (with its QSS), without creating an instance of the QComboBox within the delegate? (see comment "Q1." in example).
I don't know whether you have to do it this way, but I think your implementation will cause headaches:
cbox = QtWidgets.QComboBox(parent=self.parent())
I would not want to specify any parent here. Do you have to in order to make something work? By giving it a parent you are making it a "real, visible" combobox in the hierarchy. That's probably why you have to
blockSignals(True)
andsetVisible(False)
. Have you tried justcbox = QtWidgets.QComboBox()
here? Also, unless I am missing something, your implementation leaves an (invisible) combobox attached to the parent forever? -
HI @JonB
@JonB said in QStyledItemDelegate QSS implementation (PySide2):
No, because anything with ...Private is internal and not exposed to you/the outside world from the C++ definition. PySide is not going to allow you to access it.
Yeah I figured as much; I was curious about what
::widget
even refers to, but I wasn't able to find any implementation of it in the source code for QStyledItemDelegate or QAbstractItemDelegate.I don't know whether you have to do it this way, but I think your implementation will cause headaches:
Yes, while my implementation works, I agree it isn't ideal, which is why I'm asking if there's a way to avoid doing it this way.
Problem is I can't figure out how to inherit the QSS for QComboBox without actually creating an instance of one.I would not want to specify any parent here. Do you have to in order to make something work?
By not providing the parent, it won't inherit the QSS from a top-level widget, such as a global stylesheet applied to the QMainWindow or QDialog.
The QComboBox will have default styling instead of the one I've defined. -
@QtLand said in QStyledItemDelegate QSS implementation (PySide2):
By not providing the parent, it won't inherit the QSS from a top-level widget, such as a global stylesheet applied to the QMainWindow or QDialog.
Ahhhh, I see.
I don't think having to create a "dummy" widget to access its properties is necessarily bad, I have done it myself and seen others do it. I do think you should check whether I am right that your implementation leaves the combobox in permanent existence, though.
-
@JonB said in QStyledItemDelegate QSS implementation (PySide2):
I don't think having to create a "dummy" widget to access its properties is necessarily bad, I have done it myself and seen others do it. I do think you should check whether I am right that your implementation leaves the combobox in permanent existence, though.
That's a fair point; I significantly simplified this example so I missed that detail.
In my working code, I'm creating the dummy widget in the__init__
of the delegate, rather than withinpaint
implementation.
So while it does create a permanent invisibleQComboBox
, only one instance exists, rather than multiple premanent instances being created every timepaint
is called, as in my example code. -
@QtLand
I think https://stackoverflow.com/questions/21516991/qt-qss-and-drawcomplexcontrol from 10 years ago is going down much the same route as you have!Did you see the accepted solution:
I think that the only way is to grab widget with
QPixmap::grabWidget()
. And to use this image in delegate. Seems, that it is not possible to do because of QSS limitation? Seems scary, but interesting. Did you try it, I'm not sure whether it's indicating that should work or does not?
And see the discussion in https://stackoverflow.com/questions/19138100/how-to-draw-control-with-qstyle-and-with-specified-qss. And our own @SGaist in https://forum.qt.io/topic/37291/qss-and-drawcomplexcontrol here a mere decade ago.
-
@JonB said in QStyledItemDelegate QSS implementation (PySide2):
? Seems scary, but interesting. Did you try it, I'm not sure whether it's indicating that should work or does not?
This implementation has a few problems:
- It doesn't render the text at all, only the control.
- A painted pixmap would simply stretch the image when the width/height changes of the column/row, since the widget it was drawn from has a different size. You'd have to force the dummy widget to scale with the delegate occupied column and the associated row.
I decided to test it out anyways:
def paint(self, painter, option, index): cbox = QtWidgets.QComboBox(parent=self.parent()) cbox.blockSignals(True) cbox.setVisible(False) pxm = cbox.grab() # QPixmap.grabWidget() is deprecated. painter.save() if option.state & QtWidgets.QStyle.State_MouseOver: painter.drawPixmap(option.rect, pxm) painter.restore()
Resized the column:
-
How do I draw the combobox (with its QSS), without creating an instance of the QComboBox within the delegate? (see comment "Q1." in example).
Even if there's no better solution for my first question, I can live with the dummy widget.
How do I apply the foreground/text color? (see comment "Q2." in example).
However, this has to be possible somehow, since my example implementation is already taking into account the QSS for everything (seemingly) besides the foreground/text of the control.
Why must Qt be so difficult at times? 😭
-
Leaving my working solution here.
How do I draw the combobox (with its QSS), without creating an instance of the QComboBox within the delegate? (see comment "Q1." in example).
This remains unresolved. It's not a big deal.
How do I apply the foreground/text color? (see comment "Q2." in example).
I apparently created this "bug" in my example, it did not exist in my working code and is no longer an issue.
If the dummy widget is created inpaint
, Qt ignores the foreground/text palette.
Moving it to__init__
resolves the issue.This doesn't make any sense to me, but it is what it is. 🤷♂️
Solution
My cursor is hovering over row 3, col 4.
Respects the item's background/foreground roles as well:
Snippet
import sys from PySide2 import QtCore, QtGui, QtWidgets QSS = """ QComboBox { background: rgb(125, 125, 125); color: cyan; border: 2px solid transparent; } QComboBox QAbstractItemView { background: rgb(125, 125, 125); border: 2px solid cyan; } """ TEST_DATA = ["AAA", "BBB", "CCC", "DDD"] class ExampleComboBoxDelegate(QtWidgets.QStyledItemDelegate): def __init__(self, parent: QtWidgets.QAbstractItemView): super().__init__(parent=parent) self.parent().setMouseTracking(True) # Q1. How do I completely eliminate this step? self.__proxy_cbox = QtWidgets.QComboBox(parent=self.parent()) self.__proxy_cbox.blockSignals(True) self.__proxy_cbox.setVisible(False) def createEditor(self, parent, option, index) -> QtWidgets.QWidget: editor = QtWidgets.QComboBox(parent=parent) editor.addItems(TEST_DATA) return editor def setEditorData(self, editor, index): data = str(index.data(QtCore.Qt.DisplayRole)) item_index = editor.findText(data) if item_index >= 0: editor.setCurrentIndex(item_index) def setModelData(self, editor: QtWidgets.QWidget, model, index): data = editor.currentText() model.setData(index, data) # With QSS. def paint(self, painter, option, index): # Paint. painter.save() self.initStyleOption(option, index) widget_option = QtWidgets.QStyleOptionComboBox() widget_option.initFrom(self.__proxy_cbox) # Inherit style from QComboBox. widget_option.rect = option.rect widget_option.currentText = str(index.data(QtCore.Qt.DisplayRole)) style = self.__proxy_cbox.style() or QtWidgets.QApplication.style() # Hover: Draw QComboBox control without widget implementation. if option.state & QtWidgets.QStyle.State_MouseOver: style.drawComplexControl(QtWidgets.QStyle.CC_ComboBox, widget_option, painter, self.__proxy_cbox) style.drawControl(QtWidgets.QStyle.CE_ComboBoxLabel, widget_option, painter, self.__proxy_cbox) # Draw standard control. else: style.drawControl(QtWidgets.QStyle.CE_ItemViewItem, option, painter, self.__proxy_cbox) painter.restore() class Example(QtWidgets.QWidget): def __init__(self, parent: QtWidgets.QWidget = None): super().__init__(parent=parent) self.setStyleSheet(QSS) self.setMinimumSize(500, 500) self.__setup() self.__populate() def __setup(self): # Base. main_layout = QtWidgets.QVBoxLayout() self.setLayout(main_layout) # Styled combobox. lyt = QtWidgets.QHBoxLayout() self.__lbl = QtWidgets.QLabel("This is what the delegate should look like: ", self) self.__cbox = QtWidgets.QComboBox(self) self.__cbox.addItems(TEST_DATA) lyt.addWidget(self.__lbl) lyt.addWidget(self.__cbox) main_layout.addLayout(lyt) # Table. self.__model = QtGui.QStandardItemModel() self.__table = QtWidgets.QTableView(self) self.__table.setModel(self.__model) main_layout.addWidget(self.__table) def __populate(self): # Populate with test data. for index in range(0, 10): items = [] for item_data in TEST_DATA: item = QtGui.QStandardItem(item_data) # item.setBackground(QtGui.QColor(255, 0, 0)) items.append(item) self.__model.appendRow(items) # Delegate. self.__table.setItemDelegateForColumn(3, ExampleComboBoxDelegate(parent=self.__table)) if __name__ == '__main__': app = QtWidgets.QApplication(sys.argv) window = Example() window.show() sys.exit(app.exec_())
Thanks for the help @JonB
-
-
-