Tkinter, update widgets real time if list is modified

760 Views Asked by At

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

2

There are 2 best solutions below

1
Delrius Euphoria On BEST ANSWER

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.

5
V21 On

So I've decided that if I want to click a button, that button should be able to update the list. Hence, I bind a non-related buttons in the widget to this function:

   def sidebar_button_event(self):
    global x
    x=10
    self.scrollable_frame.destroy()
    self.after(0,self.update())

Which will then call for an update function that store the change value, and the update function will just simply overwrite the grid:

    def update(self):
    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)