【问题标题】:Best way to structure a tkinter application?构建 tkinter 应用程序的最佳方式?
【发布时间】:2013-07-02 05:44:29
【问题描述】:

以下是我典型的python tkinter程序的整体结构。

def funA():
    def funA1():
        def funA12():
            # stuff

    def funA2():
        # stuff

def funB():
    def funB1():
        # stuff

    def funB2():
        # stuff

def funC():
    def funC1():
        # stuff

    def funC2():
        # stuff


root = tk.Tk()

button1 = tk.Button(root, command=funA)
button1.pack()
button2 = tk.Button(root, command=funB)
button2.pack()
button3 = tk.Button(root, command=funC)
button3.pack()

funA funBfunC 将在用户单击按钮 1、2、3 时弹出另一个带有小部件的 Toplevel 窗口。

我想知道这是否是编写 python tkinter 程序的正确方法?当然,即使我这样写也可以,但这是最好的方式吗?这听起来很愚蠢,但是当我看到其他人编写的代码时,他们的代码并没有被一堆函数弄乱,而且大部分都有类。

是否有任何我们应该遵循的特定结构作为良好做法?在开始编写python程序之前我应该​​如何计划?

我知道编程中没有最佳实践这样的东西,我也没有要求它。在我自己学习 Python 的过程中,我只是想要一些建议和解释,以使我保持正确的方向。

【问题讨论】:

标签: python tkinter


【解决方案1】:

我提倡一种面向对象的方法。这是我开始使用的模板:

# Use Tkinter for python 2, tkinter for python 3
import tkinter as tk

class MainApplication(tk.Frame):
    def __init__(self, parent, *args, **kwargs):
        tk.Frame.__init__(self, parent, *args, **kwargs)
        self.parent = parent

        <create the rest of your GUI here>

if __name__ == "__main__":
    root = tk.Tk()
    MainApplication(root).pack(side="top", fill="both", expand=True)
    root.mainloop()

需要注意的重要事项是:

  • 我不使用通配符导入。我将包导入为“tk”,这要求我在所有命令前加上 tk.。这可以防止全局命名空间污染,此外,当您使用 Tkinter 类、ttk 类或您自己的某些类时,它会使代码完全显而易见。

  • 主应用程序是一个类。这为您的所有回调和私有函数提供了一个私有命名空间,并且通常使您更容易组织您的代码。在程序风格中,您必须自上而下编写代码,在使用函数之前定义函数等。使用这种方法时您不需要,因为直到最后一步您才真正创建主窗口。我更喜欢从tk.Frame 继承,因为我通常从创建框架开始,但这绝不是必要的。

如果您的应用有额外的顶级窗口,我建议将它们中的每一个都设为一个单独的类,继承自 tk.Toplevel。这为您提供了上面提到的所有相同优势——窗口是原子的,它们有自己的命名空间,并且代码组织良好。此外,一旦代码开始变大,就可以轻松地将每个模块放入自己的模块中。

最后,您可能需要考虑为界面的每个主要部分使用类。例如,如果您正在创建一个带有工具栏、导航窗格、状态栏和主区域的应用程序,您可以创建这些类中的每一个。这使您的主要代码非常小且易于理解:

class Navbar(tk.Frame): ...
class Toolbar(tk.Frame): ...
class Statusbar(tk.Frame): ...
class Main(tk.Frame): ...

class MainApplication(tk.Frame):
    def __init__(self, parent, *args, **kwargs):
        tk.Frame.__init__(self, parent, *args, **kwargs)
        self.statusbar = Statusbar(self, ...)
        self.toolbar = Toolbar(self, ...)
        self.navbar = Navbar(self, ...)
        self.main = Main(self, ...)

        self.statusbar.pack(side="bottom", fill="x")
        self.toolbar.pack(side="top", fill="x")
        self.navbar.pack(side="left", fill="y")
        self.main.pack(side="right", fill="both", expand=True)

由于所有这些实例共享一个共同的父级,因此父级实际上成为模型-视图-控制器架构的“控制器”部分。因此,例如,主窗口可以通过调用self.parent.statusbar.set("Hello, world") 在状态栏上放置一些东西。这允许您在组件之间定义一个简单的接口,有助于保持最小的耦合。

