Show Tooltip when text is being trimmed Show Tooltip when text is being trimmed wpf wpf

Show Tooltip when text is being trimmed


Working off of Eyjafj...whatever's idea, I arrived at a working, mostly declarative solution that at least doesn't require a custom control. The first hurdle to overcome is getting at the TextBlock. Because the ToolTip is rendered outside of the visual tree, you can't use a RelativeSource binding or ElementName to get at the TextBlock. Luckily, the ToolTip class provides a reference to its related element via the PlacementTarget property. So you can bind the ToolTip's Visibility property to the ToolTip itself and use its PlacementTarget property to access properties of the TextBlock:

<ToolTip Visibility="{Binding RelativeSource={RelativeSource Self}, Path=PlacementTarget, Converter={StaticResource trimmedVisibilityConverter}}">

The next step is using a converter to look at the TextBlock we've bound to to determine if the ToolTip should be visible or not. You can do this using the ActualWidth and the DesiredSize. ActualWidth is exactly what it sounds like; the width your TextBlock has been rendered to on the screen. DesiredSize is the width your TextBlock would prefer to be. The only problem is, DesiredSize seems to take the TextTrimming into account and does not give you the width of the full, untrimmed text. To solve this, we can re-call the Measure method passing Double.Positive infinity to, in effect, ask how wide the TextBlock would be if it its width were not constrained. This updates the DesiredSize property and then we can do the comparison:

textBlock.Measure(new Size(Double.PositiveInfinity, Double.PositiveInfinity));if (((FrameworkElement)value).ActualWidth < ((FrameworkElement)value).DesiredSize.Width)    return Visibility.Visible;

This approach is actually illustrated here as an attached behavior if you want to apply it automatically to TextBlocks or don't want to waste resources on creating ToolTips that will always be invisible. Here is the full code for my example:

The Converter:

public class TrimmedTextBlockVisibilityConverter : IValueConverter{    public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)    {        if (value == null) return Visibility.Collapsed;        FrameworkElement textBlock = (FrameworkElement)value;        textBlock.Measure(new System.Windows.Size(Double.PositiveInfinity, Double.PositiveInfinity));        if (((FrameworkElement)value).ActualWidth < ((FrameworkElement)value).DesiredSize.Width)            return Visibility.Visible;        else            return Visibility.Collapsed;    }    public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)    {        throw new NotImplementedException();    }}

The XAML:

<UserControl.Resources>    <local:TrimmedTextBlockVisibilityConverter x:Key="trimmedVisibilityConverter" /></UserControl.Resources>....<TextBlock TextTrimming="CharacterEllipsis" Text="{Binding SomeTextProperty}">    <TextBlock.ToolTip>        <ToolTip Visibility="{Binding RelativeSource={RelativeSource Self}, Path=PlacementTarget, Converter={StaticResource trimmedVisibilityConverter}}">            <ToolTip.Content>                <TextBlock Text="{Binding SomeTextProperty}"/>            </ToolTip.Content>        </ToolTip>    </TextBlock.ToolTip></TextBlock>


Based on ideas on this page and with additional algorithmic corrections from another answer I made this very portable class that can be used very easily. Its purpose is to enable trimming and show a ToolTip over the TextBlock when the text is trimmed, like it is known from many applications.

The trimming detection has proven to be precise in my application. The tool tip is shown exactly when the trimming ellipsis is shown.

XAML usage

<!-- xmlns:ui="clr-namespace:Unclassified.UI" --><TextBlock Text="Demo" ui:TextBlockAutoToolTip.Enabled="True"/>

C# usage

var textBlock = new TextBlock { Text = "Demo" };TextBlockAutoToolTip.SetEnabled(textBlock, true);

The complete class

