tkinter与GUI编程方法

3 投票
1 回答
3408 浏览
提问于 2025-04-17 04:24

希望这不是个“泛泛而谈”的话题,因为我想讨论的是如何高效地解决这些问题,而不是关于哪种图形界面编程方法最好的大辩论。

我开始用tkinter做一些图形界面编程,长话短说,我的代码很快就变得很乱。我正在尝试为一个视频游戏创建一个基于瓷砖的地图编辑器。我的主要问题似乎是:

  1. 回调函数无法返回值。
  2. 在不同窗口之间传递数据很麻烦。

我觉得这些问题的原因是我使用函数的频率远高于使用类。举个例子,我的“加载瓷砖集”窗口完全是通过函数来处理的:在主窗口点击菜单选项会调用一个加载新窗口的函数。在那个窗口中,我创建一个打开文件对话框来寻找图片,当我按下回车键时,会修改显示图片的画布(这样就能在图片上画出合适的网格)。函数、函数、函数。

我觉得很糟糕的做法是为了弥补而添加额外的参数。例如,当我创建一个瓷砖集时,生成的TileSet类的实例应该返回到主窗口,以便显示相关信息。我有一个全局变量来存储加载的瓷砖集(更糟糕的是:与我的根窗口相关的所有东西都在全局范围内!太棒了!),而且因为回调函数不返回值,我把那个列表作为参数传递给“加载瓷砖集窗口”的函数,然后这个函数再把参数传递给创建瓷砖集的函数(在窗口中点击相应按钮时调用),这样我才能把新创建的瓷砖集添加到列表中。通过函数“层级”传递参数听起来真是个糟糕的主意。这让事情变得混乱,不利于编写模块化代码,而且看起来完全没必要。

我尝试解决这个问题的方法是写一个代表整个图形界面的类,以及一些自定义的窗口类(可以由图形界面类创建和引用),这样可以实际存储相关数据。这应该能解决在窗口之间传递数据的问题。希望这也能减少我在回调中对lambda函数的过度使用。 但我在想:这样做是最好的方法吗?或者至少接近?我不想开始重写代码,然后又变成一个在不同方面同样混乱的系统。我知道我的方法不好,但我不太清楚最好的做法是什么。我得到了很多关于如何做具体事情的建议,但没有关于如何整体构建程序的建议。任何帮助都将不胜感激。

1 个回答

8

听起来你想创建一个按步骤执行的图形用户界面(GUI),但这样是行不通的。图形用户界面不是按步骤执行的,它的代码不是线性运行的,也就是说,函数不会像普通程序那样调用回调函数并返回值。你遇到的问题并不是tkinter特有的。这是基于事件的图形用户界面编程的特点——回调函数不能返回任何东西,因为调用者是一个事件,而不是一个函数。

简单来说,你必须使用某种全局对象来存储你的数据。通常这个全局对象被称为“模型”(Model)。它可以是一个全局变量,或者是一个数据库,或者是某种对象。无论如何,它必须是“全局”的;也就是说,整个图形用户界面都能访问到它。

通常,这种访问是通过一个叫做“控制器”(Controller)的第三个组件来实现的。控制器是图形用户界面(“视图”)和数据(“模型”)之间的接口。这三个组件一起构成了所谓的模型-视图-控制器模式,简称MVC。

模型、视图和控制器不一定要是三个不同的对象。通常,图形用户界面和控制器是同一个对象。对于小程序来说,这样的设计效果很好——图形用户界面的组件可以直接与数据模型进行交互。

举个例子,你可以有一个表示窗口的类,它继承自Tkinter.Toplevel。这个类可以有一个属性来表示正在编辑的数据。当用户在主窗口选择“新建”时,它会执行类似self.tileset = TileSet(filename)的操作。也就是说,它将名为tileset的属性设置为与给定文件名相关的TileSet类的一个实例。之后操作数据的函数会使用self.tileset来访问这个对象。对于那些在主窗口对象之外的函数(比如从主窗口调用的“保存所有”功能),你可以将这个对象作为参数传递,或者使用窗口对象作为控制器,请求它对其tileset进行某些操作。

这里有一个简短的例子:

import Tkinter as tk
import tkFileDialog
import datetime

class SampleApp(tk.Tk):
    def __init__(self, *args, **kwargs):
        tk.Tk.__init__(self, *args, **kwargs)
        self.windows = []
        menubar = tk.Menu(self)
        self.configure(menu=menubar)
        fileMenu = tk.Menu(self)
        fileMenu.add_command(label="New...", command=self.new_window)
        fileMenu.add_command(label="Save All", command=self.save_all)
        menubar.add_cascade(label="Window", menu=fileMenu)
        label = tk.Label(self, text="Select 'New' from the window menu")
        label.pack(padx=20, pady=40)

    def save_all(self):
        # ask each window object, which is acting both as 
        # the view and controller, to save it's data
        for window in self.windows:
            window.save()

    def new_window(self):
        filename = tkFileDialog.askopenfilename()
        if filename is not None:
            self.windows.append(TileWindow(self, filename))

class TileWindow(tk.Toplevel):
    def __init__(self, master, filename):
        tk.Toplevel.__init__(self, master)
        self.title("%s - Tile Editor" % filename)
        self.filename = filename
        # create an instance of a TileSet; all other
        # methods in this class can reference this
        # tile set
        self.tileset = TileSet(filename)
        label = tk.Label(self, text="My filename is %s" % filename)
        label.pack(padx=20, pady=40)
        self.status = tk.Label(self, text="", anchor="w")
        self.status.pack(side="bottom", fill="x")

    def save(self):
        # this method acts as a controller for the data,
        # allowing other objects to request that the 
        # data be saved
        now = datetime.datetime.now()
        self.status.configure(text="saved %s" % str(now))

class TileSet(object):
    def __init__(self, filename):
        self.data = "..."

if __name__ == "__main__":
    app = SampleApp()
    app.mainloop()

撰写回答