Search code examples
python-2.7pygobject

PyGObject, Looping buttons


I'm tying to create a loop of buttons, and execute a command when they are pressed.

k=0
for row in list:

        delete_list.append("button_delete"+str(k))
        delete_list[k] = Gtk.Button(label="Delete")
        grid.attach(delete_list[k], columns+1, k, 1, 1)
        delete_list[k].connect("clicked",globals()["on_button"+str(k)+"_clicked"])

        k+=1

The buttons are displayed correctly, but i'm having problems to connect the "clicked" signal.

delete_list[k].connect("clicked",globals()["on_button"+str(k)+"_clicked"])
KeyError: 'on_button0_clicked'

I first tought that the error was because there is no method on_button0_clicked, but i create it and i stil getting the same error.

Also, if there is some good way/advice to dynamically create the methods for the response of the buttons it would be great. I actually need to create a method for each button that uses the "k" counter.


Solution

  • To dynamically create a function which binds loop variables as locals, you need a factory function to generate them:

    def callback_factory(num):
        def new_callback(widget):
            print(widget, num)
        return new_callback
    
    for k, row in enumerate(somelist):
        button = Gtk.Button(label=str(k))
        button.connect('clicked', callback_factory(k))
    

    This avoids a common pitfall which is to create a method or lambda within the loop body causing the same thing to be printed for each button click. This is due to the environment the function is created in being bound to the generated function in which the k variable is mutating and shared between all callbacks. The pitfall can be observed with the following code which does not work as you might expect:

    for k, row in enumerate(somelist):
        def callback(widget):
            print(k)
        button = Gtk.Button(label=str(k))
        button.connect('clicked', callback)
    

    PyGObject also supports unique user data per-connect call which is passed into the callback:

    def callback(widget, num):
        print(widget, num)
    
    for k, row in enumerate(somelist):
        button = Gtk.Button(label=str(k))
        button.connect('clicked', callback, k))
    

    This allows using the same callback with a varying argument which can be cleaner in some cases.

    Side note: It is probably not a good idea to mask the builtin "list" class by assigning it to your own list instance.