Search code examples
pythonpyqt5qcheckboxqgroupbox

PyQt5 QGroupBox with QCheckBox - dismiss auto disable


I want to use a QGroupBox which will be checkable, but i don't the content widgets of QGroupBox to be disabled when the QGroupBox checkbox is Unchecked.

From reference:

checked : bool This property holds whether the group box is checked

If the group box is checkable, it is displayed with a check box. If the check box is checked, the group box's children are enabled; otherwise, the children are disabled and are inaccessible to the user.

By default, checkable group boxes are also checked.

I want to have a checkbox in QGroupBox title bar, but i don't want the above feature to be applied.

The checkbox logic will be select-all, select-none, so when the checkbox is unselected the user can modify inner QGroupBox checkbox elements.

I want to keep an interface-ui like the following:

Maybe i have to use a QFrame with QPaintEvent or QSS stylesheet, but i am not expert with this.

enter image description here

Edit: I also want triState for QGroupBox checkbox if possible.

Edit 2: I try this code:

self.main_self.ui_visible_player_list_fields_window.groupBox.changeEvent(QtCore.QEvent.EnabledChange).connect(lambda event:event.ignore())

but it has errors.

 self.main_self.ui_visible_player_list_fields_window.groupBox.changeEvent(QtCore.QEvent.EnabledChange).connect(lambda event:event.ignore())
TypeError: changeEvent(self, QEvent): argument 1 has unexpected type 'Type'

Edit: I think the following code will solve the problem:

class Custom_QGroupBox(QtWidgets.QGroupBox):

    def __init__(self, parent=None):
        super(Custom_QGroupBox, self).__init__(parent)

    def changeEvent(self, event):
        if event.type() == QtCore.QEvent.EnabledChange:
            self.blockSignals(True)
            self.setEnabled(True)
            event.ignore()
            self.blockSignals(False)
        else:
            return super(Custom_QGroupBox, self).changeEvent(event)

but it doesn't :(


Solution

  • Premise: probably not a good idea

    There are known conventions for UI elements, users are used to them and expect that doing an "action" on a well known element type would cause an also known result.
    While a group box is more of a fringe case, the default behavior of Qt follows the convention: toggling the checkbox results in toggling the enabled state of its contents.

    Since UI elements should always try to follow conventions and make the possible outcome of an user action as much predictable as possible, a better solution would be to add a "top level" group of buttons that would set all boxes as checked or unchecked ("Check all" and "Check none").

    Why doesn't it work?

    First of all, a changeEvent is not a signal, so you cannot try to "connect" it (always look for the function type in the Qt documentation, if it's a signal, it will have a [signal] notation besides its definition).
    It is an event handler, and it expects an event instance as argument, not a type.

    Then, toggling the checkbox of a group box changes the state of its children, not that of the group box, so you'll never receive a EnabledChange event when toggling it.
    That's quite obvious if you think about it: if the whole group box gets disabled when toggling its check box, you can never click it again to enable it, as input events are ignored by default for disabled widgets, and both the title and checkbox would be shown as disabled too.

    Possible solution

    There are various possible solutions, including subclassing QFrame and draw the checkbox and title, but making it compliant with the current style would be very (and unnecessarily) difficult.
    The best choice is usually the one that does less changes to the default behavior.

    In this case, my suggestion is to do two things:

    • connect to the toggled signal of the group box and override what the default behavior does (restore the enabled state unless explicitly set for that widget);
    • override the paintEvent and change the QStyleOptionGroupBox state used for the style and painter before actually drawing it;

    Since the state is internally defined for the group box, we need to override its value(s): initStyleOption() adds the option's state flag State_On when the checkbox is (theoretically) checked, or State_Off when unchecked (so, whether the child widgets are enabled or not), but we have to set the option state based on the checkbox states instead. In order to do that, we have to check all check boxes and verify whether all of them are checked, any of them is checked, or none is.

    from PyQt5 import QtCore, QtWidgets
    
    class Custom_QGroupBox(QtWidgets.QGroupBox):
        checkAllIfAny = True
        def __init__(self, *args, **kwargs):
            super(Custom_QGroupBox, self).__init__(*args, **kwargs)
            self.setCheckable(True)
            self.checkBoxes = []
            self.toggled.connect(self.toggleCheckBoxes)
    
        def addCheckBox(self, cb):
            self.checkBoxes.append(cb)
            cb.toggled.connect(self.update)
            cb.destroyed.connect(lambda: self.removeCheckBox(cb))
    
        def removeCheckBox(self, cb):
            try:
                self.checkBoxes.remove(cb)
                cb.toggled.disconnect(self.update)
            except:
                pass
    
        def allStates(self):
            return [cb.isChecked() for cb in self.checkBoxes]
    
        def toggleCheckBoxes(self):
            if self.checkAllIfAny:
                state = not all(self.allStates())
            else:
                state = not any(self.allStates())
    
            for widget in self.children():
                if not widget.isWidgetType():
                    continue
                if not widget.testAttribute(QtCore.Qt.WA_ForceDisabled):
                    # restore the enabled state in order to override the default
                    # behavior of setChecked(False); previous explicit calls for
                    # setEnabled(False) on the target widget will be ignored
                    widget.setEnabled(True)
                    if widget in self.checkBoxes:
                        widget.setChecked(state)
    
        def paintEvent(self, event):
            opt = QtWidgets.QStyleOptionGroupBox()
            self.initStyleOption(opt)
            states = self.allStates()
            if all(states):
                # force the "checked" state
                opt.state |= QtWidgets.QStyle.State_On
                opt.state &= ~QtWidgets.QStyle.State_Off
            else:
                # force the "not checked" state
                opt.state &= ~QtWidgets.QStyle.State_On
                if any(states):
                    # force the "not unchecked" state and set the tristate mode
                    opt.state &= ~QtWidgets.QStyle.State_Off
                    opt.state |= QtWidgets.QStyle.State_NoChange
                else:
                    # force the "unchecked" state
                    opt.state |= QtWidgets.QStyle.State_Off
            painter = QtWidgets.QStylePainter(self)
            painter.drawComplexControl(QtWidgets.QStyle.CC_GroupBox, opt)
    
    
    app = QtWidgets.QApplication([])
    groupBox = Custom_QGroupBox('Group options')
    
    layout = QtWidgets.QGridLayout(groupBox)
    o = 0
    for c in range(2):
        for r in range(4):
            o += 1
            cb = QtWidgets.QCheckBox('Option {}'.format(o))
            groupBox.addCheckBox(cb)
            layout.addWidget(cb, r, c)
    
    groupBox.show()
    app.exec_()