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:
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()
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:
ttk.Notebook
instead of content frame to Tooltip
<<Motion>>
event on the notebook to a callback that changed the text
property on the tooltip based on the tab being hoovered overThe 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.