<TextBlock Width="100" Text="The quick brown fox jumps over the lazy dog" TextTrimming="WordEllipsis">
<TextBlock.ToolTip>
<ToolTip DataContext="{Binding Path=PlacementTarget, RelativeSource={x:Static RelativeSource.Self}}">
<TextBlock Text="{Binding Text}"/>
</ToolTip>
</TextBlock.ToolTip>
</TextBlock>
How can I show the ToolTip
only when the text is trimmed? Like the windows desktp shortcut icons.
Using CSS, we can set the max width of the table cells, hide overflow and show ellipsis at the end of the overflowing text. When the user hovers over the long text, we can display the full content in a tooltip.
TRIM will remove extra spaces from text. Thus, it will leave only single spaces between words and no space characters at the start or end of the text. It is very useful when cleaning up text from other applications or environments. TRIM only removes the ASCII space character (32) from the text.
The ToolTip element in XAML adds a tooltip to a WPF control. The ToolTip class represents a tooltip in C#. The ToolTip property of a control represents the tool tip of the control. Code examples in this this article show how to attach Tooltips to WPF controls using C# and XAML.
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>
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"/>
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.
<!-- xmlns:ui="clr-namespace:Unclassified.UI" -->
<TextBlock Text="Demo" ui:TextBlockAutoToolTip.Enabled="True"/>
var textBlock = new TextBlock { Text = "Demo" };
TextBlockAutoToolTip.SetEnabled(textBlock, true);
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();
}
}
}
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With