Python:Tkinter:为什么是root.mainloop()而不是app.mainloop()?

5

我是Stack Overflow的新成员。我发现了这个帖子,但我不被允许在上面评论或提问,所以我想在这里引用一下:如何在Python的Tkinter中创建一个交互式列表,包括可以编辑这些列表的按钮?

from tkinter import *
import os
import easygui as eg

class App:

    def __init__(self, master):
        frame = Frame(master)
        frame.pack()

        # character box
        Label(frame, text = "Characters Editor").grid(row = 0, column = 0, rowspan = 1, columnspan = 2)
        charbox = Listbox(frame)
        for chars in []:
            charbox.insert(END, chars)
        charbox.grid(row = 1, column = 0, rowspan = 5)
        charadd = Button(frame, text = "   Add   ", command = self.addchar).grid(row = 1, column = 1)
        charremove = Button(frame, text = "Remove", command = self.removechar).grid(row = 2, column = 1)
        charedit = Button(frame, text = "    Edit    ", command = self.editchar).grid(row = 3, column = 1)

    def addchar(self):
        print("not implemented yet")
    def removechar(self):
        print("not implemented yet")
    def editchar(self):
        print("not implemented yet")


root = Tk()
root.wm_title("IA Development Kit")
app = App(root)
root.mainloop()

有人能解释一下为什么最后一行是root.mainloop()吗?作为一个Python的新手,来自过程化编程背景而没有面向对象的经验,我认为应该是app.mainloop()。

实际上,app = App(root),在代码的其余部分中从未再次使用app!我很难理解为什么root.mainloop()仍然可以工作。


因此,当您使用root初始化App类时,将运行__init__函数,其中加载所有GUI组件。 mainloopTk的一个方法,它启动窗口的事件循环。因此,我们没有必要重新引用该class(尽管在class本身内部技术上需要这样做),因为我们初始化该class时已经完成了所有需要的工作。 - anon582847382
3个回答

8
我不确定您是否会对这个答案感到满意,但是您调用root.mainloop()是因为root是具有mainloop方法的对象。在您提供的代码中,App没有mainloop函数。
简单地说,这就是tkinter的工作方式 - 您始终通过调用根窗口的mainloop方法来结束脚本。当该方法返回时,程序将退出。
让我们从头开始。最简单的非OO Tkinter程序将看起来像以下示例。请注意,这是一个python 2.x示例,并且我不使用全局导入,因为我认为全局导入是不好的。
import Tkinter as tk
root = tk.Tk()
<your widgets go here>
root.mainloop()

很多人发现仅使用过程式编程并不是编写代码的有效方式,因此他们可能会选择以面向对象的方式编写代码。将“应用程序”视为单例对象是很自然的。有很多方法可以实现这一点——你问题中的方法,不幸的是,并不是较清晰的方法之一。
在我看来,一个稍微好一些的结构如下所示:
class App(tk.Tk):
    def __init__(self, *args, **kwargs):
        tk.Tk.__init__(self, *args, **kwargs)
        <your widgets go here>
app = App()
app.mainloop()

在这种情况下,虽然现在它是App的一个方法,但mainloop仍然被调用,因为App继承自Tk。这在概念上与root.mainloop()相同,因为在这种情况下,app根窗口,即使它有不同的名称。

因此,在这两种情况下,mainloop都是属于根窗口的方法。在这两种情况下,必须调用它以使GUI正常工作。

存在第三种变化,这就是您选择的代码使用的变化。在这种变化中,有几种实现方式。问题中使用类来定义GUI,但没有从Tk继承。这完全可以,但你仍然必须在某个时候调用mainloop。由于您没有在类中创建(或继承)mainloop函数,因此必须调用与根窗口相关联的函数。我所说的变化是关于将App实例添加到根窗口的方式和位置,以及如何最终调用mainloop的方式。

个人偏好于App继承自Frame,并且将应用程序的定义展示分开。我使用的模板如下:

class App(tk.Frame):
    def __init__(self, *args, **kwargs):
        tk.Frame.__init__(self, *args, **kwargs)
        <your widgets go here>

if __name__ == "__main__":
    root = tk.Tk()
    app = App(root)
    app.pack(fill="both", expand=True)
    root.mainloop()

在这个最后的例子中,approot是两个完全不同的对象。app代表了一个存在于根窗口内部的框架。通常情况下,框架都是用来容纳其他小部件的。
因此,在所有情况下,都必须调用mainloop函数。它被调用的位置和方式取决于你的编程风格。有些人喜欢从根窗口继承,有些人则不喜欢。无论哪种情况,你都必须调用根窗口的mainloop函数。

