pythonpyside2

Changing QGroupBox checkbox visual to an expander


I've modified the behavior of a QGroupBox's checkbox to hide/show the children of the group, effectively acting as an expander. It's working great, but the only issue is that the default checkbox icon looks like it's for enabling/disabling the group, rather than expanding it. I'd like to replace it with an expander-style icon instead.

I found this post which almost answers my question (and ultimately I may end up having to use that solution): Change PySide QGroupBox checkbox image. The problem is that the answer provided there suggests using custom checkbox images, whereas I want to use the built-in OS-specific expander, like the one in QTreeView, which looks like this on my PC: QTreeView expanders

Is this even possible? Are expanders considered a stylized checkbox or something else entirely? I'm fairly new to Qt so I'm not very familiar with how styles are handled. Is this something that I would be able to query for, and if so, would it even be compatible with QCheckBox styles? There isn't much I could find about expanders on the QTreeView documentation page aside from just enabling/disabling them, and I'm currently attempting to dig through the QTreeView.cpp source code to figure out how they work.

Update

Going off of eyllanesc's answer, I'm getting closer to a solution but I'm having some issues overriding methods of QProxyStyle. I'm attempting to do something like the following:

class GroupBoxExpanderStyle(QtWidgets.QProxyStyle):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self._replaceCheckboxWithExpander = False

    def drawComplexControl(self, control, option, painter, widget):
        try:
            if control == QtWidgets.QStyle.CC_GroupBox and widget.isCheckable():
                self._replaceCheckboxWithExpander = True
            super().drawComplexControl(control, option, painter, widget)
        finally:
            self._replaceCheckboxWithExpander = False

    def drawPrimitive(self, element, option, painter, widget):
        if element == QtWidgets.QStyle.PE_IndicatorCheckBox and self._replaceCheckboxWithExpander:
            indicatorBranchOption = ... # Set up options for drawing PE_IndicatorBranch
            super().drawPrimitive(QtWidgets.QStyle.PE_IndicatorBranch, indicatorBranchOption, painter, widget)
        else:
            super().drawPrimitive(element, option, painter, widget)

Something similar appears to have been done in this code. The issue I'm running into is that my overridden drawPrimitive() function isn't being called at all for QGroupBox widgets... but it is called for other widgets with that same proxy style applied! Looking in the drawComplexControl() function of qcommonstyle.cpp, the CC_GroupBox case is calling

proxy()->drawPrimitive(PE_IndicatorCheckBox, &box, p, widget);

to draw the checkbox, so I don't understand why my overridden function isn't running.

I'm not really sure how to go about debugging this since I can't step into the C++ code to see what is actually being called. Could anyone offer any suggestions that might help me figure out why my overridden drawPrimitive() isn't getting called?

Update 2:

I've solved the mystery of why my overridden drawPrimitive() isn't getting called - it's because my application uses a root-level style sheet, which causes the QStyleSheetStyle to be used as the active style for widgets. QStyleSheetStyle directly calls its own drawPrimitive() method for CC_GroupBox rather than calling proxy()->drawPrimitive() - this seems like a bug to me, and in fact this Qt bug states that style sheets don't mix well with QProxyStyle. I'm going to try to move away from using style sheets.

eyllanesc's technique works with the Fusion style so I have accepted his answer, but it is incompatible with other styles.


Solution

  • One possible solution is to implement a QProxyStyle:

    from PySide2 import QtCore, QtGui, QtWidgets
    
    
    class GroupBoxProxyStyle(QtWidgets.QProxyStyle):
        def subControlRect(self, control, option, subControl, widget):
            ret = super(GroupBoxProxyStyle, self).subControlRect(
                control, option, subControl, widget
            )
            if (
                control == QtWidgets.QStyle.CC_GroupBox
                and subControl == QtWidgets.QStyle.SC_GroupBoxLabel
                and widget.isCheckable()
            ):
                r = self.subControlRect(
                    QtWidgets.QStyle.CC_GroupBox,
                    option,
                    QtWidgets.QStyle.SC_GroupBoxCheckBox,
                    widget,
                )
                ret.adjust(r.width(), 0, 0, 0)
            return ret
    
        def drawComplexControl(self, control, option, painter, widget):
            is_group_box = False
            if control == QtWidgets.QStyle.CC_GroupBox and widget.isCheckable():
                option.subControls &= ~QtWidgets.QStyle.SC_GroupBoxCheckBox
                is_group_box = True
            super(GroupBoxProxyStyle, self).drawComplexControl(
                control, option, painter, widget
            )
            if is_group_box and widget.isCheckable():
                opt = QtWidgets.QStyleOptionViewItem()
                opt.rect = self.proxy().subControlRect(
                    QtWidgets.QStyle.CC_GroupBox,
                    option,
                    QtWidgets.QStyle.SC_GroupBoxCheckBox,
                    widget,
                )
                opt.state = QtWidgets.QStyle.State_Children
                opt.state |= (
                    QtWidgets.QStyle.State_Open
                    if widget.isChecked()
                    else QtWidgets.QStyle.State_None
                )
                self.drawPrimitive(
                    QtWidgets.QStyle.PE_IndicatorBranch, opt, painter, widget
                )
    
    
    if __name__ == "__main__":
        import sys
    
        app = QtWidgets.QApplication(sys.argv)
        style = GroupBoxProxyStyle(app.style())
        app.setStyle(style)
    
        w = QtWidgets.QGroupBox(title="Exclusive Radio Buttons")
        w.setCheckable(True)
        vbox = QtWidgets.QVBoxLayout()
        for text in ("Radio button 1", "Radio button 2", "Radio button 3"):
            radiobutton = QtWidgets.QRadioButton(text)
            vbox.addWidget(radiobutton)
        vbox.addStretch(1)
        w.setLayout(vbox)
    
        w.resize(320, 240)
        w.show()
        sys.exit(app.exec_())
    

    enter image description here

    enter image description here

    Update:

    The conversion of the code provided by the OP to Python is as follows:

    from PySide2 import QtCore, QtGui, QtWidgets
    
    
    class GroupBoxProxyStyle(QtWidgets.QProxyStyle):
        def drawPrimitive(self, element, option, painter, widget):
            if element == QtWidgets.QStyle.PE_IndicatorCheckBox and isinstance(
                widget, QtWidgets.QGroupBox
            ):
                super().drawPrimitive(
                    QtWidgets.QStyle.PE_IndicatorArrowDown
                    if widget.isChecked()
                    else QtWidgets.QStyle.PE_IndicatorArrowRight,
                    option,
                    painter,
                    widget,
                )
            else:
                super().drawPrimitive(element, option, painter, widget)
    
    
    if __name__ == "__main__":
        import sys
    
        app = QtWidgets.QApplication(sys.argv)
        style = GroupBoxProxyStyle(app.style())
        app.setStyle(style)
    
        w = QtWidgets.QGroupBox(title="Exclusive Radio Buttons")
        w.setCheckable(True)
        vbox = QtWidgets.QVBoxLayout()
        for text in ("Radio button 1", "Radio button 2", "Radio button 3"):
            radiobutton = QtWidgets.QRadioButton(text)
            vbox.addWidget(radiobutton)
        vbox.addStretch(1)
        w.setLayout(vbox)
    
        w.resize(320, 240)
        w.show()
        sys.exit(app.exec_())