Python tkinter按钮回调意外行为

Python tkinter按钮回调意外行为,python,button,callback,tkinter,Python,Button,Callback,Tkinter,我用Python制作了一个简单的“程序启动器”。我有一个以制表符分隔的文本文件,目前只有: 记事本c:\windows\notepad.exe 写入c:\windows\write.exe 程序读取文本文件并创建一个对象数组。每个对象都有一个名称属性(例如记事本)和一个路由属性(例如C:\windows\notepad.exe)。然后,对于每个对象,应在按钮上创建一个具有正确名称的按钮,单击该按钮应使用路由执行正确的程序 这个程序几乎可以运行了。事实上,对象数组的格式是正确的,因为for循环正确

我用Python制作了一个简单的“程序启动器”。我有一个以制表符分隔的文本文件,目前只有:

记事本c:\windows\notepad.exe
写入c:\windows\write.exe

程序读取文本文件并创建一个对象数组。每个对象都有一个名称属性(例如记事本)和一个路由属性(例如C:\windows\notepad.exe)。然后,对于每个对象,应在按钮上创建一个具有正确名称的按钮,单击该按钮应使用路由执行正确的程序

这个程序几乎可以运行了。事实上,对象数组的格式是正确的,因为for循环正确地打印出两个不同的程序名和两个不同的路由。问题是这两个按钮虽然标记正确,但都启动了写入程序!我相信这个问题是在回调的某个地方出现的,但是我的Python知识还没有发展到足以解决这个问题!从下面的代码中可以看到,我尝试了一个“内联”回调,并定义了一个“runprog”函数。它们都给出了相同的结果

谢谢你的帮助

import Tkinter as tk
import subprocess

class MyClass:
    def __init__(self, thename,theroute):
        self.thename=thename
        self.theroute=theroute

myprogs = []

myfile = open('progs.txt', 'r')
for line in myfile:
    segmentedLine = line.split("\t")
    myprogs.append(MyClass(segmentedLine[0],segmentedLine[1]))
myfile.close()

def runprog(progroute):
    print(progroute)
    subprocess.call([progroute])

root = tk.Tk()
button_list=[]

for prog in myprogs:
    print(prog.thename)
    print(prog.theroute)

    button_list.append(tk.Button(root, text=prog.thename, bg='red', command=lambda: runprog(prog.theroute)))
#    button_list.append(tk.Button(root, text=prog.thename, bg='red', command= lambda: subprocess.call(prog.theroute)))

# show buttons
for button in button_list:
    button.pack(side='left', padx=10)
root.mainloop()
只需更改这一行:

button_list.append(tk.Button(root, text=prog.thename, bg='red', command=lambda: runprog(prog.theroute)))
致:

推理:当您创建lambda函数(或函数中的任何其他函数)时,它确实可以访问(在Python 2中,只读访问)外部函数范围中的变量。但是,它确实访问该范围内的“live”变量-当调用lambda时,从“prog”检索到的值将是当时“prog”的含义,在这种情况下,它将是列表中的最后一个“prog”(因为用户只会在整个界面构建很久之后单击按钮)


此更改引入了一个中间作用域(传递当前“prog”值的另一个函数体),并且在表达式运行时,prog.theroute被分配给“route”变量。对列表中的每个程序执行一次。实际回调的内部lambda使用中间作用域中的“route”变量,该变量为循环的每个过程保存不同的值。

将命令更改为如下所示:

tk.Button(..., command=lambda route=prog.theroute: runprog(route))
import functools 
...
tk.Button(..., command=functools.partial(runprog,route)
请注意lambda有一个关键字参数,您可以在该参数中将默认值设置为要与此按钮关联的路由。通过给关键字arg一个默认值,您将把这个值“绑定”到这个特定的lambda

另一个选择是使用,许多人觉得这比lambda更不吓人。使用此按钮,您的按钮将如下所示:

tk.Button(..., command=lambda route=prog.theroute: runprog(route))
import functools 
...
tk.Button(..., command=functools.partial(runprog,route)
第三种选择是将“runprog”函数移到类中,而不是移到程序的主要部分中。在这种情况下,问题变得简单得多,因为每个按钮都专门绑定到一个唯一的对象

tk.Button(..., command=prog.runprog)

不完全是重复的,但请参见感谢jsbueno。我不确定我是否完全理解你的解释,我需要一些时间来思考。不过我已经做了你建议的改变。“write”按钮工作正常(和以前一样),但“notepad”按钮给了我一个我不理解的长错误:Tkinter回调回溯中的异常(最近一次调用):文件“C:\Python27\u 32\lib\lib tk\Tkinter.py”,第1410行,在call return self.func(*args)文件“C:\Users\Matt\Desktop\Python\program\u launcher 3.py”,第28行,在runprog SUBSPROCESS.call([progroute])文件“C:\Users\Matt\Desktop\Python\program\U launcher3.py”的第19行,在runprog SUBSPROCESS.call([progroute])文件“C:\Python27\U 32\lib\SUBSPROCESS.py”的第493行,在call return Popen中,在按钮列表中追加(tk(*popenargs,**kwargs).wait()文件“C:\Python27_32\lib\subprocess.py”,第679行,在init errread,errwrite中)文件“C:\Python27_32\lib\subprocess.py”,第896行,在_execute_child startupinfo)窗口错误:[错误2]系统找不到指定的文件,不需要两个lambda。这是一个过于复杂的解决方案。哦,是的,@bryan?因此,您的解决方案只是“更神奇”而不是“更简单”——2个lambda明确说明了正在发生的事情。您通过在“func defaults”中包含一个与当前值不同的nRet函数对象来冻结该值-我不认为这会因此变得更简单-这同样符合逻辑,但我认为新手更难理解您这样做的过程中到底发生了什么。也就是说,使用YourWay是很好的,我会在我的代码中使用它——但我认为没有人会因此否决这个答案。谢谢你的帮助Bryan。我尝试了你的第一个建议,python可以编译,但是当我点击第一个按钮(记事本)时,我得到了一个很长的“tkinter回调异常”错误,我在下面发布了这个错误。我试着把记事本换成chrome,我也遇到了同样的问题!所以这可能是一个很好的建议,我的计划中还有一些其他问题。也许它不喜欢我数组中的第一个程序?(尽管当我循环时,它可以很好地打印出来)。现在,我将尝试你的第三个建议(将其转移到课堂上)万岁!!现在一切正常。布莱恩,阵列会打印出来,但我仍然无法得到你的任何建议,这让我认为这可能是一个空白问题。我在“分段线”的末尾添加了.strip()。我在使用你的第三个建议,这是最简单的。再次感谢。