Home > Enterprise >  Tkinter scrollbar not updating to cover expanded canvas
Tkinter scrollbar not updating to cover expanded canvas

Time:09-07

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

CodePudding user response:

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

    ...

CodePudding user response:

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.

  • Related