python 模态上的 tkinter 工具提示 window

python tkinter tooltip on modal window

我搜索了为应用程序实现工具提示的方法,不久前我在该站点的一些评论或答案中找到了 link to this page.
从那时起我就一直在使用这个 class,我对结果很满意。
但最近我注意到工具提示出现在模态 windows 后面,当它们引用该模态 window.

上的小部件时

在从 GitHub link 下载的代码下方,我刚刚将 from tkinter import * 替换为 import tkinter as tk,并使用前缀 [=16] =] 相应地在整个代码中。

"""Tools for displaying tool-tips.
This includes:
 * an abstract base-class for different kinds of tooltips
 * a simple text-only Tooltip class
"""
import tkinter as tk


class TooltipBase:
    """abstract base class for tooltips"""

    def __init__(self, anchor_widget):
        """Create a tooltip.
        anchor_widget: the widget next to which the tooltip will be shown
        Note that a widget will only be shown when showtip() is called.
        """
        self.anchor_widget = anchor_widget
        self.tipwindow = None

    def __del__(self):
        self.hidetip()

    def showtip(self):
        """display the tooltip"""
        if self.tipwindow:
            return
        self.tipwindow = tw = tk.Toplevel(self.anchor_widget)
        # show no border on the top level window
        tw.wm_overrideredirect(1)
        try:
            # This command is only needed and available on Tk >= 8.4.0 for OSX.
            # Without it, call tips intrude on the typing process by grabbing
            # the focus.
            tw.tk.call("::tk::unsupported::MacWindowStyle", "style", tw._w,
                       "help", "noActivates")
        except tk.TclError:
            pass

        self.position_window()
        self.showcontents()
        self.tipwindow.update_idletasks()  # Needed on MacOS -- see #34275.
        self.tipwindow.lift()  # work around bug in Tk 8.5.18+ (issue #24570)

    def position_window(self):
        """(re)-set the tooltip's screen position"""
        x, y = self.get_position()
        root_x = self.anchor_widget.winfo_rootx() + x
        root_y = self.anchor_widget.winfo_rooty() + y
        self.tipwindow.wm_geometry("+%d+%d" % (root_x, root_y))

    def get_position(self):
        """choose a screen position for the tooltip"""
        # The tip window must be completely outside the anchor widget;
        # otherwise when the mouse enters the tip window we get
        # a leave event and it disappears, and then we get an enter
        # event and it reappears, and so on forever :-(
        #
        # Note: This is a simplistic implementation; sub-classes will likely
        # want to override this.
        return 20, self.anchor_widget.winfo_height() + 1

    def showcontents(self):
        """content display hook for sub-classes"""
        # See ToolTip for an example
        raise NotImplementedError

    def hidetip(self):
        """hide the tooltip"""
        # Note: This is called by __del__, so careful when overriding/extending
        tw = self.tipwindow
        self.tipwindow = None
        if tw:
            try:
                tw.destroy()
            except tk.TclError:  # pragma: no cover
                pass


class OnHoverTooltipBase(TooltipBase):
    """abstract base class for tooltips, with delayed on-hover display"""

    def __init__(self, anchor_widget, hover_delay=1000):
        """Create a tooltip with a mouse hover delay.
        anchor_widget: the widget next to which the tooltip will be shown
        hover_delay: time to delay before showing the tooltip, in milliseconds
        Note that a widget will only be shown when showtip() is called,
        e.g. after hovering over the anchor widget with the mouse for enough
        time.
        """
        super(OnHoverTooltipBase, self).__init__(anchor_widget)
        self.hover_delay = hover_delay

        self._after_id = None
        self._id1 = self.anchor_widget.bind("<Enter>", self._show_event)
        self._id2 = self.anchor_widget.bind("<Leave>", self._hide_event)
        self._id3 = self.anchor_widget.bind("<Button>", self._hide_event)

    def __del__(self):
        try:
            self.anchor_widget.unbind("<Enter>", self._id1)
            self.anchor_widget.unbind("<Leave>", self._id2)  # pragma: no cover
            self.anchor_widget.unbind("<Button>", self._id3)  # pragma: no cover
        except tk.TclError:  # pragma: no cover
            pass
        super(OnHoverTooltipBase, self).__del__()

    def _show_event(self, event=None):
        """event handler to display the tooltip"""
        if self.hover_delay:
            self.schedule()
        else:
            self.showtip()

    def _hide_event(self, event=None):
        """event handler to hide the tooltip"""
        self.hidetip()

    def schedule(self):
        """schedule the future display of the tooltip"""
        self.unschedule()
        self._after_id = self.anchor_widget.after(self.hover_delay,
                                                  self.showtip)

    def unschedule(self):
        """cancel the future display of the tooltip"""
        after_id = self._after_id
        self._after_id = None
        if after_id:
            self.anchor_widget.after_cancel(after_id)

    def hidetip(self):
        """hide the tooltip"""
        try:
            self.unschedule()
        except tk.TclError:  # pragma: no cover
            pass
        super(OnHoverTooltipBase, self).hidetip()

    def showcontents(self):
        """content display hook for sub-classes"""
        # See ToolTip for an example
        raise NotImplementedError


