强制 WPF 工具提示留在屏幕上

Posted

技术标签:

【中文标题】强制 WPF 工具提示留在屏幕上【英文标题】:Forcing a WPF tooltip to stay on the screen 【发布时间】:2010-10-28 03:35:35 【问题描述】:

我有一个标签的工具提示,我希望它保持打开状态,直到用户 将鼠标移动到不同的控件。

我在工具提示上尝试了以下属性:

StaysOpen="True"

ToolTipService.ShowDuration = "60000"

但在这两种情况下,工具提示仅显示 5 秒。

为什么会忽略这些值?

【问题讨论】:

ShowDuration 属性在 somewhere 强制执行最大值,认为它类似于 30,000。任何大于此值的都将默认返回5000 @Dennis:我用 WPF 3.5 对此进行了测试,ToolTipService.ShowDuration="60000" 工作正常。它没有默认返回5000 @emddudley:工具提示是否真的保持打开 60000 毫秒?您可以将 ToolTipService.ShowDuration 属性设置为 any 值 >= 0(到 Int32.MaxValue),但工具提示不会在该长度内保持打开状态。 @Dennis:是的,它保持打开状态正好 60 秒。这是在 Windows 7 上。 @emddudley:这可能是不同的。这是我在针对 Windows XP 进行开发时的知识。 【参考方案1】:

前几天我还在与 WPF 工具提示搏斗。似乎无法阻止它自行出现和消失,所以最后我求助于处理Opened 事件。例如,我想阻止它打开,除非它有一些内容,所以我处理了Opened 事件,然后这样做:

tooltip.IsOpen = (tooltip.Content != null);

这是一个 hack,但它有效。

大概您可以类似地处理Closed 事件并告诉它再次打开,从而使其保持可见。

【讨论】:

ToolTip 有一个名为 HasContent 的属性,您可以改用它【参考方案2】:

您可能希望使用 Popup 而不是 Tooltip,因为 Tooltip 假定您以预定义的 UI 标准方式使用它。

我不确定为什么 StaysOpen 不起作用,但 ShowDuration 的工作原理与 MSDN 中的记录一样——它是工具提示在显示时显示的时间量。将其设置为少量(例如 500 毫秒)以查看差异。

在您的情况下,诀窍是保持“最后一个悬停控件”状态,但是一旦您拥有它,如果您正在使用动态(手动或通过绑定)更改放置目标和内容应该是相当简单的一个 Popup,或者如果您使用多个 Popup,则隐藏最后一个可见的 Popup。

就窗口大小调整和移动而言,弹出窗口存在一些问题(弹出窗口不会随容器移动),因此您在调整行为时可能还需要牢记这一点。详情请见this link。

HTH。

【讨论】:

另外请注意,弹出窗口始终位于所有桌面对象的顶部 - 即使您切换到另一个程序,弹出窗口也将是可见的,并且会隐藏其他程序的一部分。 这正是我不喜欢使用弹出窗口的原因......因为它们不会随着程序而缩小,并且它们始终位于所有其他程序之上。此外,默认情况下,调整主应用程序的大小/移动主应用程序不会移动弹出窗口。 FWIW,这个 UI 约定 is rubbish anyway。没有什么比在我阅读时消失的工具提示更令人讨厌的了。【参考方案3】:

此外,如果您想在 ToolTip 中放置任何其他控件,它就无法获得焦点,因为 ToolTip 本身可以获得焦点。所以就像 micahtan 说的,你最好的镜头是弹出窗口。

【讨论】:

【参考方案4】:

如果您只想为一个工具提示设置此项,请在具有工具提示的对象上设置持续时间,如下所示:

<Label ToolTipService.ShowDuration="12000" Name="lblShowTooltip" Content="Shows tooltip">
    <Label.ToolTip>
        <ToolTip>
            <TextBlock>Hello world!</TextBlock>
        </ToolTip>
    </Label.ToolTip>
</Label>

我会说选择这种设计是因为它允许相同的工具提示在不同的控件上具有不同的超时时间。

如果您希望在整个应用中全局使用此功能,请参阅已接受的答案。

【讨论】:

也可以直接指定ToolTip的内容,不需要显式的&lt;ToolTip&gt;,这样可以让绑定更简单。 这应该是选择的答案,因为它是特定于上下文而不是全局的。 持续时间以毫秒为单位。默认为 5000。上面的代码指定了 12 秒。 如果您将同一个工具提示实例与多个控件一起使用,您迟早会得到“不同父级的已可视子级”异常。 这应该是正确答案的主要原因是它本着实际高级编程的精神,直接进入 XAML 代码,并且很容易注意到。除了它是全局的之外,另一个解决方案有点笨拙和冗长。我敢打赌,大多数使用它的人都忘记了他们在一周内是如何做到的。【参考方案5】:

