Многоточие в начале строки в WPF ListView - PullRequest
10 голосов
/ 05 марта 2009

У меня есть WPF ListView (GridView), а шаблон ячейки содержит TextBlock. Если я добавлю: TextTrimming="CharacterEllipsis" TextWrapping="NoWrap" на TextBlock, многоточие появится в конце моей строки, когда столбец станет меньше, чем длина строки. Мне нужно иметь многоточие в начале строки.

т.е. если у меня есть строка Hello World!, я бы хотел ...lo World! вместо Hello W....

Есть идеи?

Ответы [ 6 ]

6 голосов
/ 22 апреля 2016

Я столкнулся с той же проблемой и написал прикрепленное свойство, чтобы решить эту проблему (или, скажем, предоставить эту функцию). Пожертвуйте мой код здесь:

* 1003 ИСПОЛЬЗОВАНИЕ * <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 может быть Start, Middle (стиль 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;
            }
        }
    }
}
4 голосов
/ 28 сентября 2017

Я реализовал (скопировал) приведенный выше код TextBlockTrimmer, и он отлично работал для загрузки, но TextBlock.Text не обновлялся бы впоследствии, если был привязан к измененному свойству View Model. Я обнаружил, что это сработало до

  1. Определите свойство DependencyProperty с именем TextBlockText в TextBlockTrimmer, аналогично приведенному выше свойству EllipsisPosition, включая метод OnTextBlockTextChanged().
  2. В методе OnTextBlockTextChanged() установите _originalText на newValue перед вызовом TrimText().
  3. Свяжите свойство TextBlockText со свойством View Model (в XAML ниже оно называется SomeText)
  4. Свяжите свойство TextBlock.Text со свойством TextBlockTrimmer.TextBlockText в XAML:

    <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.TextBlockText и TextBlock.Text с SomeText (но это меня не устраивает).

4 голосов
/ 05 марта 2009

К сожалению, сегодня это невозможно в WPF, как вы можете видеть из документации .

(Раньше я работал в Microsoft над WPF, эта функция, к сожалению, нам не удавалось - не уверен, планируется ли ее выпуск в будущей версии)

2 голосов
/ 09 марта 2009

Вы можете попробовать использовать ValueConverter (см. IValueConverter interface ), чтобы изменить строки, которые должны отображаться в окне списка самостоятельно. То есть при реализации метода Convert вы должны проверить, длиннее ли строки, чем доступное пространство, а затем изменить их на ... плюс правую часть строки.

1 голос
/ 18 февраля 2010

Вот пример того, как сделать эффективное вырезание текста с помощью рекурсивного логарифмического алгоритма:

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

Предположим, у вас есть поле TextBlock с именем textBlock, и вы хотите обрезать текст в нем с заданной максимальной шириной с добавлением многоточия. Следующий метод вызывает ClipTextToWidth, чтобы установить текст для поля textBlock:

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

Надеюсь, это поможет!

0 голосов
/ 07 февраля 2011

В случае, если кто-то еще наткнется на этот вопрос, как я, вот еще одна ветка с гораздо лучшим ответом (не принимая кредит):

Автоматическое закрепление и добавление точек в метке WPF

...