这个回答已经足够令人满意了。我想我真正困惑的是,当一切都结束时,app和root是同一个对象实例吗?从过程化背景出发,如果app和root是不同的实例,那么所有的小部件都会被打包到app实例中。因此,如果我调用root.mainloop(),我不希望看到新打包的小部件。但是如果我调用app.mainloop(),直觉上对我来说看到所有打包的小部件是有意义的。但我猜测app和root是同一个实例。 - Nuuk
由于app和root是同一个实例,因此任何打包到app中的内容也会在root中可见,因此最终,root.mainloop()将显示所有打包到app中的小部件。 - Nuuk
@Nuuk:在最后一个例子中,app和root是两个_不同的_对象。root是所有其他小部件进入其中的根窗口。它是Tk类的一个实例,并且每个tkinter应用程序必须恰好有一个此类的实例。app是App类的一个实例,它是Frame的子类。框架通常用作其他小部件的容器,在这种情况下,组成应用程序的小部件(除了根窗口)被打包在此框架中。app和root是两个完全不同的东西。root是app的容器。 - Bryan Oakley
哇,非常感谢您那优雅的解释。这真的有所不同。现在我知道Frame和Tk是两个完全不同的类了。一切都开始变得更加清晰了。我认为接下来我需要学习Python类、子类如何工作、继承等等。我相信这就是所有混淆的根源。您看,当我看到app = App(root)时,我以为这意味着app是root的一个子类,而root是Tk类的一个实例。但是您非常优雅地回答了我的问题,并指出了真正的问题:我只需要学习Python类即可。 - Nuuk

1
我测试了两种方式,如下所示:
一种使用"app." + ".pack()"进行编写,另一种调用"mainframe." + ".grid()"
 #-*- coding: utf-8 -*-
#THIS IS THE "MAINFRAME." - PART
from Tkinter import *
import ttk

def show():
    p = password.get() #get password from entry
    print(p)


root = Tk()
root.title("Ingos first program")

mainframe = ttk.Frame(root, padding="30 30 60 12")
mainframe.grid(column=0, row=0, sticky=(N, W, E, S))
mainframe.columnconfigure(0, weight=1)
mainframe.rowconfigure(0, weight=1)


password = StringVar() #Password variable
passEntry = Entry(mainframe, textvariable=password, show='*').grid(column=3, row=3, sticky=S)
submit = Button(mainframe, text='Show Console',command=show).grid(column=3, row=4, sticky=S)

root.mainloop()
def show():
    p = password.get() #get password from entry
    print(p)

#THIS IS THE "APP."-PART. BOTH WORKS FINE.
app = Tk()
app.title("Ingos first program")
password = StringVar() #Password variable
passEntry = Entry(app, textvariable=password, show='#').pack()
submit = Button(app, text='Show Console',command=show).pack()

app.mainloop()

这个实例在Python 2.7中运行良好。即使应用程序可以处理“mainloop()”的测试,该脚本也会打开两个窗口,一个接一个(如果您关闭第一个窗口),并且第一个程序已经格式化,没有尝试在pack()夹具中编写colum=3...等内容。
我仍然启动了Tkinter,所以请不要和我争论,只是尝试...希望我能帮助回答您的问题。 祝一切顺利,Ingo

0

App 对象只是您的应用程序代码,您调用 App(root) 的原因是创建一个使用您的类的实例,该实例然后可以访问您的根窗口。

它在 __init__ 方法中接收此引用:

def __init__(self, master):
    # master refers to the root window now
    ...

您可以查看完整的App对象定义(由以class App:开头的代码块给出),并且它甚至没有mainloop方法,因此要启动主Tkinter循环,您必须在根窗口上调用它。

在{{link1:Python2文档中的示例}}中,他们确实像您怀疑的那样调用了它,但请注意,他们的示例类是Tk对象Frame的子类。 在您的示例代码中,App是一个不继承任何内容的旧式类。


我认为你使用的词有点不清楚。你调用 App(root) 的原因不是“保存引用”,而是实际创建应用程序。唯一保存引用的是当你将结果分配给变量 app 时。所以,再次强调,你不是“调用”它来保存引用,而是“将其分配给变量”来保存引用。 - Bryan Oakley
我的意思是你为什么要用root作为参数调用App,但我想我明白你的意思了。 - Two-Bit Alchemist
1
你还是弄错了。你不是将root作为参数调用它以保存引用,而是因为App类要求你告诉它在哪个窗口中创建。 - Bryan Oakley
我仍然认为我们只是在互相说过而已,但我调整了措辞。我的观点是,App类需要传入窗口,以便它可以使用它(告诉它要在哪个窗口中创建窗口,并根据应用程序的功能可能有其他用途)。 - Two-Bit Alchemist
正如大家所看到的,我的面向对象知识仍然有限。我的理解是root.mainloop()是显示的主GUI窗口。然后app = App(root)是我们创建App类实例的地方,但是在代码中没有其他地方使用app。然而,通过创建App类的实例,app中的def __init__(self, master):中的所有内容突然出现在主GUI窗口(root.mainloop())中。如果这不是询问的正确位置(因为我真的不懂面向对象编程),我很乐意被重定向到其他可能解释的讨论。 - Nuuk

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接