【问题标题】:Ellipsis at start of string in WPF ListViewWPF ListView中字符串开头的省略号
【发布时间】:2010-10-11 09:32:35
【问题描述】:

我有一个 WPF ListView (GridView) 并且单元格模板包含一个 TextBlock。如果我在TextBlock 上添加:TextTrimming="CharacterEllipsis" TextWrapping="NoWrap",当列小于字符串长度时,我的字符串末尾会出现一个省略号。我需要的是在字符串的开头有省略号。

即如果我有字符串Hello World!,我想要...lo World!,而不是Hello W...

有什么想法吗?

【问题讨论】:

  • 也许将标题更改为“WPF ListView 中字符串开头的省略号”
  • 我同意戴夫的观点,但如果您不想走那么远,至少将您的帖子命名为“左侧省略号”。
  • 我的左手和右手仍然无法正确:P
  • 知道如何更改标题吗?

标签: c# wpf string listview ellipsis


【解决方案1】:

我遇到了同样的问题并写了一个附加属性来解决这个问题(或者说,提供这个功能)。在这里捐赠我的代码:

用法

<controls:TextBlockTrimmer EllipsisPosition="Start">
    <TextBlock Text="Excuse me but can I be you for a while"
               TextTrimming="CharacterEllipsis" />
</controls:TextBlockTrimmer>

不要忘记在您的 Page/Window/UserControl 根目录中添加命名空间声明:

xmlns:controls="clr-namespace:Hillinworks.Wpf.Controls"

TextBlockTrimmer.EllipsisPosition 可以是StartMiddle(mac 样式)或End。很确定你可以从他们的名字中找出哪个是哪个。

代码

TextBlockTrimmer.cs

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Markup;

namespace Hillinworks.Wpf.Controls
{
    enum EllipsisPosition
    {
        Start,
        Middle,
        End
    }

    [DefaultProperty("Content")]
    [ContentProperty("Content")]
    internal class TextBlockTrimmer : ContentControl
    {
        private class TextChangedEventScreener : IDisposable
        {
            private readonly TextBlockTrimmer _textBlockTrimmer;

            public TextChangedEventScreener(TextBlockTrimmer textBlockTrimmer)
            {
                _textBlockTrimmer = textBlockTrimmer;
                s_textPropertyDescriptor.RemoveValueChanged(textBlockTrimmer.Content,
                                                            textBlockTrimmer.TextBlock_TextChanged);
            }

            public void Dispose()
            {
                s_textPropertyDescriptor.AddValueChanged(_textBlockTrimmer.Content,
                                                         _textBlockTrimmer.TextBlock_TextChanged);
            }
        }

        private static readonly DependencyPropertyDescriptor s_textPropertyDescriptor =
            DependencyPropertyDescriptor.FromProperty(TextBlock.TextProperty, typeof(TextBlock));

        private const string ELLIPSIS = "...";

        private static readonly Size s_inifinitySize = new Size(double.PositiveInfinity, double.PositiveInfinity);

        public EllipsisPosition EllipsisPosition
        {
            get { return (EllipsisPosition)GetValue(EllipsisPositionProperty); }
            set { SetValue(EllipsisPositionProperty, value); }
        }

        public static readonly DependencyProperty EllipsisPositionProperty =
            DependencyProperty.Register("EllipsisPosition",
                                        typeof(EllipsisPosition),
                                        typeof(TextBlockTrimmer),
                                        new PropertyMetadata(EllipsisPosition.End,
                                                             TextBlockTrimmer.OnEllipsisPositionChanged));

        private static void OnEllipsisPositionChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            ((TextBlockTrimmer)d).OnEllipsisPositionChanged((EllipsisPosition)e.OldValue,
                                                             (EllipsisPosition)e.NewValue);
        }

        private string _originalText;

        private Size _constraint;

        protected override void OnContentChanged(object oldContent, object newContent)
        {
            var oldTextBlock = oldContent as TextBlock;
            if (oldTextBlock != null)
            {
                s_textPropertyDescriptor.RemoveValueChanged(oldTextBlock, TextBlock_TextChanged);
            }

            if (newContent != null && !(newContent is TextBlock))
                // ReSharper disable once LocalizableElement
                throw new ArgumentException("TextBlockTrimmer access only TextBlock content", nameof(newContent));

            var newTextBlock = (TextBlock)newContent;
            if (newTextBlock != null)
            {
                s_textPropertyDescriptor.AddValueChanged(newTextBlock, TextBlock_TextChanged);
                _originalText = newTextBlock.Text;
            }
            else
                _originalText = null;

            base.OnContentChanged(oldContent, newContent);
        }


