Forcer une info-bulle WPF à rester à l'écran

119

J'ai une info-bulle pour une étiquette et je veux qu'elle reste ouverte jusqu'à ce que l'utilisateur déplace la souris vers un contrôle différent.

J'ai essayé les propriétés suivantes dans l'info-bulle:

StaysOpen="True"

et

ToolTipService.ShowDuration = "60000"

Mais dans les deux cas, l'info-bulle ne s'affiche que pendant exactement 5 secondes.

Pourquoi ces valeurs sont-elles ignorées?

TimothyP
la source
Il y a une valeur maximale appliquée quelque part pour la ShowDurationpropriété, pensez que c'est quelque chose comme 30,000. Tout ce qui est supérieur à cela et il reviendra par défaut 5000.
Dennis le
2
@Dennis: J'ai testé cela avec WPF 3.5 et j'ai ToolTipService.ShowDuration="60000"travaillé. Il ne revenait pas par défaut à 5000.
M. Dudley
@emddudley: L'info-bulle reste-t-elle réellement ouverte pendant 60000 ms? Vous pouvez définir la ToolTipService.ShowDurationpropriété sur n'importe quelle valeur> = 0 (sur Int32.MaxValue), mais l'info-bulle ne restera pas ouverte pendant cette longueur.
Dennis
2
@Dennis: Oui, il est resté ouvert pendant exactement 60 secondes. Ceci est sur Windows 7.
M. Dudley
@emddudley: Cela pourrait être la différence. C'était une connaissance de l'époque où je développais contre Windows XP.
Dennis

Réponses:

113

Mettez simplement ce code dans la section d'initialisation.

ToolTipService.ShowDurationProperty.OverrideMetadata(
    typeof(DependencyObject), new FrameworkPropertyMetadata(Int32.MaxValue));
John Whiter
la source
C'était la seule solution qui fonctionnait pour moi. Comment pouvez-vous adapter ce code pour définir la propriété Placement sur Top? new FrameworkPropertyMetadata("Top")ne fonctionne pas.
InvalidBrainException
6
J'ai marqué ceci (après presque 6 ans, désolé) comme la bonne réponse car cela fonctionne réellement sur toutes les versions prises en charge de Windows et le maintient ouvert pendant 49 jours, ce qui devrait être assez long: p
TimothyP
1
J'ai également mis cela dans mon événement Window_Loaded et cela fonctionne très bien. La seule chose que vous devez faire est de vous assurer que vous vous débarrassez de tout "ToolTipService.ShowDuration" que vous avez défini dans votre XAML, les durées que vous définissez dans XAML remplaceront le comportement que ce code essaie d'obtenir. Merci à John Whiter d'avoir fourni la solution.
nicko
1
FWIW, je préfère celui-ci précisément parce qu'il est global - je veux que toutes les info-bulles de mon application persistent plus longtemps sans plus de fanfare. Cela vous permet toujours d'appliquer de manière sélective une valeur plus petite à des endroits spécifiques au contexte, exactement comme dans l'autre réponse. (Mais comme toujours, cela n'est valable que si vous êtes l'application - si vous écrivez une bibliothèque de contrôles ou autre chose, vous ne devez utiliser que des solutions spécifiques au contexte; l'état global n'est pas à vous de jouer.)
Miral
1
Cela peut être dangereux! Wpf utilise en interne TimeSpan.FromMilliseconds () lors de la définition de l'intervalle du minuteur qui double les calculs. Cela signifie que lorsque la valeur est appliquée au minuteur à l'aide de la propriété Interval, vous pouvez obtenir ArgumentOutOfRangeException.
janvier
191

TooltipService.ShowDuration fonctionne, mais vous devez le définir sur l'objet ayant l'info-bulle, comme ceci:

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

Je dirais que cette conception a été choisie car elle permet la même info-bulle avec différents délais d'expiration sur différents contrôles.

Martin Konicek
la source
4
Il vous permet également de spécifier le contenu du ToolTipdirectement, sans explicite <ToolTip>, ce qui peut simplifier la liaison.
svick
15
Cela devrait être la réponse choisie car elle est spécifique au contexte et non globale.
Vlad
8
La durée est en millisecondes. La valeur par défaut est 5000. Le code ci-dessus spécifie 12 secondes.
Contango
1
Si vous utilisez la même instance d'info-bulle avec plusieurs contrôles, vous obtiendrez tôt ou tard une exception "enfant déjà visuel d'un parent différent".
springy76
1
La principale raison pour laquelle cela devrait être la bonne réponse est que c'est dans l'esprit de la programmation de haut niveau, directement dans le code XAML et facile à remarquer. L'autre solution est un peu hacky et verbeuse en plus du fait qu'elle est globale. Je parie que la plupart des gens qui ont utilisé cela ont oublié comment ils l'ont fait en une semaine.
j riv
15

Cela me rendait aussi fou ce soir. J'ai créé une ToolTipsous - classe pour gérer le problème. Pour moi, sur .NET 4.0, la ToolTip.StaysOpenpropriété n'est pas "vraiment" reste ouverte.

Dans la classe ci-dessous, utilisez la nouvelle propriété ToolTipEx.IsReallyOpenau lieu de property ToolTip.IsOpen. Vous obtiendrez le contrôle que vous souhaitez. Via l' Debug.Print()appel, vous pouvez regarder dans la fenêtre de sortie du débogueur combien de fois this.IsOpen = falseest appelé! Tant pis StaysOpen, ou devrais-je dire "StaysOpen"? Prendre plaisir.

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

