Tkinter scrollbar not updating to cover expanded canvas

Question:

I’m having a scrollbar issue with a Tkinter GUI that I’m creating. The GUI contains a class Gen_Box that reproduces a given widget vertically downwards as many times as the widgets add_btn is called. Obviously, this runs off the window frame pretty quickly.

I’ve tried adding a scrollbar to account for this running off the frame. I know these are pretty complicated to add in tkinter and require some hacking around. I referenced this video by Codemy. While I can see the scrollbar to the right of the frame, it doesn’t expand accomodate the added widgets (see screenshots below). I’ve been tinkering around with this all day but am at a loss.

Starting frame:
Starting frame
Frame after second Monitor widget is added:
Frame after second Monitor widget is added

Also as a note, I’m using CustomTkinter to stylize the widgets.

Current code. Scrollbar is set in the Form.__init__() function. Any help on this issue or other input here is much appreciated.

class Form(tk.Frame):
    def __init__(self, root):
        self.root = root

        self.main_frame = tk.Frame(self.root)
        self.main_frame.grid(sticky='NSEW')
        
        self.canvas = tk.Canvas(self.main_frame, height=750, width=750)
        self.canvas.grid(row=0, column=0, sticky='NSEW')
        self.canvas.grid_rowconfigure(0, weight=1)

        self.scrollbar = tk.Scrollbar(self.main_frame, orient=VERTICAL)
        self.scrollbar.config(command=self.canvas.yview)
        self.scrollbar.grid(row=0, column=1, sticky='NSE')

        self.canvas.config(yscrollcommand=self.scrollbar.set)
        self.canvas.bind("<Configure>", lambda e: self.canvas.configure(scrollregion=self.canvas.bbox(ALL)))

        self.inner_frame = tk.Frame(self.canvas)
        self.canvas.create_window((0,0), window=self.inner_frame, anchor='nw')        
        self.load()
        

    def load(self):
        self.right = ctk.CTkFrame(self.inner_frame, bg_color='#09275d', fg_color='#09275d', height=750, width=750)
        

        basics_label = ctk.CTkLabel(self.right, text='Setup', text_font=('Helvetica', 24), text_color='#f5d397')
        basics_label.grid(row=0, column=0, sticky='NW', pady=10)
        name_frame = ctk.CTkFrame(self.right, fg_color='#E4E7F1', bg_color='#09275d')
        name_frame.grid(row=1, columnspan=2, sticky=NW,pady=5)
        name_label = ctk.CTkLabel(name_frame, text='Name', text_font=("MS Sans Serif", 15))
        name_label.grid(row=1, column=0, padx=2, pady=2, sticky=NW)
        name_entry = ctk.CTkEntry(name_frame, width=250, border_width=1, corner_radius=10)
        name_entry.grid(row=1, column=1, padx=2, pady=2)

        start_frame = ctk.CTkFrame(self.right, corner_radius=2, fg_color='#E4E7F1', bg_color='#09275d')
        start_frame.grid(row=2, columnspan=2, sticky=NW,pady=5)
        start_url_label = ctk.CTkLabel(start_frame, text='Start URL', text_font=("MS Sans Serif", 15))
        start_url_label.grid(row=2, column=0, sticky=NW, pady=2, padx=2)
        start_urls_frame = ctk.CTkFrame(start_frame, fg_color='#E4E7F1', bg_color='#09275d')
        start_urls_frame.grid(row=2, column=1, pady=2, padx=2)
        start_urls = Gen_Box(start_urls_frame, Include)

        include_frame = ctk.CTkFrame(self.right, fg_color='#E4E7F1', bg_color='#09275d', corner_radius=5)
        include_frame.grid(row=3, columnspan=2, sticky=NW,pady=5)
        include_url_label = ctk.CTkLabel(include_frame, text='Include URL', text_font=("Myriad", 15))
        include_url_label.grid(row=3, column=0, sticky=NW, padx=2, pady=2)
        include_url_entry = ctk.CTkFrame(include_frame, fg_color='#E4E7F1', bg_color='#09275d')
        include_url_entry.grid(row=3, column=1, columnspan=2, padx=2, pady=2)
        url_patterns = Gen_Box(include_url_entry, Include)

        depth_frame = ctk.CTkFrame(self.right, fg_color='#E4E7F1', bg_color='#09275d', corner_radius=5)
        depth_frame.grid(row=4, columnspan=2, sticky=NW, pady=5)
        depth_label = ctk.CTkLabel(depth_frame, text='Max Depth', text_font=("Myriad", 15))
        depth_label.grid(row=4, column=0, sticky=NW, padx=2, pady=2)
        depth_level = ctk.IntVar(depth_frame)
        depth_level.set(1)
        depth_options = ctk.CTkOptionMenu(master=depth_frame, variable=depth_level, values=[str(i) for i in range(1,11)], fg_color='white', button_color='#6AA6DE', width=25, corner_radius=10)
        depth_options.grid(row=4, column=1, sticky=NW, padx=2, pady=2)

        profile_frame = ctk.CTkFrame(self.right, fg_color='#E4E7F1', bg_color='#09275d', corner_radius=5)
        profile_frame.grid(row=5, columnspan=2, sticky=NW, pady=5)
        profile_label = ctk.CTkLabel(profile_frame, text='Chrome Profile', text_font=("Myriad", 15))
        profile_label.grid(row=5, column=0, sticky=NW, padx=2, pady=2)
        profile_level = ctk.StringVar(profile_frame)
        profile_level.set('False')
        profile_options = ctk.CTkOptionMenu(master=profile_frame, variable=profile_level, values=['True', 'False'], fg_color='white', button_color='#6AA6DE', width=25, corner_radius=10)
        profile_options.grid(row=5, column=1, sticky=NW, padx=2, pady=2)

        headless_frame = ctk.CTkFrame(self.right, fg_color='#E4E7F1', bg_color='#09275d', corner_radius=5)
        headless_frame.grid(row=6, columnspan=2, sticky=NW, pady=5)
        headless_label = ctk.CTkLabel(headless_frame, text='Headless', text_font=("Myriad", 15))
        headless_label.grid(row=6, column=0, sticky=NW, padx=2, pady=2)
        headless_level = ctk.StringVar(headless_frame)
        headless_level.set('True')
        headless_options = ctk.CTkOptionMenu(master=headless_frame, variable=headless_level, values=['True', 'False'], fg_color='white', button_color='#6AA6DE', width=25, corner_radius=10)
        headless_options.grid(row=6, column=1, sticky=NW, padx=2, pady=2)

        delay_frame = ctk.CTkFrame(self.right, fg_color='#E4E7F1', bg_color='#09275d', corner_radius=5)
        delay_frame.grid(row=7, columnspan=2, sticky=NW, pady=5)
        delay_label = ctk.CTkLabel(delay_frame, text='Delay', text_font=("Myriad", 15))
        delay_label.grid(row=7, column=0, sticky=NW, padx=2, pady=2)
        delay_level = ctk.IntVar(delay_frame)
        delay_level.set(0)
        delay_options = ctk.CTkOptionMenu(master=delay_frame, variable=delay_level, values=[str(i) for i in range(1,11)], fg_color='white', button_color='#6AA6DE', width=25, corner_radius=10)
        delay_options.grid(row=7, column=1, sticky=NW, padx=2, pady=2)

        download_frame = ctk.CTkFrame(self.right, fg_color='#E4E7F1', bg_color='#09275d')
        download_frame.grid(row=8, columnspan=2, sticky=NW,pady=5)
        download_label = ctk.CTkLabel(download_frame, text='Download Path', text_font=("MS Sans Serif", 15))
        download_label.grid(row=8, column=0, padx=2, pady=2, sticky=NW)
        download_entry = ctk.CTkEntry(download_frame, width=350, border_width=1, corner_radius=10)
        download_entry.grid(row=8, column=1, padx=2, pady=2)

        self.monitors_label = ctk.CTkLabel(self.right, text='Monitors', text_font=('Helvetica', 24), text_color='#f5d397')
        self.monitors_label.grid(sticky='NW', row=9, column=0, pady=10)
        self.monitor_frame = ctk.CTkFrame(self.right, bg_color='#09275d', fg_color='#09275d')
        Gen_Box(self.monitor_frame, Monitor)
        self.monitor_frame.grid(row=10, sticky='NW')

        self.right.grid(row=0)