【讨论】:

  • @Bryan Oakley 你知道互联网上有什么好的示例代码可以研究它们的结构吗?
  • 我支持面向对象的方法。但是,根据我的经验,避免在调用 GUI 的类上使用继承是一个好主意。如果 Tk 和 Frame 对象都是不从任何东西继承的类的属性,它会为您提供更大的灵活性。通过这种方式,您可以更轻松地访问 Tk 和 Frame 对象(并且不那么模糊),并且如果您不希望销毁一个对象,则不会销毁类中的所有内容。我忘记了这在某些程序中为何如此重要的确切原因,但它确实可以让你做更多的事情。
  • @gcb:是的,任何类都会给你一个私有命名空间。为什么要对 Frame 进行子类化?无论如何,我通常都会创建一个框架,所以它是一个需要管理的类(框架的子类,与从对象继承的类,以框架作为属性)。我已经稍微改写了答案以使其更清楚。感谢您的反馈。
  • 这里的 OOP 方法很好,也很花哨(而且微不足道),但是分配职责呢?哪个类应该负责创建每个小部件?哪个类应该负责以正确的方式布局它们?如何以不打破它们之间界限的方式管理控制器-gui耦合?
  • @madtyn:没有必要保存对parent 的引用,除非您以后要使用它。我没有保存它,因为我的示例中的代码都不需要保存它。
【解决方案2】:

将每个顶级窗口放入它自己的单独类中,可以让您重用代码并更好地组织代码。窗口中存在的任何按钮和相关方法都应在此类中定义。这是一个例子(取自here):

import tkinter as tk

class Demo1:
    def __init__(self, master):
        self.master = master
        self.frame = tk.Frame(self.master)
        self.button1 = tk.Button(self.frame, text = 'New Window', width = 25, command = self.new_window)
        self.button1.pack()
        self.frame.pack()
    def new_window(self):
        self.newWindow = tk.Toplevel(self.master)
        self.app = Demo2(self.newWindow)

class Demo2:
    def __init__(self, master):
        self.master = master
        self.frame = tk.Frame(self.master)
        self.quitButton = tk.Button(self.frame, text = 'Quit', width = 25, command = self.close_windows)
        self.quitButton.pack()
        self.frame.pack()
    def close_windows(self):
        self.master.destroy()

def main(): 
    root = tk.Tk()
    app = Demo1(root)
    root.mainloop()

if __name__ == '__main__':
    main()

另见:

希望对您有所帮助。

【讨论】:

  • 为什么我应该使用root 作为Demo1 对象的参数,为什么不呢?从当前线程和互联网上的许多答案中可以看出。
【解决方案3】:

这不是一个糟糕的结构;它会工作得很好。但是,当有人单击按钮或其他东西时,您必须在函数中具有执行命令的函数

所以你可以做的是为这些编写类,然后在类中拥有处理按钮点击等命令的方法。

这是一个例子:

import tkinter as tk

class Window1:
    def __init__(self, master):
        pass
        # Create labels, entries,buttons
    def button_click(self):
        pass
        # If button is clicked, run this method and open window 2


class Window2:
    def __init__(self, master):
        #create buttons,entries,etc

    def button_method(self):
        #run this when button click to close window
        self.master.destroy()

def main(): #run mianloop 
    root = tk.Tk()
    app = Window1(root)
    root.mainloop()

if __name__ == '__main__':
    main()

通常具有多个窗口的 tk 程序是多个大类,在 __init__ 中创建所有条目、标签等,然后每个方法都处理按钮单击事件

实际上并没有正确的方法来做到这一点,只要它可读并且您可以轻松地解释它,只要对您有用并完成工作,因为如果您不能轻松地解释您的程序,可能有更好的方法去做。

看看Thinking in Tkinter

【讨论】:

  • 《Thinking in Tkinter》提倡全球导入,我认为这是非常糟糕的建议。
  • 这是真的,我不建议你使用全局变量,只是一些主要的类方法结构,你是对的 :)
【解决方案4】:

OOP 应该是方法,frame 应该是一个类变量而不是实例变量

from Tkinter import *
class App:
  def __init__(self, master):
    frame = Frame(master)
    frame.pack()
    self.button = Button(frame, 
                         text="QUIT", fg="red",
                         command=frame.quit)
    self.button.pack(side=LEFT)
    self.slogan = Button(frame,
                         text="Hello",
                         command=self.write_slogan)
    self.slogan.pack(side=LEFT)
  def write_slogan(self):
    print "Tkinter is easy to use!"

root = Tk()
app = App(root)
root.mainloop()

参考:http://www.python-course.eu/tkinter_buttons.php