Petite diatribe: Pourquoi Microsoft n'a-t-il pas rendu les DependencyPropertypropriétés (getters / setters) virtuelles afin que nous puissions accepter / rejeter / ajuster les changements dans les sous-classes? Ou en faire un virtual OnXYZPropertyChangedpour chacun DependencyProperty? Pouah.

---Éditer---

Ma solution ci-dessus semble étrange dans l'éditeur XAML - l'info-bulle est toujours affichée, bloquant du texte dans Visual Studio!

Voici une meilleure façon de résoudre ce problème:

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

Un peu de 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: Il y a quelque chose de différent dans les classes ToolTipet ContextMenu. Les deux ont des classes "service", comme ToolTipServiceet ContextMenuService, qui gèrent certaines propriétés, et les deux utilisent Popupcomme contrôle parent "secret" pendant l'affichage. Enfin, j'ai remarqué que TOUS les exemples XAML ToolTip sur le Web n'utilisent pas ToolTipdirectement la classe . Au lieu de cela, ils intègrent un StackPanelavec l' TextBlockart. Des choses qui vous font dire: "hmmm ..."

kevinarpe
la source
1
Vous devriez obtenir plus de votes sur votre réponse juste pour sa rigueur. +1 de moi.
Hannish
ToolTipService doit être placé sur l'élément parent, voir la réponse de Martin Konicek ci-dessus.
Jeson Martajaya
8

Vous souhaiterez probablement utiliser Popup au lieu de Tooltip, car Tooltip suppose que vous l'utilisez selon les normes d'interface utilisateur prédéfinies.

Je ne sais pas pourquoi StaysOpen ne fonctionne pas, mais ShowDuration fonctionne comme documenté dans MSDN - c'est la durée pendant laquelle l'info-bulle est affichée LORSQUE elle est affichée. Réglez-le sur une petite quantité (par exemple 500 ms) pour voir la différence.

L'astuce dans votre cas est de conserver l'état "dernier contrôle survolé", mais une fois que vous l'avez fait, il devrait être assez simple de changer la cible de placement et le contenu de manière dynamique (soit manuellement, soit via une liaison) si vous utilisez un Popup, ou masquer le dernier Popup visible si vous en utilisez plusieurs.

Il y a quelques pièges avec les fenêtres contextuelles en ce qui concerne le redimensionnement et le déplacement de la fenêtre (les fenêtres contextuelles ne bougent pas avec les conteneurs), vous voudrez peut-être également garder cela à l'esprit lorsque vous peaufinez le comportement. Voir ce lien pour plus de détails.

HTH.

micahtan
la source
3
Méfiez-vous également que les fenêtres contextuelles sont toujours au-dessus de tous les objets du bureau - même si vous passez à un autre programme, la fenêtre contextuelle sera visible et obscurcira une partie de l'autre programme.
Jeff B
C'est exactement pourquoi je n'aime pas utiliser les popups ... parce qu'ils ne rétrécissent pas avec le programme et ils restent au top de tous les autres programmes. De plus, le redimensionnement / déplacement de l'application principale ne déplace pas la fenêtre contextuelle avec elle par défaut.
Rachel le
FWIW, cette convention d'interface utilisateur est de toute façon nul . Peu de choses sont plus ennuyeuses qu'une info-bulle qui disparaît pendant que je la lis.
Roman Starkov
7

Si vous souhaitez spécifier que seuls certains éléments de votre Windowont effectivement une ToolTipdurée indéfinie, vous pouvez définir un Styledans votre Window.Resourcespour ces éléments. Voici un Stylepour Buttonqui a un tel 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.">
    ...

On peut également ajouter Style.Resourcesau Stylepour changer l'apparence de celui- ToolTipci, par exemple:

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

Remarque: lorsque j'ai fait cela, j'ai également utilisé BasedOndans le Styleafin que tout le reste défini pour la version de mon contrôle personnalisé avec une normale ToolTipsoit appliqué.

Jonathan Allan
la source
5

Je ne luttais avec l'info-bulle WPF que l'autre jour. Il ne semble pas possible de l'empêcher d'apparaître et de disparaître par lui-même, alors j'ai finalement eu recours à la gestion de l' Openedévénement. Par exemple, je voulais l'empêcher de s'ouvrir à moins qu'il ne contienne du contenu, j'ai donc géré l' Openedévénement, puis j'ai fait ceci:

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

C'est un hack, mais ça a marché.

Vous pouvez probablement gérer l' Closedévénement de la même manière et lui dire de s'ouvrir à nouveau, le gardant ainsi visible.

Daniel Earwicker
la source
ToolTip a une propriété appelée HasContent que vous pouvez utiliser à la place
benPearce
2

Juste pour être complet: dans le code, cela ressemble à ceci:

ToolTipService.SetShowDuration(element, 60000);
Ulrich Beckert
la source
0

De plus, si vous souhaitez mettre un autre contrôle dans votre info-bulle, il ne sera pas focalisable car une info-bulle elle-même peut obtenir le focus. Donc, comme l'a dit micahtan, votre meilleur coup est un Popup.

Carlo
la source
0

J'ai résolu mon problème avec le même code.

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

Aravind S
la source
-4
ToolTipService.ShowDurationProperty.OverrideMetadata(
    typeof(DependencyObject), new FrameworkPropertyMetadata(Int32.MaxValue));

Ça marche pour moi. Copiez cette ligne dans votre constructeur de classe.

Vibhuti Sharma
la source
3
ceci est un copier-coller de la réponse acceptée avec le deuxième plus grand nombre de votes positifs
CodingYourLife