TomSchimansky / CustomTkinter

A modern and customizable python UI-library based on Tkinter
MIT License
10.84k stars 1.02k forks source link

Tabview Binding On Tab Change #2278

Closed CatInAHatIsBack closed 4 months ago

CatInAHatIsBack commented 4 months ago

Background

Hi. I'm making a UI that creates tabs in a tab view when you select items from a tree and displays information in that tab. There seems to be a problem with CTK or Tkinter where for example after changing a tab or a frame, subframes don't always render, this can usually be fixed with a self.component.update_idletasks() or something similar.

Problem

After pressing the tab the frame inside does not render. There are 2 ways to make the tab / frame render 1) when i move my cursor away from the tab button i just clicked 2) I call update_idletasks().

What i need

I need an event similar to TreeviewSelect for TreeView or some workaround. The event can be triggered on tab select, change, click etc...

I have tried with new_tab.bind("<Button-1>", command=self.myRefresh) but does not change when i click a tab button, but the frame of the tab

Code

class MyTabView(ctk.CTkTabview):
    def __init__(self, master, **kwargs):
        super().__init__(master, **kwargs)
        self.tab_contents = {}  # To keep track of the content of each tab

    def add_tab_with_content(self, tab_name, tab_dict, fields_and_values):

        if tab_name in self.tab_contents:
            self.set(tab_name)
            self.myRefresh() 
            raise ValueError(f"Tab named '{tab_name}' already exists.")

        new_tab = self.add(tab_name)  # Add the tab

        ## Seems to bind to the frame and not the button
        new_tab.bind("<Button-1>", command=self.myRefresh)

        self.tab_contents[tab_name] = {"dict": tab_dict, "entry_widgets": {}}

        # Get the frame for this tab and populate it
        self.tab_frame = self.tab(tab_name)
        self.populate_tab(self.tab_frame, tab_name, tab_dict, fields_and_values)

        self.set(tab_name)

    def populate_tab(self, tab_frame, tab_name, tab_dict, fields_and_values):

        scrollable_frame = ctk.CTkScrollableFrame(tab_frame)
        scrollable_frame.pack(fill='both', expand=True, padx=10, pady=10)

        # Display dictionary items in the scrollable frame
        dict_frame = ctk.CTkFrame(scrollable_frame, corner_radius=10)  # Specify corner radius for rounded corners if desired
        dict_frame.pack(fill='x', padx=10, pady=(10, 0), expand=True)

        for key, value in tab_dict.items():
            label = ctk.CTkLabel(dict_frame, text=f"{key}: {value}")
            label.pack(pady=(5, 5), padx= (50,0), expand=True,  anchor="w")

        input_fields_frame = ctk.CTkFrame(scrollable_frame)
        input_fields_frame.pack(fill='both', expand=True, padx=10, pady=10)
        self.populate_input_fields(tab_name, input_fields_frame, fields_and_values)

    def populate_input_fields(self, tab_name, frame, fields_and_values):

        entry_widgets = self.tab_contents[tab_name]["entry_widgets"]
        for row, (name, value) in enumerate(fields_and_values.items()):
            label = ctk.CTkLabel(frame, text=f"{name}:")
            label.grid(row=row, column=0, pady=(10, 10), sticky="w")
            entry = ctk.CTkEntry(frame)
            entry.grid(row=row, column=1, pady=(10, 10), sticky="ew", padx=(5, 20))
            frame.grid_columnconfigure(1, weight=1)

            # Store reference to the entry widget
            entry_widgets[name] = entry
            entry.insert(0, value)

    def get_tab_field_values(self, tab_name):

        if tab_name not in self.tab_contents:
            raise ValueError(f"Tab named '{tab_name}' does not exist.")
        entry_widgets = self.tab_contents[tab_name]["entry_widgets"]
        values = {name: entry.get() for name, entry in entry_widgets.items()}
        return values

    def myRefresh(self, event = None):
        print("myRefresh")
        self.tab_frame.update_idletasks()
JanPanthera commented 4 months ago

ChatGPT / I hope it will help you 😸

