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.)
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 item
s. 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
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)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With