class Gen_Box:
    def __init__(self, master, gen_obj, existing=None):
        self.master = master
        self.gen_obj = gen_obj
        self.existing = existing
        self.load(existing)

    def load(self, existing=None):
        if existing:
            self.obj_rows = [self.gen_obj(key) for key in existing.keys()]
        else:
            self.obj_rows = [self.gen_obj(0)]

        self.frame = ctk.CTkFrame(self.master, fg_color='#09275d')
        
        for index, obj in enumerate(self.obj_rows):

            add = lambda row=index+1 : self.add(row)
            remove = lambda row=index : self.remove(row)
            obj.row_no = index

            if existing:
                obj.load(self.frame, existing=existing[index])
            else:
                obj.load(self.frame)
            
            obj.add_btn.configure(command=add)
            obj.remove_btn.configure(command=remove)

        self.frame.grid(sticky='NSEW')
        self.master.update()
        

    def add(self, row):
        self.obj_rows.insert(row, self.gen_obj(row))
        existing_entries = self.save_existing()
        self.frame.destroy()
        self.load(existing_entries)


    def remove(self, row):
        self.obj_rows.pop(row)
        existing_entries = self.save_existing()
        self.frame.destroy()
        self.load(existing_entries)
        

    def save_existing(self):
        existing_dict = {}

        for index, object in enumerate(self.obj_rows):
            try:
                existing_dict[index] = object.generate_scheme()
            except AttributeError as e:
                existing_dict[index] = None

        return existing_dict


