2

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

V21
  • 57
  • 6
  • For the purpose of providing an answer, can we assume that the length of the list doesn't change, or could it be that some widgets will need to be deleted or added? – Bryan Oakley Feb 12 '23 at 19:45
  • @BryanOakley Hi Bryan, in this case, the length of the list do change, sometime the whole content of it change as well since I'm trying to implement sorting,adding and deleting functionalities. Thank you – V21 Feb 12 '23 at 19:47
  • You will have to create a function that gets called each time the list changes. And inside that you could use tkinter _control variable_ to update the value – Delrius Euphoria Feb 12 '23 at 20:27
  • Hi @DelriusEuphoria is that tkinter control variable you mention is the config that will modify the value for each of them? – V21 Feb 12 '23 at 20:34
  • It is `StringVar`, `IntVar` and so on – Delrius Euphoria Feb 12 '23 at 20:37
  • from what I understand, wouldn't that just change the value of each widget instead of increase it amount or decrease it? – V21 Feb 12 '23 at 20:41
  • What exactly are you looking for. If the list becomes upto `range(50)`, you want the amount of widgets to also reduce? What about the widget text (how would they change with respect to the size of the list)? – Delrius Euphoria Feb 12 '23 at 21:11
  • @DelriusEuphoria, yes it is exactly what I need, I'm trying to find a way to change the size of the whole list that'd affect the widget as well, decrease the total amount of them and if I decided to delete one and add in another with different name for example then it'd also change according to that, basically real time update that frame. So for example if it's only range to (50) then that'd be CTkswitch1- 50 and that's it. There's one way that I found and I just wanted to know if there's any better way to do this – V21 Feb 12 '23 at 22:11
  • 1
    Okay, the way you are doing it does seem intuitive, but it would be inefficient to create and destroy these widgets each time. Maybe you can think of a better approach like check the difference between the existing list and the new updated list to see the if there are more items or less. From there, if there are more items then you could update the text of the existing widget and then make the remaining extra widgets required. If there is less, then you could change the text of the widgets and then delete the extra widgets – Delrius Euphoria Feb 12 '23 at 22:40

2 Answers2

4

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.

Delrius Euphoria
  • 14,910
  • 3
  • 15
  • 46
0

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)
V21
  • 57
  • 6
  • If you don't destroy the old widgets, you've just created a memory leak where you just keep using more and more memory for all of the new widgets. – Bryan Oakley Feb 12 '23 at 20:00
  • ohh, thank you for mentioning, I'll add that in right now – V21 Feb 12 '23 at 20:02
  • The delay range between each update is what I wanted to improve the most :( so adding it in just lengthened the delay by like 1/3 more – V21 Feb 12 '23 at 20:04
  • Also, it's likely going to be far more efficient to just modify the existing widgets rather than destroy and recreate them. – Bryan Oakley Feb 12 '23 at 20:04
  • But in this case scenario where whole content might change, wouldn't it be better to replace the frame?(the name of CTkScrollableFrame still remain the same though, just the switch generated changed) I got stuck when figuring how to delete all those invisible switch so I just decided to go with the frame – V21 Feb 12 '23 at 20:05