        private void TextBlock_TextChanged(object sender, EventArgs e)
        {
            _originalText = ((TextBlock)sender).Text;
            this.TrimText();
        }

        protected override Size MeasureOverride(Size constraint)
        {
            _constraint = constraint;
            return base.MeasureOverride(constraint);
        }

        protected override Size ArrangeOverride(Size arrangeBounds)
        {
            var result = base.ArrangeOverride(arrangeBounds);
            this.TrimText();
            return result;
        }

        private void OnEllipsisPositionChanged(EllipsisPosition oldValue, EllipsisPosition newValue)
        {
            this.TrimText();
        }

        private IDisposable BlockTextChangedEvent()
        {
            return new TextChangedEventScreener(this);
        }


        private static double MeasureString(TextBlock textBlock, string text)
        {
            textBlock.Text = text;
            textBlock.Measure(s_inifinitySize);
            return textBlock.DesiredSize.Width;
        }

        private void TrimText()
        {
            var textBlock = (TextBlock)this.Content;
            if (textBlock == null)
                return;

            if (DesignerProperties.GetIsInDesignMode(textBlock))
                return;


            var freeSize = _constraint.Width
                           - this.Padding.Left
                           - this.Padding.Right
                           - textBlock.Margin.Left
                           - textBlock.Margin.Right;

            // ReSharper disable once CompareOfFloatsByEqualityOperator
            if (freeSize <= 0)
                return;

            using (this.BlockTextChangedEvent())
            {
                // this actually sets textBlock's text back to its original value
                var desiredSize = TextBlockTrimmer.MeasureString(textBlock, _originalText);


                if (desiredSize <= freeSize)
                    return;

                var ellipsisSize = TextBlockTrimmer.MeasureString(textBlock, ELLIPSIS);
                freeSize -= ellipsisSize;
                var epsilon = ellipsisSize / 3;

                if (freeSize < epsilon)
                {
                    textBlock.Text = _originalText;
                    return;
                }

                var segments = new List<string>();

                var builder = new StringBuilder();

                switch (this.EllipsisPosition)
                {
                    case EllipsisPosition.End:
                        TextBlockTrimmer.TrimText(textBlock, _originalText, freeSize, segments, epsilon, false);
                        foreach (var segment in segments)
                            builder.Append(segment);
                        builder.Append(ELLIPSIS);
                        break;

                    case EllipsisPosition.Start:
                        TextBlockTrimmer.TrimText(textBlock, _originalText, freeSize, segments, epsilon, true);
                        builder.Append(ELLIPSIS);
                        foreach (var segment in ((IEnumerable<string>)segments).Reverse())
                            builder.Append(segment);
                        break;

                    case EllipsisPosition.Middle:
                        var textLength = _originalText.Length / 2;
                        var firstHalf = _originalText.Substring(0, textLength);
                        var secondHalf = _originalText.Substring(textLength);

                        freeSize /= 2;

                        TextBlockTrimmer.TrimText(textBlock, firstHalf, freeSize, segments, epsilon, false);
                        foreach (var segment in segments)
                            builder.Append(segment);
                        builder.Append(ELLIPSIS);

                        segments.Clear();

                        TextBlockTrimmer.TrimText(textBlock, secondHalf, freeSize, segments, epsilon, true);
                        foreach (var segment in ((IEnumerable<string>)segments).Reverse())
                            builder.Append(segment);
                        break;
                    default:
                        throw new NotSupportedException();
                }

                textBlock.Text = builder.ToString();
            }
        }


        private static void TrimText(TextBlock textBlock,
                                     string text,
                                     double size,
                                     ICollection<string> segments,
                                     double epsilon,
                                     bool reversed)
        {
            while (true)
            {
                if (text.Length == 1)
                {
                    var textSize = TextBlockTrimmer.MeasureString(textBlock, text);
                    if (textSize <= size)
                        segments.Add(text);

                    return;
                }

                var halfLength = Math.Max(1, text.Length / 2);
                var firstHalf = reversed ? text.Substring(halfLength) : text.Substring(0, halfLength);
                var remainingSize = size - TextBlockTrimmer.MeasureString(textBlock, firstHalf);
                if (remainingSize < 0)
                {
                    // only one character and it's still too large for the room, skip it
                    if (firstHalf.Length == 1)
                        return;

                    text = firstHalf;
                    continue;
                }

                segments.Add(firstHalf);

                if (remainingSize > epsilon)
                {
                    var secondHalf = reversed ? text.Substring(0, halfLength) : text.Substring(halfLength);
                    text = secondHalf;
                    size = remainingSize;
                    continue;
                }

                break;
            }
        }
    }
}

