pythontkinterwidgetcustomtkintermainloop

Tkinter, update widgets real time if list is modified


Let say I've a list of widgets that are generated by tkinter uisng a loop (it's customtkinter in this case but since tkinter is more well known so I think it'd be better to make an example with it), each widgets lie in the same frame with different label text. Here is an example for the code:

    x=0
    self.scrollable_frame = customtkinter.CTkScrollableFrame(self, label_text="CTkScrollableFrame")
    self.scrollable_frame.grid(row=1, column=2, padx=(20, 0), pady=(20, 0), sticky="nsew")
    self.scrollable_frame.grid_columnconfigure(0, weight=1)
    self.scrollable_frame_switches = []
    for i in range(x,100):
        switch = customtkinter.CTkSwitch(master=self.scrollable_frame, text=f"CTkSwitch {i}")
        switch.grid(row=i, column=0, padx=10, pady=(0, 20))
        self.scrollable_frame_switches.append(switch)

demonstration

My question is, if the list that help generated those widgets change (in this case it's just a loop ranging from 0-100, might change the widgets text, list size..), what would be the best way for real time update the tkinter window contents?

Ps: I've tried to look for my answer from many places but as of right now, the best answer I can come up with is to update the whole frame with same grid but changed list content, I'll put it bellow. Is there any way better than this? Thank you


Solution

  • Like I said before, while the existing answer might work, it might be inefficient since you are destroying and creating new widgets each time there is a change. Instead of this, you could create a function that will check if there is a change and then if there is extra or less items, the changes will take place:

    from tkinter import *
    import random
    
    root = Tk()
    
    
    def fetch_changed_list():
        """Function that will change the list and return the new list"""
        MAX = random.randint(5, 15)
    
        # Create a list with random text and return it
        items = [f'Button {x+1}' for x in range(MAX)]
        return items
    
    
    def calculate():
        global items
    
        # Fetch the new list
        new_items = fetch_changed_list()
    
        # Store the length of the current list and the new list
        cur_len, new_len = len(items), len(new_items)
    
        # If the length of new list is more than current list then
        if new_len > cur_len:
            diff = new_len - cur_len
    
            # Change text of existing widgets
            for idx, wid in enumerate(items_frame.winfo_children()):
                wid.config(text=new_items[idx])
    
            # Make the rest of the widgets required
            for i in range(diff):
                Button(items_frame, text=new_items[cur_len+i]).pack()
    
        # If the length of current list is more than new list then
        elif new_len < cur_len:
            extra = cur_len - new_len
    
            # Change the text for the existing widgets
            for idx in range(new_len):
                wid = items_frame.winfo_children()[idx]
                wid.config(text=new_items[idx])
    
            # Get the extra widgets that need to be removed
            extra_wids = [wid for wid in items_frame.winfo_children()
                          [-1:-extra-1:-1]]  # The indexing is a way to pick the last 'n' items from a list
    
            # Remove the extra widgets
            for wid in extra_wids:
                wid.destroy()
    
            # Also can shorten the last 2 steps into a single line using
            # [wid.destroy() for wid in items_frame.winfo_children()[-1:-extra-1:-1]]
    
        items = new_items  # Update the value of the main list to be the new list
        root.after(1000, calculate)  # Repeat the function every 1000ms
    
    
    items = [f'Button {x+1}' for x in range(8)]  # List that will keep mutating
    
    items_frame = Frame(root)  # A parent with only the dynamic widgets
    items_frame.pack()
    
    for item in items:
        Button(items_frame, text=item).pack()
    
    root.after(1000, calculate)
    
    root.mainloop()
    

    The code is commented to make it understandable line by line. An important thing to note here is the items_frame, which makes it possible to get all the dynamically created widgets directly without having the need to store them to a list manually.

    The function fetch_changed_list is the one that changes the list and returns it. If you don't want to repeat calculate every 1000ms (which is a good idea not to repeat infinitely), you could call the calculate function each time you change the list.

    def change_list():
        # Logic to change the list
        ...
    
        calculate() # To make the changes
    

    After calculating the time for function executions, I found this:

    Widgets redrawn Time before (in seconds) Time after (in seconds)
    400 0.04200148582458496 0.024012088775634766
    350 0.70701003074646 0.21500921249389648
    210 0.4723021984100342 0.3189823627471924
    700 0.32096409797668457 0.04197263717651367

    Where "before" is when destroying and recreating and "after" is only performing when change is needed.