0

I am configuring a tkinter Menu option on the fly, with a passed in list of menu items and callback functions.

However, whichever menu item is actually selected the same callback is called. I think it is to do with the cmd_func var used to build the lambda changing as the loop that builds the menu options iterates. The value in the lambda ends up changing as the loop iterates so all the lambda's end up pointing ot the last value cmd_func takes? However I can't see how to solve the problem? - I did try enumerating menu_cmds when building the menu, and using the index var to index directly into the array menu_cmds to get at the function value rather than having the intermediate cmd_func var, but that didnt help.

This code replicates the issue;

from tkinter import *

class a:
    def __init__(self):
        self.root=Tk()
        self.m=[ ('name_a',self.command_a),
            ('name_b',self.command_b) ]
        self.control_frame=Frame(self.root)
        self.control_frame.pack(side=TOP,fill=X)
        self.control=control(self.control_frame,self.m)
        self.control.pack()

    def command_a(self,data):
        print("command_a %s,%s" % data)

    def command_b(self,data):
        print("command_b %s,%s" % data)


class control(Frame):
    def __init__(self,parent,menu_cmds):
        Frame.__init__(self,parent)
        self.menu_cmds=menu_cmds
        self.canvas=Canvas(self,width=800,height=400)
        self.canvas.pack(side=LEFT,anchor=W)
        self.canvas.create_rectangle(100,100,300,300,fill="#FF0000")
        self.canvas.bind("<Button-3>", self.canvas_context_popup)
        self.build_canvas_context_menu()

    def build_canvas_context_menu(self):
        self.canvas_context_menu=Menu(self.canvas, tearoff=0)
        for menu_text,cmd_func in self.menu_cmds:
            print("menu %s" % menu_text)
            cmd=lambda : self.canvas_context_menu_select(cmd_func)
            self.canvas_context_menu.add_command(label=menu_text,
                                            command=cmd)

    def canvas_context_popup(self, event):
        try:
            self.popup_at_x=event.x_root-self.canvas.winfo_rootx()
            self.popup_at_y=event.y_root-self.canvas.winfo_rooty()
            self.canvas_context_menu.tk_popup(event.x_root, event.y_root, 0)
        finally:
            self.canvas_context_menu.grab_release()

    def canvas_context_menu_select(self,cmd_func):
        x=self.popup_at_x
        y=self.popup_at_y
        cmd_func((x,y))



GUI = a()
GUI.root.geometry('1000x600')
GUI.root.update()
GUI.root.mainloop()

Any help much appreciated!

Matt Warren
  • 669
  • 8
  • 18
  • Does this answer your question? [How do lexical closures work?](https://stackoverflow.com/questions/233673/how-do-lexical-closures-work) – manveti Jan 07 '21 at 22:07
  • I had an idea it was something to do with closures; I'll have a read and see if that clears it up :) – Matt Warren Jan 07 '21 at 22:08
  • `cmd=lambda x=cmd_func : self.canvas_context_menu_select(x)` - common problem with `lambda` in loop – furas Jan 07 '21 at 22:08
  • perfect; reading the above and seeing your answer I see the problem now. Thankyou – Matt Warren Jan 07 '21 at 22:11
  • The meat of the issue is that the loop doesn't create a new scope -- `menu_text` and `cmd_func` are actually in the `build_canvas_context_menu` scope, so all three lambda functions are referencing the same `cmd_func` which is getting overwritten each pass through the loop. – manveti Jan 07 '21 at 22:11

1 Answers1

0

manveti and furas above had it right away, thankyou!

The solution is to change the lambda to be

cmd=lambda x=cmd_func : self.canvas_context_menu_select(x)

For a better explanation than I can give; refer to the comment and info linked by manveti above.

Matt Warren
  • 669
  • 8
  • 18