2009-04-08 5 views
6

J'ai un menubutton, qui, lorsqu'il est cliqué, devrait afficher un menu contenant une séquence spécifique de chaînes. Exactement ce que les chaînes sont dans cette séquence, nous ne savons pas avant l'exécution, de sorte que le menu qui apparaît doit être généré à ce moment. Voici ce que j'ai:Création dynamique d'un menu dans Tkinter. (expressions lambda?)

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>") 

La partie importante est la substance sous la rubrique « si menu_tags: » - menu_tags Supposer la liste [ « pile », « over », « flux »]. Alors ce que je veux faire est effectivement ceci:

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) 

où add_tag_stack() est définie comme:

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

et ainsi de suite. Le problème est que la variable 'tag' prend la valeur 'stack', puis la valeur 'over' et ainsi de suite, et qu'elle n'est pas évaluée avant l'appel de new_command, point auquel la variable 'tag 'est juste' flux '. Ainsi, le tag qui est ajouté est toujours le dernier sur le menu, peu importe ce que l'utilisateur clique dessus. J'utilisais à l'origine un lambda, et je pensais que peut-être définir explicitement la fonction ci-dessus pourrait mieux fonctionner. De toute façon, le problème se produit. J'ai essayé d'utiliser une copie de la variable 'tag' (soit avec "current_tag = tag" ou en utilisant le module de copie) mais cela ne le résout pas. Je ne suis pas sûr pourquoi.

Mon esprit commence à errer vers des choses comme "eval" mais j'espère que quelqu'un peut penser à une manière intelligente qui n'implique pas des choses aussi horribles.

Merci beaucoup!

(Dans le cas où il est pertinent, Tkinter .__ version__ retourne 'Révision $: 67083 $ et je suis en utilisant Python 2.6.1 sous Windows XP.)

Répondre

6

Tout d'abord, votre problème n'a rien à voir avec Tkinter; il est préférable de le réduire à un simple code démontrant votre problème, afin que vous puissiez l'expérimenter plus facilement. Voici une version simplifiée de ce que vous faites que j'ai expérimenté. Je substitue une dict à la place du menu, pour le rendre facile d'écrire un petit cas de test.

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

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

    map[item] = new_command 

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

Maintenant, quand nous exécutons cela, comme vous l'avez dit, nous obtenons:

flow 
flow 
flow 

La question ici est la notion de champ de Python. En particulier, l'instruction for n'introduit pas un nouveau niveau de portée, ni une nouvelle liaison pour item; donc il met à jour la même variable item à chaque fois à travers la boucle, et toutes les fonctions new_command() font référence à ce même élément.

Ce que vous devez faire est d'introduire un nouveau niveau de portée, avec une nouvelle liaison, pour chacun des item s.La meilleure façon de le faire est de l'envelopper dans une nouvelle définition de la fonction:

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

    map[item] = item_command(item) 

Maintenant, si vous substituez que dans le programme précédent, vous obtenez le résultat souhaité:

stack 
over 
flow 
+0

Eh bien, je pensais qu'il pourrait y avoir une solution spécifique à Tkinter. Quelqu'un disant "Non non non, la façon dont vous faites ceci est la fonction spéciale Tkinter.somethingOrOther()" Merci pour l'aide! – MatrixFrog

+0

Pas de problème! Je voulais juste souligner qu'une bonne première étape consiste à essayer d'isoler un petit exemple du problème, pour voir s'il s'agit d'un problème de langage ou d'un problème d'API. –

2

Ce genre de chose est tout à fait un problème commun dans Tkinter , Je pense.

Essayez ceci (au point approprié):

def new_command(tag=tag): 
    self.add_tag(tag) 
+0

Je ne sais pas pourquoi cela fonctionne, mais c'est le cas! Bien que j'aime l'explication détaillée de l'autre réponse, je vais utiliser celle-ci car elle est un peu plus courte. Merci! – MatrixFrog

+0

Je pense que cela fonctionne à cause de l'argument par défaut tag = tag dans la liste des arguments de fonctions. Cela crée le nom 'tag' dans la portée de la fonction, pointant vers la balise de valeur dans la portée de l'invocateur. Corrigez-moi si je me trompe, je suis actuellement aux prises avec des questions de portée similaires. –

+0

Fondamentalement, la construction 'for x in ..' est l'un des rares endroits dans python où un nom (x) est rebond. Vous devez donc créer un nouveau nom ayant la même valeur que x au point approprié. Vous faites cela avec 'tag = tag' qui est évalué à la définition de la fonction, et pas plus tard à l'exécution de la fonction. –

0

J'ai eu un erreur similaire. Seul le dernier élément de la liste a été affiché. Correction en réglant

command=lambda x=i: f(x)

Notez le x=i après lambda. Cette affectation fait que votre variable locale i va directement dans la fonction f(x) de votre command. Espérons, cet exemple simple aidera:

# Using lambda keyword to create a dynamic menu. 
import tkinter as tk 

def f(x): 
    print(x) 

root = tk.Tk() 
menubar = tk.Menu(root) 
root.configure(menu=menubar) 
menu = tk.Menu(menubar, tearoff=False) 
l = ['one', 'two', 'three'] 
for i in l: 
    menu.add_command(label=i, command=lambda x=i: f(x)) 
menubar.add_cascade(label='File', menu=menu) 
root.mainloop() 
Questions connexes