使用Tkinter进行子类化

1 投票
1 回答
7757 浏览
提问于 2025-04-18 00:50

我正在用Python制作一个应用程序,现在一切都运作良好。不过到目前为止,所有的代码都在一个文件里。刚开始的时候,代码比较简单,但随着功能的增加,代码变得越来越复杂。

我发现代码变得很难理解,所以我决定把代码拆分成模块和类。

经过一番努力,我终于把这些东西整理好了,让它们都能正常工作。不过,我发现关于用Python制作复杂图形界面(GUI)的资料不多,因此我使用类来创建各种小部件。

我做了一个小示例应用程序,展示了以下几点:

  1. 将图形界面的代码和操作代码分开。在我的示例中,操作代码由一个单独的类处理,这也可以是一个单独的模块。
  2. 通过子类化一个容器来创建自定义小部件,在我的示例中是Tkinter.LabelFrame。
  3. 使用虚拟或自定义事件,这些事件会传播,用来触发主代码中的操作。
  4. 与子类/小部件交换数据。

这篇文章的目的有两个。

  1. 我希望其他人能从我解决这些问题的过程中受益。
  2. 也许其他人可以进一步改进这个示例。

我的示例有四个源文件。

  1. start.py。这个模块只负责启动应用程序,创建一个Gui类的对象。

    import main
    
    if __name__ == '__main__':
        title = "Test"
        gui = main.Gui(title)
    
  2. main.py。这个模块包含Gui类,并持有图形界面的根元素。

    import Tkinter
    import action
    import widget
    
    class Gui():
        def __init__(self, title):
            self.root = Tkinter.Tk()
            self.root.protocol("WM_DELETE_WINDOW", self.applicationExit)
            self.root.title(title)
    
            #create the action object
            self.process = action.Adder()
    
            #create the input frame
            self.frameIn = widget.Input(self.root)
            self.frameIn.grid(row=0, column=0, padx = 5, pady =5, ipadx = 5, ipady = 5, sticky = Tkinter.N)
    
            #create the output frame
            self.frameOut = widget.Output(self.root)
            self.frameOut.grid(row=1, column=0, padx = 5, pady =5, ipadx = 5, ipady = 5, sticky = Tkinter.N)
    
            #bind events
            self.root.bind("<<input_submit>>", self.__submit)
    
            self.root.mainloop()
    
        def applicationExit(self):
            self.root.destroy()
    
        def __submit(self, event = None):
            value = self.frameIn.getValue()
            result = self.process.addValue(value)
            self.frameOut.outputText.set(result)
    
  3. widget.py。这个模块包含两个自定义小部件,这些小部件在图形界面中使用。

    import Tkinter
    
    class Input(Tkinter.LabelFrame):
        def __init__(self, master):
            Tkinter.LabelFrame.__init__(self, master, text = "Input")
            self.inputText = Tkinter.StringVar()
    
            #create entry box
            self.entInput = Tkinter.Entry(self, textvariable = self.inputText, width = 20,)
            self.entInput.grid(row = 0, column = 0, padx = 5, pady = 2, sticky = Tkinter.N)
    
            #create submite button
            self.btnSubmit = Tkinter.Button(self, text = "Add", width = 10,
                command = self.__handlerSubmitButton)
            self.btnSubmit.grid(row = 1, column = 0, padx = 5, pady = 2, sticky = Tkinter.N)
    
        def getValue(self):
            value = self.inputText.get()
            if value.isdigit():
                return int(value)
            else:
                None
    
        def __handlerSubmitButton(self, event = None):
            self.btnSubmit.event_generate("<<input_submit>>")
    
    class Output(Tkinter.LabelFrame):
        def __init__(self, master):
            Tkinter.LabelFrame.__init__(self, master, text = "Output")
            self.outputText = Tkinter.StringVar()
    
            #create out put label box
            self.lblOutput = Tkinter.Label(self, textvariable = self.outputText, width = 20,
                anchor = Tkinter.E)
            self.lblOutput.grid(row = 0, column = 0, padx = 5, pady = 2, sticky = Tkinter.N)
    
        def setValue(self, value):
            self.outputText.set(value)
    
  4. action.py。这个模块包含执行应用程序实际任务的代码。

    class Adder():
        def __init__(self):
            self.count = 0
    
        def addValue(self, value):
            if value:
                self.count += value
            return self.count
    

任何改进都非常欢迎。

1 个回答

11

通常,创建一个Tkinter应用程序的标准方法是有一个叫做Application的根对象,或者其他类似的名字,它继承自Tkinter.Frame,然后创建所有定义你界面的组件:

import Tkinter as tk

class Application(tk.Frame):
    
    def __init__(self, root, *args, **kwargs):
        tk.Frame.__init__(self, root, *args, **kwargs)
        ... #do other initialisation
        self.grid() #or pack()

... 

if __name__ == '__main__':
    root = tk.Tk()
    app = Application(root)
    root.mainloop()

这种方法的好处有两个:

  • 你现在有了一个可以触发Tkinter事件和行为的对象(因为Tkinter有自己的一套组件层级),同时也可以用普通的类方法来拦截这些行为。
  • 你的根类可以传递你自己的消息处理方案(用于处理需求4“与子类/组件交换数据”),这样可以和你在构建界面时形成的自然层级保持一致。

举个例子来说明后一点:

class Message(object):
    def __init__(self, kind, data):
        self.kind = kind
        self.data = data

class Application(tk.Frame):
    def __init__(self, root, *args, **kwargs):
        self.widgets = []
        ... #do widget declarations

    def message_downstream(self, message):
        for widget in self.widgets:
            widget.receive_message(message)
    
    def message_upstream(self, message):
        #do some logic based on the message
        ...

class Widget(tk.Button):
    def __init__(self, master, name, *args, **kwargs):
        tk.Button.__init__(self, master, *args, **kwargs)
        self.master = master
        #perhaps set command event to send a message
        self['command'] = lambda: self.message_upstream(Message(self.name, "I Got Clicked"))
    
    def message_downstream(self, message):
        #similar to above
        pass
    
    def message_upstream(self, message):
        self.master.message_upstream(self, message)

这种方法在你的应用中引入了责任链模式,因为你现在可以在链的任何一点控制消息的流动(也就是说,可以做某事或者把消息传递给上游或下游,但通过不同的路径)。不过要小心,好的应用设计通常会把模型-视图-控制器模式融入代码中,如果在你的“视图”代码的责任链中引入了“控制”代码,可能会导致混乱。

在Tkinter层级中使用责任链的最佳方法是将代码限制在界面相关的内容上,而把其他所有代码,比如修改数据的代码,交给合适的控制器,比如你提到的动作类。

那么,为什么要使用这样的模式呢?当你的界面以复杂的方式相互作用时。例如,某个子菜单中的控件会改变另一个框架中可见的内容。这种行为并不真正依赖于模型,所以像上面那样实现是可行的。

我曾经写过一个Python代码编辑器,它会在你输入时自动编译并在另一个窗口中运行代码(这实际上变得有点烦人),显示代码输出或发生的异常。我使用责任链来收集编辑器组件中的代码,并将程序输出发送到输出窗口。我还用它来同时对两个窗口应用语法高亮的变化。

撰写回答