WPF - 左侧的可拉伸文本块,最大宽度和左侧溢出

Posted

技术标签:

【中文标题】WPF - 左侧的可拉伸文本块,最大宽度和左侧溢出【英文标题】:WPF - Stretchable TextBlock on the left with max width and overlflow to the left 【发布时间】:2020-10-11 21:37:20 【问题描述】:

我想在 WPF 中的窗口中添加一个TextBlock(或类似名称),但它必须具有以下属性:

TextBlock 从窗口左侧开始(伪代码:TextBlock.Left == 0 与父级空间有关) 根据文字内容向右增长(随着文字增加,Width增加,但Left不变) 不要超出父级的宽度(伪:TextBlock.MaxWidth == Parent.Width) 当文本长度超过最大宽度时,隐藏部分应该在左边! (没有换行!!!假设我有文本“Hi, this is a text”,可见部分应该是“...is a text”)。

我似乎无法将最大宽度设置为等于父级的大小...这不应该是一件容易的事吗?

我尝试了很多与网格、堆栈面板、文本对齐、FlowDirection、Horizo​​ntalAlignment 的组合,我尝试过的方法都不能解决这个问题,这应该很简单。我的文本要么从右侧开始,要么从左侧开始但溢出到右侧。

我怎样才能做到这一点? *** 上可用的解决方案似乎都有一个固定的最大宽度,这对我的解决方案来说是不行的,我必须让它跟随窗口。

【问题讨论】:

@NEBEZ,更新了问题的详细信息,我希望现在清楚了。 这能回答你的问题吗? Ellipsis at start of string in WPF ListView 【参考方案1】:

创建一个名为 TextBlockTrimmer 的类并编写以下内容。

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;
            
        
    

然后按如下方式使用:

<local:TextBlockTrimmer EllipsisPosition="Start" Grid.Column="0">
            <TextBlock Text="Excuse me but can I be you for a while"
               TextTrimming="CharacterEllipsis" />
</local:TextBlockTrimmer>

取自here

【讨论】:

以上是关于WPF - 左侧的可拉伸文本块,最大宽度和左侧溢出的主要内容,如果未能解决你的问题,请参考以下文章

如何制作可拉伸的自定义 UITablecell

Flex box 防止溢出并保持右侧项目固定宽度和左侧项目收缩

iOS 自定义键盘可拉伸图像停止拉伸

WPF中复选框左侧的文字?

WPF 工具栏:如何删除夹点和溢出

WPF中复选框左侧的文本?