To address the issue with rendering tab contents in your CTkTabview class from the custom tkinter library (CTK), you can try a different approach to trigger the refresh of the tab's content upon selection. Instead of binding a refresh function to the mouse click event on the tab (which seems to bind to the frame and not the tab button), you could bind a refresh function to the tab change event.

One common way to handle tab changes in tkinter (and potentially in CTK if it follows a similar pattern) is to use the <<NotebookTabChanged>> virtual event, which is triggered when a new tab is selected in a ttk.Notebook widget. If CTK's CTkTabview is built on top of or in a similar way to tkinter's ttk.Notebook, this event might also be available or a similar custom event might exist in CTK.

Here's a modified version of your class that attempts to use a virtual event for refreshing the tab content. If CTkTabview doesn't support <<NotebookTabChanged>> or an equivalent event, you might need to look into the CTK documentation or source code to find the correct event name:

class MyTabView(ctk.CTkTabview):
    def __init__(self, master, **kwargs):
        super().__init__(master, **kwargs)
        self.tab_contents = {}

        # Bind a handler to the tab changed event (replace with the correct event for CTK if different)
        self.bind("<<NotebookTabChanged>>", self.on_tab_change)

    def add_tab_with_content(self, tab_name, tab_dict, fields_and_values):
        if tab_name in self.tab_contents:
            self.set(tab_name)  # This should trigger the tab change event and refresh
            raise ValueError(f"Tab named '{tab_name}' already exists.")

        new_tab = self.add(tab_name)
        self.tab_contents[tab_name] = {"dict": tab_dict, "entry_widgets": {}}

        self.tab_frame = self.tab(tab_name)
        self.populate_tab(self.tab_frame, tab_name, tab_dict, fields_and_values)

        self.set(tab_name)  # This should also trigger the tab change event and refresh

    # Other methods remain unchanged

    def on_tab_change(self, event=None):
        selected_tab = self.select()  # This method should return the currently selected tab
        self.tab_frame = self.tab(selected_tab)
        self.tab_frame.update_idletasks()  # Refresh the content of the selected tab

This code assumes that self.select() and self.tab() methods are available and work similarly to ttk.Notebook's methods, where self.select() returns the currently selected tab, and self.tab(tab_id) returns the tab content frame. Adjust these method calls if CTK has different methods for accessing the selected tab and its content.

If CTK does not provide a built-in event for tab changes, you may need to implement a custom mechanism to detect tab changes, possibly by overriding the method that changes tabs or by setting up a polling mechanism to detect when the selected tab has changed, though the latter is less efficient.

CatInAHatIsBack commented 4 months ago

I mean no offence but ChatGPT auto replies are really not useful, it just becomes spam. first of all i have already tried, and anyone who hasn't shouldn't post an issue.

@JanPanthera in the ctk_base_class you have this code.

 def bind(self, sequence=None, command=None, add=None):
        raise NotImplementedError
JanPanthera commented 4 months ago

Then bind to the tk widget the ctk widget uses? You can directly access it, you don't need a bind wrapper?

JanPanthera commented 4 months ago

First of all, it was an offence. Secondly, customtkinters base class is more like an abstract/interface. The bind method is implemented inside the actual concrete implementations, the widgets. BUT since customtkinter is build on top of tkinter it uses tkinter widgets and with this, it don't need to overwride or create a wrapper bind method, so just bind to the underlying tk widget instead.. Think around the corner and try to be friendly.

CatInAHatIsBack commented 4 months ago

Then bind to the tk widget the ctk widget uses? You can directly access it, you don't need a bind wrapper?

@JanPanthera Thank you, this ( You can directly access it, you don't need a bind wrapper? ) helped. Didn't think it had a callback at the time.

For anyone having a similar problem in the future, the ctk class most likely has a callback like CTkTabview has _segmented_button_callback You can find it by looking through the git repo, or

print(dir(self.{Your_ctkwidget}))

You can extend is as i have below

def _segmented_button_callback(self, selected_name):
        super()._segmented_button_callback(selected_name)  # Call the original callback logic
        self.myRefresh()