【问题标题】:Tkinter OOP - Class Instance Management With Multiple Top Level WindowsTkinter OOP - 具有多个顶级窗口的类实例管理
【发布时间】:2023-01-29 23:57:29
【问题描述】:

我最初的方法基于以下示例:

Best way to structure a tkinter application?

我正在尝试创建一个更复杂的应用程序,涉及多个子应用程序,每个子应用程序都显示在一个单独的顶级窗口中。所有子应用程序都需要能够相互交换信息。因此,我打算在主应用程序中使用类变量来完成此要求。

此刻我被困在一点:

我不知道如何在创建子应用程序类实例后正确地销毁它。

“app_one”窗口可以打开一次,然后可以正常关闭。当然,如果窗口第二次打开,则会发生错误,因为只有“Toplevel”窗口实例被销毁,而子应用程序类实例本身没有被销毁。

如何在关闭“Toplevel”窗口时正确实现子应用程序类实例的销毁?

除此之外,我也非常感谢关于如何根据以下“Python”和“tkinter”最佳实践范例改进我的模板结构的任何其他建议:

    import tkinter
    import tkinter.filedialog
    
    
    class main_app(tkinter.Tk):
    
        root = None
        var_one = None
        sub_app_one_instance = None
        sub_app_two_instance = None
        sub_app_thr_instance = None
    
        def __init__(self):
    
            super().__init__()
            main_app.root = self  # <<< in order to be able to refer to root window in other classes
            main_app.var_one = 99  # <<< class variables in order to exchange data between main app and sub apps
            main_app.sub_app_one_instance = None
            main_app.sub_app_two_instance = None
            main_app.sub_app_thr_instance = None
    
            self.create_tkinter_interface_main_app()
    
        def create_tkinter_button(self, button_destination, button_text, button_width):
            tkinter_button = tkinter.Button(button_destination, text=button_text, width=button_width)
            return tkinter_button
    
        def create_tkinter_label(self, label_destination, label_text):
            tkinter_label = tkinter.Label(label_destination, text=label_text)
            return tkinter_label
    
        def create_tkinter_interface_main_app(self):
    
            frame_main = tkinter.Frame(self)
            frame_sub_one = tkinter.Frame(frame_main, bg="red")
            frame_sub_two = tkinter.Frame(frame_main, bg="green")
            frame_sub_thr = tkinter.Frame(frame_main, bg="blue")
    
            label_app = self.create_tkinter_label(frame_main, "application")
            label_one = self.create_tkinter_label(frame_sub_one, "menu one")
            label_two = self.create_tkinter_label(frame_sub_two, "menu two")
            label_thr = self.create_tkinter_label(frame_sub_thr, "menu thr")
    
            button_one = self.create_tkinter_button(frame_sub_one, "app_one", 20)
            button_one.configure(command=self.sub_app_one_open)
    
            button_two = self.create_tkinter_button(frame_sub_one, "app_two", 20)
    
            label_app.pack(side=tkinter.TOP, fill=tkinter.X, expand=tkinter.NO)
            frame_sub_one.pack(side=tkinter.LEFT, fill=tkinter.BOTH, expand=tkinter.YES)
            frame_sub_two.pack(side=tkinter.LEFT, fill=tkinter.BOTH, expand=tkinter.YES)
            frame_sub_thr.pack(side=tkinter.LEFT, fill=tkinter.BOTH, expand=tkinter.YES)
            label_one.pack(side=tkinter.TOP, fill=tkinter.NONE, expand=tkinter.NO, padx=10, pady=10)
            button_one.pack(side=tkinter.TOP, fill=tkinter.NONE, expand=tkinter.NO, padx=10, pady=10)
            button_two.pack(side=tkinter.TOP, fill=tkinter.NONE, expand=tkinter.NO, padx=10, pady=10)
            label_two.pack(side=tkinter.TOP, fill=tkinter.NONE, expand=tkinter.NO, padx=10, pady=10)
            label_thr.pack(side=tkinter.TOP, fill=tkinter.NONE, expand=tkinter.NO, padx=10, pady=10)
            frame_main.pack(fill=tkinter.BOTH, expand=tkinter.TRUE)
    
        def sub_app_one_open(self):
            if not main_app.sub_app_one_instance == None:
                main_app.sub_app_one_instance.sub_app_one_move_to_front()
                return None
            else:
                main_app.sub_app_one_instance = sub_app_one()
    
    
    class sub_app_one(main_app):  # <<< inherit from main application in order to be able to access class variables
        def __init__(self):
            self.sub_app_one_toplevel_instance = tkinter.Toplevel(main_app.root)
            self.sub_app_one_toplevel_instance.protocol("WM_DELETE_WINDOW", self.sub_app_one_close_toplevel)
            print(main_app.var_one)  # <<< access information from main app
    
        def sub_app_one_move_to_front(self):
            self.sub_app_one_toplevel_instance.attributes("-topmost", 0x01)  # <<< set window state
            self.sub_app_one_toplevel_instance.attributes("-topmost", 0x00)  # <<< reset window state
    
        def sub_app_one_close_toplevel(self):
            self.sub_app_one_toplevel_instance.destroy()
            self.sub_app_one_toplevel_instance = None
    
    
    if __name__ == "__main__":
        main_app_instance = main_app()
        main_app_instance.mainloop()