【讨论】:

  • 您只能在 Python 2 上使用 TKinter。我建议在 Python 3 上使用 tkinter。我还将最后三行代码放在 main() 函数下并调用它程序结束。我会绝对避免使用from module_name import *,因为它会污染全局命名空间并降低可读性。
  • 如果还导入了tkinter 扩展模块,您如何区分button1 = tk.Button(root, command=funA)button1 = ttk.Button(root, command=funA)?使用* 语法,两行代码看起来都是button1 = Button(root, command=funA)。我不建议使用这种语法。
  • 我想知道App(root) 方法的标准,而不是其他示例中的App()。另外,为什么Frame 应该是一个类变量,或者不是别的东西。
  • carloswm85 我也是。 @Bryan Oakley 可以解释一下吗?
【解决方案5】:

使用类组织您的应用程序可以让您和与您一起工作的其他人轻松地调试问题并轻松改进应用程序。

您可以像这样轻松地组织您的应用程序:

class hello(Tk):
    def __init__(self):
        super(hello, self).__init__()
        self.btn = Button(text = "Click me", command=close)
        self.btn.pack()
    def close():
        self.destroy()

app = hello()
app.mainloop()

【讨论】:

  • 这个例子可能有效,但我无法让它工作。
【解决方案6】:

我喜欢这样做的方式就像 Bryan Oakley 的回答。 这是一个由 Sentdex 在 Youtube 上制作的示例,去查看他的“GUIs with Tkinter”播放列表。

我认为把它放在这里真的很重要,因为它是 OP 的一个很好的例子,所以它也回答了这个被 35 人提出但没有得到回答的答案;

@Bryan Oakley 你知道互联网上有什么好的示例代码吗? 研究它们的结构? – Chris Aung 2013 年 7 月 5 日 8:35

import tkinter as tk

LARGE_FONT= ("Verdana", 12)

class SeaofBTCapp(tk.Tk):
    """
    tkinter example app with OOP
    """

    def __init__(self, *args, **kwargs):
        
        tk.Tk.__init__(self, *args, **kwargs)
        container = tk.Frame(self)

        container.pack(side="top", fill="both", expand = True)

        container.grid_rowconfigure(0, weight=1)
        container.grid_columnconfigure(0, weight=1)

        self.frames = {}

        for frame_class in (StartPage,PageOne, PageTwo):

            frame = frame_class(container, self)

            self.frames[frame_class] = frame

            frame.grid(row=0, column=0, sticky="nsew")
    

        self.show_frame(StartPage)

    def show_frame(self, cont):
        """
        Put specific frame on top
        """

        frame = self.frames[cont]
        frame.tkraise()

        
class StartPage(tk.Frame):
    """
    Starting frame for app
    """

    def __init__(self, parent, controller):
        tk.Frame.__init__(self,parent,bg='grey')
        label = tk.Label(self, text="Start Page", font=LARGE_FONT)
        label.pack(pady=10,padx=10)

        button_page1 = tk.Button(self, text = 'Visit Page 1', command= lambda: controller.show_frame(PageOne))
        button_page1.pack()

        button_page2 = tk.Button(self, text = 'Visit Page 2', command= lambda: controller.show_frame(PageTwo))
        button_page2.pack()

class PageOne(tk.Frame):
    """
    First page of program
    """

    def __init__(self,parent,controller):
        tk.Frame.__init__(self,parent,bg='light blue')
        label = tk.Label(self, text="Page one", font=LARGE_FONT)
        label.pack(pady=10,padx=10)

        button_home = tk.Button(self, text = 'Back to Home', command= lambda: controller.show_frame(StartPage))
        button_home.pack()

        button_home = tk.Button(self, text = 'Go to page2', command= lambda: controller.show_frame(PageTwo))
        button_home.pack()

class PageTwo(tk.Frame):
    """
    First page of program
    """

    def __init__(self,parent,controller):
        tk.Frame.__init__(self,parent,bg='light green')
        label = tk.Label(self, text="Page two", font=LARGE_FONT)
        label.pack(pady=10,padx=10)

        button_home = tk.Button(self, text = 'Back to Home', command= lambda: controller.show_frame(StartPage))
        button_home.pack()

        button_home = tk.Button(self, text = 'Go to page1', command= lambda: controller.show_frame(PageOne))
        button_home.pack()




app = SeaofBTCapp()
app.mainloop()

