Display message when hovering over something with mouse cursor in Python
Asked Answered
P

9

46

I have a GUI made with TKinter in Python. I would like to be able to display a message when my mouse cursor goes, for example, on top of a label or button. The purpose of this is to explain to the user what the button/label does or represents.

Is there a way to display text when hovering over a tkinter object in Python?

Piper answered 5/12, 2013 at 11:49 Comment(1)
Possible duplicate of What is the simplest way to make tooltips in Tkinter?Wrestling
P
34

You need to set a binding on the <Enter> and <Leave> events.

Note: if you choose to pop up a window (ie: a tooltip) make sure you don't pop it up directly under the mouse. What will happen is that it will cause a leave event to fire because the cursor leaves the label and enters the popup. Then, your leave handler will dismiss the window, your cursor will enter the label, which causes an enter event, which pops up the window, which causes a leave event, which dismisses the window, which causes an enter event, ... ad infinitum.

For simplicity, here's an example that updates a label, similar to a statusbar that some apps use. Creating a tooltip or some other way of displaying the information still starts with the same core technique of binding to <Enter> and <Leave>.

import Tkinter as tk

class Example(tk.Frame):
    def __init__(self, *args, **kwargs):
        tk.Frame.__init__(self, *args, **kwargs)
        self.l1 = tk.Label(self, text="Hover over me")
        self.l2 = tk.Label(self, text="", width=40)
        self.l1.pack(side="top")
        self.l2.pack(side="top", fill="x")

        self.l1.bind("<Enter>", self.on_enter)
        self.l1.bind("<Leave>", self.on_leave)

    def on_enter(self, event):
        self.l2.configure(text="Hello world")

    def on_leave(self, enter):
        self.l2.configure(text="")

if __name__ == "__main__":
    root = tk.Tk()
    Example(root).pack(side="top", fill="both", expand="true")
    root.mainloop()
Pierette answered 5/12, 2013 at 11:59 Comment(2)
Doesn't this just change the text of the label? If so, that's not what the OP wanted.Kuhn
@user2188329: the OP didn't explicitly say they wanted a tooltip. They asked to "display a message". They might have wanted a tooltip or they might not. Regardless, the first sentence describes the mechanism whether the data is shown as a tooltip or in a statusbar.Pierette
L
61

I think this would meet your requirements.

Here's what the output looks like:

the output

First, A class named ToolTip which has methods showtip and hidetip is defined as follows:

from tkinter import *

class ToolTip(object):

    def __init__(self, widget):
        self.widget = widget
        self.tipwindow = None
        self.id = None
        self.x = self.y = 0

    def showtip(self, text):
        "Display text in tooltip window"
        self.text = text
        if self.tipwindow or not self.text:
            return
        x, y, cx, cy = self.widget.bbox("insert")
        x = x + self.widget.winfo_rootx() + 57
        y = y + cy + self.widget.winfo_rooty() +27
        self.tipwindow = tw = Toplevel(self.widget)
        tw.wm_overrideredirect(1)
        tw.wm_geometry("+%d+%d" % (x, y))
        label = Label(tw, text=self.text, justify=LEFT,
                      background="#ffffe0", relief=SOLID, borderwidth=1,
                      font=("tahoma", "8", "normal"))
        label.pack(ipadx=1)

    def hidetip(self):
        tw = self.tipwindow
        self.tipwindow = None
        if tw:
            tw.destroy()

def CreateToolTip(widget, text):
    toolTip = ToolTip(widget)
    def enter(event):
        toolTip.showtip(text)
    def leave(event):
        toolTip.hidetip()
    widget.bind('<Enter>', enter)
    widget.bind('<Leave>', leave)

The widget is where you want to add the tip. For example, if you want the tip when you hover over a button or entry or label, the instance of the same should be provided at the call time.

Quick note: the code above uses from tkinter import * which is not suggested by some of the programmers out there, and they have valid points. You might want to make necessary changes in such case.

