Elipsis al inicio de la cadena en WPF ListView
Pregunta
Tengo un ListView
( GridView
) de WPF y la plantilla de la celda contiene un TextBlock
. Si agrego: TextTrimming = " CharacterEllipsis " TextWrapping = " NoWrap "
en el TextBlock
, aparecerá una elipsis al final de mi cadena cuando la columna sea más pequeña que la longitud de la cadena. Lo que necesito es tener los puntos suspensivos al principio de la cadena.
I.e. si tengo la cadena Hello World!
, me gustaría ... lo World!
, en lugar de Hello W ...
.
¿Alguna idea?
Solución
Puede intentar usar un ValueConverter (consulte IValueConverter interface ) para cambiar las cadenas que deberían aparecer en el cuadro de lista. Es decir, en la implementación del método Convert, usted probaría si las cadenas son más largas que el espacio disponible y luego las cambia a ... más el lado derecho de la cadena.
Otros consejos
Estaba enfrentando el mismo problema y escribí una propiedad adjunta para resolver esto (o para decir, proporcionar esta característica). Dona mi código aquí:
USO
<controls:TextBlockTrimmer EllipsisPosition="Start">
<TextBlock Text="Excuse me but can I be you for a while"
TextTrimming="CharacterEllipsis" />
</controls:TextBlockTrimmer>
No olvide agregar una declaración de espacio de nombres en su raíz de Page / Window / UserControl:
xmlns:controls="clr-namespace:Hillinworks.Wpf.Controls"
TextBlockTrimmer.EllipsisPosition
puede ser Start
, Middle
(estilo mac) o End
. Bastante seguro de que puedes averiguar cuál es cuál de sus nombres.
CÓDIGO
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;
}
}
}
}
Lamentablemente, esto no es posible en WPF hoy, ya que puede ver de la documentación .
(solía trabajar en Microsoft en WPF, esta fue una característica que desafortunadamente no pudimos hacer, no estoy seguro si está planeada para una versión futura)
Implementé (copié) el código TextBlockTrimmer
anterior y funcionó muy bien para cargar, pero el TextBlock.Text
no se actualizaría más adelante, si estuviera vinculado a una propiedad de View Model que cambiado
Lo que encontré que funcionó fue para
- Defina un DependencyProperty llamado
TextBlockText
enTextBlockTrimmer
, similar a la propiedadEllipsisPosition
de arriba, incluyendo unOnTextBlockTextChanged ()
método. - En el método
OnTextBlockTextChanged ()
, configure_originalText
ennewValue
antes de llamar aTrimText ()
. - Vincule la propiedad
TextBlockText
a la propiedad Ver modelo (llamadaSomeText
en el XAML a continuación) -
Enlace la propiedad
TextBlock.Text
a la propiedadTextBlockTrimmer.TextBlockText
en el 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>
También funcionó si enlazaba TextBlockTrimmer.TextBlockText
y TextBlock.Text
a SomeText
(pero hacerlo me molesta).
Aquí hay un ejemplo de cómo hacer un recorte de texto eficiente con un algoritmo logarítmico recursivo:
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;
}
Supongamos que tiene un campo TextBlock
llamado textBlock
, y desea recortar el texto en un ancho máximo determinado, con los puntos suspensivos adjuntos. El siguiente método llama a ClipTextToWidth
para configurar el texto para el campo 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;
}
}
Espero que ayude!
En caso de que alguien más se tropiece con esta pregunta como yo lo hice, aquí hay otro hilo con una respuesta mucho mejor (sin tomar crédito):