Tkinter按钮工具提示线程问题

时间:2019-01-01 20:11:24

标签: python multithreading tkinter

嗨,我正在尝试创建一个Tkinter按钮,该按钮在悬停时带有工具提示气泡。我希望工具提示在输入时被延迟,并且仅显示一定的时间。我正在使用线程显示工具提示,以免阻止单击按钮。我似乎非常接近解决方案,但是我认为存在一个与线程有关的问题。工具提示是没有任何窗口装饰(标题栏,边框等)的顶层窗口。输入按钮时,会显示工具提示,但有时会带有装饰,有时没有,有时也会出现在正确的位置,有时则没有。我不知道为什么会发生这种行为,下面我张贴了用于解决此问题的测试代码,但现在我迷路了,不胜感激。

from time import sleep
from tkinter import Tk, Button, Label, Toplevel
from threading import Thread


class _Button(Button):
    def __init__(self, parent, *args, **kwargs):
        self.tooltip_text = kwargs.pop('tooltip', None)
        super().__init__(*args, **kwargs)

        self.t = None
        self.parent = parent
        self.btn_tooltip = None

    def tooltip(self):
        if not self.t:
            self.t = Thread(target=self.tooltip_render)
            self.t.start()

    def tooltip_render(self):
        sleep(0.5)
        if not self.btn_tooltip:
            self.btn_tooltip = Toplevel()
            self.btn_tooltip.wm_overrideredirect(True)

            x, y, cx, cy = self.bbox('insert')
            x += self.winfo_rootx() + 25
            y += self.winfo_rooty() + 25

            self.btn_tooltip.geometry('+%d+%d' % (x, y))
            label = Label(
                self.btn_tooltip, text=self.tooltip_text, background='yellow', borderwidth=1,
            )
            label.pack(ipadx=5, ipady=2)

            sleep(1)
            if self.btn_tooltip:
                self.btn_tooltip.destroy()
                self.btn_tooltip = None
                self.t = None


def enter(e):
    e.widget.tooltip()


def leave(e):
    if e.widget.btn_tooltip:
        e.widget.btn_tooltip.destroy()
        e.widget.btn_tooltip = None
        e.widget.t = None


root = Tk()

bt1 = _Button(root, text='Button 1', tooltip='Tooltip1')
bt1.bind('<Enter>', enter)
bt1.bind('<Leave>', leave)
bt1.grid()

bt2 = _Button(root, text='Button 2', tooltip='Tooltip2')
bt2.bind('<Enter>', enter)
bt2.bind('<Leave>', leave)
bt2.grid(row=0, column=1)


root.mainloop()

2 个答案:

答案 0 :(得分:1)

您不需要进行穿线即可显示工具提示,然后在几秒钟后消失。即使您有丰富的线程处理经验,线程处理也很困难。如果您不这样做,则更加困难,而将其与基于事件的程序结合起来则更加困难。

相反,您应该使用tkinter提供的功能。即,after方法可以安排代码运行到将来。

您需要做的就是使用after显示工具提示,然后再次使用after将其关闭。

基本模式如下:

def tooltip_render(self):
    # create the tooltip
    self.btn_tooltip = Toplevel()
    ... 
    <the rest of your code to render the tooltip> 
    ...

    # schedule it to go away
    self.after(1000, self.btn_tooltip.destroy)

接下来,使用render_tooltip再次呼叫after

def tooltip(self):
    self.after(500, self.tooltip_render)

仅此一项并不能为您提供完美的工具提示。如果用户快速移动鼠标,您仍然需要避免尝试渲染和销毁多个工具提示,但这可以为您提供通用框架,而无需求助于线程。

答案 1 :(得分:-1)

from tkinter import Tk, Toplevel, TclError, Label, Button


class Tooltip:
    def __init__(self, widget, text, delay=750, duration=1500):
        self.widget = widget
        self._tooltip = None

        self._hide_id = None
        self._render_id = None
        self._tooltip_text = text
        self._tooltip_delay = delay
        self._tooltip_duration = duration

        self._enter_bind = self.widget.bind("<Enter>", self.show)
        self._leave_bind = self.widget.bind("<Leave>", self.hide)
        self._button_bind = self.widget.bind("<Button>", self.hide)

    def __del__(self):
        try:
            self.widget.unbind("<Enter>", self._enter_bind)
            self.widget.unbind("<Leave>", self._leave_bind)
            self.widget.unbind("<Button>", self._button_bind)
        except TclError:
            pass

    def show(self, _):
        def render():
            if not self._tooltip:
                self._tooltip = tw = Toplevel(self.widget)
                tw.wm_overrideredirect(True)

                x, y = 20, self.widget.winfo_height() + 1
                root_x = self.widget.winfo_rootx() + x
                root_y = self.widget.winfo_rooty() + y
                self._tooltip.wm_geometry("+%d+%d" % (root_x, root_y))

                label = Label(
                    self._tooltip,
                    text=self._tooltip_text,
                    justify='left',
                    background="#ffffe0",
                    relief='solid',
                    borderwidth=1
                )
                label.pack()
                self._tooltip.update_idletasks()  # Needed on MacOS -- see #34275.
                self._tooltip.lift()
                self._hide_id = self.widget.after(self._tooltip_duration, self.hide)

        if self._tooltip_delay:
            if self._render_id:
                self.widget.after_cancel(self._render_id)
            self._render_id = self.widget.after(self._tooltip_delay, render)
        else:
            render()

    def hide(self, _=None):
        try:
            if self._hide_id:
                self.widget.after_cancel(self._hide_id)
            if self._render_id:
                self.widget.after_cancel(self._render_id)
        except TclError:
            pass

        tooltip = self._tooltip
        if self._tooltip:
            try:
                tooltip.destroy()
            except TclError:
                pass
            self._tooltip = None


def app():
    top = Toplevel()
    top.title("Test tooltip")

    button1 = Button(top, text="Button 1")
    button1.pack()
    Tooltip(button1, "Tooltip for Button 1", delay=500, duration=1500)

    button2 = Button(top, text="Button 2")
    button2.pack()
    Tooltip(button2, "Tooltip for Button 2", delay=0, duration=1500)


if __name__ == '__main__':
    tk = Tk()

    app(tk)

    tk.mainloop()