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_()