【问题讨论】:

  • 要隐藏TopLevel,您可以使用toplevel.wm_withdraw()。要再次显示它,请使用toplevel.wm_deiconify()。当然,要使其正常工作,您还需要删除 self.sub_app_one_toplevel_instance = None 行。另一个提示:使一个类继承自另一个类并不能使其能够访问实例的变量;如果你想这样做,你应该给sub_app_main_one一个master参数,然后使用master.&lt;var_name&gt;作为你需要访问的任何main_app变量。另外,类名应该在CamelCase中,所以例如main_app变成了MainApp
  • 您好,感谢您的评论,但是如何在关闭 Toplevel 窗口时销毁类实例“sub_app_one”?
  • 好吧,如果您将 sub_app_one 实例设为 sub_app_one_open() 中的局部变量,一旦该函数被调用完毕,该变量就会被遗忘。您是否尝试删除sub_app_one_open() 中的所有代码,并添加sub_app_one() 行?这应该有效,以便忘记 sub_app_one 实例,并销毁窗口。另一种选择是编写您自己的自定义 TopLevel 类来代替 sub_app_one。但我也很好奇为什么你想销毁sub_app_one实例吗?

标签: python python-3.x oop tkinter


【解决方案1】:

# <<< 从主应用程序继承以便能够访问类变量

这不是继承的工作方式。如果 sub_app_one 需要访问主应用程序中的变量,它应该通过 main_app 的实例获取它们。你绝对应该不是继承自main_app

当你使用继承时,你是在说子类是一个主课。意思是,你最终得到两个主要类。一个是原版,一个和原版一模一样,只是做了一些修改。这意味着您最终会得到两个 tk.Tk 实例,这不是 tkinter 的设计用途。你可以做到,但这种行为并不直观,而且很少是正确的做法。

您应该做的是在创建实例时将 main_app 的实例传递到您的 sub_app_one 中。它看起来像这样:

class main_app(tkinter.Tk):
    ...
    def sub_app_one_open(self):
        ...
        main_app.sub_app_one_instance = sub_app_one(main_app=self)
        ...

class sub_app_one():
    def __init__(self, main_app):
        self.main_app = main_app
        ...

至于删除窗口的处理,最简单的解决方案是让你的子类成为tk.Toplevel的子类。这样,您就可以使用 tkinter 的内置事件处理来了解小部件何时被销毁。

这是一个工作示例:

import tkinter as tk

