137

I have a tooltip for a Label and I want it to stay open until the user moves the mouse to a different control.

I have tried the following properties on the tooltip:

StaysOpen="True"

and

ToolTipService.ShowDuration = "60000"

But in both cases the tooltip is only displayed for exactly 5 seconds.

Why are these values being ignored?

Contango
  • 76,540
  • 58
  • 260
  • 305
TimothyP
  • 21,178
  • 26
  • 94
  • 142
  • There is a maximum value enforced *somewhere* for the `ShowDuration` property, think it is something like `30,000`. Anything greater than that and it will default back to `5000`. – Dennis Sep 29 '11 at 10:43
  • 2
    @Dennis: I tested this with WPF 3.5 and `ToolTipService.ShowDuration="60000"` worked. It did not default to back to `5000`. – sourcenouveau Dec 02 '11 at 15:28
  • 1
    @emddudley: Does the ToolTip actually stay open for 60000ms? You can set the `ToolTipService.ShowDuration` property to *any* value >= 0 (to Int32.MaxValue) however the tooltip will not stay open for that length. – Dennis Dec 02 '11 at 16:34
  • 2
    @Dennis: Yes, it stayed open for exactly 60 seconds. This is on Windows 7. – sourcenouveau Dec 02 '11 at 18:23
  • @emddudley: That could be the difference. This was knowledge from when I was developing against Windows XP. – Dennis Dec 02 '11 at 18:41
  • @M.Dudley it didn't work for me. I had to use the overriding method in John Whiter's solution below. – InvalidBrainException Jul 18 '14 at 13:26
  • Anyone know why they introduced this stupid property in first place? And I'm writing it as an user. There are few things more annoying then disappearing text when you are in the middle of reading it. I feel like been trolled... – Pawcio Sep 29 '21 at 10:54

11 Answers11

223

If you want to set this for just one tooltip, set the duration on the object having the Tooltip, like this:

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

I'd say that this design was chosen because it allows same tooltip with different timeouts on different controls.

If you want this globally for your whole app, see the accepted answer.

Martin Konicek
  • 39,126
  • 20
  • 90
  • 98
  • 4
    It also allows you to specify the content of the `ToolTip` directly, without explicit ``, which can make binding simpler. – svick Nov 05 '12 at 11:36
  • 16
    This should be the chosen answer as it is context specific and not global. – Vlad Apr 24 '15 at 16:54
  • 9
    The duration is in milliseconds. The default is 5000. The code above specifies 12 seconds. – Contango Jun 10 '15 at 11:59
  • 1
    If you use the same tooltip instance with multiple controls you sooner or later will get a "already visual child of a different parent" exception. – springy76 May 04 '16 at 07:57
  • 1
    The main reason this should be the correct answer is that it's in the spirit of actual high level programming, right into the XAML code, and easy to notice. The other solution is kinda hacky and verbose besides the point it's global. I bet most people that used that forgot about how they did it in a week. – j riv Jun 21 '17 at 13:53
  • 1
    As counterpoint for the 'this should be the answer' comments: sure if you need this in one single place, this is fine. But applications should be consistent in style and behavior so it makes quite a lot of sense to apply this particular option globally. If the code behind way is the way which just works and with the least amount of friction, it's a nice and KISS solution. At moments like that you don't want to strictly adhere to whatever principle if that actually makes life harder. Programming good practices aren't strict rules but guidelines. – stijn Nov 21 '20 at 09:31
  • Good point! Edited the answer to show the two different approaches - local or global. When I originally wrote this answer, I needed to set the duration just on a single tooltip in my app because that tooltip had a long message. – Martin Konicek Nov 26 '20 at 19:37
131

Just put this code in initialization section.

ToolTipService.ShowDurationProperty.OverrideMetadata(
    typeof(DependencyObject), new FrameworkPropertyMetadata(Int32.MaxValue));