using System;using System.Globalization;using System.Windows;using System.Windows.Controls;using System.Windows.Data;using System.Windows.Media;namespace Unclassified.UI{    /// <summary>    /// Shows a ToolTip over a TextBlock when its text is trimmed.    /// </summary>    public class TextBlockAutoToolTip    {        /// <summary>        /// The Enabled attached property.        /// </summary>        public static readonly DependencyProperty EnabledProperty = DependencyProperty.RegisterAttached(            "Enabled",            typeof(bool),            typeof(TextBlockAutoToolTip),            new FrameworkPropertyMetadata(new PropertyChangedCallback(OnAutoToolTipEnabledChanged)));        /// <summary>        /// Sets the Enabled attached property on a TextBlock control.        /// </summary>        /// <param name="dependencyObject">The TextBlock control.</param>        /// <param name="enabled">The value.</param>        public static void SetEnabled(DependencyObject dependencyObject, bool enabled)        {            dependencyObject.SetValue(EnabledProperty, enabled);        }        private static readonly TrimmedTextBlockVisibilityConverter ttbvc = new TrimmedTextBlockVisibilityConverter();        private static void OnAutoToolTipEnabledChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs args)        {            TextBlock textBlock = dependencyObject as TextBlock;            if (textBlock != null)            {                bool enabled = (bool)args.NewValue;                if (enabled)                {                    var toolTip = new ToolTip                    {                        Placement = System.Windows.Controls.Primitives.PlacementMode.Relative,                        VerticalOffset = -3,                        HorizontalOffset = -5,                        Padding = new Thickness(4, 2, 4, 2),                        Background = Brushes.White                    };                    toolTip.SetBinding(UIElement.VisibilityProperty, new System.Windows.Data.Binding                    {                        RelativeSource = new System.Windows.Data.RelativeSource(System.Windows.Data.RelativeSourceMode.Self),                        Path = new PropertyPath("PlacementTarget"),                        Converter = ttbvc                    });                    toolTip.SetBinding(ContentControl.ContentProperty, new System.Windows.Data.Binding                    {                        RelativeSource = new System.Windows.Data.RelativeSource(System.Windows.Data.RelativeSourceMode.Self),                        Path = new PropertyPath("PlacementTarget.Text")                    });                    toolTip.SetBinding(Control.ForegroundProperty, new System.Windows.Data.Binding                    {                        RelativeSource = new System.Windows.Data.RelativeSource(System.Windows.Data.RelativeSourceMode.Self),                        Path = new PropertyPath("PlacementTarget.Foreground")                    });                    textBlock.ToolTip = toolTip;                    textBlock.TextTrimming = TextTrimming.CharacterEllipsis;                }            }        }        private class TrimmedTextBlockVisibilityConverter : IValueConverter        {            // Source 1: https://stackoverflow.com/a/21863054            // Source 2: https://stackoverflow.com/a/25436070            public object Convert(object value, Type targetType, object parameter, CultureInfo culture)            {                var textBlock = value as TextBlock;                if (textBlock == null)                    return Visibility.Collapsed;                Typeface typeface = new Typeface(                    textBlock.FontFamily,                    textBlock.FontStyle,                    textBlock.FontWeight,                    textBlock.FontStretch);                // FormattedText is used to measure the whole width of the text held up by TextBlock container                FormattedText formattedText = new FormattedText(                    textBlock.Text,                    System.Threading.Thread.CurrentThread.CurrentCulture,                    textBlock.FlowDirection,                    typeface,                    textBlock.FontSize,                    textBlock.Foreground,                    VisualTreeHelper.GetDpi(textBlock).PixelsPerDip);                formattedText.MaxTextWidth = textBlock.ActualWidth;                // When the maximum text width of the FormattedText instance is set to the actual                // width of the textBlock, if the textBlock is being trimmed to fit then the formatted                // text will report a larger height than the textBlock. Should work whether the                // textBlock is single or multi-line.                // The width check detects if any single line is too long to fit within the text area,                // this can only happen if there is a long span of text with no spaces.                bool isTrimmed = formattedText.Height > textBlock.ActualHeight ||                    formattedText.MinWidth > formattedText.MaxTextWidth;                return isTrimmed ? Visibility.Visible : Visibility.Collapsed;            }            public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)            {                throw new NotImplementedException();            }        }    }}


I found the simplest solution to extend TextBlock and compare text lengths to determine whether to show tooltip, i.e.,

public class ToolTipTextBlock : TextBlock   {      protected override void OnToolTipOpening(ToolTipEventArgs e)      {         if (TextTrimming != TextTrimming.None)         {           e.Handled = !IsTextTrimmed();         }      }      private bool IsTextTrimmed()      {         var typeface = new Typeface(FontFamily, FontStyle, FontWeight, FontStretch);         var formattedText = new FormattedText(Text, CultureInfo.CurrentCulture, FlowDirection, typeface, FontSize, Foreground);         return formattedText.Width > ActualWidth;      }   }

Then simply use this custom text block in xaml as follows:

<local:ToolTipTextBlock Text="This is some text that I'd like to show tooltip for!"    TextTrimming="CharacterEllipsis"    ToolTip="{Binding Text,RelativeSource={RelativeSource Self}}"    MaxWidth="10"/>