Search code examples
python-3.xtkinterpython-idlettk

Hovertip/Tooltip for each item in Python ttk combobox


This is about ttk combobox. I need to show help-info via Tooltip for each item (text) in ttk.combobox.

The minimum example is following:

import tkinter as tk
from tkinter import ttk
from idlelib.tooltip import Hovertip
names =["One", "Two", "Three"]
root = tk.Tk()
root.title("Combo Test GUI")
Frame1 = ttk.Frame(root, padding="3 3 12 12")
Frame1.grid(column= 0, row = 0)
label_1 = ttk.Label(Frame1, text="Select Item:", anchor=tk.W)
label_1.grid(column= 0, row = 1)
item1 = ttk.Combobox(Frame1, state="readonly", values=names, width=35)
item1.grid(column= 0, row = 2)
m1 = Hovertip(item1, "One test")
m2 = Hovertip(label_1, "Another test")
root.mainloop()

The problem: I want to bind the Hovertip to the text in the combo list, i.e. "One", "Two", "Three".

When the user hovers mouse over "One", the description "This is option One", should be shown in the Hovertip/Tooltip; and same for the other items "Two" and "Three" also.

I have searched everywhere, on Stackoverflow, in the reference docs, other websites, but it seems that Hovertip/Tooltip can only be used with widget, and not with simple text in the combobox.

Is there any way at all to make it possible?


Solution

  • The idea is to make a class similar to Hovertip but that works with listbox items instead of widgets. The current item is stored in self._current_item and when the mouse moves in the listbox, the displaying of the tooltip is rescheduled if the item changes. The text for each item is stored in a dictionary self.tips = {item index: text, ...}.

    The main issue is that the Listbox displaying the Combobox's choices is not directly accessible with python. So I had to use some Tcl commands to do it:

    proc callback {y} {
         event generate .!combobox <<OnMotion>> -y $y
    }
    
    set popdown [ttk::combobox::PopdownWindow .!combobox]
    bind $popdown.f.l <Motion> {callback %y}
    

    The above code generates a virtual event <<OnMotion>> in the combobox when the mouse moves (where .!combobox is the name of the Combobox widget and y is the mouse relative y coordinate in the widget).

    Then I bind the _on_motion() method of the combobox to this <<OnMotion>> event to check if the current item has changed. To get the current item I use the Listbox method nearest(y) but from Tcl.

    I also modified the get_position() method to display the tooltip just below the current item and showcontents() to display the text corresponding to the item.

    Here is the full code:

    import tkinter as tk
    from tkinter import ttk
    from idlelib.tooltip import OnHoverTooltipBase
    
    class ComboboxTip(OnHoverTooltipBase):
        def __init__(self, combobox_widget, hover_delay=1000):
            super(ComboboxTip, self).__init__(combobox_widget, hover_delay=hover_delay)
            self.tips = {}
            self._current_item = 0
    
            combobox_widget.tk.eval("""
    proc callback {y} {
         event generate %(cb)s <<OnMotion>> -y $y
    }
    
    set popdown [ttk::combobox::PopdownWindow %(cb)s]
    bind $popdown.f.l <Motion> {callback %%y}
    """ % ({"cb": combobox_widget}))
    
            self._id4 = combobox_widget.bind("<<OnMotion>>", self._on_motion)
    
        def _on_motion(self, event):
            current_item = int(self.anchor_widget.tk.eval("$popdown.f.l nearest %i" % event.y))
            if current_item != self._current_item:
                self._current_item = current_item
                self.hidetip()
                if current_item in self.tips:
                    self.schedule()
                else:
                    self.unschedule()
    
        def __del__(self):
            try:
                self.anchor_widget.unbind("<<OnMotion>>", self._id4)
            except tk.TclError:
                pass
            super(ComboboxTip, self).__del__()
    
        def add_tooltip(self, index, text):
            self.tips[index] = text
    
        def get_position(self):
            """choose a screen position for the tooltip"""
            try:
                h = self.anchor_widget.winfo_height()
                bbox = self.anchor_widget._getints(self.anchor_widget.tk.eval("$popdown.f.l bbox %i" % self._current_item))
                return bbox[0] + bbox[2], bbox[1] + bbox[-1] + h
            except Exception:
                return 20, self.anchor_widget.winfo_height() + 1
    
        def showcontents(self):
            label = tk.Label(self.tipwindow, text=self.tips[self._current_item], justify=tk.LEFT,
                             background="#ffffe0", relief=tk.SOLID, borderwidth=1)
            label.pack()
    
    
    names = ["One", "Two", "Three"]
    root = tk.Tk()
    cb = ttk.Combobox(root, values=names)
    cb.pack()
    
    t = ComboboxTip(cb)
    t.add_tooltip(0, "This is One")
    t.add_tooltip(1, "This is Two")
    t.add_tooltip(2, "This is Three")
    
    root.mainloop()