Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Dynamically creating a menu in Tkinter. (lambda expressions?)

I have a menubutton, which when clicked should display a menu containing a specific sequence of strings. Exactly what strings are in that sequence, we do not know until runtime, so the menu that pops up must be generated at that moment. Here's what I have:

class para_frame(Frame):
    def __init__(self, para=None, *args, **kwargs):
        # ...

        # menu button for adding tags that already exist in other para's
        self.add_tag_mb = Menubutton(self, text='Add tags...')

        # this menu needs to re-create itself every time it's clicked
        self.add_tag_menu = Menu(self.add_tag_mb,
                                 tearoff=0,
                                 postcommand = self.build_add_tag_menu)

        self.add_tag_mb['menu'] = self.add_tag_menu

    # ...

    def build_add_tag_menu(self):
        self.add_tag_menu.delete(0, END) # clear whatever was in the menu before

        all_tags = self.get_article().all_tags()
        # we don't want the menu to include tags that already in this para
        menu_tags = [tag for tag in all_tags if tag not in self.para.tags]

        if menu_tags:
            for tag in menu_tags:
                def new_command():
                    self.add_tag(tag)

                self.add_tag_menu.add_command(label = tag,
                                              command = new_command)
        else:
            self.add_tag_menu.add_command(label = "<No tags>")

The important part is the stuff under "if menu_tags:" -- Suppose menu_tags is the list ['stack', 'over', 'flow']. Then what I want to do is effectively this:

self.add_tag_menu.add_command(label = 'stack', command = add_tag_stack)
self.add_tag_menu.add_command(label = 'over', command = add_tag_over)
self.add_tag_menu.add_command(label = 'flow', command = add_tag_flow)

where add_tag_stack() is defined as:

def add_tag_stack():
    self.add_tag('stack')

and so on.

The problem is, the variable 'tag' takes on the value 'stack' and then the value 'over' and so on, and it doesn't get evaluated until new_command is called, at which point the variable 'tag' is just 'flow'. So the tag that gets added is always the last one on the menu, no matter what the user clicks on.

I was originally using a lambda, and I thought maybe explicitly defining the function as above might work better. Either way the problem occurs. I've tried using a copy of the variable 'tag' (either with "current_tag = tag" or using the copy module) but that doesn't solve it. I'm not sure why.

My mind is starting to wander towards things like "eval" but I'm hoping someone can think of a clever way that doesn't involve such horrible things.

Much thanks!

(In case it's relevant, Tkinter.__version__ returns '$Revision: 67083 $' and I'm using Python 2.6.1 on Windows XP.)

like image 719
Tyler Avatar asked Apr 08 '09 02:04

Tyler


2 Answers

First of all, your problem doesn't have anything to do with Tkinter; it's best if you reduce it down to a simple piece of code demonstrating your problem, so you can experiment with it more easily. Here's a simplified version of what you're doing that I experimented with. I'm substituting a dict in place of the menu, to make it easy to write a small test case.

items = ["stack", "over", "flow"]
map = { }

for item in items:
    def new_command():
        print(item)

    map[item] = new_command

map["stack"]()
map["over"]()
map["flow"]()

Now, when we execute this, as you said, we get:

flow
flow
flow

The issue here is Python's notion of scope. In particular, the for statement does not introduce a new level of scope, nor a new binding for item; so it is updating the same item variable each time through the loop, and all of the new_command() functions are referring to that same item.

What you need to do is introduce a new level of scope, with a new binding, for each of the items. The easiest way to do that is to wrap it in a new function definition:

for item in items:
    def item_command(name):
        def new_command():
            print(name)
        return new_command

    map[item] = item_command(item)

Now, if you substitute that into the preceding program, you get the desired result:

stack
over
flow
like image 78
Brian Campbell Avatar answered Sep 21 '22 11:09

Brian Campbell


That kind of thing is quite a common problem in Tkinter, I think.

Try this (at the appropriate point):

def new_command(tag=tag):
    self.add_tag(tag)
like image 45
John Fouhy Avatar answered Sep 18 '22 11:09

John Fouhy