2
votes

I'm currently working with tkinter for a school-assignment, creating a graphical user-interface for a user to enter their inputs. I have decided to split the inputs in various pages, in order to not overwhelm the user with questions, and not need to scroll.
Each page has a series of Label and Entry, separated on a right and a left divisor, and I've somehow managed to get this to work on each page, with some effort. This is a simplified version of my working code:

import tkinter as tk


class Layers(tk.Frame):
    def __init__(self, root):
        super().__init__(root)

        self.layers = []

        self.layers.append(Welcome_Page(self))
        self.layers.append(Form_1(self))
        self.layers.append(Form_2(self))

        for layer in self.layers:
            layer.add_form(self)
            layer.add_buttons(self)
            layer.grid(row=0, column=0, sticky="nsew")

        self.layers[0].tkraise()


class Welcome_Page(tk.Frame):
    def __init__(self, root):
        super().__init__(root, width=600, height=800, background="red")

    def add_buttons(self, root):
        self.next = tk.Button(self, text="Next page", width=25, height=5, command=self.master.layers[1].tkraise)
        self.next.place(relx=1, rely=1, anchor="se")

        self.prev = tk.Button(self, text="Quit", width=25, height=5, command=self.master.master.destroy)
        self.prev.place(relx=0, rely=1, anchor="sw")
        pass

    def add_form(self, root):
        self.text_label = tk.Label(self, text="Welcome to this program")
        self.text_label.place(relx=0.5, rely=0, anchor="n")
        pass



class Form_1(tk.Frame):
    def __init__(self, root):
        super().__init__(root, width=600, height=800, background="yellow")

    def add_buttons(self, root):
        self.next = tk.Button(self, text="Next page", width=25, height=5, command=self.master.layers[2].tkraise)
        self.next.place(relx=1, rely=1, anchor="se")

        self.prev = tk.Button(self, text="Back", width=25, height=5, command=self.master.layers[0].tkraise)
        self.prev.place(relx=0, rely=1, anchor="sw")
        pass

    def add_form(self, root):
        self.text_label = tk.Label(self, text="Personal data")
        self.text_label.place(relx=0.5, rely=0, anchor="n")

        self.container_left = tk.Frame(self, background="#BAFFCE")
        self.container_right = tk.Frame(self, background="#72FF9A")
        self.container_left.grid(row=0, column=0, sticky="nsew")
        self.container_right.grid(row=0, column=1, sticky="nsew")
        self.grid_columnconfigure(0, weight=1, uniform="group1")
        self.grid_columnconfigure(1, weight=1, uniform="group1")
        self.grid_rowconfigure(0, weight=1)

        self.last_name_label = tk.Label(self.container_right, text="Last name")
        self.last_name_space = tk.Entry(self.container_right, text="lastname")
        self.last_name_label.grid(row=0, column=0, padx=(10,0), pady=(10,0))
        self.last_name_space.grid(row=0, column=1, padx=(5, 0), pady=(10,0))

        pass


class Form_2(tk.Frame):
    def __init__(self, root):
        super().__init__(root, width=600, height=800, background="gray")

    def add_buttons(self, root):
        self.next = tk.Button(self, text="Next page", width=25, height=5)
        self.next.place(relx=1, rely=1, anchor="se")

        self.prev = tk.Button(self, text="Back", width=25, height=5, command=self.master.layers[1].tkraise)
        self.prev.place(relx=0, rely=1, anchor="sw")
        pass

    def add_form(self, root):
        self.text_label = tk.Label(self, text="Third page")
        self.text_label.place(relx=0.5, rely=0, anchor="n")
        pass


if __name__ == '__main__':
    root = tk.Tk()
    root.geometry("600x800")

    window = Layers(root)
    window.pack(expand=True, fill="both")
    root.mainloop()

Nevertheless, while splitting each page into two different Frame() containers I have stumbled across two issues:

  • Setting the internal padding of the frame with ipadx and ipady doesn't seem to do anything. I have manually set each element inside it at its position with padx and pady, though, and this works fine, but I believe I should be able to use the internal padding for this instead on container_left and container_right.
  • Setting the containers for each page is redundant, since they will all be split into two frames. I have tried the following, but it doesn't work as I'm expecting (not at all).
