Eduardo
Eduardo

Reputation: 641

Connecting multiples signal/slot in a for loop in pyqt

I'm connecting multiple signal/slots using a for loop in PyQt. The code is bellow:

# Connect Scan Callbacks
for button in ['phase', 'etalon', 'mirror', 'gain']:
    getattr(self.ui, '{}_scan_button' .format(button)).clicked.connect(
        lambda: self.scan_callback(button))

What I expect:

What I'm getting:

For reference, the slot method:

def scan_callback(self, scan):
    print(scan) # Here I always get 'gain'
    if self.scanner.isWorking:
        self.scanner.isWorking = False
        self.scan_thread.terminate()
        self.scan_thread.wait()
    else:
        self.scanner.isWorking = True
        self.scan_thread.start()
        getattr(self.ui, '{}_scan_button' .format(
            scan)).setText('Stop Scan')
        getattr(self, '_signal{}Scan' .format(scan)).emit()

Upvotes: 3

Views: 4468

Answers (3)

SWB
SWB

Reputation: 11

As noted here : Connecting slots and signals in PyQt4 in a loop Using functools.partial is a nice workaround for this problem.

Have been struggling with same problem as OP for a day.

Upvotes: 1

offeltoffel
offeltoffel

Reputation: 2801

My preferred way of iterating over several widgets in pyqt is storing them as objects in lists.

myButtons = [self.ui.phase_scan_button, self.ui.etalon_scan_button,
             self.ui.mirror_scan_button, self.ui.gain_scan_button]
for button in myButtons:
    button.clicked.connect(lambda _, b=button: self.scan_callback(scan=b))

If you need the strings "phase", "etalon", "mirror" and "gain" separately, you can either store them in another list, or create a dictionary like

myButtons_dict = {"phase": self.ui.phase_scan_button,
                 "etalon": self.ui.etalon_scan_button,
                 "mirror": self.ui.mirror_scan_button,
                 "gain": self.ui.gain_scan_button}

for button in myButtons_dict:
    myButtons_dict[button].clicked.connect(lambda: _, b=button self.scan_callback(scan=b))

Note, how I use the lambda expression with solid variables that are then passed into the function self.scan_callback. This way, the value of button is stored for good.

Upvotes: 6

GPhilo
GPhilo

Reputation: 19123

Your lambdas do not store the value of button when it is defined. The code describing the lambda function is parsed and compiled but not executed until you actually call the lambda. Whenever any of the buttons is clicked, the current value of variable button is used. At the end of the loop, button contains "gain" and this causes the behaviour you see.

Try this:

funcs = []
for button in ['phase', 'etalon', 'mirror', 'gain']:
    funcs.append( lambda : print(button))

for fn in funcs:
  fn()

The output is:

gain
gain
gain
gain

Extending the example, as a proof that the lambdas don't store the value of button note that if button stops existing, you'll have an error:

del button
for fn in funcs:
  fn()

which has output

funcs.append( lambda : print(button))
NameError: name 'button' is not defined

Upvotes: 3

Related Questions