class Main(tk.Tk):
    def __init__(self):
        super().__init__()
        self.sub_app_one = None
        b1 = tk.Button(self, text="Open Sub App One", command=self.sub_app_one_open)
        b1.pack(padx=20, pady=20)

    def sub_app_one_open(self):
        if self.sub_app_one is None:
            self.sub_app_one = Sub_app_one(self)
        else:
            self.sub_app_one.deiconify()
        self.sub_app_one.bind("<Destroy>", self._child_destroyed)

    def _child_destroyed(self, event):
        if event.widget == self.sub_app_one:
            print("sub_app_one has been destroyed")
            self.sub_app_one = None

class Sub_app_one(tk.Toplevel):
    def __init__(self, main_app):
        self.main_app = main_app
        super().__init__(main_app)
        label = tk.Label(self, text="This is sub app one")
        label.pack(padx=50, pady=50)

main = Main()
main.mainloop()

【讨论】:

    【解决方案2】:

    这是我在过去几周处理此类应用程序后可以提供的一些建议。我会把我的回答分成两部分:

    1. 我自己的代码中的一些示例

    2. 如果这是一个长期项目,建议会有所帮助



      我的代码中的示例

      1. 管理您的 GUI

        我建议您有一个主类来管理您的应用程序。此类将您的应用程序的所有框架/页面放在彼此之上,并将您需要的框架/页面提升到顶部。我为每一帧都有一个单独的文件,因为每一帧的代码都可能很长。这是“Main.py”文件:

            import tkinter as tk
            #importing frames that we will create soon!
            from Frame1 import Frame1
            frome Frame2 import Frame2
            frome Frame3 import Frame3
        
            class Main(tk.Tk):
                def __init__(self):
                    tk.Tk.__init__(self) 
        
                    #Sets the screen size of the GUI
                    self.width= self.winfo_screenwidth() 
                    self.height= self.winfo_screenheight()
                    self.geometry(str(self.width) + 'x' + str(self.height) + '+0+0')
        
                    self.title('project_title')
        
                    #The container is a frame that contains the projects's frames
                    container = tk.Frame(self, 
                                         height=self.height, 
                                         width=self.width)
        
                    #Pack the container to the root
                    container.pack(side="top", fill="both", expand=True)
        
                    #Fixes pack location of the container using grid
                    container.grid_rowconfigure(0, weight=1)
                    container.grid_columnconfigure(0, weight=1)
        
                    #Create empty dictionary of frames
                    self.frames = {}
        
                    #Use container as master frame for imported frames 
                    for Frame in (Frame1, Frame2, Frame3):
                        self.frames[Frame] = Frame(container)
                        self.frames[Frame].grid(row=0, column=0, sticky="nsew")  
        
                    #Define buttons to switch GUI pages
                    self.frame1_btn = tk.Button(self, 
                                                   text='Frame1', 
                                                   width=15, 
                                                   font=('Arial', 10),
                                                   command=self.show_frame1)
                    self.frame1_btn.place(relx=0.75, 
                                          rely=0.025, 
                                          anchor='center')
        
                    self.frame2_btn = tk.Button(self, 
                                               text='Frame2', 
                                               width=15, 
                                               font=('Arial', 10),
                                               command=self.show_frame2)
                    self.frame2_btn.place(relx=0.85, 
                                          rely=0.025, 
                                          anchor='center')
        
                    self.frame3_btn = tk.Button(self, 
                                               text='Frame3', 
                                               width=15, 
                                               font=('Arial', 10),
                                               command=self.show_frame3)
                    self.frame3_btn.place(relx=0.95, 
                                          rely=0.025, 
                                          anchor='center')
        
                    #The start page is raised to the top at the beginning.
                    self.show_frame1()
        
                #Raises the selected frame to the top.
                def show_frame1(self):
                    frame = self.frames[Frame1]
                    frame.tkraise()
        
                def show_frame2(self):
                    frame = self.frames[Frame2]
                    frame.tkraise()
        
                def show_frame3(self):
                    frame = self.frames[Frame3]
                    frame.tkraise()
        
            def main():
                app = Main()
                app.mainloop()
        
            if __name__ == '__main__':
                main()
        

        您有一个管理 GUI 的“主”文件。现在您必须创建稍后将成为应用程序页面的框架。最佳实践:将它们分别保存在一个单独的文件中,然后将它们导入 Main.py!这是一个示例 Frame.py 文件:

        import tkinter as tk
        
        class Frame1(tk.Frame):
            def __init__(self, ParentFrame):
                tk.Frame.__init__(self, ParentFrame)
        
                #The label acts as a title for each main frame.
                self.label = tk.Label(self, 
                                      text='App_Name: Frame1',
                                      font=('Arial', 20))
        
                self.label.pack(padx=10, pady=10, anchor='w')
        

        根据需要对尽可能多的帧重复此操作。然后,您的目录中应该有以下文件:

        Main.py、Frame1.py、Frame2.py、Frame3.py等

        尝试运行 Main.py 文件! GUI 现在看起来仍然很无聊,但是您将用自己的代码填充每一帧。


        1. 销毁框架

        假设您有一个只想使用一次的起始页。使用后应销毁。让我们的“Frame1.py”文件成为起始页。它的代码应该是这样的:

        import tkinter as tk
        
        class Frame1(tk.Frame):
            def __init__(self, ParentFrame):
                tk.Frame.__init__(self, ParentFrame)
        
                #The label acts as a title for each main frame.
                self.label = tk.Label(self, 
                                      text='App_Name: Frame1',
                                      font=('Arial', 20))
        
                self.label.pack(padx=10, pady=10, anchor='w')
        
                self.selfdestruct_btn = tk.Button(self, 
                                                  text='selfdestruct',
                                                  command=self.selfdestruct)
                
                self.selfdestruct_btn.pack(pady=20)
        
            def selfdestruct(self):
                self.destroy()
        

        如果您单击起始页上的“selfdestruct”按钮,该框架将被正确销毁。但是,您仍然可以看到您在“Main.py”中定义的“第 1 帧”按钮,即使单击它没有产生任何结果。返回“Main.py”文件并删除您定义此按钮的部分。无论如何您都不需要它,因为当您启动您的应用程序时,起始页将被“self.show_frame1()”行提升到顶部。

        我希望这能帮助你解决你的问题。以下是我想给您的一些其他提示:



        进一步的建议

        1. 文件结构

          良好的文件结构可以帮助您对代码有一个概览,并使修复错误变得更加容易。您可能想要一个与此类似的目录:

          • Main.py 文件
          • Frame1 的文件夹
          • Frame2 的文件夹
          • Frame3 的文件夹
          • 可能是一个数据库
          • 您应用程序的任何其他主要组件

          为什么要为应用程序的每个框架使用文件夹?当您为每个帧编写代码时,您可能会开始用子帧填充每个帧。每个子帧都有自己的功能,可能需要大量代码。因此,我会为每个定义一个类重大的您使用的子框架由大量代码描述。如果您有多个这样的子框架类,您甚至可能希望在框架文件夹中有一个“子框架”文件夹。您也可以将那些仅供此框架使用的文件放在框架文件夹中。假设您必须执行一些复杂的计算并定义一个执行此操作的类。您可以将此类保存在框架文件夹中的单独文件中。

          1. 快速 GUI 加载

          尽量不要让您的计算机在启动您的程序时加载一堆不必要的东西。最好的情况是它只需要打包所有页面/框架及其按钮。不要让它加载巨大的图片文件或任何类似的东西。让它通过点击一个按钮来做这些事情。

          1. 漂亮不会赢得任何奖品

          ...至少这里可能不是这种情况。在继续之前,我不会花太多时间来完美地格式化每个按钮,因为无论如何你可能会一直移动东西。一旦您将所有需要的元素打包到一个框架中,您就可以考虑格式化了。

          这就是我现在要分享的全部内容。如果您有任何后续问题,请随时询问!

          最好, 安塞尔

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-09-27
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多