It was well discussed how to add a tool tip to a button.
how-do-i-display-tooltips-in-tkinter
I'm aware of one answer (this thread above) how to add tool tips to menu items.
how-can-i-add-a-tooltip-to-menu-item
I tried to combine them making necessary changes to both approaches. Cons: I know no way how to reliably get the bbox for the menu items. To cope with that I had to resort to use the monospaced font TkFixedFont and some assumptions about menu item margins/borders. They are in the code.
tipwin.py
import re
import tkinter as tk
def parse_geometry(g):
m = re.match(r'(\d+)x(\d+)\+(\d+)\+(\d+)', g)
return map(int, m.groups())
class TipWin(tk.Toplevel):
WRAPLENGTH = 180
def __init__(self, parent, start, end, text):
x, y = list(parse_geometry(parent.master.geometry()))[-2:]
self.parent, self.start, self.end, self.text = \
parent, start, end, text
super().__init__(parent)
self.wm_overrideredirect(True)
label = tk.Label(
self, text=self.text, justify='left',
background="#ffffff", relief='solid', borderwidth=1,
wraplength=self.WRAPLENGTH)
self.geometry(f'+{x + start}+{y}')
label.pack(ipadx=1)
def destroy(self):
super().destroy()
menubar.py
import tkinter as tk
from tipwin import TipWin
# Don't know how to get bbox for menubar. Resorted to use monospaced
# font and assumption.
CW = 8 # monospaced char width
PAD = 13 # menu item margins (left+right)
class Menu(tk.Menu):
def __init__(self, parent, **kwargs):
super().__init__(parent, **kwargs)
self.parent = parent
self.tiplist = [] # ((start1, end1, TIP1), ...)
self.activetip = -1 # active tip index or -1
self.nexttip = 1 # next tip index
self.bind('<Enter>', self.on_enter)
self.bind('<Motion>', self.on_motion)
self.bind('<Leave>', self.on_leave)
def on_enter(self, event):
self.hidetip()
if self.activetip != 1:
self.showtip(self.activetip, event)
def on_leave(self, event):
self.hidetip()
def on_motion(self, event):
x = event.x
for i in range(len(self.tiplist)):
if self.tiplist[i][0] <= x < self.tiplist[i][1]:
self.showtip(i, event)
return
self.hidetip()
def add_command(self, *args, **kwargs):
tooltip = kwargs.get('tooltip')
if tooltip:
del kwargs['tooltip']
label = kwargs.get('label')
super().add_command(*args, **kwargs)
self.addtip(label, tooltip)
def addtip(self, label, tooltip):
def _w(nc):
return nc * CW + PAD
x1 = self.nexttip
self.nexttip += _w(len(label))
self.tiplist.append((x1, self.nexttip, tooltip))
def showtip(self, i, event):
if self.activetip != -1:
if i != self.activetip:
self.hidetip()
t = self.tiplist[i][2]
if t:
if self.activetip != i:
# create a tooltip
self.tipwin = TipWin(self, *self.tiplist[i])
self.activetip = i
def hidetip(self):
# destroy the tooltip
if self.activetip != -1:
if self.tipwin:
self.tipwin.destroy()
self.tipwin = None
self.activetip = -1
class App(tk.Tk):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.title('Menubar')
self.geometry('600x300+400+300')
# tearoff is a MUST, without it <Enter> isn't fired
menubar = Menu(self, tearoff=0)
self.config(menu=menubar)
menubar.add_command(
label='Notes',
font='TkFixedFont',
tooltip='Edit items metadata')
menubar.add_command(
label='Application',
font='TkFixedFont',
tooltip='Save Toplevel windows geometry')
menubar.add_command(
label='Quit',
font='TkFixedFont',
tooltip='Quit')
if __name__ == '__main__':
App().mainloop()
It was tested for Python 3.9.2, Debian GNU/Linux 11 (bullseye).
