如何确定我的TextBlock文本是否被裁剪?

49

下面的文本块如预期一样换行并裁剪。当文本被裁剪时,显示省略号"..."。

<TextBlock 
    MaxWidth="60" 
    MaxHeight="60" 
    Text="This is some long text which I would like to wrap."
    TextWrapping="Wrap" 
    TextTrimming="CharacterEllipsis" />

我想在被截断的文本上方显示一个包含完整文本的工具提示,但只有在文本被截断时才显示。我不确定如何可靠地确定是否正在显示“…”。

如何确定文本是否被截断?

5个回答

32
因为Alek的回答中的链接已经失效了,我在wayback机器上找到了缓存的链接。你无法下载文章中链接的代码,所以这里提供了一个预先组装好的版本。在尝试使其工作时,我遇到了一两个问题,因此这个代码与文章中的示例略有不同。
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;

namespace TextBlockService
{
    //Based on the project from http://web.archive.org/web/20130316081653/http://tranxcoder.wordpress.com/2008/10/12/customizing-lookful-wpf-controls-take-2/
    public static class TextBlockService
    {
        static TextBlockService()
        {
            // Register for the SizeChanged event on all TextBlocks, even if the event was handled.
            EventManager.RegisterClassHandler(
                typeof(TextBlock),
                FrameworkElement.SizeChangedEvent,
                new SizeChangedEventHandler(OnTextBlockSizeChanged),
                true);
        }


        private static readonly DependencyPropertyKey IsTextTrimmedKey = DependencyProperty.RegisterAttachedReadOnly("IsTextTrimmed", 
            typeof(bool), 
            typeof(TextBlockService), 
            new PropertyMetadata(false));

        public static readonly DependencyProperty IsTextTrimmedProperty = IsTextTrimmedKey.DependencyProperty;

        [AttachedPropertyBrowsableForType(typeof(TextBlock))]
        public static Boolean GetIsTextTrimmed(TextBlock target)
        {
            return (Boolean)target.GetValue(IsTextTrimmedProperty);
        }


        public static readonly DependencyProperty AutomaticToolTipEnabledProperty = DependencyProperty.RegisterAttached(
            "AutomaticToolTipEnabled",
            typeof(bool),
            typeof(TextBlockService),
            new FrameworkPropertyMetadata(true, FrameworkPropertyMetadataOptions.Inherits));

        [AttachedPropertyBrowsableForType(typeof(DependencyObject))]
        public static Boolean GetAutomaticToolTipEnabled(DependencyObject element)
        {
            if (null == element)
            {
                throw new ArgumentNullException("element");
            }
            return (bool)element.GetValue(AutomaticToolTipEnabledProperty);
        }

        public static void SetAutomaticToolTipEnabled(DependencyObject element, bool value)
        {
            if (null == element)
            {
                throw new ArgumentNullException("element");
            }
            element.SetValue(AutomaticToolTipEnabledProperty, value);
        }

        private static void OnTextBlockSizeChanged(object sender, SizeChangedEventArgs e)
        {
            TriggerTextRecalculation(sender);
        }

        private static void TriggerTextRecalculation(object sender)
        {
            var textBlock = sender as TextBlock;
            if (null == textBlock)
            {
                return;
            }

            if (TextTrimming.None == textBlock.TextTrimming)
            {
                textBlock.SetValue(IsTextTrimmedKey, false);
            }
            else
            {
                //If this function is called before databinding has finished the tooltip will never show.
                //This invoke defers the calculation of the text trimming till after all current pending databinding
                //has completed.
                var isTextTrimmed = textBlock.Dispatcher.Invoke(() => CalculateIsTextTrimmed(textBlock), DispatcherPriority.DataBind);
                textBlock.SetValue(IsTextTrimmedKey, isTextTrimmed);
            }
        }

        private static bool CalculateIsTextTrimmed(TextBlock textBlock)
        {
            if (!textBlock.IsArrangeValid)
            {
                return GetIsTextTrimmed(textBlock);
            }

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

            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 "formattedText.MinWidth > formattedText.MaxTextWidth" 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.
            return (formattedText.Height > textBlock.ActualHeight || formattedText.MinWidth > formattedText.MaxTextWidth);
        }

    }
}