class Monitor:
    def __init__(self, row_no):
        self.row_no = row_no

    def generate_scheme(self):
        return_dict = {}
        scheme_dict = {
                'output': self.output.get,
                'includes': self.includes.save_existing,
                'selectors': self.selectors.save_existing
        }

        for key, value in scheme_dict.items():
            return_dict[key] = value()

        return return_dict

    
    def load(self, frame, existing=None):
        self.monitor_frame = ctk.CTkFrame(frame, fg_color='#E4E7F1', bg_color='#09275d')
        
        self.button_frame = ctk.CTkFrame(frame, bg_color='#09275d', fg_color='#09275d')
        self.button_frame.grid(row=self.row_no, column=1, sticky='NSEW', pady=10, padx=2)
        
        self.add_btn = ctk.CTkButton(self.button_frame, text='+', corner_radius=2, width=50, height=50, fg_color='#ecae3e')
        self.add_btn.grid(row=self.row_no, column=0, padx=10)
        self.remove_btn = ctk.CTkButton(self.button_frame, text='-', corner_radius=2, width=50, height=50, fg_color='#ecae3e')
        self.remove_btn.grid(row=self.row_no, column=1, padx=10)

        self.top_frame = ctk.CTkFrame(self.monitor_frame)
        self.top_frame.grid(sticky='NSEW', pady=10, padx=10)
        self.include_frame = ctk.CTkFrame(self.top_frame, fg_color='#E4E7F1', bg_color='#E4E7F1')
        self.include_frame.grid(row=0, sticky='NSEW')
        self.include_label = ctk.CTkLabel(self.include_frame, text='Include URLs', text_font=("Myriad", 15))
        self.include_label.grid(row=0, sticky='NW')
        if not existing:
            self.includes = Gen_Box(self.include_frame, Include)
        else:
            self.includes = Gen_Box(self.include_frame, Include, existing=existing['includes'])

        self.selector_frame = ctk.CTkFrame(self.top_frame, fg_color='#E4E7F1', bg_color='#E4E7F1')
        self.selector_frame.grid(row=1, sticky='NSEW')
        self.selector_label = ctk.CTkLabel(self.selector_frame, text='Selectors (Optional)', text_font=("Myriad", 15))
        self.selector_label.grid(row=1, sticky='NW')
        if not existing:
            self.selectors = Gen_Box(self.selector_frame, Include)
        else:
            self.selectors = Gen_Box(self.selector_frame, Include, existing=existing['selectors'])

        self.output_frame = ctk.CTkFrame(self.monitor_frame, fg_color='#E4E7F1', bg_color='#E4E7F1')
        self.output_frame.grid(row=2, pady=5, padx=2, sticky='NSEW', columnspan=2)
        self.output_label = ctk.CTkLabel(self.output_frame, text='Output', text_font=("Myriad", 15))
        self.output_label.grid(row=2, column=0)
        self.output = ctk.StringVar(self.output_frame)
        if existing:
            self.output.set(existing['output'])
        else:
            self.output.set('TXT')
        self.output_options = ['TXT', 'PDF']
        self.output_menu = ctk.CTkOptionMenu(self.output_frame, variable=self.output, values=self.output_options, fg_color='white', button_color='#6AA6DE', width=25, corner_radius=10)
        self.output_menu.grid(row=2, column=1)

        self.actions_frame = ctk.CTkFrame(self.monitor_frame, bg_color='#E4E7F1', fg_color='#E4E7F1')
        self.actions_frame.grid(row=3, pady=5)
        self.add_actions_btn = ctk.CTkButton(self.actions_frame, text='Actions Editor ▼', text_font=("Myriad", 10), fg_color='#3eecae', corner_radius=10)
        self.add_actions_btn.grid(sticky='NSEW')
        self.add_actions_btn.configure(width=self.actions_frame.winfo_width())

        self.monitor_frame.grid(row=self.row_no, pady=10, padx=10)
        frame.update()