class Layers(tk.Frame):
    def __init__(self, root):
        super().__init__(root)

        self.layers = []

        self.layers.append(Welcome_Page(self))
        self.layers.append(Form_1(self))
        self.layers.append(Form_2(self))

        for layer in self.layers:
            layer.add_form(self)
            layer.add_buttons(self)
            layer.grid(row=0, column=0, sticky="nsew")

            layer.container_left = tk.Frame(layer, background="#BAFFCE")
            layer.container_right = tk.Frame(layer, background="#72FF9A")
            layer.container_left.grid(row=0, column=0, sticky="nsew")
            layer.container_right.grid(row=0, column=1, sticky="nsew")
            layer.grid_columnconfigure(0, weight=1, uniform="group1")
            layer.grid_columnconfigure(1, weight=1, uniform="group1")
            layer.grid_rowconfigure(0, weight=1)

            print(layer)

        self.layers[0].tkraise()

The error I'm getting is AttributeError: 'Form_1' object has no attribute 'container_right'. What I get from this is that I haven't created the variable inside the class, but at some other place instead, even though I'm using layer.. How can I create the variables inside the classes, without reusing the code?

Any other suggestion is appreciated too, as I'm fairly new to Python and Tkinter.

2

2 Answers

0
votes

You can see the effect of ipadx and ipady in your first code by changing line 17 to:

layer.grid(row=0, column=0, sticky="nsew", ipadx=30, ipady=30,)
0
votes

I know you mentioned wanting to keep the class structure the same, but it might be worth it to introduce a superclass for your form pages. From there, you can define add_form and add_buttons instance methods, and just call them internally from the __init__ of the superclass, instead of looping through the pages. So, like:

class FormPage(tk.Frame):
    def __init__(self, parent):
        tk.Frame.__init__(self, parent, width=600, height=800, background="yellow")
        self.parent=parent
    
        self.add_buttons()
        self.add_form()

    def add_buttons(self):
        self.next = tk.Button(self, text="Next page", width=25, height=5)
        self.next.place(relx=1, rely=1, anchor="se")

        self.prev = tk.Button(self, text="Back", width=25, height=5)
        self.prev.place(relx=0, rely=1, anchor="sw")

    def add_form(self):
        self.text_label = tk.Label(self) #use the configure method in child classes to set the text
        self.text_label.place(relx=0.5, rely=0, anchor="n")

        self.container_left = tk.Frame(self, background="#BAFFCE")
        self.container_right = tk.Frame(self, background="#72FF9A")
        self.container_left.grid(row=0, column=0, sticky="nsew")
        self.container_right.grid(row=0, column=1, sticky="nsew")
        self.grid_columnconfigure(0, weight=1, uniform="group1")
        self.grid_columnconfigure(1, weight=1, uniform="group1")
        self.grid_rowconfigure(0, weight=1)

So, the FormPage's initialisation will call the add_buttons and add_form methods automatically when a form page is instantiated. Then for a specific form, you could do:

class Form_1(FormPage):
    def __init__(self, parent):
        tk.Frame.__init__(parent) #call FormPage's initialisation

    def add_buttons(self):
        self.next.configure(command=self.parent.layers[1].tkraise)
        ...#add the specific functionality of the buttons using configure
    
    def add_form(self):
        super().add_form()
        self.text_Label.configure(text="Personal Information")

        self.last_name_label = tk.Label(self.container_right, text="Last name")
        self.last_name_space = tk.Entry(self.container_right, text="lastname")
        self.last_name_label.grid(row=0, column=0, padx=(10,0), pady=(10,0))
        self.last_name_space.grid(row=0, column=1, padx=(5, 0), pady=(10,0))

So, any child of FormPage has the attributes container_left and container_right. Then, if certain forms need more button, you can just override the method add_buttons in that form's class. Likewise for any other method. Then, you just need a place to store all your pages, like your Layers class. IMO, you don't need to place all the layers on a grid, because you'll be calling tkraise from the navigation buttons anyway. I think that your Layers class could be reduced to:

class Layers(tk.Tk):
    def __init__(self):
        super().__init__(self)

        self.layers = []

        for F in {Welcome_Page, Form_1, Form_2}:
            self.layers.append(F(self))

        self.layers[0].tkraise()

In general, inheritance hierarchies are a great way to reduce code repetition. Hope this helps :)