Ellissi all'inizio della stringa in ListView WPF
Domanda
Ho un ListView
( GridView
) e il modello di cella contiene un TextBlock
. Se aggiungo: TextTrimming = " CharacterEllipsis " TextWrapping = " NoWrap "
sul TextBlock
, appariranno dei puntini di sospensione alla fine della mia stringa quando la colonna diventa più piccola della lunghezza della stringa. Quello di cui ho bisogno è di avere i puntini di sospensione all'inizio della stringa.
vale a dire. se ho la stringa Hello World!
, vorrei ... lo World!
, invece di Hello W ...
.
Qualche idea?
Soluzione
Potresti provare a usare un ValueConverter (vedi IValueConverter interfaccia ) per modificare le stringhe che dovrebbero essere visualizzate nella casella di riepilogo. Cioè, nell'implementazione del metodo Convert, verifichi se le stringhe sono più lunghe dello spazio disponibile, e poi le cambieresti in ... più il lato destro della stringa.
Altri suggerimenti
Stavo affrontando lo stesso problema e ho scritto una proprietà collegata per risolvere questo problema (o per dire, fornire questa funzione). Dona il mio codice qui:
Uso
<controls:TextBlockTrimmer EllipsisPosition="Start">
<TextBlock Text="Excuse me but can I be you for a while"
TextTrimming="CharacterEllipsis" />
</controls:TextBlockTrimmer>
Non dimenticare di aggiungere una dichiarazione dello spazio dei nomi nella tua radice Page / Window / UserControl:
xmlns:controls="clr-namespace:Hillinworks.Wpf.Controls"
TextBlockTrimmer.EllipsisPosition
può essere Start
, Middle
(stile mac) o End
. Abbastanza sicuro che puoi capire quale è quale dai loro nomi.
codice
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;
}
}
}
}
Sfortunatamente, questo non è possibile oggi in WPF, come puoi vedere dalla documentazione .
(Lavoravo in Microsoft su WPF, questa era una caratteristica che purtroppo non ci eravamo fatti fare - non sono sicuro che sia pianificato per una versione futura)
Ho implementato (copiato) il precedente codice TextBlockTrimmer
e funzionava benissimo per il caricamento, ma il TextBlock.Text
non si sarebbe aggiornato successivamente, se associato a una proprietà View Model che cambiato.
Quello che ho scoperto che ha funzionato è stato
- Definisci una DependencyProperty chiamata
TextBlockText
inTextBlockTrimmer
, simile alla proprietàEllipsisPosition
sopra, incluso unOnTextBlockTextChanged ()
metodo. - Nel metodo
OnTextBlockTextChanged ()
, imposta_originalText
sunewValue
prima di chiamareTrimText ()
. - Associa la proprietà
TextBlockText
alla proprietà Visualizza modello (denominataSomeText
nella XAML in basso) -
Associa la proprietà
TextBlock.Text
alla proprietàTextBlockTrimmer.TextBlockText
in 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>
Ha funzionato anche se ho associato sia TextBlockTrimmer.TextBlockText
che TextBlock.Text
a SomeText
(ma facendo così mi bug).
Ecco un esempio di come eseguire un efficace ritaglio del testo con un algoritmo logaritmico ricorsivo:
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;
}
Supponi di avere un campo TextBlock
chiamato textBlock
e che desideri ritagliarvi il testo ad una larghezza massima, con i puntini di sospensione aggiunti. Il seguente metodo chiama ClipTextToWidth
per impostare il testo per il 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;
}
}
Spero che ti aiuti!
Nel caso in cui qualcun altro si imbatti in questa domanda come ho fatto io, ecco un altro thread con una risposta molto migliore (non prendersi il merito):