John Whiter
  • 1,492
  • 1
  • 11
  • 5
  • This was the only solution that worked for me. How can you adapt this code to set the Placement property to Top? `new FrameworkPropertyMetadata("Top")` doesn't work. – InvalidBrainException Jul 16 '14 at 11:33
  • This worked perfectly for me, thanks for this! I put mine in the `Window_Loaded` event handler, which is invoked just before the window content is rendered. – dbeachy1 Feb 17 '15 at 02:28
  • 9
    I have marked this (after nearly 6 years years, sorry) as the correct answer because this actually works on all supported versions of Windows and keeps it open for 49 days, which should be long enough :p – TimothyP Mar 30 '15 at 09:12
  • After experimenting with different options based on advice from this posting, I settled with recommendation below by Martin Konicek, which allows the timeout to be context specific. – JGeerWM Jan 29 '16 at 16:30
  • 1
    I also put this in my Window_Loaded event and it works great. The only thing you have to do is make sure that you get rid of any "ToolTipService.ShowDuration" that you have set in your XAML, the durations you set in XAML will override the behavior that this code is trying to achieve. Thanks to John Whiter for providing the solution. – nicko Jan 03 '17 at 02:53
  • The solution below from @Martin Konicek is the one that works best IMO. Here is the link: http://stackoverflow.com/questions/896574/forcing-a-wpf-tooltip-to-stay-on-the-screen/1152450#1152450 – Ceco Mar 08 '17 at 09:44
  • 1
    FWIW, I prefer this one precisely because it's global -- I want all tooltips in my app to persist longer without further fanfare. Doing this still allows you to selectively apply a smaller value in context-specific places, as well, exactly as in the other answer. (But as always, this is only valid if you are the application -- if you're writing a control library or something else then you *must* only use context-specific solutions; global state is not yours to play with.) – Miral Jan 17 '18 at 05:24
  • As Miral said, it is global, that is to say you must make sure to call this code only once in your app, e. g. in App.xaml.cs or a main window or so. – IngoB May 08 '20 at 12:31
  • 1
    This can be dangerous! Wpf internally uses TimeSpan.FromMilliseconds() when setting the timer interval which does double calculations. This means that when the value is applied to the timer using the Interval property you can get ArgumentOutOfRangeException. – jan May 22 '20 at 11:31
  • 1
    Be careful with this solution. As others have pointed out, it needs to be called once. I am writing 2 plugin apps (completely different projects) that are loaded into 3rd party app and using this setting in both my plugins crashes the second plugin during loading, because it was already called by my first plugin. Looks like the 3rd party app that loads my plugins holds the global settings. – Sikor Sep 27 '22 at 17:38
18

This was also driving me crazy tonight. I created a ToolTip subclass to handle the issue. For me, on .NET 4.0, the ToolTip.StaysOpen property is not "really" stays open.

In the class below, use the new property ToolTipEx.IsReallyOpen, instead of property ToolTip.IsOpen. You will get the control you want. Via the Debug.Print() call, you can watch in the debugger Output window just how many times this.IsOpen = false is called! So much for StaysOpen, or should I say "StaysOpen"? Enjoy.

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);
        }
    }
}

Small rant: Why didn't Microsoft make DependencyProperty properties (getters/setters) virtual so we can accept/reject/adjust changes in subclasses? Or make a virtual OnXYZPropertyChanged for each and every DependencyProperty? Ugh.

---Edit---

My solution above looks weird in the XAML editor -- the tooltip is always showing, blocking some text in Visual Studio!

Here is a better way to solve this problem:

Some 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>

Some code:

// 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);
    }
}

Conclusion: Something is different about classes ToolTip and ContextMenu. Both have "service" classes, like ToolTipService and ContextMenuService, that manage certain properties, and both use Popup as a "secret" parent control during display. Finally, I noticed ALL the XAML ToolTip examples on the Web do not use class ToolTip directly. Instead, they embed a StackPanel with TextBlocks. Things that make you say: "hmmm..."

kevinarpe
  • 20,319
  • 26
  • 127
  • 154
8

If you want to specify that only certain elements in your Window have effectively indefinite ToolTip duration you can define a Style in your Window.Resources for those elements. Here is a Style for Button that has such a 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.">
    ...

One can also add Style.Resources to the Style to change the appearance of the ToolTip it shows, for example:

<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>

Note: When I did this I also used BasedOn in the Style so everything else defined for the version of my custom control with a normal ToolTip would be applied.

Jonathan Allan
  • 427
  • 6
  • 10
8

You probably want to use Popup instead of Tooltip, since Tooltip assumes that you're using it in the pre-defined UI-standards way.

