Search code examples
pythonpyqtpyqt5qbuttongroup

subclassing QGroupBox so that it can be member of QButtonGroup


QButtonGroups can have checkboxes. But you cannot add them to a QButtonGroup because they do not inherit QAbstractButton.

It would be really nice for some UIs to be able to have a few QGroupBoxes with exclusive checkboxes. That is, you check one and the other QGroupBoxes are automatically unchecked.

In an ideal world, I could do something like this:

import sys
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import (QGroupBox, QWidget, QApplication, 
                             QAbstractButton, QButtonGroup)

class SuperGroup(QGroupBox, QAbstractButton):
    def __init__(self, title, parent=None):
        super(SuperGroup, self).__init__(title, parent)
        self.setCheckable(True)
        self.setChecked(False)

class Example(QWidget):

    def __init__(self):
        super().__init__()

        sg1 = SuperGroup(title = 'Super Group 1', parent = self)
        sg1.resize(200,200)
        sg1.move(20,20)

        sg2 = SuperGroup(title = 'Super Group 2', parent = self)
        sg2.resize(200,200)
        sg2.move(300,20)

        self.bgrp = QButtonGroup()
        self.bgrp.addButton(sg1)
        self.bgrp.addButton(sg2)


        self.setGeometry(300, 300, 650, 500)
        self.setWindowTitle('SuperGroups!')
        self.show()



if __name__ == '__main__':

    app = QApplication(sys.argv)
    ex = Example()
    sys.exit(app.exec_())

This code fails as soon as you try to add a SuperGroup to the button group. PyQt5 explicitly does not support multiple inheritance. But there are some examples out in the wild, like from this blog.

In this simple example, it would be easy to manage the clicks programmatically. But as you add more group boxes, it gets more messy. Or what if you want a QButtonGroup with buttons, check boxes, and group boxes? Ugh.


Solution

  • It is not necessary to create a class that inherits from QGroupBox and QAbstractButton (plus it is not possible in pyqt or Qt/C++). The solution is to create a QObject that handles the states of the other QGroupBox when any QGroupBox is checked, and I implemented that for an old answer for Qt/C++ so this answer is just a translation:

    import sys
    from PyQt5.QtCore import pyqtSlot, QObject, Qt
    from PyQt5.QtWidgets import QGroupBox, QWidget, QApplication, QButtonGroup
    
    
    class GroupBoxManager(QObject):
        def __init__(self, parent=None):
            super().__init__(parent)
            self._groups = []
    
        @property
        def groups(self):
            return self._groups
    
        def add_group(self, group):
            if isinstance(group, QGroupBox):
                group.toggled.connect(self.on_toggled)
                self.groups.append(group)
    
        @pyqtSlot(bool)
        def on_toggled(self, state):
            group = self.sender()
            if state:
                for g in self.groups:
                    if g != group and g.isChecked():
                        g.blockSignals(True)
                        g.setChecked(False)
                        g.blockSignals(False)
    
            else:
                group.blockSignals(True)
                group.setChecked(False)
                group.blockSignals(False)
    
    
    class Example(QWidget):
        def __init__(self):
            super().__init__()
    
            sg1 = QGroupBox(
                title="Super Group 1", parent=self, checkable=True, checked=False
            )
            sg1.resize(200, 200)
            sg1.move(20, 20)
    
            sg2 = QGroupBox(
                title="Super Group 2", parent=self, checkable=True, checked=False
            )
            sg2.resize(200, 200)
            sg2.move(300, 20)
    
            self.bgrp = GroupBoxManager()
            self.bgrp.add_group(sg1)
            self.bgrp.add_group(sg2)
    
            self.setGeometry(300, 300, 650, 500)
            self.setWindowTitle("SuperGroups!")
            self.show()
    
    
    if __name__ == "__main__":
    
        app = QApplication(sys.argv)
        ex = Example()
        sys.exit(app.exec_())