Home > OS >  tkinter OOP - class instance management with multiple top level windows
tkinter OOP - class instance management with multiple top level windows

Time:05-02

My initial approach is based on the following example:

Best way to structure a tkinter application?

I am trying to create a more complex application involving multiple sub applications each displayed in a separate top level window. All sub applications need to be able to exchange information among each other. Hence, I intend to use class variables in the main application in order to accomplish this requirement.

At the moment I am stuck at one point:

I do not know how to properly implement the destruction of the sub application class instance once it has been created.

The "app_one" window can be opened exactly one time and then it can be properly closed. Off course, if the window is opened a second time an error occurs since only the "Toplevel" window instance has been destroyed, but not the sub application class instance itself.

How do I properly implement the destruction of the sub application class instance upon closing the "Toplevel" window ?

Besides that, I am also very grateful for any other advise on how to improve the structure of my template below in terms of following "Python" as well as "tkinter" best practice paradigms:

    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()

CodePudding user response:

# <<< inherit from main application in order to be able to access class variables

This is not how inheritance works. If sub_app_one needs access to variables in the main application, it should get them via the instance of main_app. You should definitely not be inheriting from main_app.

When you use inheritance, you are saying that the subclass is a main class. Meaning, you end up with two main classes. One is the original, and one is exactly like the original, with some modifications. That means you end up with two instances of tk.Tk which is not how tkinter is designed to be used. You can do it, but the behavior is not intuitive, and is rarely the right thing to do.

What you should do instead is pass in the instance of main_app into your sub_app_one when creating the instance. It would look something like this:

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
        ...

As for the handling of deleting the window, the simplest solution is to make your child classes subclasses of tk.Toplevel. With that, you can use tkinter's built-in event handling to know when the widget has been destroyed.

Here's a working example:

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()

CodePudding user response:

Here is some advice that I can give after having worked on a application like this for the past few weeks. I will split my answer into two parts:

  1. Some examples from my own code

  2. Advice that will be helpful if this is a long-term project



Examples from my code

  1. Managing your GUI

I would advise you to have one main class that manages your application. This class puts all the frames/pages of your applicaton on top of each other and raises the one you need to the top. I have a separate file for every frame, since the code for each of them can get very long. This is the 'Main.py' file:

    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()

You have a 'main' file that manages the GUI. Now you have to create the frames that will later be the pages of your application. Best practice: keep them each in a separate file and import them in Main.py! This is an example Frame.py file:

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')

Repeat this for as many frames as you need. You should then have the following files in your directory:

Main.py, Frame1.py, Frame2.py, Frame3.py, etc.

Try running the Main.py file! The GUI still looks boring right now, but you will fill each frame with your own code.


  1. Destroying frames

Let's say you have a start page that you only want to use once. It should be destroyed after its use. Let our 'Frame1.py' file be that start page. The code for it should look like this:

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()

If you click the 'selfdestruct' button on the start page, this frame will be properly destroyed. However, you can still see the 'Frame 1' button that you defined in 'Main.py', even if clicking it produces no result. Go back to the 'Main.py' file and delete the part where you defined this button. You don't need it anyway, since the start page will be raised to the top by the line 'self.show_frame1()' when you start your application.

I hope that helps you with your question. Here are some other tips I would like to give you:



Further advice

  1. File scructure

A good file structure helps you to have an overview of your code and will make fixing errors much easier. You might want to have a directory that is similar to this:

  • Main.py file
  • Folder for Frame1
  • Folder for Frame2
  • Folder for Frame3
  • possibly a database
  • any other major components of your application

Why use folders for each frame of your application? When you write the code for each of these frames, you will probably start filling each frames with subframes. Each subframe has its own function and may need lots of code. I would therefore define a class for every major subframe you use that is described by lots of code. If you have multiple of those subframe classes, you might even want to have a 'subframes' folder within the frame folder. You can also put those files in the frame folder that are used only by this frame. Lets say you have to perform some complex calculation and define a class that does just that. You can keep this class in a separate file in the frame folder.

  1. Fast GUI loadup

Try not making your computer load a bunch of unncessary stuff when it starts your program. The best case scenario would be that it only has to pack all of your pages/frames and their buttons. Don't make it load huge picture files or anything of that sort. Make it do these thing by the click of a button instead.

  1. Pretty won't win any prizes

...at least that's probably not the case here. I wouldn't spend too much time formatting each button perfectly before moving on, since you are likely to move things around all the time anyway. Once you have all elements that you need packed to a frame, you can think about formatting.

That's all I'm going to share for now. Feel free to ask if you have any follow up questions!

Best, Ansel

  • Related