Search code examples
pythonclasstkintersubclass

How to create a Frame/Canvas using class in Python and use it many times with different widgets inside it?


I am new in Python. I am developing a program which actually is a small shell for phpMyAdmin. In my program I need in some Windows (which is created by TKinter) draw a scrolled canvas. I've written a class like this to create a Scrolled Canvas. It's working great when I use it in every where but problem is I need different widgets(Like: Buttons/ Labels/ Entry/ Combo-box, ... ) inside it in every Windows. I looked over the net and found a solution (How to create Tkinter Widgets inside Parent class from Sub class) but that one makes my program so complicated. So I'm looking for a better solution that keeps my code simple. Here is my Class which create Scrolled Form/Canvas:

class ScrolledCanvas(tk.Frame):
    def __init__(self, master_frame, back_color, label_frame_text, border, x, y, w, h):
        super(ScrolledCanvas, self).__init__()
        dynamic_widget_frame = LabelFrame(master_frame, relief=SUNKEN, bg=back_color,
                                          text=label_frame_text, bd=border)
        dynamic_widget_frame.place(x=x, y=y)
        mycanvas = Canvas(dynamic_widget_frame, width=w, height=h)
        mycanvas.pack(side=LEFT)
        yscollbar = ttk.Scrollbar(dynamic_widget_frame, orient='vertical', command=mycanvas.yview)
        yscollbar.pack(side=RIGHT, fill='y')
        mycanvas.configure(yscrollcommand=yscollbar.set)
        myframe = Frame(mycanvas)
        mycanvas.create_window((0, 0), window=myframe, anchor="nw")
        mycanvas.bind('<Configure>', lambda es: mycanvas.configure(scrollregion=mycanvas.bbox('all')))
        close_btn = Button(myframe, text='X', command=dynamic_widget_frame.destroy, fg='red')
        close_btn.grid()

This sample code explains how we can create a widget inside a class from outside it. But makes my code extremely long and complicated. Because I've multi different widgets in Windows.

import tkinter as tk

root = tk.Tk()

class MainApp(tk.Frame):
    def __init__(self, master):
        super().__init__(master)

        #a child frame of MainApp object
        self.frame1 = tk.Frame(self)

        tk.Label(self.frame1, text="This is MainApp frame1").pack()

        self.frame1.grid(row=0, column=0, sticky="nsew")


        #another child frame of MainApp object
        self.frame2 = SearchFrame(self)

        self.frame2.grid(row=0, column=1, sticky="nsew")




    def create_labels(self, master):
        return tk.Label(master, text="asd")


class SearchFrame(tk.Frame):
    def __init__(self, master):
        super().__init__(master)

        self.label = tk.Label(self, text="this is SearchFrame")
        self.label.pack()

        master.label1 = MainApp.create_labels(self, master)

        master.label1.grid()


mainAppObject = MainApp(root)
mainAppObject.pack()

root.mainloop()

Solution

  • First off, you're not creating the class correctly. You aren't passing master when you call super().__init__, and then you're explicitly putting the child widgets in the master.

    The proper way to create a class that behaves like a widget is to pass the master to super.__init__, and then make sure every child widget is a child of the instance or a descendant of the widget.

    class ScrolledCanvas(tk.Frame):
        def __init__(self, master_frame, ...):
            super().__init__(master_frame)
    
            dynamic_widget_frame = LabelFrame(self, ...)
    
            ...
    

    Next, make sure that my_frame is an instance attribute so that it can be referenced outside of the class. I would also give it a slightly better name:

    class ScrolledCanvas(tk.Frame):
        def __init__(self, master_frame, ...):
            ...
            self.frame = tk.Frame(mycanvas)
            mycanvas.create_window((0, 0), window=self.frame, anchor="nw")
            ...
    

    Once you do these two things, you can create as many instances as you want and put them anywhere in your UI. You can then add additional widgets to the frame attribute of the object.

    # create a scrolled canvas in the root
    root = tk.Tk()
    sc1 = ScrolledCanvas(root, ...)
    sc1.pack(fill="both", expand=True)
    
    # add widgets to this scrolled frame
    label1 = tk.Label(sc1.frame, text="I am in the first window")
    label1.pack(side="top")
    
    # create another scrolled frame in a popup window
    popup = tk.Toplevel(root)
    sc2 = ScrolledCanvas(popup, ...)
    sc2.pack(fill="both", expand=True)
    
    # add widgets to this second scrolled frame
    label2 = tk.Label(sc2.frame, text="This is in the popup")
    label2.pack(side="top")
    

    One last step is to be sure to bind to the <Configure> event of the inner frame so that you can reconfigure the scrollregion when the frame changes size.

    class ScrolledCanvas(tk.Frame):
        def __init__(self, master_frame, ...):
            ...
            self.frame.bind("<Configure>", lambda event: mycanvas.configure(scrollregion=mycanvas.bbox("all"))
            ...