Friday, August 29, 2014

Connecting signals for an array of buttons

The Find panel has an array of 24 UserButton objects, which are custom QPushButtons. Two signals from each button are significant. The built-in clicked() signal causes one action to happen. A custom signal, generated during a control-click, causes a different action to happen. In version 1, under PyQt4, the code to connect these signals looked like this.

        for i in range(UserButtonMax):
            self.connect(self.userButtons[i], SIGNAL("clicked()"),
                                lambda b=i : self.userButtonClick(b) )
            self.connect(self.userButtons[i], SIGNAL("userButtonLoad"),
                                lambda b=i : self.userButtonLoad(b) )

The slot for each signal is a lambda that just calls the relevant function passing the integer index of the button. So in the userButtonClick(self,button) method, button is the index of the button.

It took a while to work out how to generate a succession of lambda functions, each passing a different number. I'm still not sure exactly why

lambda b=i : self.userButtonClick(b)

passes the literal value of i as it was when the expression was evaluated, while

lambda: self.userButtonClick(i)

does not. Lambdas are magic. Anyway, here is the very similar code for version 2, using the new and improved PyQt5 signal syntax.

        for j in range(FindPanel.USER_BUTTON_MAX):
            self.user_buttons[j].clicked.connect(
                lambda b=j : self.user_button_click(b)
                )
            self.user_buttons[j].user_button_ctl_click.connect(
                lambda b=j : self.user_button_load(b)
                )

The only difference between those, besides trivial changes of nomenclature, is the use of .clicked.connect instead of SIGNAL("clicked()"), I swear. I have looked at those until my eyes were bloodshot and they are the same.

But of course they don't work the same.

Well, actually, the custom button does work. When I control-click on a user-button, the self.user_button_load() method is called with its index number from 0 to 23, just like it ought to be. But a normal click calls the self.user_button_click() method with an argument of... wait for it... False. Not an int in 0..23, and the same for every button.

Now, right away I noticed that although in V1 I was invoking SIGNAL("clicked()"), in fact the clicked signal of QPushButton is documented as passing a boolean representing its checked state. Which would be False, as these are (by default and also by explicit code) not checkable buttons.

If that's the case, the only issue is how in the PyQt5 API to select the no-parameter overload of that signal. The documentation shows how to select an overloaded signal definition with a different parameter list, but not how to select one with no parameter.

But then I thought, OK, suppose that value does represent the boolean "checked" parameter of the signal. How is it getting into the parameter list of my method?!? The "slot" that receives the signal is an anonymous function defined as

lambda b=j : self.user_button_load(b)

which when it was evaluated was self.user_button_load(17) or such. That anonymous function gets a boolean as a parameter and ignores it, passing a literal when it calls user_button_load(). So howinahell does False get in there?

I've written this to the PyQt mailing list. We shall see.

No comments: