-2

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 :(

Chris P
  • 2,059
  • 4
  • 34
  • 68

2 Answers2

0

I add this code:

self.main_self.ui_visible_player_list_fields_window.groupBox.blockSignals(True)
        self.main_self.ui_visible_player_list_fields_window.groupBox.setEnabled(True)
        
        for child_widget in self.main_self.ui_visible_player_list_fields_window.groupBox.children():
            try:
                child_widget.setEnabled(True)
            except:
                pass
        self.main_self.ui_visible_player_list_fields_window.groupBox.blockSignals(False)

in toggle method and is child_widget_checkbox.stateChanged.connect method and it works.

The only thing is remaining to fully answer the question is the tristate of QGroupBox.

Chris P
  • 2,059
  • 4
  • 34
  • 68
0

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_()
musicamante
  • 41,230
  • 6
  • 33
  • 58