Search code examples
pythontkinterballoon-tiptix

How to use tix balloon on a function event?


I have a program that will count the index length of a highlighted area. I wanted to use tix balloon to show the the result. But in order to use the tix balloon, it required me bind it to the a widget. Instead of binding it to a whole widget, I only want the balloon to show once when a certain event is being called.

Demo of my program (Count start and end index of the highlighted area) enter image description here

Expected output The balloon will show the result beside the highlighted area enter image description here

Code

root.mainloop()from tkinter import *

import tkinter.tix as tkx

def print_count(event):
    if text.tag_ranges('sel'):
        global s0 , s1
        s0 = text.index("sel.first")
        s1 = text.index("sel.last")
        countstringstart = s0.split('.')[1]
        countstringend = s1.split('.')[1]
        print(countstringstart)
        print(countstringend)
        waitshowballon()

root = tkx.Tk()
global text
text = Text(root)
text.bind('<ButtonRelease-1>', print_count)
text.pack()

def waitshowballon():
    tooltip = tkx.Balloon(root, initwait=100)
    tooltip.bind_widget(text, balloonmsg=s0)

root.mainloop()


Solution

  • Create your own tooltip. tix don't seem to provide position parameter.

    Creating your own tooltip is quite simpler than you think. This answer will give you an idea on how to make your own tooltip.

    Create a show function that takes the indexes as parameters. Use .bbox(index) to get the position of the index and display the tooltip just above the selected letter.

    Sample code:

    from tkinter import *
    
    class TextToolTip:
    
        def __init__(self, root, widget:Text, delay=0):
            
            self.parent = root
            self.delay = delay
            self.widget = widget
            
            self.top = None
            self.pos_x, self.pos_y = 0, 0
    
            self.i0, self.i1 = "", ""
            self.after_id = None
    
            self.msg=''
            
            self.bind()
            
        def bind(self):
            self.parent.bind('<Button>', self.hide)
            self.parent.bind('<Configure>', self.move)
            self.widget.tag_bind('sel', '<Enter>', self._display) #  remove this if hover tooltip is not necessary
            self.widget.tag_bind('sel', '<Leave>', self.tempHide)
                    
    
        def tempHide(self, event):
            # similar to hide method the only difference is that this does't reset self.i0 and self.i1
            if self.top:
                self.top.destroy()
                self.top = None
    
        
        def hide(self, event=None):
            # destroys the top level
            if self.top:
                self.top.destroy()
                self.top = None
          
            self.i0, self.i1 = "", ""
    
        def move(self, event):
            # moves the tooltip along with the window
            if self.top:
                x, y = self.calcPos()
                if x and y:
                    self.top.wm_geometry(f"+{x}+{y}")
                    
                else:
                    self.hide()
    
        def _display(self, event=None):
            # creates the tool tip
            if self.top is None:
                self.top = Toplevel(self.parent)
                self.top.wm_overrideredirect(True)
                
                label = Label(self.top, text=self.msg, justify='left',
                               background="black", foreground='white',
                              relief='solid', borderwidth=1)
    
                label.pack()
                
                x, y = self.calcPos()
                if x and y:
                    self.top.wm_geometry(f"+{x}+{y}")
                        
                else:
                    self.hide()
                    return         
    
        def calcPos(self):  # calculates the position to display the tool tip
            
            try:
                self.widget.update_idletasks()
                b1, b2 = self.widget.bbox(self.i0), self.widget.bbox(self.i1)
                win_x, win_y = self.parent.winfo_x(), self.parent.winfo_y()
                return win_x + ((b2[0]+b1[0])//2), win_y + b1[1]
    
            except Exception:
    
                return None, None
                
        def show(self, msg, i0, i1):  
            """ pass in the index and the message (note: must pass a valid index ctrl+a might not provide
                    correct index. A simple conditionl stmt to check should do the job.)"""
            if self.top:
                self.hide()
    
            if self.after_id:
                self.parent.after_cancel(self.after_id)
                self.after_id = None
            
            self.i0, self.i1 = i0, i1
            self.msg = msg
            self.after_id = self.parent.after(self.delay, self._display)
            
            
    def print_count(event):
        if text.tag_ranges('sel'):
            global s0 , s1
            s0 = text.index("sel.first")
            s1 = text.index("sel.last")
            countstringstart = s0.split('.')[1]
            countstringend = s1.split('.')[1]
    
            waitshowballon(countstringstart, countstringend)
    
        else:
            tooltip.hide()    
    
        
    root = Tk()
    
    text = Text(root)
    text.bind('<<Selection>>', print_count)
    text.pack()
    
    tooltip = TextToolTip(root, text, 500)  # pass in the rot, text widget and delay
    
    
    def waitshowballon(cs, ce):
     
        tooltip.show(f"{cs}-{ce}", s0, s1)
    
    root.mainloop()