To move the tip to your desired location, you can change x and y in the code. The function CreateToolTip() helps to create this tip easily. Just pass the widget and string you want to display in the tipbox to this function, and you're good to go.

This is how you call the above part:

button = Button(root, text = 'click mem')
button.pack()
CreateToolTip(button, text = 'Hello World\n'
                 'This is how tip looks like.'
                 'Best part is, it\'s not a menu.\n'
                 'Purely tipbox.')

Do not forget to import the module if you save the previous outline in different python file, and don't save the file as CreateToolTip or ToolTip to avoid confusion. This post from Fuzzyman shares some similar thoughts, and worth checking out.

Louls answered 25/6, 2019 at 7:44 Comment(5)
Seems like a Very nice solution but I for my part get this error: TypeError: cannot unpack non-iterable NoneType object :/Conveyancing
Fixed it... But thanks. If I remember right I needed to integrate it into the layout first to register it to a location. But I don't really remember ^^Conveyancing
I really like this, but it would also be nice to introduce a bit of delay from the "Enter" event to the tooltip showing up.Speciality
Your code looks awfully close to this voidspace.org.uk/python/weblog/arch_d7_2006_07_01.shtml#e387 are you the owner of that page and have reposted it here? If not, it would be a good idea to cite your source, rather than claiming that code as your own.Gemmiparous
this is exactly what I am looking for, thanks very muchCamp
P
34

You need to set a binding on the <Enter> and <Leave> events.

Note: if you choose to pop up a window (ie: a tooltip) make sure you don't pop it up directly under the mouse. What will happen is that it will cause a leave event to fire because the cursor leaves the label and enters the popup. Then, your leave handler will dismiss the window, your cursor will enter the label, which causes an enter event, which pops up the window, which causes a leave event, which dismisses the window, which causes an enter event, ... ad infinitum.

For simplicity, here's an example that updates a label, similar to a statusbar that some apps use. Creating a tooltip or some other way of displaying the information still starts with the same core technique of binding to <Enter> and <Leave>.

import Tkinter as tk

class Example(tk.Frame):
    def __init__(self, *args, **kwargs):
        tk.Frame.__init__(self, *args, **kwargs)
        self.l1 = tk.Label(self, text="Hover over me")
        self.l2 = tk.Label(self, text="", width=40)
        self.l1.pack(side="top")
        self.l2.pack(side="top", fill="x")

        self.l1.bind("<Enter>", self.on_enter)
        self.l1.bind("<Leave>", self.on_leave)

    def on_enter(self, event):
        self.l2.configure(text="Hello world")

    def on_leave(self, enter):
        self.l2.configure(text="")

if __name__ == "__main__":
    root = tk.Tk()
    Example(root).pack(side="top", fill="both", expand="true")
    root.mainloop()
Pierette answered 5/12, 2013 at 11:59 Comment(2)
Doesn't this just change the text of the label? If so, that's not what the OP wanted.Kuhn
@user2188329: the OP didn't explicitly say they wanted a tooltip. They asked to "display a message". They might have wanted a tooltip or they might not. Regardless, the first sentence describes the mechanism whether the data is shown as a tooltip or in a statusbar.Pierette
Z
14

You can refer to this- HoverClass

It is exactly what you need. Nothing more, nothing less

from Tkinter import *
import re

class HoverInfo(Menu):
    def __init__(self, parent, text, command=None):
       self._com = command
       Menu.__init__(self,parent, tearoff=0)
       if not isinstance(text, str):
          raise TypeError('Trying to initialise a Hover Menu with a non string type: ' + text.__class__.__name__)
       toktext=re.split('\n', text)
       for t in toktext:
          self.add_command(label = t)
       self._displayed=False
          self.master.bind("<Enter>",self.Display )
          self.master.bind("<Leave>",self.Remove )

    def __del__(self):
       self.master.unbind("<Enter>")
       self.master.unbind("<Leave>")

    def Display(self,event):
       if not self._displayed:
          self._displayed=True
          self.post(event.x_root, event.y_root)
       if self._com != None:
          self.master.unbind_all("<Return>")
          self.master.bind_all("<Return>", self.Click)

    def Remove(self, event):
     if self._displayed:
       self._displayed=False
       self.unpost()
     if self._com != None:
       self.unbind_all("<Return>")

    def Click(self, event):
       self._com()