【讨论】:

  • 如果您的 TextBlock 的 Text 属性绑定到 VM,我认为这不起作用。每当绑定更新时,TextTrimmer 都不会获得更改后的值。
  • @JasonStevenson bcunning 的回答为这个问题提供了解决方案
  • 非常有用。与其他 cmets 结合,您就有了一个可行的解决方案! Tnx!
【解决方案2】:

我实现(复制)了上面的TextBlockTrimmer 代码,它非常适合加载,但如果绑定到更改的视图模型属性,TextBlock.Text 之后不会更新。 我发现有效的是

  1. TextBlockTrimmer 中定义一个名为TextBlockText 的DependencyProperty,类似于上面的EllipsisPosition 属性,包括一个OnTextBlockTextChanged() 方法。
  2. OnTextBlockTextChanged() 方法中,在调用TrimText() 之前将_originalText 设置为newValue
  3. TextBlockText 属性绑定到视图模型属性(在下面的XAML 中称为SomeText
  4. TextBlock.Text 属性绑定到XAML 中的TextBlockTrimmer.TextBlockText 属性:

    <controls:TextBlockTrimmer EllipsisPosition="Middle" TextBlockText="{Binding SomeText, Mode=OneWay}"
        <TextBlock Text="{Binding TextBlockText, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type controls:TextBlockTrimmer}}}" HorizontalAlignment="Stretch"/>
    </controls:TextBlockTrimmer>
    

如果我将TextBlockTrimmer.TextBlockTextTextBlock.Text 绑定到SomeText,它也可以工作(但这样做让我很烦)。

【讨论】:

    【解决方案3】:

    不幸的是,这在今天的 WPF 中是不可能的,你可以看到from the documentation

    (我曾在 Microsoft 从事 WPF 工作,很遗憾,这是我们没有去做的一项功能——不确定是否计划在未来的版本中使用)

    【讨论】:

      【解决方案4】:

      您可以尝试使用 ValueConverter (cf.IValueConverter interface) 自己更改应该在列表框中显示的字符串。 也就是说,在 Convert 方法的实现中,你会测试字符串是否比可用空间长,然后将它们更改为 ... 加上字符串的右侧。

      【讨论】:

      • 是的,这就是我最后所做的,它就像一个魅力;)谢谢!
      • 我了解 ValueConverter 如何提供一个可以拦截事物的点,但是您从哪里获得可用空间,以及您使用什么方法来判断字符串/子字符串是否适合该空间?
      • 请回答以上问题!!我们都知道 IValueConverter 接口的存在!您的回答就像是在说:“为了做到这一点,请编写执行此操作的代码!”我们知道我们应该在 IVlaueConverter 中编写一些代码。它是什么?如何?什么时候?谁?在哪里?!
      • @SimonLevy:你能在这里发布你的解决方案吗?
      【解决方案5】:

      这是一个如何使用递归对数算法进行高效文本剪辑的示例:

      private static string ClipTextToWidth(
          TextBlock reference, string text, double maxWidth)
      {
          var half = text.Substring(0, text.Length/2);
      
          if (half.Length > 0)
          {
              reference.Text = half;
              var actualWidth = reference.ActualWidth;
      
              if (actualWidth > maxWidth)
              {
                  return ClipTextToWidth(reference, half, maxWidth);
              }
      
              return half + ClipTextToWidth(
                  reference,
                  text.Substring(half.Length, text.Length - half.Length),
                  maxWidth - actualWidth);
          }
          return string.Empty;
      }
      

      假设您有一个名为textBlockTextBlock 字段,并且您希望以给定的最大宽度剪切其中的文本,并附加省略号。以下方法调用ClipTextToWidthtextBlock 字段设置文本:

      public void UpdateTextBlock(string text, double maxWidth)
      {
          if (text != null)
          {
              this.textBlock.Text = text;
      
              if (this.textBlock.ActualWidth > maxWidth)
              {
                  this.textBlock.Text = "...";
                  var ellipsisWidth = this.textBlock.ActualWidth;
      
                  this.textBlock.Text = "..." + ClipTextToWidth(
                      this.textBlock, text, maxWidth - ellipsisWidth);
              }
          }
          else
          {
              this.textBlock.Text = string.Empty;
          }
      }
      

      希望有帮助!

      【讨论】:

      • 您不能在设置文本后立即依赖 TextBlock.ActualWidth 准确。需要进行布局传递。
      【解决方案6】:

      感谢您的帮助 hillin 和 bcunning。
      为完整起见,这里的代码必须附加到hillinbcunning 描述的代码中。

      TextBlockTrimmer.cs

      public string TextBlockText
      {
        get => (string)GetValue(TextBlockTextProperty);
        set => SetValue(TextBlockTextProperty, value);
      }
      
      public static readonly DependencyProperty TextBlockTextProperty =
        DependencyProperty.Register("TextBlockText",
                                    typeof(string),
                                    typeof(TextBlockTrimmer),
                                    new PropertyMetadata("", OnTextBlockTextChanged));
      
      private static void OnTextBlockTextChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
      {
        ((TextBlockTrimmer)d).OnTextBlockTextChanged((string)e.OldValue, (string)e.NewValue);
      }
      
      private void OnTextBlockTextChanged(string oldValue, string newValue)
      {
        _originalText = newValue;
        this.TrimText();
      }
      

      我在 ComboBox 中使用它,对我来说它是这样工作的。
      XAML:

      <ComboBox ItemsSource="{Binding MyPaths}" SelectedItem="{Binding SelectedPath}" ToolTip="{Binding SelectedPath}">
        <ComboBox.ItemTemplate>
          <DataTemplate>
            <controls:TextBlockTrimmer EllipsisPosition="Start" TextBlockText="{Binding Mode=OneWay}">
              <TextBlock Text="{Binding}" ToolTip="{Binding}"/>
            </controls:TextBlockTrimmer>
          </DataTemplate>
        </ComboBox.ItemTemplate>
      </ComboBox>
      

      【讨论】:

        【解决方案7】:

        您可以使用IMultiValueConverter 自己修剪文本来实现这一点。

        在转换方法中,您测试字符串长度,如果它比TextBlock.ActualWidth 长,则将其修剪。

        这是我使用的实现:

        public class StartTrimmingConverter :IMultiValueConverter
        {
          public object Convert(object[] values, Type targetType, object parameter,
              CultureInfo culture)
          {
            if (values.Length != 2 || !(values[1] is TextBlock))
              return string.Empty;
        
            TextBlock reference = values[1] as TextBlock;
            return GetTrimmedText(reference, values[0].ToString());
          }
        
          public object[] ConvertBack(object value, Type[] targetTypes, object parameter,
              CultureInfo culture) => throw new NotImplementedException();
        
          private static string GetTrimmedText(TextBlock reference, string text)
          {
            if (text != null)
            {
              double maxWidth = reference.ActualWidth - 
                      reference.Padding.Left - reference.Padding.Right;
        
              if (MeasureString(reference, text).Width > maxWidth)
              {
                double ellipsisWidth = MeasureString(reference, "...").Width;
        
                return "..." + ClipTextToWidth(reference, text,
                    maxWidth - ellipsisWidth);
              }
              else
                return text;
            }
            else
              return string.Empty;
          }
        
          private static string ClipTextToWidth(TextBlock reference, string text,
              double maxWidth)
          {
            int start = (int)Math.Ceiling(text.Length / 2.0f);
            string half = text.Substring(start, text.Length / 2);
        
            if (half.Length > 0)
            {
              double actualWidth = MeasureString(reference, half).Width;
        
              if (MeasureString(reference, half).Width > maxWidth)
              {
                return ClipTextToWidth(reference, half, maxWidth);
              }
        
              return ClipTextToWidth(reference, text.Substring(0, start),
                maxWidth - actualWidth) + half;
            }
            return string.Empty;
          }
        
          private static Size MeasureString(TextBlock reference, string candidate)
          {
            FormattedText formattedText = new FormattedText(
                candidate,
                CultureInfo.CurrentCulture,
                FlowDirection.LeftToRight,
                new Typeface(reference.FontFamily, reference.FontStyle,
                             reference.FontWeight, reference.FontStretch),
                reference.FontSize,
                Brushes.Black,
                new NumberSubstitution(),
                1);
        
            return new Size(formattedText.Width, formattedText.Height);
          }
        }
        

        对于 XAML 的使用:

        <Resources>
            <my:StartTrimmingConverter x:Key="trimConv" />
        </Resources>
        ...
        <TextBlock>
            <TextBlock.Text>
                <MultiBinding Converter="{StaticResource trimConv}">
                    <Binding Path="PropertyName"/>
                    <Binding RelativeSource="{RelativeSource Self}"/>
                </MultiBinding>
            </TextBlock.Text>
        </TextBlock>
        

        (感谢丹尼尔对文本剪切的递归对数算法的回答)

        【讨论】:

          【解决方案8】:

          如果其他人像我一样偶然发现这个问题,这里有另一个更好的答案(不计分):

          Auto clip and append dots in WPF label

          【讨论】:

          • @simon 希望在文本的 开头使用省略号。他已经指出他知道TextTrimming="CharacterEllipsis"
          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2019-07-24
          • 1970-01-01
          • 2015-06-19
          • 2021-10-24
          • 2010-12-09
          • 1970-01-01
          • 2015-09-02
          相关资源
          最近更新 更多