2009-04-08 2 views
6

Ich habe einen Menubutton, der beim Anklicken ein Menü anzeigen sollte, das eine bestimmte Sequenz von Strings enthält. Genau welche Strings in dieser Sequenz sind, wissen wir erst zur Laufzeit, also muss das Menü, das erscheint, in diesem Moment generiert werden. Hier ist, was ich habe:Dynamisches Erstellen eines Menüs in Tkinter. (Lambda-Ausdrücke?)

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

Der wichtigste Teil ist das Zeug unter "wenn menu_tags:" - Es sei menu_tags ist die Liste [ 'Stack', 'über', 'flow']. Und was ich tun möchte, ist effektiv dies:

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) 

wo add_tag_stack() ist definiert als:

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

und so weiter.

Das Problem ist, die Variable 'tag' nimmt den Wert 'stack' und dann den Wert 'over' und so weiter, und es wird nicht ausgewertet, bis new_command aufgerufen wird, an welcher Stelle die Variable 'tag "ist nur" fließen ". Das Tag, das hinzugefügt wird, ist immer das letzte im Menü, unabhängig davon, auf was der Benutzer klickt.

Ich habe ursprünglich ein Lambda verwendet, und ich dachte, dass vielleicht die Funktion wie oben definiert, könnte besser funktionieren. In beiden Fällen tritt das Problem auf. Ich habe versucht, eine Kopie der Variable 'tag' (entweder mit "current_tag = tag" oder mit dem Kopiermodul), aber das löst es nicht. Ich bin mir nicht sicher warum.

Mein Verstand beginnt in Richtung Dinge wie "eval" zu wandern, aber ich hoffe, dass jemand an einen schlauen Weg denken kann, der solche schrecklichen Dinge nicht einschließt.

Vielen Dank!

(Falls es relevant ist, Tkinter .__ version__ gibt '$ Revision: 67083 $' und ich bin mit Python 2.6.1 auf Windows XP.)

Antwort

6

Zunächst einmal Ihr Problem nichts mit Tkinter zu tun haben; Es ist am besten, wenn Sie es auf ein einfaches Stück Code reduzieren, um Ihr Problem zu demonstrieren, damit Sie leichter experimentieren können. Hier ist eine vereinfachte Version von dem, mit dem Sie experimentiert haben. Ich ersetze ein Diktat anstelle des Menüs, um es einfach zu machen, einen kleinen Testfall zu schreiben.

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

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

    map[item] = new_command 

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

Nun, wenn wir dies ausführen, wie Sie gesagt haben, erhalten wir:

flow 
flow 
flow 

Die Frage ist hier Begriff Python of scope. Insbesondere führt die for-Anweisung weder eine neue Ebene des Umfangs noch eine neue Bindung für item ein; Es aktualisiert also die gleiche item Variable jedes Mal durch die Schleife, und alle new_command() Funktionen beziehen sich auf den gleichen Artikel.

Was Sie tun müssen, ist eine neue Ebene des Umfangs einzuführen, mit einer neuen Bindung, für jeden der item s.Der einfachste Weg, dies zu tun, ist es in einer neuen Funktionsdefinition zu wickeln:

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

    map[item] = item_command(item) 

Nun, wenn Sie, dass in dem vorhergehenden Programm ersetzen, das gewünschte Ergebnis erhalten:

stack 
over 
flow 
+0

Nun, ich dachte, es könnte eine Tkinter-spezifische Lösung gegeben haben. Jemand, der sagt: "Nein, nein, nein, wie du das machst, ist die spezielle Funktion Tkinter.somethingOrOther()" Danke für die Hilfe! – MatrixFrog

+0

Kein Problem! Ich wollte nur darauf hinweisen, dass ein guter erster Schritt darin besteht, ein kleines Beispiel des Problems zu isolieren, um zu sehen, ob es sich um ein Sprachproblem oder ein API-Problem handelt. –

2

So etwas ist ein recht häufiges Problem in Tkinter , Ich denke.

Versuchen Sie, diese (an der entsprechenden Stelle):

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

Ich weiß nicht genau, warum das funktioniert, aber es tut! Obwohl ich die ausführliche Erklärung der anderen Antwort mag, werde ich diese verwenden, da sie etwas kürzer ist. Vielen Dank! – MatrixFrog

+0

Ich denke, es funktioniert wegen der Tag = Tag Standard-Argument in der Liste Funktionen Argumente. Dadurch wird der Name 'tag' innerhalb des Funktionsbereichs erstellt und auf das Wert-Tag im Bereich des Aufrufers verwiesen. Bitte korrigieren Sie mich, wenn ich falsch liege, ich bin gerade mit ähnlichen Scoping-Fragen beschäftigt. –

+0

Grundsätzlich ist das Konstrukt "for x in .." einer der wenigen Orte in Python, an dem ein Name (x) zurückspringt. Sie müssen also einen neuen Namen mit dem gleichen Wert wie x an der entsprechenden Stelle erstellen. Dies tun Sie mit 'tag = tag', das bei der Funktionsdefinition und nicht später bei der Funktionsausführung ausgewertet wird. –

0

ich hatte ähnlicher Fehler Nur der letzte Eintrag in der Liste wurde angezeigt. Feste von

command=lambda x=i: f(x)

Hinweis die x=i nach lambda Einstellung. Diese Zuweisung bewirkt, dass Ihre lokale Variable i direkt in die f(x) Funktion Ihrer command geht. Hoffe, dieses einfache Beispiel wird helfen:

# 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()