I'm not sure why StaysOpen doesn't work, but ShowDuration works as documented in MSDN -- it's the amount of time the Tooltip is displayed WHEN it's displayed. Set it to a small amount (e.g. 500 msec) to see the difference.

The trick in your case is maintaining the "last hovered control" state, but once you have that it should be fairly trivial to change the placement target and the content dynamically (either manually, or via binding) if you're using one Popup, or hiding the last visible Popup if you're using multiple.

There are some gotchas with Popups as far as Window resizing and moving (Popups don't move w/the containers), so you may want to also have that in mind while you're tweaking the behavior. See this link for more details.

HTH.

micahtan
  • 18,530
  • 1
  • 38
  • 33
  • 3
    Also beware that Popups are always on top of all desktop objects - even if you switch to another program, the popup will be visible and obscure part of the other program. – Jeff B Jul 21 '09 at 13:53
  • That's exactly why I don't like using popups.... because they don't shrink with the program and they stay on top of all other programs. Also, resizing/moving the main application doesn't move the popup with it by default. – Rachel Feb 04 '11 at 13:08
  • FWIW, this UI convention [is rubbish anyway](http://ux.stackexchange.com/questions/3931/tooltips-with-infinite-timeout). Few things are more annoying than a tooltip that disappears while I'm reading it. – Roman Starkov Dec 13 '14 at 18:53
6

I was wrestling with the WPF Tooltip only the other day. It doesn't seem to be possible to stop it from appearing and disappearing by itself, so in the end I resorted to handling the Opened event. For example, I wanted to stop it from opening unless it had some content, so I handled the Opened event and then did this:

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

It's a hack, but it worked.

Presumably you could similarly handle the Closed event and tell it to open again, thus keeping it visible.

Daniel Earwicker
  • 114,894
  • 38
  • 205
  • 284
3

Just for the sake of completeness: In code it looks like this:

ToolTipService.SetShowDuration(element, 60000);
  • This is the best solution that works because it is not global . But you need to put this line after `InitializeComponent()` – gil123 Feb 26 '22 at 12:34
0

Got my issue fixed with the same code.

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

Aravind S
  • 59
  • 1
  • 3
0

(almost) Simple XAML version for ALL tooltips:

Put this style in your window resources (and add the missing types you commonly use)

<Style x:Key="LongToolTipStyle" TargetType="FrameworkElement">
    <Setter Property="ToolTipService.ShowDuration" Value="20000"/>
</Style>
<Style TargetType="TextBlock" BasedOn="{StaticResource LongToolTipStyle}"/>
<Style TargetType="Button" BasedOn="{StaticResource LongToolTipStyle}"/>
<Style TargetType="TextBox" BasedOn="{StaticResource LongToolTipStyle}"/>
<Style TargetType="RadioButton" BasedOn="{StaticResource LongToolTipStyle}"/>
<Style TargetType="CheckBox" BasedOn="{StaticResource LongToolTipStyle}"/>
<Style TargetType="ComboBox" BasedOn="{StaticResource LongToolTipStyle}"/>
<Style TargetType="Grid" BasedOn="{StaticResource LongToolTipStyle}"/>
<Style TargetType="StackPanel" BasedOn="{StaticResource LongToolTipStyle}"/>
<Style TargetType="DockPanel" BasedOn="{StaticResource LongToolTipStyle}"/>
<Style TargetType="Image" BasedOn="{StaticResource LongToolTipStyle}"/>

Might be a bit boring, but you can keep it in a resource dictionary. It's sad that we can't apply these styles to subclasses.

Downside

Whenever you create another style, you must remember to make that style BasedOn="{StaticResource LongToolTipStyle}"

Daniel Möller
  • 84,878
  • 18
  • 192
  • 214
0

Also if you ever want to put any other control in your ToolTip, it won't be focusable since a ToolTip itself can get focus. So Like micahtan said, your best shot is a Popup.

Carlo
  • 25,602
  • 32
  • 128
  • 176
-4
ToolTipService.ShowDurationProperty.OverrideMetadata(
    typeof(DependencyObject), new FrameworkPropertyMetadata(Int32.MaxValue));

It is working for me. Copy this line into your class constructor.

Sergey Glotov
  • 20,200
  • 11
  • 84
  • 98