使用Tkinter进行子类化
我正在用Python制作一个应用程序,现在一切都运作良好。不过到目前为止,所有的代码都在一个文件里。刚开始的时候,代码比较简单,但随着功能的增加,代码变得越来越复杂。
我发现代码变得很难理解,所以我决定把代码拆分成模块和类。
经过一番努力,我终于把这些东西整理好了,让它们都能正常工作。不过,我发现关于用Python制作复杂图形界面(GUI)的资料不多,因此我使用类来创建各种小部件。
我做了一个小示例应用程序,展示了以下几点:
- 将图形界面的代码和操作代码分开。在我的示例中,操作代码由一个单独的类处理,这也可以是一个单独的模块。
- 通过子类化一个容器来创建自定义小部件,在我的示例中是Tkinter.LabelFrame。
- 使用虚拟或自定义事件,这些事件会传播,用来触发主代码中的操作。
- 与子类/小部件交换数据。
这篇文章的目的有两个。
- 我希望其他人能从我解决这些问题的过程中受益。
- 也许其他人可以进一步改进这个示例。
我的示例有四个源文件。
start.py。这个模块只负责启动应用程序,创建一个Gui类的对象。
import main if __name__ == '__main__': title = "Test" gui = main.Gui(title)
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)
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)
action.py。这个模块包含执行应用程序实际任务的代码。
class Adder(): def __init__(self): self.count = 0 def addValue(self, value): if value: self.count += value return self.count
任何改进都非常欢迎。
1 个回答
通常,创建一个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代码编辑器,它会在你输入时自动编译并在另一个窗口中运行代码(这实际上变得有点烦人),显示代码输出或发生的异常。我使用责任链来收集编辑器组件中的代码,并将程序输出发送到输出窗口。我还用它来同时对两个窗口应用语法高亮的变化。