<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                    xmlns:tbs="clr-namespace:TextBlockService">
    <!--
    Rather than forcing *all* TextBlocks to adopt TextBlockService styles,
    using x:Key allows a more friendly opt-in model.
    -->

    <Style TargetType="TextBlock" x:Key="TextBlockService">
        <Style.Triggers>
            <MultiTrigger>
                <MultiTrigger.Conditions>
                    <Condition Property="tbs:TextBlockService.AutomaticToolTipEnabled" Value="True" />
                    <Condition Property="tbs:TextBlockService.IsTextTrimmed" Value="True"/>
                </MultiTrigger.Conditions>

                <Setter Property="ToolTip" Value="{Binding RelativeSource={x:Static RelativeSource.Self}, Path=Text}" />
            </MultiTrigger>
        </Style.Triggers>
    </Style>
</ResourceDictionary>

好的,像使用Wayback Machine那样很不错。我总是忘记它还在那里。 - Mark A. Donohoe
它已经是“Opt-In”了,因为您必须使用AutomaticToolTipEnabled启用它。(响应于代码注释“与其强制所有TextBlock采用TextBlockService样式, 使用x:Key可以采用更友好的选择模型。”) - Kelly Elton
@KellyElton 我不想在窗口的每个 TextBlock 上都放置不必要的触发器。如果你发现它不会造成任何开销,你可以将其应用于每个文本框。这个应用程序是一个数据输入应用程序,有一个带有可编辑单元格的表格,它添加了几百个文本框。 - Scott Chamberlain
不知道为什么,每行中 formattedText.Height 始终比 textBlock.ActualHeight 大 0.04。换句话说,单行的 formattedText.Height - textBlock.ActualHeight 等于 0.04,两行时则为 0.08。 - huang

9

我最近没有做过太多WPF方面的工作,所以我不确定这是否是您要找的内容,但请看这篇文章:自定义“好看”的WPF控件 - 第二部分。它有点复杂,但似乎解决了您提出的同样问题。更新:该网站似乎已经不存在了,但您可以在档案中找到这篇文章。请参考Scott Chamberlain的答案和示例代码(感谢Scott)。


上述文章中提到的算法正是我正在寻找的 - 谢谢! - Andrew Jackson
2
@l33t 我也需要这个链接,这里是 Wayback Machine 缓存的页面副本 - Scott Chamberlain
8
这个回答原本应该说明如何解决问题,而不仅仅是提供一个链接。 - sydan
同意@sydan的观点。不要简单地提供链接,这是不好的S.O.行为。如果你这样做,最终我们会陷入同样的境地...一个标记答案实际上并没有解决问题。 - Mark A. Donohoe
@ygoe 这就是为什么我在答案中包含了实现该方法的所有细节和链接 ;) (此外,它似乎又可以使用了) - Scott Chamberlain
显示剩余2条评论

5

如果TextBlock是ListBoxItem DataTemplate的一部分,上面的解决方案对我无效。我提出另一个解决方案:

public class MyTextBlock : System.Windows.Controls.TextBlock
{

    protected override void OnToolTipOpening(WinControls.ToolTipEventArgs e)
    {
        if (TextTrimming != TextTrimming.None)
        {
            e.Handled = !IsTextTrimmed(); 
        }
    }

    private bool IsTextTrimmed()
    {
        Measure(new Size(Double.PositiveInfinity, Double.PositiveInfinity));
        return ActualWidth < DesiredSize.Width;
    }
}

XAML:
  <MyTextBlock Text="{Binding Text}" TextTrimming="CharacterEllipsis" ToolTip="{Binding Text}" />

这是一个不错的干净解决方案。但是它对我不起作用:我的TextBlock被修剪了(我看到省略号),但我的DesiredSize.Width等于ActualWidth。 - thehelix
你有考虑过 TextBlock 的 margin 和 padding 吗? - huang

3

在bidy的回答基础上进行扩展。这将创建一个只在未显示所有文本时显示工具提示的TextBlock。 工具提示将被调整大小以适应内容(与默认工具提示不同,后者将保持单行框并截断文本)。

