1 '''Michael Lange <klappnase at 8ung dot at>
   2 The ToolTip class provides a flexible tooltip widget for Tkinter; it is based on IDLE's ToolTip
   3 module which unfortunately seems to be broken (at least the version I saw).
   4 INITIALIZATION OPTIONS:
   5 anchor :        where the text should be positioned inside the widget, must be on of "n", "s", "e", "w", "nw" and so on;
   6                 default is "center"
   7 bd :            borderwidth of the widget; default is 1 (NOTE: don't use "borderwidth" here)
   8 bg :            background color to use for the widget; default is "lightyellow" (NOTE: don't use "background")
   9 delay :         time in ms that it takes for the widget to appear on the screen when the mouse pointer has
  10                 entered the parent widget; default is 1500
  11 fg :            foreground (i.e. text) color to use; default is "black" (NOTE: don't use "foreground")
  12 follow_mouse :  if set to 1 the tooltip will follow the mouse pointer instead of being displayed
  13                 outside of the parent widget; this may be useful if you want to use tooltips for
  14                 large widgets like listboxes or canvases; default is 0
  15 font :          font to use for the widget; default is system specific
  16 justify :       how multiple lines of text will be aligned, must be "left", "right" or "center"; default is "left"
  17 padx :          extra space added to the left and right within the widget; default is 4
  18 pady :          extra space above and below the text; default is 2
  19 relief :        one of "flat", "ridge", "groove", "raised", "sunken" or "solid"; default is "solid"
  20 state :         must be "normal" or "disabled"; if set to "disabled" the tooltip will not appear; default is "normal"
  21 text :          the text that is displayed inside the widget
  22 textvariable :  if set to an instance of Tkinter.StringVar() the variable's value will be used as text for the widget
  23 width :         width of the widget; the default is 0, which means that "wraplength" will be used to limit the widgets width
  24 wraplength :    limits the number of characters in each line; default is 150
  25 
  26 WIDGET METHODS:
  27 configure(**opts) : change one or more of the widget's options as described above; the changes will take effect the
  28                     next time the tooltip shows up; NOTE: follow_mouse cannot be changed after widget initialization
  29 
  30 Other widget methods that might be useful if you want to subclass ToolTip:
  31 enter() :           callback when the mouse pointer enters the parent widget
  32 leave() :           called when the mouse pointer leaves the parent widget
  33 motion() :          is called when the mouse pointer moves inside the parent widget if follow_mouse is set to 1 and the
  34                     tooltip has shown up to continually update the coordinates of the tooltip window
  35 coords() :          calculates the screen coordinates of the tooltip window
  36 create_contents() : creates the contents of the tooltip window (by default a Tkinter.Label)
  37 '''
  38 # Ideas gleaned from PySol
  39 
  40 import Tkinter
  41 
  42 class ToolTip:
  43     def __init__(self, master, text='Your text here', delay=1500, **opts):
  44         self.master = master
  45         self._opts = {'anchor':'center', 'bd':1, 'bg':'lightyellow', 'delay':delay, 'fg':'black',\
                      'follow_mouse':0, 'font':None, 'justify':'left', 'padx':4, 'pady':2,\
                      'relief':'solid', 'state':'normal', 'text':text, 'textvariable':None,\
                      'width':0, 'wraplength':150}
  46         self.configure(**opts)
  47         self._tipwindow = None
  48         self._id = None
  49         self._id1 = self.master.bind("<Enter>", self.enter, '+')
  50         self._id2 = self.master.bind("<Leave>", self.leave, '+')
  51         self._id3 = self.master.bind("<ButtonPress>", self.leave, '+')
  52         self._follow_mouse = 0
  53         if self._opts['follow_mouse']:
  54             self._id4 = self.master.bind("<Motion>", self.motion, '+')
  55             self._follow_mouse = 1
  56 
  57     def configure(self, **opts):
  58         for key in opts:
  59             if self._opts.has_key(key):
  60                 self._opts[key] = opts[key]
  61             else:
  62                 KeyError = 'KeyError: Unknown option: "%s"' %key
  63                 raise KeyError
  64 
  65     ##----these methods handle the callbacks on "<Enter>", "<Leave>" and "<Motion>"---------------##
  66     ##----events on the parent widget; override them if you want to change the widget's behavior--##
  67 
  68     def enter(self, event=None):
  69         self._schedule()
  70 
  71     def leave(self, event=None):
  72         self._unschedule()
  73         self._hide()
  74 
  75     def motion(self, event=None):
  76         if self._tipwindow and self._follow_mouse:
  77             x, y = self.coords()
  78             self._tipwindow.wm_geometry("+%d+%d" % (x, y))
  79 
  80     ##------the methods that do the work:---------------------------------------------------------##
  81 
  82     def _schedule(self):
  83         self._unschedule()
  84         if self._opts['state'] == 'disabled':
  85             return
  86         self._id = self.master.after(self._opts['delay'], self._show)
  87 
  88     def _unschedule(self):
  89         id = self._id
  90         self._id = None
  91         if id:
  92             self.master.after_cancel(id)
  93 
  94     def _show(self):
  95         if self._opts['state'] == 'disabled':
  96             self._unschedule()
  97             return
  98         if not self._tipwindow:
  99             self._tipwindow = tw = Tkinter.Toplevel(self.master)
 100             # hide the window until we know the geometry
 101             tw.withdraw()
 102             tw.wm_overrideredirect(1)
 103 
 104             if tw.tk.call("tk", "windowingsystem") == 'aqua':
 105                 tw.tk.call("::tk::unsupported::MacWindowStyle", "style", tw._w, "help", "none")
 106 
 107             self.create_contents()
 108             tw.update_idletasks()
 109             x, y = self.coords()
 110             tw.wm_geometry("+%d+%d" % (x, y))
 111             tw.deiconify()
 112 
 113     def _hide(self):
 114         tw = self._tipwindow
 115         self._tipwindow = None
 116         if tw:
 117             tw.destroy()
 118 
 119     ##----these methods might be overridden in derived classes:----------------------------------##
 120 
 121     def coords(self):
 122         # The tip window must be completely outside the master widget;
 123         # otherwise when the mouse enters the tip window we get
 124         # a leave event and it disappears, and then we get an enter
 125         # event and it reappears, and so on forever :-(
 126         # or we take care that the mouse pointer is always outside the tipwindow :-)
 127         tw = self._tipwindow
 128         twx, twy = tw.winfo_reqwidth(), tw.winfo_reqheight()
 129         w, h = tw.winfo_screenwidth(), tw.winfo_screenheight()
 130         # calculate the y coordinate:
 131         if self._follow_mouse:
 132             y = tw.winfo_pointery() + 20
 133             # make sure the tipwindow is never outside the screen:
 134             if y + twy > h:
 135                 y = y - twy - 30
 136         else:
 137             y = self.master.winfo_rooty() + self.master.winfo_height() + 3
 138             if y + twy > h:
 139                 y = self.master.winfo_rooty() - twy - 3
 140         # we can use the same x coord in both cases:
 141         x = tw.winfo_pointerx() - twx / 2
 142         if x < 0:
 143             x = 0
 144         elif x + twx > w:
 145             x = w - twx
 146         return x, y
 147 
 148     def create_contents(self):
 149         opts = self._opts.copy()
 150         for opt in ('delay', 'follow_mouse', 'state'):
 151             del opts[opt]
 152         label = Tkinter.Label(self._tipwindow, **opts)
 153         label.pack()
 154 
 155 ##---------demo code-----------------------------------##
 156 
 157 def demo():
 158     root = Tkinter.Tk(className='ToolTip-demo')
 159     l = Tkinter.Listbox(root)
 160     l.insert('end', "I'm a listbox")
 161     l.pack(side='top')
 162     t1 = ToolTip(l, follow_mouse=1, text="I'm a tooltip with follow_mouse set to 1, so I won't be placed outside my parent")
 163     b = Tkinter.Button(root, text='Quit', command=root.quit)
 164     b.pack(side='bottom')
 165     t2 = ToolTip(b, text='Enough of this')
 166     root.mainloop()
 167 
 168 if __name__ == '__main__':
 169     demo()

Screenshot of the ToolTip Demo:

None: ToolTip (last edited 2008-06-09 16:16:20 by TalEinat)