class Include:
    def __init__(self, row_no):
        self.row_no = row_no

    def generate_scheme(self):
        return_dict = {}
        scheme_dict = {
            'entry': self.entry.get
        }

        for key, value in scheme_dict.items():
            return_dict[key] = value()

        return return_dict

    def load(self, master, existing=None):
        entry_frame = ctk.CTkFrame(master, bg_color='#E4E7F1', fg_color='#E4E7F1')
        self.entry = ctk.CTkEntry(entry_frame, corner_radius=10, border_width=1, width=300, bg_color='#E4E7F1')
        if existing:
            self.entry.insert(END, existing['entry'])
        self.entry.grid(row=self.row_no, column=0, pady=2, ipadx=2, sticky=NSEW)        
        button_frame = ctk.CTkFrame(master, bg_color='#E4E7F1', fg_color='#E4E7F1')
        self.add_btn = ctk.CTkButton(button_frame, text='+', height=10, width=10, corner_radius=5, text_font=("Helvetica", 12), fg_color='#6AA6DE', bg_color='#E4E7F1', text_color='#E4E7F1')
        self.add_btn.grid(row=self.row_no, column=1, padx=3, pady=5)
        self.remove_btn = ctk.CTkButton(button_frame, text='-', height=10, width=10, corner_radius=5, text_font=("Helvetica", 12), fg_color='#6AA6DE', bg_color='#E4E7F1', text_color='#E4E7F1')
        self.remove_btn.grid(row=self.row_no, column=2, padx=3, pady=5)
        entry_frame.grid(row=self.row_no, column=0, sticky='NSEW')
        button_frame.grid(row=self.row_no, column=1, sticky='NSEW')




            




root = ctk.CTk()
root.configure(bg='#09275d')
root.geometry('1000x650')
Form(root)
root.mainloop()

Asked By: Frank Chlumsky

||

Answers:

Try modifying the scrollregion. See the example at https://dafarry.github.io/tkinterbook/canvas.htm which uses event.x and event.y to get the size.

Answered By: Curly Joe

When a new monitor section is added, it is the inner frame (self.inner_frame) get resized, not the canvas (self.canvas). So the <Configure> event should be bound on the inner frame instead of the canvas:

class Form(tk.Frame):
    def __init__(self, root):
        ...
        self.canvas.config(yscrollcommand=self.scrollbar.set)
        ### ----- don't bind on canvas
        #self.canvas.bind("<Configure>", lambda e: self.canvas.configure(scrollregion=self.canvas.bbox(ALL)))

        self.inner_frame = tk.Frame(self.canvas)
        ### ----- bind on inner frame instead
        self.inner_frame.bind("<Configure>", lambda e: self.canvas.configure(scrollregion=self.canvas.bbox(ALL)))
        self.canvas.create_window((0,0), window=self.inner_frame, anchor='nw')
        self.load()

    ...
Answered By: acw1668