Example app using HoverInfo:

from Tkinter import *
from HoverInfo import HoverInfo
class MyApp(Frame):
   def __init__(self, parent=None):
      Frame.__init__(self, parent)
      self.grid()
      self.lbl = Label(self, text='testing')
      self.lbl.grid()

      self.hover = HoverInfo(self, 'while hovering press return \n for an exciting msg', self.HelloWorld)

   def HelloWorld(self):
      print('Hello World')

app = MyApp()
app.master.title('test')
app.mainloop()

Screenshot:

Testing hoverbox

Zachariah answered 5/12, 2013 at 11:51 Comment(3)
Thank you, I'll have a look at that. I wasn't aware of the "hovering box" expressionPiper
While this solution is clever, HoverInfo behaves more like a menu of items than as a static infobox.Wrestling
Hi, even though it loads on Enter, it doesn't remove it self if I Leave the parent widget. I have to click out of the parent to make it disappear. Any ideas why?Morez
N
2

I have a very hacky solution but it has some advantages over the current answers so I figured I would share it.

lab=Label(root,text="hover me")
lab.bind("<Enter>",popup)

def do_popup(event):
    # display the popup menu
    root.after(1000, self.check)
    popup = Menu(root, tearoff=0)
    popup.add_command(label="Next")
    popup.tk_popup(event.x_root, event.y_root, 0)

def check(event=None):
    x, y = root.winfo_pointerxy()
    widget = root.winfo_containing(x, y)
    if widget is None:
        root.after(100, root.check)
    else:
        leave()

def leave():
    popup.delete(0, END)

The only real issue with this is it leaves behind a small box that moves focus away from the main window If anyone knows how to solve these issues let me know

Neoterize answered 4/1, 2019 at 22:30 Comment(1)
What are the advantages to using this solution?Wendell
P
2

I wanted to contribute to the answer of @squareRoot17 as he inspired me to shorten his code while providing the same functionality:

import tkinter as tk

class ToolTip(object):
    def __init__(self, widget, text):
        self.widget = widget
        self.text = text

        def enter(event):
            self.showTooltip()
        def leave(event):
            self.hideTooltip()
        widget.bind('<Enter>', enter)
        widget.bind('<Leave>', leave)

    def showTooltip(self):
        self.tooltipwindow = tw = tk.Toplevel(self.widget)
        tw.wm_overrideredirect(1) # window without border and no normal means of closing
        tw.wm_geometry("+{}+{}".format(self.widget.winfo_rootx(), self.widget.winfo_rooty()))
        label = tk.Label(tw, text = self.text, background = "#ffffe0", relief = 'solid', borderwidth = 1).pack()

    def hideTooltip(self):
        tw = self.tooltipwindow
        tw.destroy()
        self.tooltipwindow = None

This class can then be imported and used as:

import tkinter as tk
from tooltip import ToolTip

root = tk.Tk() 

your_widget = tk.Button(root, text = "Hover me!")
ToolTip(widget = your_widget, text = "Hover text!")

root.mainloop()
Plan answered 8/10, 2020 at 9:57 Comment(3)
You can shorten it even more if you use widget.bind('<Enter>', lambda e:self.showtip)Glucinum
This did not work for meHymanhymen
this really simplifies the gameGynecic
J
1

If anyone is on Mac OSX and tool tip isn't working, check out the example in:

https://github.com/python/cpython/blob/master/Lib/idlelib/tooltip.py

Basically, the two lines that made it work for me on Mac OSX were:

    tw.update_idletasks()  # Needed on MacOS -- see #34275.
    tw.lift()  # work around bug in Tk 8.5.18+ (issue #24570)
Jenks answered 29/5, 2020 at 14:56 Comment(0)
B
1

Here is a simple solution to your problem that subclasses the tk.Button object. We make a special class that tk.Button inherits from, giving it tooltip functionality. The same for tk.Labels.

I don't know what would be cleanest and the easiest way to maintain code for keeping track of the text that goes into the tooltips. I present here one way, in which I pass unique widget IDs to MyButtons, and access a dictionary for storing the tooltip texts. You could store this file as a JSON, or as a class attribute, or as a global variable (as below). Alternatively, perhaps it would be better to define a setter method in MyButton, and just call this method every time you create a new widget that should have a tooltip. Although you would have to store the widget instance in a variable, adding one extra line for all widgets to include.

One drawback in the code below is that the self.master.master syntax relies on determining the "widget depth". A simple recursive function will catch most cases (only needed for entering a widget, since by definition you leave somewhere you once were).

Anyway, below is a working MWE for anyone interested.

import tkinter as tk


tooltips = {
    'button_hello': 'Print a greeting message',
    'button_quit': 'Quit the program',
    'button_insult': 'Print an insult',
    'idle': 'Hover over button for help',
    'error': 'Widget ID not valid'
}


class ToolTipFunctionality:
    def __init__(self, wid):
        self.wid = wid
        self.widet_depth = 1
        self.widget_search_depth = 10

        self.bind('<Enter>', lambda event, i=1: self.on_enter(event, i))
        self.bind('<Leave>', lambda event: self.on_leave(event))

    def on_enter(self, event, i):
        if i > self.widget_search_depth:
            return
        try:
            cmd = f'self{".master"*i}.show_tooltip(self.wid)'
            eval(cmd)
            self.widget_depth = i
        except AttributeError:
            return self.on_enter(event, i+1)

    def on_leave(self, event):
        cmd = f'self{".master" * self.widget_depth}.hide_tooltip()'
        eval(cmd)


class MyButton(tk.Button, ToolTipFunctionality):
    def __init__(self, parent, wid, **kwargs):
        tk.Button.__init__(self, parent, **kwargs)
        ToolTipFunctionality.__init__(self, wid)


class MyLabel(tk.Label, ToolTipFunctionality):
    def __init__(self, parent, wid, **kwargs):
        tk.Label.__init__(self, parent, **kwargs)
        ToolTipFunctionality.__init__(self, wid)


class Application(tk.Tk):
    def __init__(self):
        tk.Tk.__init__(self)
        self.tooltip = tk.StringVar()
        self.tooltip.set(tooltips['idle'])

        self.frame = tk.Frame(self, width=50)
        self.frame.pack(expand=True)

        MyLabel(self.frame, '', text='One Cool Program').pack()

        self.subframe = tk.Frame(self.frame, width=40)
        self.subframe.pack()
        MyButton(self.subframe, 'button_hello', text='Hello!', command=self.greet, width=20).pack()
        MyButton(self.subframe, 'button_insutl', text='Insult', command=self.insult, width=20).pack()
        MyButton(self.subframe, 'button_quit', text='Quit', command=self.destroy, width=20).pack()
        tk.Label(self.subframe, textvar=self.tooltip, width=20).pack()

    def show_tooltip(self, wid):
        try:
            self.tooltip.set(tooltips[wid])
        except KeyError:
            self.tooltip.set(tooltips['error'])

    def hide_tooltip(self):
        self.tooltip.set(tooltips['idle'])

    def greet(self):
        print('Welcome, Fine Sir!')

    def insult(self):
        print('You must be dead from the neck up')


