【问题标题】:How to pass a variable to a function via a Button?如何通过按钮将变量传递给函数?
【发布时间】:2021-09-22 10:07:42
【问题描述】:

我在一个名为script.py的模块中定义了以下函数

def load_file():
    file_name = filedialog.askopenfile(filetypes=['EXE files', '*.exe'])
    return file_name   

def simulation(input_path, input_par): # this works fine
    # the functionruns an EXE file ang gives me two lists lst1 and lst2
    return lst1, lst2 

现在main.py 中定义的以下按钮调用load_file 函数并且它工作正常(我从终端的函数中打印路径)。我想不通的是如何通过main.py中的按钮将我在load_file函数中获得的路径作为参数input_path传递给函数模拟。

input_var = #??
button = Button(frame_file, text='Open file', command=load_file)
button.grid(row=1, column=1)
simulation(input_path, input_var)

【问题讨论】:

    标签: python tkinter button


    【解决方案1】:

    使用tkinter 时的常用方法是使用lambda 函数。棘手的部分是获取返回值,因为command= 函数的返回值被忽略了。人们经常使用global 变量来解决这个问题,但是如果函数是在另一个模块中定义的,那么这种策略将不起作用。幸运的是 tkinter 有几个特定于类型的容器 Variable 子类,它们可以作为参数传递给允许从中检索值的函数。

    下面的可运行代码显示了使用它们的一种方法:

    script.py

    from tkinter import filedialog
    
    def load_file(input_path):
        file_name = filedialog.askopenfilename(filetypes=[('EXE files', '*.exe')])
        input_path.set(file_name)
    
    def simulation(input_path, input_par): # this works fine
        # the function runs an EXE file ang gives me two lists lst1 and lst2
        return lst1, lst2
    

    main.py

    from script import load_file, simulation
    import tkinter as tk
    
    root = tk.Tk()
    frame_file = tk.Frame(root)
    frame_file.pack()
    
    input_path = tk.StringVar()
    input_var = 42
    button = tk.Button(frame_file, text='Open file', command=lambda: load_file(input_path))
    button.grid(row=1, column=1)
    button2 = tk.Button(frame_file, text='Run simulation',
                        command=lambda: simulation(input_path.get(), input_var))
    button2.grid(row=1, column=2)
    
    root.mainloop()
    

    更新

    这里有一个稍微复杂但更灵活的版本,它可以处理更多类型的返回值,包括用户定义的类(不仅仅是tkinter 支持的四种——bool、float、int、string)。自动支持更多类型的返回数据,也意味着调用函数不需要修改。

    它通过定义一个 CallBack 类来实现这一点,该类的实例“包装”要调用的函数并将它们的返回值保存在 tkinter StringVar 小部件中,以便以后可以通过调用方法 get_result() 来检索它们该类的实例具有。为了处理更多数据类型,它使用 Python 的内置 pickle 模块来临时存储数据。

    script2.py

    from tkinter import filedialog
    
    def load_file():
        file_name = filedialog.askopenfilename(filetypes=[('EXE files', '*.exe')])
        return file_name
    
    def simulation(input_path, input_par): # this works fine
        # the function runs an EXE file and gives me two lists lst1 and lst2
        print(f'simulation({input_path=}, {input_par=}) called')
        list1, list2 = ['param', input_par], list('spam')
        return list1, list2
    

    main2.py

    from script2 import load_file, simulation
    import pickle
    import tkinter as tk
    from tkinter import messagebox
    
    
    class CallBack:
        def __init__(self, func):
            self.func = func
            self.var = tk.StringVar()
        def get_result(self):
            pickled_result = self.var.get()
            return pickle.loads(pickled_result.encode('latin1'))  # Unpickle and return it.
        def __call__(self):
            res = self.func()
            pickled_result = pickle.dumps(res).decode('latin1')  # Pickle result.
            self.var.set(pickled_result)  # And save it in tkinter StringVar.
    
    
    root = tk.Tk()
    frame_file = tk.Frame(root)
    frame_file.pack()
    
    input_par = 42
    callback1 = CallBack(load_file)  # Wrap function from other module.
    
    button = tk.Button(frame_file, text='Open file', command=callback1)
    button.grid(row=1, column=1)
    
    def run_sim():
        """ Run simulation function and display value returned. """
        # Wrap other function in other module to allow results to be retrieved.
        callback2 = CallBack(lambda: simulation(callback1.get_result(), input_par))
        callback2()  # Call the function and save its return value.
        results = callback2.get_result()  # Get function's return value.
        messagebox.showinfo(title='Simulation Results', message=str(results))
    
    button2 = tk.Button(frame_file, text='Run simulation', command=run_sim)
    button2.grid(row=1, column=2)
    
    root.mainloop()
    

    【讨论】:

    • 谢谢!现在它起作用了。所以基本上你在 main 中定义了一个全局变量,作为模块中函数的输入给出,然后从模块中的函数中设置它,对吗? StringVar 可以是一个列表吗?
    • 不客气。该变量不必是 main 中的全局变量,只需存在足够长的时间,以便在需要时随时可用(这通常但不总是排除局部变量)。一种替代方法是类实例的属性。以我的answer 为例。 StringVar 不能是一个列表,但你可以有一个StringVars 列表,或者一个包含该类型(或其他类型)值的字典。
    • 另请注意,tkinter Variable 类还有其他用途,可以更轻松地创建 GUI。例如,当您创建Button 或Label 小部件时,您可以通过text= 或textvariable= 选项指定它们显示的文本。如果您使用后者,并且稍后程序中的某些内容更改了指定 StringVar 变量的值,则与之关联的小部件也将自动更新(因为您不必手动重新配置它或它们)。
    • @drSlump:刚刚添加了一个更新,应该允许函数返回列表和许多其他类型。
    • 谢谢。所以如果我想在程序中更新它的值,textvarible 必须是 StringVar?
    【解决方案2】:

    您可以使用partial 方法来传递参数。

    simulation = partial(simulation, input_path, input_par)  
    buttonCal = tk.Button(root, text="Simulate", command=simulation)
    

    【讨论】:

    • 非常感谢!但我想要一个按钮来保存路径和一个按钮来运行模拟。实际上,我将有几个按钮用于几种模拟。这真的可能吗?
    • 如果你必须返回值,你可以查看这个线程 - stackoverflow.com/questions/13099908/…
    【解决方案3】:

    我认为您的问题是重复的,只是从标题中看不出来,因为它的措辞有点奇怪:

    How to return value from tkinter button callback

    除了线程中的内容之外,我可以提出一个解决方案,但它确实不干净。

    您可以使用 lambda 或已经提到的 partial 方法来传递可变对象,如 @vnk 所述。使用 lambdas(或部分应用的函数)是向 Tkinter 回调传递参数的常用方法。

    由于我假设您的路径可能是一个不可变的字符串,因此您应该使用诸如 pathlike.Path 之类的对象,或者自己创建一个对象并将路径字符串包装到其中。不幸的是,您的回调也必须为此配合。

    path_container = [] # You could define a class to have cleaner code than having to use the magic 0 index.
    
    tk.Button(root, text="Open file", command=(lambda: load_file(path_container)))
    # Your load_file has to know that its argument is a list whose 0-th element must  contain the path,
    # or, if you opt for the more readable approach, that the .path attribute of your
    # path container object should be used.
    

    如果你真的很疯狂,你甚至可以创建一个函数来改变一个可变对象,这样你就不必改变你的 load_file 函数:

    def store_path(path_container, path):
        path_container[0] = path # Or path_container.path = path, if you're doing it cleaner.
    
    tk.Button(root, text="Open file", command=(lambda: store_path(path_container, load_file())))
    

    lambda 很有用,因为它从父作用域捕获变量,例如path_container。 需要一个帮助器,因为适用于 lambda 的限制是它们的主体必须是单个表达式,而在 Python 中,赋值(实际上是名称绑定)不是。它有许多其他语言,例如 C。

    老实说,最明智的解决方案可能是将回调设计为使用 pathlib.Path 对象。如果您的程序变大,您将受益于拥有适当的路径类而不是字符串,即使您不使用 Tkinter。

    【讨论】:

    • 非常感谢您的反馈。我不明白为什么它如此复杂。在我看来,tkinter 管理它的方式如此扭曲。
    • @drSlump 这很复杂,因为 Tkinter 只是 Python 内置的 Tcl 解释器的接口,而 Tcl 是一种古老的语言,使用全局状态是公认的约定,所以它们的回调机制是这样的,反过来又被 Tkinter 镜像。您也可以使用全局变量来交换信息,但即使是我提供的可变本地解决方案也比这要好得多。编辑:我编辑了答案以强调我认为在你的回调中使用 pathlib.Path 将是最好的解决方案,独立于 Tkinter。
    • 谢谢。 tkinter 是创建基于 pthon 的 GUI 的唯一选择吗?
    • @drSlump,不,一点也不。事实上,对于现代 GUI,绝对不推荐使用它,因为您无法在其中创建操作系统原生小部件。有 PyQt6、PySide(官方,与 PyQt 相对,但在开发上落后)和 wxWidgets。我相信还有用于 GTK 的 Python 绑定。
    • @Larry:虽然您对另一个问题是重复的问题是正确的,但不幸的是它没有可接受的答案,而且它确实有一个答案是错误的(即使它已修复,当被调用的函数在另一个模块中时没有帮助,就像这里的情况一样.但是你错了tkinter无法创建OS-native小部件(请参阅它包含的ttk-themed小部件). 此外,正如您可能想象的那样,tkinter 现在是标准库的一部分这一事实使其在大多数(如果不是全部)竞争对手中具有明显的优势。
    猜你喜欢
    • 2015-01-28
    • 2018-04-19
    • 2016-12-23
    • 1970-01-01
    • 2013-07-11
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2017-04-25
    相关资源
    最近更新 更多