pythontkintertabswidgetttk

How to access the actual tab widget of ttk.Notebook?


I need to access the actual tab widget that ttk.Notebook creates when some other widget (usually a frame) is added to it (and then called a "tab" in a Tkinter parlance).

I pictured the exact thing needed below to avoid any confusion: enter image description here

The children attribute on the Notebook object returns an empty dictionary for me (but calling tabs() on it returns a tuple of string tab_id's of the frames added to it).

My use case is that I'd like to attach a custom tooltip widget to an actual tab (but not to the tab's contents). My custom Tooltip object needs a tk.Widget object as a parent for a tk.Toplevel it spawns to work. When I pass tab contents to it (a frame widget added to the notebook that I have an easy access to), a tooltip spawns inside the notebook obscuring the view which is not very helpful. Hence, I'd like to attach it to the actual tab widget only .

The code for Tooltip class is below.

class ToolTip:
    X_OFFSET = 20
    Y_OFFSET = 20
    JUSTIFY = tk.LEFT
    BG_COLOR = "#ffffe0"  # light-yellowish
    RELIEF = tk.SOLID
    BORDER_WIDTH = 1

    @property
    def text(self) -> str:
        return self.__text

    @text.setter
    def text(self, value: str) -> None:
        self.__text = value

    @property
    def x(self) -> int:
        return self._widget.winfo_rootx() + self._x

    @property
    def y(self) -> int:
        return self._widget.winfo_rooty() + self._y

    def __init__(self, widget: tk.Widget, text: str) -> None:
        self._widget = widget
        self.text = text
        self._tip_window = None
        self._tip_showing_id = None
        self._x = self._y = 0
        self._bind_callbacks()

    def _bind_callbacks(self) -> None:
        self._entering_id = self._widget.bind("<Enter>", self._on_entered)
        self._leaving_id = self._widget.bind("<Leave>", self._on_left)
        self._button_pressing_id = self._widget.bind("<ButtonPress>", self._on_left)

    def _unbind_callbacks(self) -> None:
        self._widget.unbind("<Enter>", self._entering_id)
        self._widget.unbind("<Leave>", self._leaving_id)
        self._widget.unbind("<ButtonPress>", self._button_pressing_id)

    def _on_entered(self, event: tk.Event) -> None:
        self._x, self._y = event.x, event.y
        self._schedule()

    def _on_left(self, _) -> None:
        self._unschedule()
        self._hide_tip()

    def _schedule(self) -> None:
        self._unschedule()
        self._tip_showing_id = self._widget.after(1500, self._show_tip)

    def _unschedule(self) -> None:
        widget_id = self._tip_showing_id
        self._tip_showing_id = None
        if widget_id:
            self._widget.after_cancel(widget_id)

    def _show_tip(self) -> None:
        if self._tip_window or not self.text:
            return
        self._tip_window = tw = tk.Toplevel(self._widget)
        tw.wm_overrideredirect(True)
        tw.wm_geometry('+%d+%d' % (self.x, self.y))
        label = tk.Label(
            self._tip_window,
            text=self.text,
            justify=self.JUSTIFY,
            background=self.BG_COLOR,
            relief=self.RELIEF,
            borderwidth=self.BORDER_WIDTH
        )
        label.pack(ipadx=5, ipady=2)

    def _hide_tip(self) -> None:
        tw = self._tip_window
        self._tip_window = None
        if tw:
            tw.destroy()

UPDATE

So, the whole question turned out to be quite moot considering the tooltip use case I described. My blunder was failing to see that passing the ttk.Notebook itself to my Tooltip object (instead of passing a tab's content frame like I tried) results in tooltips spawning over tabs and not in the middle of content - just like I wanted.

Still, the answer I got from 'patthoyts' was essential because it enabled me to change a tooltip's text in regards to the tab beeing hoovered over - and that way achieving exactly the effect I wanted.

In summary:


Solution

  • The Tcl tklib tooltip library has support for binding tooltips to notebook tabs. From a brief look at that code it is binding the notebook Motion event and using the notebook index method to check if the event was raised over a tab (ie: nb.index(f"@{ev.x},{ev.y} if ev is the event object and nb is the notebook widget). Then use an after event to schedule the tooltip unless Leave or Motion events detect the pointer left the tab when the after call can be cancelled.