Search code examples
python-2.7signalspyqt4qcheckbox

pyqt4 correctly connecting QCheckbox state


I have a class QWidget class containing a QLabel. The class generates QCheckbox'es in a QVBox. I am trying to connect each checkbox to the method nameCheckBox which will update the QLabel to display the title of the last checked box. However when a checkbox is effectively un/checked, it is always detected as Unchecked. Also the returned name is always the last created checkbox. I don't understand where my mistake is. Here is my code:

import sys
from PyQt4 import QtCore
from PyQt4.QtGui import *
from MenusAndToolbars import MenuWindow

class checkBoxWidget(QWidget):
    """
    This widget has a QVBox which contains a QLabel and QCheckboxes.
    Qcheckbox number is connected to the label.
    """

    def __init__(self):
        QWidget.__init__(self)
        self.__setUI()

    def __setUI(self):
        vbox = QVBoxLayout(self)
        label = QLabel('Last clicked button: ' + "None", self)

        vbox.addWidget(label)

        listCB = []

        for i in range(10):
            listCB.append( QCheckBox('CheckBox Nb. ' + str(i+1) ) )
            listCB[i].stateChanged.connect(lambda: self.nameCheckBox(label,  listCB[i]) )
            vbox.addWidget( listCB[i] )


    def nameCheckBox(self, label, checkBox):
        if checkBox.isChecked():
            print "Checked: " + checkBox.text()
            label.setText('Last clicked button: ' + checkBox.text())
        else:
            print "Unchecked: " + checkBox.text()



def main():
    app = QApplication(sys.argv)
    window = QMainWindow()
    window.setCentralWidget( checkBoxWidget() )
    window.show()
    #window = WidgetWindow()
    sys.exit(app.exec_())

if __name__ == "__main__":
    main()

EDIT 1

I found several "hack" solutions.

SOLUTION 1 : Creating a callBack function does the trick:

def callBack(self, list, index, label):
    return lambda: self.nameCheckBox(label, list[index])

Then I connect the QCheckbox().stateChanged signal this way:

listCB[i].stateChanged.connect( self.callBack(listCB, i, label) )

SOLUTION 2: using the partial module:

First we import the module:

from functools import partial

Then the signal connection is done this way:

listCB[i].stateChanged.connect( partial( self.nameCheckBox, label, listCB[i] ) )

However I would like to use lambda expression in one line. Especially I would like to understand how it works. Following links I understood the issue is about lambda scope. As Oleh Prypin advised me, I wrote:

listCB[i].stateChanged.connect(lambda i=i: self.nameCheckBox(label,  listCB[i]) )

Here the variable i is a new one. However my original issue remains. I then tried this out of curiosity:

listCB[i].stateChanged.connect( lambda label=label, listCB=listCB, i=i: self.nameCheckBox(label, listCB[i] ) )

But I get the following error:

Traceback (most recent call last):
Checked: CheckBox Nb. 2
  File "Widgets.py", line 48, in <lambda>
    listCB[i].stateChanged.connect( lambda label=label, listCB=listCB, i=i: self.nameCheckBox(label, listCB[i] ) )
  File "Widgets.py", line 59, in nameCheckBox
    label.setText('Last clicked button: ' + checkBox.text())
AttributeError: 'int' object has no attribute 'setText'

Here it seems the correct button is recognized when un/checked. However it seems the new label variable is seen as an int? What happens here?


Solution

  • lambda: self.nameCheckBox(label,  listCB[i])
    

    binds to the variable i, meaning the value of i will be the one at the moment the lambda is called, not when it's created, which is, in this case, always 9.
    Possible fix:

    lambda i=i: self.nameCheckBox(label, listCB[i])
    

    There is a lot of general information on this topic. Starting points: Google search, another question Creating lambda inside a loop.


    Unfortunately, my fix didn't work, because that signal provides an argument checked to the function it calls, overriding that default argument with a 0 or 2 depending on the check state. This will work (ignore the unwanted argument):

    lambda checked, i=i: self.nameCheckBox(label, listCB[i])
    

    And here is an alternative way to write that class:

    class CheckBoxWidget(QWidget):
        def __init__(self):
            QWidget.__init__(self)
            self.setupUi()
    
        def setupUi(self):
            vbox = QVBoxLayout(self)
            self.label = QLabel("Last clicked button: None")
    
            vbox.addWidget(self.label)
    
            for i in range(10):
                cb = QCheckBox("CheckBox Nb. " + str(i+1))
                cb.stateChanged.connect(self.nameCheckBox)
                vbox.addWidget(cb)
    
        def nameCheckBox(self, checked):
            checkBox = self.sender()
            if checked:
                print("Checked: " + checkBox.text())
                self.label.setText("Last clicked button: " + checkBox.text())
            else:
                print("Unchecked: " + checkBox.text())