if __name__ == '__main__':
    app = Application()
    app.mainloop()
Bromley answered 17/12, 2020 at 17:12 Comment(0)
B
0

The best way I have found to create a popup help window is to use the tix.Balloon. I have modified it below to make it look better and show an example (note the use of tix.Tk):

import tkinter as tk
import tkinter.tix as tix


class Balloon(tix.Balloon):

    # A modified tix popup balloon (to change the default delay, bg and wraplength)

    init_after = 1250  # Milliseconds
    wraplength = 300  # Pixels

    def __init__(self, master):
        bg = root.cget("bg")
        # Call the parent
        super().__init__(master, initwait=self.init_after)
        # Change background colour
        for i in self.subwidgets_all():
            i.config(bg=bg)
        # Modify the balloon label
        self.message.config(wraplength=self.wraplength)


root = tix.Tk()

l = tk.Label(root, text="\n".join(["text"] * 5))
l.pack()

b = Balloon(root.winfo_toplevel())
b.bind_widget(l, balloonmsg="Some random text")

root.mainloop()


OLD ANSWER:

Here is an example using <enter> and <leave> as @bryanoakley suggested with a toplevel (with overridedirect set to true). Use the hover_timer class for easy use of this. This needs the widget and help-text (with an optional delay argument - default 0.5s) and can be easily called just by initiating the class and then cancelling it.

import threading, time
from tkinter import *

class hover_window (Toplevel):

    def __init__ (self, coords, text):
        super ().__init__ ()
        self.geometry ("+%d+%d" % (coords [0], coords [1]))
        self.config (bg = "white")
        Label (self, text = text, bg = "white", relief = "ridge", borderwidth = 3, wraplength = 400, justify = "left").grid ()
        self.overrideredirect (True)
        self.update ()
        self.bind ("<Enter>", lambda event: self.destroy ())

class hover_timer:

    def __init__ (self, widget, text, delay = 2):
        self.wind, self.cancel_var, self.widget, self.text, self.active, self.delay = None, False, widget, text, False, delay
        threading.Thread (target = self.start_timer).start ()

    def start_timer (self):
        self.active = True
        time.sleep (self.delay)
        if not self.cancel_var: self.wind = hover_window ((self.widget.winfo_rootx (), self.widget.winfo_rooty () + self.widget.winfo_height () + 20), self.text)
        self.active = False

    def delayed_stop (self):
        while self.active: time.sleep (0.05)
        if self.wind:
            self.wind.destroy ()
            self.wind = None

    def cancel (self):
        self.cancel_var = True
        if not self.wind: threading.Thread (target = self.delayed_stop).start ()
        else:
            self.wind.destroy ()
            self.wind = None

def start_help (event):
    # Create a new help timer
    global h
    h = hover_timer (l, "This is some additional information.", 0.5)

def end_help (event):
    # If therre is one, end the help timer
    if h: h.cancel ()

if __name__ == "__main__":

    # Create the tkinter window
    root = Tk ()
    root.title ("Hover example")

    # Help class not created yet
    h = None

    # Padding round label
    Frame (root, width = 50).grid (row = 1, column = 0)
    Frame (root, height = 50).grid (row = 0, column = 1)
    Frame (root, width = 50).grid (row = 1, column = 2)
    Frame (root, height = 50).grid (row = 2, column = 1)

    # Setup the label
    l = Label (root, text = "Hover over me for information.", font = ("sans", 32))
    l.grid (row = 1, column = 1)
    l.bind ("<Enter>", start_help)
    l.bind ("<Leave>", end_help)

    # Tkinter mainloop
    root.mainloop ()
Banker answered 19/10, 2018 at 16:44 Comment(0)
H
0

Try Hovertip, a super simple tooltip solution.

Checkout here: https://mcmap.net/q/261798/-how-do-i-display-tooltips-in-tkinter

Hideandseek answered 19/9, 2023 at 7:6 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.