using System;
using System.Windows;
using System.Windows.Controls;

namespace MyComponents
{
    public class CustomTextBlock : TextBlock
    {
        protected override void OnInitialized(EventArgs e)
        {
            // we want a tooltip that resizes to the contents -- a textblock with TextWrapping.Wrap will do that
            var toolTipTextBlock = new TextBlock();
            toolTipTextBlock.TextWrapping = TextWrapping.Wrap;
            // bind the tooltip text to the current textblock Text binding
            var binding = GetBindingExpression(TextProperty);
            if (binding != null)
            {
                toolTipTextBlock.SetBinding(TextProperty, binding.ParentBinding);
            }

            var toolTipPanel = new StackPanel();
            toolTipPanel.Children.Add(toolTipTextBlock);
            ToolTip = toolTipPanel;

            base.OnInitialized(e);
        }

        protected override void OnToolTipOpening(ToolTipEventArgs e)
        {
            if (TextTrimming != TextTrimming.None)
            {
                e.Handled = !IsTextTrimmed();
            }
        }

        private bool IsTextTrimmed()
        {
            Measure(new Size(Double.PositiveInfinity, Double.PositiveInfinity));
            return ActualWidth < DesiredSize.Width;
        }
    }
}

XAML使用:

    <Window ...
        xmlns:components="clr-namespace:MyComponents"
     ... >
    
    <components:CustomTextBlock Text="{Binding Details}" TextTrimming="CharacterEllipsis" />

0

我在使用Alex的答案时遇到了一些小问题,不得不稍微改变我的逻辑来澄清文本块中的文本是否被修剪。

var formattedText = new FormattedText(
            Text, System.Threading.Thread.CurrentThread.CurrentCulture, FlowDirection, typeface, FontSize,
            Foreground, VisualTreeHelper.GetDpi( this ).PixelsPerDip ) { MaxTextWidth = ActualWidth };
        //Measure(new Size(Double.PositiveInfinity, Double.PositiveInfinity));

 return ( Math.Floor(formattedText.Height ) > ActualHeight || Math.Floor( formattedText.MinWidth ) > ActualWidth;

这对我来说完美地运作。

我定义了一个启用省略号的 TextBlock 用户控件。然后,我为 OnMouseUp 和 OnMouseDown 定义了 2 个函数,以便当用户单击具有溢出的文本块时,它会显示一个带有完整值的工具提示。

这是 OnMouseDown 函数。

private void TextBlockWithToolTipView_OnMouseDown(
        object sender,
        MouseButtonEventArgs e )
    {
        var typeface = new Typeface(
            FontFamily,
            FontStyle,
            FontWeight,
            FontStretch);

        var formattedText = new FormattedText(
            Text, System.Threading.Thread.CurrentThread.CurrentCulture, FlowDirection, typeface, FontSize,
            Foreground, VisualTreeHelper.GetDpi( this ).PixelsPerDip ) { MaxTextWidth = ActualWidth };

        if (Math.Floor(formattedText.Height) > ActualHeight || Math.Floor(formattedText.MinWidth) > ActualWidth )
        {
            if( ToolTip is ToolTip tt )

            {
                {
                    if( tt.PlacementTarget == null )
                    {
                        tt.PlacementTarget = this;
                    }

                    tt.IsOpen = true;
                    e.Handled = true;
                }
            }
        }
    }

这是 Xaml 的部分

<TextBlock 
         ToolTipService.IsEnabled="True"
         MouseDown="TextBlockWithToolTipView_OnMouseDown"
         MouseLeave="TextBlockWithToolTipView_OnMouseLeave"   
         TextTrimming="CharacterEllipsis"
         TextWrapping="WrapWithOverflow">
<TextBlock.ToolTip>
        <ToolTip 
            DataContext="{Binding Path=PlacementTarget, RelativeSource={x:Static RelativeSource.Self}}">
        <TextBlock Text="{Binding Path=Text, Mode=OneWay }"
                       TextWrapping="Wrap"/>
        </ToolTip>
    </TextBlock.ToolTip>
</TextBlock>

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接