2

Tkinter mixes bindings of different widgets when the widgets are created in a loop, but when the widgets are created separately, the bindings are correct. I cannot figure out why this is the case.

Wrong bindings

Here's a working example to reproduce the problem. Two widgets (images that should work as buttons that are highlighted when hovering over) are created in a loop and packed in a second loop.

import tkinter as tk
import tkinter.ttk as ttk


IMG_DEFAULT = 'add_default.png'
IMG_HOVER = 'add_hover.png'

class Application(tk.Tk):
    def __init__(self, master=None, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.title('WRONG!')
        self.img_default = tk.PhotoImage(file=IMG_DEFAULT)
        self.img_hover = tk.PhotoImage(file=IMG_HOVER)
        self.create_widgets()
        self.pack_widgets()

    def create_widgets(self):
        self.buttons = []
        for i in range(2):
            button = ttk.Label(image=self.img_default)
            button.bind('<Button-1>', ...)  # Do something
            button.bind('<Enter>', lambda e: button.config(image=self.img_hover))
            button.bind('<Leave>', lambda e: button.config(image=self.img_default))
            self.buttons.append(button)

    def pack_widgets(self):
        for button in self.buttons:
            button.pack(padx=100, pady=10)

app = Application()
app.mainloop()

As can be seen from the image below, the bindings are wrong: when I hover over the upper button, the lower button is highlighted.

enter image description here

Correct binding

When the widgets are created outside a loop, the key bindings are correct.

import tkinter as tk
import tkinter.ttk as ttk


IMG_DEFAULT = 'add_default.png'
IMG_HOVER = 'add_hover.png'

class Application(tk.Tk):
    def __init__(self, master=None, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.title('RIGHT!')
        self.img_default = tk.PhotoImage(file=IMG_DEFAULT)
        self.img_hover = tk.PhotoImage(file=IMG_HOVER)
        self.create_widgets()
        self.pack_widgets()

    def create_widgets(self):
        button1 = ttk.Label(image=self.img_default)
        button1.bind('<Button-1>', ...)   # Do something
        button1.bind('<Enter>', lambda e: button1.config(image=self.img_hover))
        button1.bind('<Leave>', lambda e: button1.config(image=self.img_default))

        button2 = ttk.Label(image=self.img_default)
        button2.bind('<Button-1>', ...)   # Do something
        button2.bind('<Enter>', lambda e: button2.config(image=self.img_hover))
        button2.bind('<Leave>', lambda e: button2.config(image=self.img_default))

        self.buttons = [button1, button2]

    def pack_widgets(self):
        for button in self.buttons:
            button.pack(padx=100, pady=10)

app = Application()
app.mainloop()

As can be seen from the image, now the bindings are correct: when I hover over the upper button, the upper button is highlighted and when I hover over the lower, it is highlighted.

enter image description here

Can anyone explain why the second case works as intended but the first does not? In my application, I have a more general use case with an unknown number of buttons. How can I get the correct bindings in place?

EDIT

As pointed out by Bryan Oakley in his comment, my question is a duplicate of Tkinter assign button command in loop with lambda. Both the solutions of Henry (https://stackoverflow.com/a/66663554/12646289) and acw1668 provide the solution.

Thanks a lot for your help! I have upvoted the comments and accepted Henry's answer.

1 Answers1

0

This problem is caused by your lambdas. When the binding is called, it uses the current value of button for config. If you made 4 buttons they'd all change the image on the last one because it is the last value of button. (I'm not sure if I've explained it very well, please look at this answer for more detail). You need to change the lambda to

lambda e, b = button: b.config(...)

This creates a reference to the particular button so when the binding is called, the image for that button changes.

Henry
  • 3,472
  • 2
  • 12
  • 36