3

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?

kaligne
  • 3,098
  • 9
  • 34
  • 60

1 Answers1

3
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())
Community
  • 1
  • 1
Oleh Prypin
  • 33,184
  • 10
  • 89
  • 99
  • Thanks I edited my post with a callback solution and the syntax that i should use like you wrote, however it still does not work, maybe there is also an issue with `listCB`'s scope? – kaligne Dec 24 '14 at 15:00
  • Thank you very much both methods work like a charm. Being new I tend to forget the `sender()` method which proves to be useful. I guess I systematically have to check values returned by signals in the documentation? – kaligne Dec 24 '14 at 15:47
  • This (```lambda checked, i=i: self.nameCheckBox(label, listCB[i])```) worked perfectly for me, thank you! – Nicholas Barrow May 03 '22 at 01:11