Search code examples
pythonfor-looptkinter

tkinter.Button 'command' parameter not set correctly


I use Tkinter for show database search results and show data with a for loop, and I set the command parameter [for widget tkinter.Button] with lambda func, but when I click on a data for full show, instead of running show func with correct parameters, I run show func with last data. My Code:

import tkinter
from tkinter import Button, ttk

def show_results(result: dict):
    def show(text):
        w = tkinter.Tk()
        w.geometry('500x450')
        result_lb = ttk.Label(master=w, text=text)
        result_lb.place(x=5, y=5)
        w.mainloop()

    win = tkinter.Tk()
    win.geometry('500x800')
    x = 5
    y = 5
    for res in result:
        r = result[res]
        print(r)
        cmd = lambda: show(r)
        Button(master=win, text=f"    {r}    ", command=cmd).place(x=x, y=y)
        y += 25
    win.mainloop()


show_results({'0': ['jack', '18', 'newyork'], '1': ['alex', '20', 'texas']})

Also I use tkinter.ttk.Button, but this bug not solved.


Solution

  • The bug is due to the fact that the lambda finds the value of r at the point that the button is called, and, as the for loop has already finished, the value would be the last index, or 1. This explained more clearly in this post here: tkinter creating buttons in for loop passing command arguments (pointed out by @001)

    When you use that lambda to define your function, the open_this call doesn't get the value of the variable i at the time you define the function. Instead, it makes a closure, which is sort of like a note to itself saying "I should look for what the value of the variable i is at the time that I am called". Of course, the function is called after the loop is over, so at that time i will always be equal to the last value from the loop.

    To fix this, we need to do (also from the same answer) cmd = lambda row=r: show(row), here defining the value of row to r, but not when cmd is called, instead, when cmd is assigned.

    A simple example of this is this:

    import tkinter
    from tkinter import Button, ttk
    
    
    def show(text):
        w = tkinter.Tk()
        w.geometry('500x450')
        result_lb = ttk.Label(master=w, text=text)
        result_lb.place(x=5, y=5)
        w.mainloop()
    
    def show_results(result: dict):
    
    
        win = tkinter.Tk()
        win.geometry('500x800')
        x = 5
        y = 5
        for key, value in result.items():
            r = value
            print(key, value)
            cmd = lambda row=r: show(row)
            Button(master=win, text=f"    {r}    ", command=cmd).place(x=x, y=y)
            y += 25
        win.mainloop()
    
    testvar = {'0': ['jack', '18', 'newyork'],
               '1': ['alex', '20', 'texas']}
    
    show_results(testvar)
    

    The reason that this works is because the variable row is getting defined when the lambda evaluates (probably not the right word for it though), not when the lambda is called.