class Hovertip(OnHoverTooltipBase):
    """A tooltip that pops up when a mouse hovers over an anchor widget."""
    def __init__(self, anchor_widget, text, hover_delay=1000):
        """Create a text tooltip with a mouse hover delay.
        anchor_widget: the widget next to which the tooltip will be shown
        hover_delay: time to delay before showing the tooltip, in milliseconds
        Note that a widget will only be shown when showtip() is called,
        e.g. after hovering over the anchor widget with the mouse for enough
        time.
        """
        super(Hovertip, self).__init__(anchor_widget, hover_delay=hover_delay)
        self.text = text

    def showcontents(self):
        label = tk.Label(self.tipwindow, text=self.text, justify=tk.LEFT,
                         background="#ffffe0", relief=tk.SOLID, borderwidth=1)
        label.pack()


现在用一些代码来说明我遇到的问题:


class PopupWindow:
    def __init__(self, parent):
        self.parent = parent
        self.gui = tk.Toplevel(self.parent)
        self.gui.geometry("100x30")
        self.gui.wait_visibility()
        self.ok_button = tk.Button(self.gui, text="OK", command=self.on_ok_button)
        self.ok_button.pack()
        Hovertip(self.ok_button, text="OK button", hover_delay=500)

    def on_ok_button(self):
        self.gui.destroy()

    def show(self):
        self.gui.grab_set()
        # Hovertip(self.ok_button, text="OK button", hover_delay=500)
        self.gui.wait_window()
        return 0


class App(tk.Frame):
    def __init__(self, parent, *args, **kwargs):
        tk.Frame.__init__(self, parent, *args, **kwargs)
        self.parent = parent
        button = tk.Button(parent, text="Button -- no hover delay", command=self.button)
        button.pack()
        Hovertip(button, "This is tooltip\ntext for button.", hover_delay=0)

    def button(self):
        window = PopupWindow(self.parent)
        window.show()


if __name__ == '__main__':
    root = tk.Tk()
    App(root)
    root.mainloop()

您会注意到模态 window 中“确定”按钮的工具提示出现在 window 后面(我正在更改 window 的几何形状,否则它会太小了,我们实际上不会注意到这一点)。
当然,这在具有多个小部件的真实 window 中会成为一个问题,其中一些小部件的提示根本不会被看到。

显然有两种方法可以解决这个问题:一种是删除 PopupWindow class 的 __init__ 中的行 self.gui.wait_visibility(); 另一种是删除show()方法中的self.gui.grab_set()
对于这些中的任何一个, window 不再是模态的(如果我的意思正确:我的意思是我希望 window 保持在顶部并防止父 window 在它存在时发生变化).

show 方法中的注释行是我尝试通过在 grab_set 之后定义工具提示来解决它,以便它可能出现在顶部,但它也不起作用。

我想一定有一种方法可以正确地使用这个 class 作为工具提示。
我该怎么做?

谢谢。

我找到了解决方案。
显然,有人遇到了同样的问题,尽管工具提示的 class 不同。
我指的是 this question and answer.

解决方案是在 TooltipBase class.
showtip 方法中的某处添加行 tw.wm_attributes("-topmost", 1) 我把它作为最后一行来做,但我不确定它在其他地方是否行得通;我知道如果紧跟在 tw.wm_overrideredirect(1).

之后就不会了