1

I am working on a Python application that plots data from a large file containing records from lots and lots of sources. One of the options I am trying to give the user is the option to only plot for a subset of these sources if so desired. I accomplish this by first reading the files, finding out how many unique things there are, and then creating a QCheckBox() for each, named after its source (each source has a unique name). In this particular case, the data file is parsed into a giant dictionary where the keys are the unique source. I want to connect to the stateChange() event for each checkbox and then disable plotting for that source when the box is unchecked. Which in this case would be adding/removing the source from a list of sources when the box is checked/unchecked. The problem I am running into is that all of my checkboxes end up connecting to the final source in my list.

Initially, the window that is created looks correct, each button is named appropriately. Every time a button gets pressed, the btnstate() is supposed to simply print the text associated with that button. The method works if you can explicitly define each button, as shown by the radio buttons in the example. If you click either, you will get the correct name of the button printed, but when unchecking/rechecking ANY of the check boxes, btnstate prints "test4".

What am I doing wrong?

Here is the code (sources changed to dummy values):

import sys
from PyQt4.QtGui import *

def btnstate(b):
    print b.text()

def main():
   app = QApplication([])
   widget = QWidget()
   layout = QVBoxLayout()
   widget.setLayout(layout)
   radio_layout = QHBoxLayout()
   checkbox_layout = QHBoxLayout()

   #setup radio buttons for config pop-up window
   r1 = QRadioButton("Page Count")
   r2 = QRadioButton("Date")
   r1.toggled.connect(lambda:btnstate(r1))
   r2.toggled.connect(lambda:btnstate(r2))
   radio_layout.addWidget(r1)
   radio_layout.addWidget(r2)

   cbs = []
   for idx, serial in enumerate(["test1", "test2", "test3", "test4"]):
      temp = QCheckBox(serial)
      temp.setText(serial)
      temp.setChecked(True)
      checkbox_layout.addWidget(temp)
      temp.stateChanged.connect(lambda:btnstate(temp))
      cbs.append(temp)

  layout.addLayout(radio_layout)
  layout.addLayout(checkbox_layout) 
  widget.show()
  sys.exit(app.exec_())


if __name__ == '__main__':
  main()
eyllanesc
  • 235,170
  • 19
  • 170
  • 241

1 Answers1

0

I think the reason that this is happening has to do with how Python binds and unbinds references inside the loop. Because temp is being redefined on each iteration, the slot is also being updated, so that it effectively calls the same lambda for every button. That's sort of hand-wavy, because my understanding of the details of Python referencing is not super deep. But I know that the Python bindings to Qt have a lot of problems with Python's referencing and garbage collection, e.g. when deleting widgets, because the Qt object hierarchy doesn't totally work with Python.

Anyway, more practically, there is a pretty easy fix. You can use the functools.partial method to define a partial function as the slot, rather than a lambda. Bind the button as the first object, leaving the button state (emitted as the signal argument) unbound. Like so:

import functools

def btnstate(button, state):
    print button.text()

Then in the loop:

for idx, serial in enumerate(['test1', 'test2', 'test3', 'test4']):
    temp = QCheckBox(serial)
    checkbox_layout.addWidget(temp)
    temp.stateChanged.connect(functools.partial(btnstate, serial))

Running this, I now get the correct labels printed when each box is check/unchecked.

Edit:

See this post for another example of Python's reference counting interacting with strange ways with Qt's object hierarchy.

Community
  • 1
  • 1
bnaecker
  • 6,152
  • 1
  • 20
  • 33
  • Each `lambda` is different, but they all see the same `temp` variable, which is simply bound to the last checkbox created in the loop. An equivalent solution to `partial` is to cache the current variable as a default argument: `lambda state, temp=temp: btnstate(temp)`. The question is actually about scopes, not garbage-collection, and it's purely Python-related. – ekhumoro Jan 26 '17 at 02:46
  • @ekhumoro Ah, right, scoping is the issue not GC. Thanks for pointing this out, it's been a while since I thought about this kind of detail. – bnaecker Jan 26 '17 at 03:30