1 '''Michael Lange <klappnase (at) freakmail (dot) de>
   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',\
  46                       'follow_mouse':0, 'font':None, 'justify':'left', 'padx':4, 'pady':2,\
  47                       'relief':'solid', 'state':'normal', 'text':text, 'textvariable':None,\
  48                       'width':0, 'wraplength':150}
  49         self.configure(**opts)
  50         self._tipwindow = None
  51         self._id = None
  52         self._id1 = self.master.bind("<Enter>", self.enter, '+')
  53         self._id2 = self.master.bind("<Leave>", self.leave, '+')
  54         self._id3 = self.master.bind("<ButtonPress>", self.leave, '+')
  55         self._follow_mouse = 0
  56         if self._opts['follow_mouse']:
  57             self._id4 = self.master.bind("<Motion>", self.motion, '+')
  58             self._follow_mouse = 1
  59     
  60     def configure(self, **opts):
  61         for key in opts:
  62             if self._opts.has_key(key):
  63                 self._opts[key] = opts[key]
  64             else:
  65                 KeyError = 'KeyError: Unknown option: "%s"' %key
  66                 raise KeyError
  67     
  68     ##----these methods handle the callbacks on "<Enter>", "<Leave>" and "<Motion>"---------------##
  69     ##----events on the parent widget; override them if you want to change the widget's behavior--##
  70     
  71     def enter(self, event=None):
  72         self._schedule()
  73         
  74     def leave(self, event=None):
  75         self._unschedule()
  76         self._hide()
  77     
  78     def motion(self, event=None):
  79         if self._tipwindow and self._follow_mouse:
  80             x, y = self.coords()
  81             self._tipwindow.wm_geometry("+%d+%d" % (x, y))
  82     
  83     ##------the methods that do the work:---------------------------------------------------------##
  84     
  85     def _schedule(self):
  86         self._unschedule()
  87         if self._opts['state'] == 'disabled':
  88             return
  89         self._id = self.master.after(self._opts['delay'], self._show)
  90 
  91     def _unschedule(self):
  92         id = self._id
  93         self._id = None
  94         if id:
  95             self.master.after_cancel(id)
  96 
  97     def _show(self):
  98         if self._opts['state'] == 'disabled':
  99             self._unschedule()
 100             return
 101         if not self._tipwindow:
 102             self._tipwindow = tw = Tkinter.Toplevel(self.master)
 103             # hide the window until we know the geometry
 104             tw.withdraw()
 105             tw.wm_overrideredirect(1)
 106 
 107             if tw.tk.call("tk", "windowingsystem") == 'aqua':
 108                 tw.tk.call("::tk::unsupported::MacWindowStyle", "style", tw._w, "help", "none")
 109 
 110             self.create_contents()
 111             tw.update_idletasks()
 112             x, y = self.coords()
 113             tw.wm_geometry("+%d+%d" % (x, y))
 114             tw.deiconify()
 115     
 116     def _hide(self):
 117         tw = self._tipwindow
 118         self._tipwindow = None
 119         if tw:
 120             tw.destroy()
 121                 
 122     ##----these methods might be overridden in derived classes:----------------------------------##
 123     
 124     def coords(self):
 125         # The tip window must be completely outside the master widget;
 126         # otherwise when the mouse enters the tip window we get
 127         # a leave event and it disappears, and then we get an enter
 128         # event and it reappears, and so on forever :-(
 129         # or we take care that the mouse pointer is always outside the tipwindow :-)
 130         tw = self._tipwindow
 131         twx, twy = tw.winfo_reqwidth(), tw.winfo_reqheight()
 132         w, h = tw.winfo_screenwidth(), tw.winfo_screenheight()
 133         # calculate the y coordinate:
 134         if self._follow_mouse:
 135             y = tw.winfo_pointery() + 20
 136             # make sure the tipwindow is never outside the screen:
 137             if y + twy > h:
 138                 y = y - twy - 30
 139         else:
 140             y = self.master.winfo_rooty() + self.master.winfo_height() + 3
 141             if y + twy > h:
 142                 y = self.master.winfo_rooty() - twy - 3
 143         # we can use the same x coord in both cases:
 144         x = tw.winfo_pointerx() - twx / 2
 145         if x < 0:
 146             x = 0
 147         elif x + twx > w:
 148             x = w - twx
 149         return x, y
 150 
 151     def create_contents(self):
 152         opts = self._opts.copy()
 153         for opt in ('delay', 'follow_mouse', 'state'):
 154             del opts[opt]
 155         label = Tkinter.Label(self._tipwindow, **opts)
 156         label.pack()
 157 
 158 ##---------demo code-----------------------------------##
 159 
 160 def demo():
 161     root = Tkinter.Tk(className='ToolTip-demo')
 162     l = Tkinter.Listbox(root)
 163     l.insert('end', "I'm a listbox")
 164     l.pack(side='top')
 165     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")
 166     b = Tkinter.Button(root, text='Quit', command=root.quit)
 167     b.pack(side='bottom')
 168     t2 = ToolTip(b, text='Enough of this')
 169     root.mainloop()
 170 
 171 if __name__ == '__main__':
 172     demo()    

Screenshot of the ToolTip Demo:

tooltip.png

tkinter: ToolTip (last edited 2010-08-19 17:38:42 by p54AA6BF4)