pythonloopstkinterlambda

Lambda Function in Widget Binding Uses Last Iteration's Values


I have a loop to generate widgets dynamically, and the commands for the buttons are set using lambdas, but the event bindings for Listboxes don't seem to work well:

def fn1():

    def cbf(e, param1, param2):
        val = param2.get(param2.curselection())
        param1.delete(0, tk.END)
        param1.insert(0, val)
        
    
    def fn2():

        for x in range(n):

            entry = Entry(root, textvariable=sometextvar, bg="somecolour")
            
            lb = Listbox(root, height=someheight)
            lb.insert(0, *["Some", "values"])
            entry.bind("<FocusIn>", lambda e, entry=entry: lb.grid(row=entry.grid_info()["row"] - 1, column=2, pady=0, sticky=""))
            entry.bind("<FocusOut>", lambda e: lb.grid_remove())
            lb.bind("<<ListboxSelect>>", lambda e, param1=entry, param2=lb: cbf(e, param1=param1, param2=param2))

fn1()

I'm not sure how, but the lb.grid_remove() for <FocusOut> and the grid(...) for <FocusIn> work correctly. The one for <<ListboxSelect>> doesn't work correctly.

It works only for the last widget because, somehow, only the last widget is retained after the loop, though I'm capturing the variables when the lambda is defined (lambda e, param1=entry, param2=lb:). It also works only for the last widget for <KeyRelease>.

I've also tried functools.partial like:

lb.bind("<<ListboxSelect>>", partial(cbf, None, param1=entry, param2=lb))

both as keyword and positional arguments. I got TypeError: mainfile.<locals>.cbf() got multiple values for argument 'param1' tkinter for this one.

I've also tried around 25 other permutations and combinations of function definitions, wrapping them and nested callbacks, etc. I also don't want to use classes, and I want something like what I'm currently doing. Library functions like functools.partial are also not preferred.

Why does it work for commands of buttons and some bindings, but not for others?

Here's a MRE:

from tkinter import Tk, Entry, Listbox, StringVar
import tkinter as tk


def fn1():
    
    root = Tk()
    
    def cbf(e, param1, param2):
        val = param2.get(param2.curselection())
        param1.delete(0, tk.END)
        param1.insert(0, val)
    
    def fn2(n):
        for x in range(n):
            
            sometextvar = StringVar()
            someheight = 5
            
            entry = Entry(root, textvariable=sometextvar, bg="red")
            
            lb = Listbox(root, height=someheight)
            lb.insert(0, *["Some", "values"])
            entry.bind("<FocusIn>",
                       lambda e, entry=entry: lb.grid(row=entry.grid_info()["row"] - 1, column=2, pady=0, sticky=""))
            entry.bind("<FocusOut>", lambda e: lb.grid_remove())
            lb.bind("<<ListboxSelect>>", lambda e, param1=entry, param2=lb: cbf(e, param1=param1, param2=param2))
            
            entry.grid(row=x + 2, column=4)

    fn2(5)
    
    for r in range(root.grid_size()[1]):
        root.rowconfigure(r, weight=1)
    
    root.mainloop()

fn1()

Why is the value of the last entry set when I'm selecting the options ('Some' or 'values') from the Listbox meant for another entry? And why does the focus mechanism work?


Solution

  • When you use bind, you need to remember that tk will always send a positional parameter containing an event object. You also need to assign default values to any other arguments so that they are bound at the time the lambda is defined rather than at the time it executes.

    The correct binding for <FocusOut> is this:

    entry.bind("<FocusOut>", lambda e, lb=lb: lb.grid_remove())
    

    The correct binding for <FocusIn> is this:

    entry.bind("<FocusIn>", lambda e, entry=entry, lb=lb: lb.grid(row=entry.grid_info()["row"] - 1, column=2, pady=0, sticky=""))
    

    If you are unaware, the event object contains a reference to the widget that received the event. So, in the <FocusIn> binding you can use e.widget instead of entry to cut down on the complexity of your code.