Search code examples
pythonpyqt5python-3.7qradiobuttonqbuttongroup

Multiple Radio button groups are linking, causing strange behavior


My program generates forms on the fly based on SQL data. I make two radio buttons and a QLineEdit entry right next to them. When the radio button on the right is checked, the QLineEdit is enabled correctly. The problem comes from the radio buttons linking and causing exclusive actions between themselves. When the program starts, they look like this: Program start

Then when I click the first "No", it changes how I expect and enables the QLineEdit. Normal behavior

Now I want to click "No" for "Serial#: RC1" also. This is where the behavior starts to go wrong. The No button is clicked and deselects all the row above.

Deselects the top row

If I try to click "No" on the top row again, the "yes" on the second row deselects.

second row deselects

Finally, I can click the Selected radio buttons and deselect everything until I'm left with one active radio button. At this point, I cannot have any more selected buttons than just this one. Clicking on a deselected button will activate it and deselect the previously active button.

One selected Button Left

I generate the buttons on the fly from helper functions that put radio buttons in QButtonGroups. I thought this would be enough to stop this behavior, but I'm wrong. What i would like is the radio buttons on each row to no respond to the actions on other radio buttons on other rows.

# !/user/bin/env python
import os
import sys
from PyQt5 import uic
from PyQt5.QtWidgets import *
from PyQt5.QtGui import *


class Radio(QDialog):
    def __init__(self, app):
        super(Radio, self).__init__()
        self.bundle_dir = os.path.dirname(__file__)
        gui_path = os.path.join(self.bundle_dir, 'ui', 'radio_bt_test.ui')
        self.ui = uic.loadUi(gui_path, self)
        self.num_of_row = 4
        self.formLayout = self.findChild(QFormLayout, "formLayout")
        self.radio_bt_lineEdit_connections = dict()  # to help link the radio buttons and lineEdit
        self.add_rows()
        self.show()

    def add_rows(self):
        """
        Adds pairs of radio buttons with a lineEdit to each row of the form layout
        :return: 
        """
        for i in range(self.num_of_row):
            lbl = QLabel("Label#" + str(i))
            hbox = QHBoxLayout()
            buttons = self.new_radio_pair()
            entry = self.new_entry("Value if No")
            entry.setEnabled(False)
            self.radio_bt_lineEdit_connections[buttons[-1]] = entry
            # adding connection to dictionary for later event handling
            buttons[-1].toggled.connect(self.radio_bt_changed)
            for button in buttons:
                hbox.addWidget(button)
            hbox.addWidget(entry)
            self.formLayout.addRow(lbl, hbox)

    def new_radio_pair(self, texts=('Yes', 'No')) -> list:
        """
        Makes a pair of radio buttons in a button group for creating data entries in "Part" grouping on the fly
        :param texts: The texts that will go on the two buttons. The more texts that are added to make more radio buttons
        :return: A list with QRadioButtons that are all part of the same QButtonGroup
        """
        group = QButtonGroup()
        buttons = []
        for text in texts:
            bt = QRadioButton(text)
            bt.setFont(QFont('Roboto', 11))
            if text == texts[0]:
                bt.setChecked(True)
            group.addButton(bt)
            buttons.append(bt)

        return buttons

    def radio_bt_changed(self) -> None:
        """
        Helps the anonymous radio buttons link to the anonymous QLineEdits that are made for data fields
        :return: None
        """
        sender = self.sender()
        assert isinstance(sender, QRadioButton)
        lineEdit = self.radio_bt_lineEdit_connections[sender]
        assert isinstance(lineEdit, QLineEdit)
        if sender.isChecked():
            lineEdit.setEnabled(True)
        else:
            lineEdit.setEnabled(False)
            lineEdit.clear()

    def new_entry(self, placeholder_text: str = "") -> QLineEdit:
        """
        Makes a new QLineEdit object for creating data entries in "Part" grouping on the fly
        :return: A new QLineEdit with appropriate font and size policy
        """
        entry = QLineEdit()
        entry.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed)
        entry.setFont(QFont('Roboto', 11))
        entry.setStyleSheet("background-color: rgb(239, 241, 243);")
        # with style sheets, past anything in here between the css tags
        entry.setMaxLength(15)
        entry.setPlaceholderText(placeholder_text)
        return entry


def main():
    app = QApplication(sys.argv)
    radio = Radio(app)
    sys.exit(app.exec())

main()

Is it maybe because I declare the QButtonGroup then forget about them? Does a garbage collector come and erase them because I don't have a variable assigned or is there another problem that I'm missing.

The ui was designed on QtDesigner and is just a dialog box with a form layout on it.

<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>Dialog</class>
 <widget class="QDialog" name="Dialog">
  <property name="geometry">
   <rect>
    <x>0</x>
    <y>0</y>
    <width>491</width>
    <height>382</height>
   </rect>
  </property>
  <property name="windowTitle">
   <string>Dialog</string>
  </property>
  <property name="styleSheet">
   <string notr="true">background-color: rgb(219, 221, 223);</string>
  </property>
  <widget class="QWidget" name="formLayoutWidget">
   <property name="geometry">
    <rect>
     <x>80</x>
     <y>40</y>
     <width>301</width>
     <height>291</height>
    </rect>
   </property>
   <layout class="QFormLayout" name="formLayout"/>
  </widget>
 </widget>
 <resources/>
 <connections/>
</ui>


Solution

  • The object that should be used to make the row buttons exclusive is "group" but this is a local variable that is destroyed when the new_radio_pair method finishes executing, causing it not to behave as previously thought.

    The solution is to extend the life cycle and for this there are several options such as making it an attribute of the class, adding it to a container or class that has a longer life cycle, or in the case of QObjects, passing it another QObject as a parent(like self) that has a longer life cycle, and that is the best solution for this case:

    group = QButtonGroup(self)