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()
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?
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())