我有一个 WPF ListView
( GridView
) 并且单元格模板包含一个TextBlock
. 如果我在TextTrimming="CharacterEllipsis" TextWrapping="NoWrap"
上添加: TextBlock
,当列变得小于字符串的长度时,我的字符串末尾会出现一个省略号。我需要的是在字符串的开头有省略号。
即,如果我有字符串Hello World!
,我想要...lo World!
,而不是Hello W...
.
有任何想法吗?
我遇到了同样的问题并写了一个附加属性来解决这个问题(或者说,提供这个功能)。在这里捐赠我的代码:
<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
. 很确定你可以从他们的名字中找出哪个是哪个。
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;
}
}
}
}
我实现(复制)了上面的TextBlockTrimmer
代码,它非常适合加载,但TextBlock.Text
如果绑定到更改的视图模型属性,之后不会更新。我发现有效的是
TextBlockText
,TextBlockTrimmer
类似于EllipsisPosition
上面的属性,包括一个OnTextBlockTextChanged()
方法。OnTextBlockTextChanged()
方法中,在调用之前设置_originalText
为.newValue
TrimText()
TextBlockText
到 View Model 属性(SomeText
在下面的 XAML 中调用)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
(但这样做让我很烦)。
不幸的是,这在今天的 WPF 中是不可能的,正如您从文档中看到的那样。
(我曾经在 Microsoft 从事 WPF 工作,不幸的是,这是我们没有去做的一个功能——不确定它是否计划在未来的版本中使用)
您可以尝试使用 ValueConverter (参见IValueConverter interface)来更改自己应该在列表框中显示的字符串。也就是说,在 Convert 方法的实现中,您将测试字符串是否比可用空间长,然后将它们更改为 ... 加上字符串的右侧。
这是一个如何使用递归对数算法进行有效文本剪辑的示例:
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;
}
}
希望有帮助!
感谢您的帮助 hillin 和 bcunning。为了完整起见
,这里是必须附加到由 bcunning描述的hillin代码的代码。
文本块修剪器.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>
您可以使用IMultiValueConverter自己修剪文本来实现这一点。
在 convert 方法中,您测试字符串长度,如果它比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>
(感谢丹尼尔对用于文本剪辑的递归对数算法的回答)
万一其他人像我一样偶然发现了这个问题,这是另一个答案更好的线程(不计分):