Search code examples
pythonpython-3.xtkintertkinter-layout

Attempting to nest several frames within a single frame with Tkinter. How do I accomplish this in an object oriented fashion?


My code basically does this:

my problem

Which is clearly not what I want to try. For further clarification I would like my window to look similar to this:

an ideal solution

from tkinter import *
import tkinter as tk
from tkinter import ttk

root = tk.Tk()

class Encoding(tk.Tk):
    def __init__(self, parent, *args, **kwargs):
        tk.Frame.__init__(self, parent, *args, **kwargs)
        self.mode = StringVar()
##      If I remove the next line it breaks it entirely.
        self.encoding_frame = ttk.Frame(parent)
        self.encrypt = ttk.Radiobutton(self.encoding_frame, text='Encrypt', variable=self.mode, value='encrypt')
        self.decrypt = ttk.Radiobutton(self.encoding_frame, text='Decrypt', variable=self.mode, value='decrypt')
        self.encrypt.grid(column=0, row=0, ipadx=2, sticky=W)
        self.decrypt.grid(column=0, row=1, ipadx=2, sticky=W)
        self.encoding_frame.grid(column=0, columnspan=3, row=2, sticky=S)


class MainApplication(tk.Frame, Encoding):
    # Create a main frame here.
    # Would like frames to be nested within this frame. This seems redundant but nesting with a main
    # frame allows for consistent themes, and gives additional control of layout between subframes.
    # Inheritance is confusing.
    def __init__(self, parent, *args, **kwargs):
        tk.Frame.__init__(self, parent, *args, **kwargs)
        self.main_frame = ttk.LabelFrame(parent, text="Main Window", width=500, height=500)
        self.main_frame['borderwidth'] = 3
        self.main_frame['relief'] = 'raised'
        self.main_frame.grid(column=0, row=0)
        self.encoding = Encoding(self)
##      I wrote the following line hoping that I could have main_frame become the parent frame.
        self.encoding.encoding_frame = ttk.LabelFrame(self.main_frame)


if __name__ == "__main__":
    app = MainApplication(root)
    root.mainloop()

I am clearly not getting something right. The whole reason I rewrote the program is so that I could gain a greater understanding/confidence with object oriented code. I am hoping that I can get better insight with this, so any help would be amazing.


Solution

  • There are are several problems going on with your code.

    Perhaps the biggest problem is that Encoding inherits from tk.Tk, MainApplication inherits from tk.Frame and Encoding (making it both a root window and a frame), and then MainApplication creates an instance of Encoding. Plus, you explicitly create another instance of tk.Tk(), giving you two root windows. That all needs to be untangled.

    Inheritance create a "is a" relationship. By having MainApplication inherit from Encoding you are saying that MainApplication is a Encoding object. That is not the case in your code - an Encoding object represents only a small part of the application. For that you want composition, not inheritance, ie: MainApplication has a Encoding object.

    So, the first step is to remove Encoding from the list of classes that MainApplication inherits from.

    Another thing that can probably be removed is self.encoding_frame. I see no reason to have it since MainApplication itself is a frame. Instead, have MainApplication inherit from ttk.LabelFrame rather than tk.Frame.

    The final thing is that since MainApplication creates Encoding, it should be responsible for calling grid or pack on the instance of Encoding.

    Altogether, MainApplication can be pared down to this:

    class MainApplication(ttk.LabelFrame):
        def __init__(self, parent, *args, **kwargs):
            ttk.LabelFrame.__init__(self, parent, *args, **kwargs)
    
            self.configure(text="Main Window")
            self['borderwidth'] = 3
            self['relief'] = 'raised'
    
            self.encoding = Encoding(self)
            self.encoding.grid(row=0, column=0, sticky="ew")
    

    That's not 100% complete, but it's a good place to start. Based on your image I'm guessing you'll have other classes for other parts of the main application -- the message widget, the key widgets, and the transcription window.

    For Encoding, much of the same advice applies. Since it's only part of the application, it shouldn't inherit from tk.Tk. Instead, you can inherit from ttk.Frame and then remove self.encoding_frame since the Encoding object itself is already a frame.

    With those changes, Encoding should look something like the following. Notice how the radiobuttons have self as their parent. If you're creating proper objects, all widgets inside the class need to be a child of the class itself or one of its descendants. A class like this should never put anything in parent except itself.

    class Encoding(ttk.Frame):
        def __init__(self, parent, *args, **kwargs):
            ttk.Frame.__init__(self, parent, *args, **kwargs)
    
            self.mode = StringVar()
            self.encrypt = ttk.Radiobutton(self, text='Encrypt', variable=self.mode, value='encrypt')
            self.decrypt = ttk.Radiobutton(self, text='Decrypt', variable=self.mode, value='decrypt')
    
            self.encrypt.grid(column=0, row=0, ipadx=2, sticky=W)
            self.decrypt.grid(column=0, row=1, ipadx=2, sticky=W)
    

    Finally, since MainApplication is now a frame -- instead of inheriting from Encoding which inherits from tk.Tk -- the block of code that creates an instance of MainApplication needs to be responsible for calling pack or grid. Since MainApplication is the only widget directly inside of the root window, pack is the best choice since you don't have to remember to configure row and column weights to get proper behavior when the window is resized.

    Also, I recommend creating root in the same block rather than at the very start of the program.

    Your bottom block of code should look like this:

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