也可以在这里找到代码:[https://pythonprogramming.net/change-show-new-frame-tkinter/]

【讨论】:

    【解决方案7】:

    了解如何构建程序的最佳方法可能是阅读其他人的代码,尤其是当它是一个有很多人参与的大型程序时。看了很多项目的代码后,你应该对共识风格应该有一个概念。

    Python 作为一门语言的特殊之处在于,对于如何格式化代码有一些强有力的指导方针。首先是所谓的“Python之禅”:

    • 美胜于丑。
    • 显式优于隐式。
    • 简单胜于复杂。
    • 复杂胜于复杂。
    • 平面优于嵌套。
    • 稀疏优于密集。
    • 可读性很重要。
    • 特殊情况不足以打破规则。
    • 虽然实用胜过纯粹。
    • 错误绝不应该悄无声息地过去。
    • 除非明确静音。
    • 面对模棱两可,拒绝猜测的诱惑。
    • 应该有一种——最好只有一种——明显的方法。
    • 虽然这种方式一开始可能并不明显,除非您是荷兰人。
    • 现在总比没有好。
    • 虽然从来没有比现在更好正确
    • 如果实现难以解释,那就是个坏主意。
    • 如果实现易于解释,那可能是个好主意。
    • 命名空间是一个非常棒的想法——让我们做更多这样的事情!

    在更实用的层面上,有PEP8,Python 的样式指南。

    考虑到这些,我想说您的代码风格并不适合,尤其是嵌套函数。通过使用类或将它们移动到单独的模块中,找到一种将它们展平的方法。这将使您的程序结构更容易理解。

    【讨论】:

    • -1 用于使用 Python 之禅。虽然这都是很好的建议,但它并没有直接解决所提出的问题。去掉最后一段,这个答案几乎可以适用于这个网站上的每个 python 问题。这是很好的、积极的建议,但它没有回答问题。
    • @BryanOakley 我不同意你的观点。是的,Python 之禅是广泛的,可以用来解决许多问题。他在最后一段中确实提到选择类或将函数放在单独的模块中。他还提到了 PEP8,一个 Python 风格指南,并引用了它。虽然不是一个直接的答案,但我认为这个答案是可信的,因为它提到了许多可以采取的不同路线。这只是我的看法
    • 我来这里是为了寻找这个特定问题的答案。即使对于一个开放式问题,我也无法对这个回答做任何事情。 -1 也来自我。
    • 没办法,问题是要构建一个 tkinter 应用程序,与样式/编码/zen 指南无关。就像引用@Arbiter“虽然不是直接答案”一样简单,所以,这不是答案。这就像“也许是,也许不是”,带有禅意。
    【解决方案8】:

    我个人不使用面向对象的方法,主要是因为它 a) 只会碍事; b) 你将永远将它作为一个模块重用。

    但这里没有讨论的是您必须使用线程或多处理。总是。否则你的应用会很糟糕。

    只需做一个简单的测试:启动一个窗口,然后获取一些 URL 或其他任何内容。更改是您的 UI 在网络请求发生时不会更新。这意味着,您的应用程序窗口将被破坏。取决于您使用的操作系统,但大多数情况下,它不会重绘,您在窗口上拖动的任何内容都会贴在上面,直到该过程返回到 TK 主循环。

    【讨论】:

    • 你说的根本不是真的。我已经编写了数以百计的基于 tk 的应用程序,包括个人和商业应用程序,而且几乎从未使用过线程。线程有它们的位置,但在编写 tkinter 程序时您必须使用它们是不正确的。如果你有长时间运行的函数,你可能需要线程或多处理,但是你可以编写许多不需要线程的程序。
    • 我认为,如果您将答案改写为更清楚一点,那将是一个更好的答案。有一个在 tkinter 中使用线程的规范示例也会很有帮助。
    • 不关心成为最佳答案,因为它有点离题。但请记住,从线程/乘法开始非常容易。如果以后必须添加,那就是一场失败的战斗。而如今,绝对没有任何应用程序永远不会与网络通信。即使您忽略并认为“我只有很少的磁盘 IO”,明天您的客户会决定该文件将存在于 NFS 上,而您正在等待网络 IO,而您的应用程序似乎已死。
    • @erm3nda:“每个连接到网络或执行 IO 写入的应用程序使用线程或子进程都会快得多” - 这根本不是真的。线程不一定会使您的程序更快,并且在某些情况下会使它变慢。在 GUI 编程中,使用线程的主要原因是能够运行一些否则会阻塞 GUI 的代码。
    • @erm3nda:不,我不是根本不需要线程。很多事情肯定需要它们(嗯,线程或多处理)。只是有一大类 GUI 应用程序适合 tkinter,但根本不需要线程。是的,“安装程序、记事本和其他简单工具”属于这一类。这个世界由更多的这些“简单工具”组成,而不是由 word、excel、photoshop 等组成。另外,请记住这里的上下文是 tkinter。 Tkinter 通常不用于非常大型、复杂的应用程序。
    猜你喜欢
    • 2017-07-09
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-06-25
    • 1970-01-01
    • 1970-01-01
    • 2011-09-10
    • 1970-01-01
    相关资源
    最近更新 更多