只需将此代码放在初始化部分。

ToolTipService.ShowDurationProperty.OverrideMetadata(
    typeof(DependencyObject), new FrameworkPropertyMetadata(Int32.MaxValue));

【讨论】:

这是唯一对我有用的解决方案。如何调整此代码以将 Placement 属性设置为 Top? new FrameworkPropertyMetadata("Top") 不起作用。 我已将此标记为正确答案(近 6 年后,抱歉),因为这实际上适用于所有受支持的 Windows 版本,并保持打开 49 天,这应该足够长:p 我也把它放在了我的 Window_Loaded 事件中,效果很好。您唯一需要做的就是确保删除您在 XAML 中设置的任何“ToolTipService.ShowDuration”,您在 XAML 中设置的持续时间将覆盖此代码试图实现的行为。感谢 John Whiter 提供的解决方案。 FWIW,我更喜欢这个,因为它是全球性的——我希望我的应用程序中的所有工具提示能够持续更长时间,而不会再大张旗鼓。这样做仍然允许您有选择地在特定于上下文的位置应用较小的值,就像在另一个答案中一样。 (但与往常一样,这仅在您是应用程序时才有效——如果您正在编写控件库或其他东西,那么您必须仅使用特定于上下文的解决方案;全局状态不是您的玩。) 这可能很危险! Wpf 在设置执行双重计算的计时器间隔时在内部使用 TimeSpan.FromMilliseconds() 。这意味着当使用 Interval 属性将该值应用于计时器时,您可以获得 ArgumentOutOfRangeException。【参考方案6】:
ToolTipService.ShowDurationProperty.OverrideMetadata(
    typeof(DependencyObject), new FrameworkPropertyMetadata(Int32.MaxValue));

它对我有用。将此行复制到您的类构造函数中。

【讨论】:

这是接受的答案的复制和粘贴,获得第二多的赞成【参考方案7】:

这也让我今晚发疯了。我创建了一个ToolTip 子类来处理这个问题。对我来说,在 .NET 4.0 上,ToolTip.StaysOpen 属性并不是“真正”保持打开状态。

在下面的类中,使用新属性ToolTipEx.IsReallyOpen,而不是属性ToolTip.IsOpen。你会得到你想要的控制。通过Debug.Print() 调用,您可以在调试器输出窗口中查看this.IsOpen = false 被调用了多少次! StaysOpen 这么多,还是我应该说 "StaysOpen"?享受吧。

public class ToolTipEx : ToolTip

    static ToolTipEx()
    
        IsReallyOpenProperty =
            DependencyProperty.Register(
                "IsReallyOpen",
                typeof(bool),
                typeof(ToolTipEx),
                new FrameworkPropertyMetadata(
                    defaultValue: false,
                    flags: FrameworkPropertyMetadataOptions.None,
                    propertyChangedCallback: StaticOnIsReallyOpenedChanged));
    

    public static readonly DependencyProperty IsReallyOpenProperty;

    protected static void StaticOnIsReallyOpenedChanged(
        DependencyObject o, DependencyPropertyChangedEventArgs e)
    
        ToolTipEx self = (ToolTipEx)o;
        self.OnIsReallyOpenedChanged((bool)e.OldValue, (bool)e.NewValue);
    

    protected void OnIsReallyOpenedChanged(bool oldValue, bool newValue)
    
        this.IsOpen = newValue;
    

    public bool IsReallyOpen
    
        get
        
            bool b = (bool)this.GetValue(IsReallyOpenProperty);
            return b;
        
        set  this.SetValue(IsReallyOpenProperty, value); 
    

    protected override void OnClosed(RoutedEventArgs e)
    
        System.Diagnostics.Debug.Print(String.Format(
            "OnClosed: IsReallyOpen: 0, StaysOpen: 1", this.IsReallyOpen, this.StaysOpen));
        if (this.IsReallyOpen && this.StaysOpen)
        
            e.Handled = true;
            // We cannot set this.IsOpen directly here.  Instead, send an event asynchronously.
            // DispatcherPriority.Send is the highest priority possible.
            Dispatcher.CurrentDispatcher.BeginInvoke(
                (Action)(() => this.IsOpen = true),
                DispatcherPriority.Send);
        
        else
        
            base.OnClosed(e);
        
    

小吐槽:为什么微软不将DependencyProperty 属性(getter/setter)虚拟化,以便我们可以接受/拒绝/调整子类中的更改?或者为每个DependencyProperty 制作一个virtual OnXYZPropertyChanged?呃。

---编辑---

我上面的解决方案在 XAML 编辑器中看起来很奇怪——工具提示总是显示,在 Visual Studio 中阻止了一些文本!

这里有一个更好的方法来解决这个问题:

一些 XAML:

<!-- Need to add this at top of your XAML file:
     xmlns:System="clr-namespace:System;assembly=mscorlib"
-->
<ToolTip StaysOpen="True" Placement="Bottom" HorizontalOffset="10"
        ToolTipService.InitialShowDelay="0" ToolTipService.BetweenShowDelay="0"
        ToolTipService.ShowDuration="x:Static Member=System:Int32.MaxValue"
>This is my tooltip text.</ToolTip>

一些代码:

// Alternatively, you can attach an event listener to FrameworkElement.Loaded
public override void OnApplyTemplate()

    base.OnApplyTemplate();

    // Be gentle here: If someone creates a (future) subclass or changes your control template,
    // you might not have tooltip anymore.
    ToolTip toolTip = this.ToolTip as ToolTip;
    if (null != toolTip)
    
        // If I don't set this explicitly, placement is strange.
        toolTip.PlacementTarget = this;
        toolTip.Closed += new RoutedEventHandler(OnToolTipClosed);
    


protected void OnToolTipClosed(object sender, RoutedEventArgs e)

    // You may want to add additional focus-related tests here.
    if (this.IsKeyboardFocusWithin)
    
        // We cannot set this.IsOpen directly here.  Instead, send an event asynchronously.
        // DispatcherPriority.Send is the highest priority possible.
        Dispatcher.CurrentDispatcher.BeginInvoke(
            (Action)delegate
                
                    // Again: Be gentle when using this.ToolTip.
                    ToolTip toolTip = this.ToolTip as ToolTip;
                    if (null != toolTip)
                    
                        toolTip.IsOpen = true;
                    
                ,
            DispatcherPriority.Send);
    

结论:ToolTipContextMenu 的类有些不同。两者都有“服务”类,如ToolTipServiceContextMenuService,它们管理某些属性,并且在显示期间都使用Popup 作为“秘密”父控件。最后,我注意到ALLWeb 上的XAML ToolTip 示例不直接使用ToolTip 类。相反,他们嵌入了StackPanelTextBlocks。让你说:“嗯……”

【讨论】:

您的答案应该得到更多的投票,只是因为它的彻底性。来自我的 +1。 ToolTipService 需要放置在父元素上,参见上面 Martin Konicek 的回答。【参考方案8】:

为了完整起见: 在代码中它看起来像这样:

ToolTipService.SetShowDuration(element, 60000);

【讨论】:

【参考方案9】:

如果您想指定只有 Window 中的某些元素具有 有效地无限期ToolTip 持续时间您可以在Window.Resources 中为这些元素定义Style。这是 StyleButton 有这样的 ToolTip

<Window
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:sys="clr-namespace:System;assembly=mscorlib"
    ...>
    ...
    <Window.Resources>
        <Style x:Key="ButtonToolTipIndefinate" TargetType="x:Type Button">
            <Setter Property="ToolTipService.ShowDuration"
                    Value="x:Static Member=sys:Int32.MaxValue"/>
        </Style>
        ...
    </Window.Resources>
    ...
    <Button Style="DynamicResource ButtonToolTipIndefinate"
            ToolTip="This should stay open"/>
    <Button ToolTip="This Should disappear after the default time.">
    ...

还可以在Style 中添加Style.Resources 以更改其显示的ToolTip 的外观,例如:

<Style x:Key="ButtonToolTipTransparentIndefinate" TargetType="x:Type Button">
    <Style.Resources>
        <Style x:Key="x:Type ToolTip" TargetType="x:Type ToolTip">
            <Setter Property="Background" Value="Transparent"/>
            <Setter Property="BorderBrush" Value="Transparent"/>
            <Setter Property="HasDropShadow" Value="False"/>
        </Style>
    </Style.Resources>
    <Setter Property="ToolTipService.ShowDuration"
            Value="x:Static Member=sys:Int32.MaxValue"/>
</Style>

注意:当我这样做时,我还在Style 中使用了BasedOn,因此将应用为具有普通ToolTip 的自定义控件版本定义的所有其他内容。

【讨论】:

【参考方案10】:

用相同的代码解决了我的问题。

ToolTipService.ShowDurationProperty.OverrideMetadata( typeof(DependencyObject), new FrameworkPropertyMetadata(Int32.MaxValue));

【讨论】:

以上是关于强制 WPF 工具提示留在屏幕上的主要内容,如果未能解决你的问题,请参考以下文章

WPF 一个弧形手势提示动画

在 iPhone 屏幕上的任意位置点击时关闭工具提示

如何使用 extjs 3.4 将工具提示位置保持在任何屏幕上?

WPF滑块的工具提示字符串格式不起作用[重复]

当前控件的工具提示中的 WPF C# 状态栏标签内容

移动对象后工具提示不会消失