Home > Back-end >  Tkinter notebook <<NotebookTabChanged>> problem
Tkinter notebook <<NotebookTabChanged>> problem

Time:04-18

I am building a UI based on a Tkinter "notebook" object with several tabs. Whenever the user clicks a tab I want to update the info on the frame according to the current state of the underlying object before displaying it to the user.

This does not seem to work:

The following demo builds a notebook with three pages. The routine load_page is bound to the NotebookTabChanged event and should be called every time a tab is clicked. This routine should open a message window identifying the tab that fired it, and updates the info on the page (with current time):

import tkinter as tk
import tkinter.messagebox as msg
from tkinter import ttk
import time

class DemoPage(tk.Frame):  # A notebook page
    def __init__(self, container, name: str = ""):
        # call the superclass init to build the page
        tk.Frame.__init__(self, container, highlightbackground="blue", highlightthickness=1)
        self._name = name  # The name of the page (appears on the page tab)
        container.add(self, text=name)  # add the notebook tab & associate w. page
        # event binding
        container.bind("<<NotebookTabChanged>>", func=self.load_page)  # load current table 
        # insert texts
        self.const_label = tk.Label(self, text="This is "   self._name)
        self.const_label.pack(side='top', fill='x')
        self.var_label = tk.Label(self, text="Updated "   time.ctime())
        self.var_label.pack(side='top', fill='x')

    def load_page(self, event):
        self.var_label["text"] = "Updated "   time.ctime()
        msg.showinfo(self._name, "Tab clicked")

if __name__ == "__main__":
    tk_win = tk.Tk()  # establish the root tkinter window
    tk_win.title("Master Sequence")
    tk_win.geometry("600x400")
    # initiate UI
    nb = ttk.Notebook(tk_win)  # establish notebook 
    nb.pack(expand=True, fill='both')  # widget geometry: Notebook will fill the root window
    page1 = DemoPage(nb,"Page 1")  # initiate tab object in the notebook
    page2 = DemoPage(nb,"Page 2")  # initiate tab object in the notebook
    page3 = DemoPage(nb,"Page 3")  # initiate tab object in the notebook
    # wait for user input
    tk_win.mainloop()

Note that the event is triggered automatically for page 3 when the app is first run (without clicking its tab). Thereafter, every time any tab is clicked, the event is triggered for tab 3, and the info in the frame is updated in page 3 only

What am I doing wrong?

CodePudding user response:

NotebookTabChanged is not only click event - it can be generated also using code - and when you create new tabs then it also generate this event. You should first create all tabs and later bind event. it may need to use tk_win.update().

But this event work different then you expect - it is assigned to Notebook, not to tabs - so you replace previus bind with new bind and finally it runs last assgned function which change time in last tab.

You should assing one function which checks which tab is active and change time in this tab. But it can be simpler if you keep tabs on list - so you can get index of active tab and use it to access tab on list.

def on_tab_change(event):
    index = nb.index(nb.select())
    print('index:', index)
    
    pages[index].load_page()

pages = [
    DemoPage(nb, "Page 1"),
    DemoPage(nb, "Page 2"),
    DemoPage(nb, "Page 3"),
]

nb.bind("<<NotebookTabChanged>>", on_tab_change)

Full working code.

import tkinter as tk
import tkinter.messagebox as msg
from tkinter import ttk
import time

class DemoPage(tk.Frame):  # A notebook page
    def __init__(self, container, name: str = ""):
        # call the superclass init to build the page
        tk.Frame.__init__(self, container, highlightbackground="blue", highlightthickness=1)
        self._name = name  # The name of the page (appears on the page tab)
        container.add(self, text=name)  # add the notebook tab & associate w. page
    
    
        # insert texts
        self.const_label = tk.Label(self, text="This is "   self._name)
        self.const_label.pack(side='top', fill='x')
        
        self.var_label = tk.Label(self, text="Updated "   time.ctime())
        self.var_label.pack(side='top', fill='x')

    def load_page(self):
        self.var_label["text"] = "Updated "   time.ctime()
        msg.showinfo(self._name, "Tab clicked")

def on_tab_change(event):
    index = nb.index(nb.select())
    print('index:', index)
    
    pages[index].load_page()

if __name__ == "__main__":
    tk_win = tk.Tk()  # establish the root tkinter window
    tk_win.title("Master Sequence")
    tk_win.geometry("600x400")

    # initiate UI
    nb = ttk.Notebook(tk_win)  # establish notebook 
    nb.pack(expand=True, fill='both')  # widget geometry: Notebook will fill the root window
    
    pages = [
        DemoPage(nb, "Page 1"),
        DemoPage(nb, "Page 2"),
        DemoPage(nb, "Page 3"),
    ]
    
    tk_win.update() # update all tabs before assigning `event`

    # assign only one function
    nb.bind("<<NotebookTabChanged>>", on_tab_change)
    
    tk_win.mainloop()

CodePudding user response:

Thank you furas.

Indeed, it seems that NotebookTabChanged does not operate in the way I need. A much simpler solution to deliver the same functionality would be to change the line:

container.bind("<<NotebookTabChanged>>", func=self.load_page)

to:

self.bind("<Expose>", func=self.load_page)

When the user clicks the relevant tab the "DemoPage" frame is exposed and fires off the event.

  • Related