TextTrimming from left
Asked Answered
C

4

23

Is there a way to specify text trimming on a TextBlock to be from the left side?

I've manage to accomplish two out of three scenarios (the third being the one I need):

  1. Regular trimming

    <TextBlock 
        VerticalAlignment="Center" 
        Width="80" 
        TextTrimming="WordEllipsis"
        Text="A very long text that requires trimming" />
    
    // Result: "A very long te..."
    
  2. Left trimming

    <TextBlock 
        VerticalAlignment="Center" 
        Width="80" 
        FlowDirection="RightToLeft"
        TextTrimming="WordEllipsis"
        Text="A very long text that requires trimming." />
    
    // Result: "...A very long te"
    
  3. Left trimming where the end of the text is seen

    // Desired result: "...uires trimming"
    

Does anyone know if this is possible? Thanks.

Christianechristiania answered 19/1, 2012 at 14:12 Comment(1)
I thing you need TextTrimming="CharacterEllipsis" instead of WordEllipsis.Antonina
E
5

You can't do this out-of-the-box, but I can think of two things that might work:

1) Create an attached property for TextBlock called something like LeftTrimmingText. Then, you would set this property instead of the Text property. E.g.

  <TextBlock my:TextBlockHelper.LeftTrimmingText="A very long text that requires trimming." />

The attached property would calculate how many characters could actually be displayed, and then set the Text property of the TextBlock accordingly.

2) Create your own class which wraps a TextBlock, and add your own properties to take care of the required logic.

I think the first option is easier.

Equipotential answered 19/1, 2012 at 14:20 Comment(3)
Unfortunately the first option is not going to be easy. Silverlight does not expose a text rendering / measurement API. The best you can do is detect when trimming occurs. See this blog post: scottlogic.co.uk/blog/colin/2011/01/…Anthozoan
ColinE: The measurement API is the TextBlock itself. Basically, you create a new temporary TextBlock in code, and keep adding characters until the ActualWidth gets bigger than what you want. The temp TextBlock doesn't need to be rendered or even in the visual tree.Equipotential
"The attached property would calculate how many characters could actually be displayed"? How? That would be a guess at best, wouldn't it?Belford
R
7

If you don't care about the ellipses, but just want to see the end of the text instead of the beginning when it gets cut-off, you can wrap the TextBlock inside another container, and set its HorizontalAlignment to Right. This will cut it off just like you want, but without the elipse.

<Grid>
    <TextBlock Text="Really long text to cutoff." HorizontalAlignment="Right"/>
</Grid>
Raggedy answered 23/1, 2012 at 0:9 Comment(0)
E
5

You can't do this out-of-the-box, but I can think of two things that might work:

1) Create an attached property for TextBlock called something like LeftTrimmingText. Then, you would set this property instead of the Text property. E.g.

  <TextBlock my:TextBlockHelper.LeftTrimmingText="A very long text that requires trimming." />

The attached property would calculate how many characters could actually be displayed, and then set the Text property of the TextBlock accordingly.

2) Create your own class which wraps a TextBlock, and add your own properties to take care of the required logic.

I think the first option is easier.

Equipotential answered 19/1, 2012 at 14:20 Comment(3)
Unfortunately the first option is not going to be easy. Silverlight does not expose a text rendering / measurement API. The best you can do is detect when trimming occurs. See this blog post: scottlogic.co.uk/blog/colin/2011/01/…Anthozoan
ColinE: The measurement API is the TextBlock itself. Basically, you create a new temporary TextBlock in code, and keep adding characters until the ActualWidth gets bigger than what you want. The temp TextBlock doesn't need to be rendered or even in the visual tree.Equipotential
"The attached property would calculate how many characters could actually be displayed"? How? That would be a guess at best, wouldn't it?Belford
P
4

This style will do the job. The trick is to redefine a control template for the label. The content is then put inside a clipping canvas and aligned to the right of the canvas. The minimum width of the content is the width of the canvas so the content text will be left aligned if there is enough space and right aligned when clipped.

The ellipses is triggered to be on if the width of the content is bigger than the canvas.

<Style x:Key="LeftEllipsesLabelStyle"
       TargetType="{x:Type Label}">
    <Setter Property="Foreground"
            Value="{DynamicResource {x:Static SystemColors.ControlTextBrushKey}}" />
    <Setter Property="Background"
            Value="Transparent" />
    <Setter Property="Padding"
            Value="5" />
    <Setter Property="HorizontalContentAlignment"
            Value="Left" />
    <Setter Property="VerticalContentAlignment"
            Value="Top" />
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type Label}">
                <Grid >
                    <Grid.Resources>
                        <LinearGradientBrush x:Key="HeaderBackgroundOpacityMask" StartPoint="0,0" EndPoint="1,0">
                            <GradientStop Color="Black"  Offset="0"/>
                            <GradientStop Color="Black" Offset="0.5"/>
                            <GradientStop Color="Transparent" Offset="1"/>
                        </LinearGradientBrush>
                    </Grid.Resources>

                    <Canvas x:Name="Canvas" 
                            ClipToBounds="True" 
                            DockPanel.Dock="Top"  
                            Height="{Binding ElementName=Content, Path=ActualHeight}">

                        <Border 
                            BorderBrush="{TemplateBinding BorderBrush}"
                            Canvas.Right="0"
                            Canvas.ZIndex="0"
                            BorderThickness="{TemplateBinding BorderThickness}"
                            Background="{TemplateBinding Background}"
                            Padding="{TemplateBinding Padding}"
                            MinWidth="{Binding ElementName=Canvas, Path=ActualWidth}"
                            SnapsToDevicePixels="true"
                            x:Name="Content"
                        >
                            <ContentPresenter
                                HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
                                Content="{Binding RelativeSource={RelativeSource AncestorType=Label}, Path=Content}"
                                RecognizesAccessKey="True"
                                SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"
                                VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
                            >
                                <ContentPresenter.Resources>
                                    <Style TargetType="TextBlock">
                                        <Setter Property="FontSize" Value="{Binding FontSize, RelativeSource={RelativeSource AncestorType={x:Type Label}}}"/> 
                                        <Setter Property="FontWeight" Value="{Binding FontWeight, RelativeSource={RelativeSource AncestorType={x:Type Label}}}"/> 
                                        <Setter Property="FontStyle" Value="{Binding FontStyle, RelativeSource={RelativeSource AncestorType={x:Type Label}}}"/> 
                                        <Setter Property="FontFamily" Value="{Binding FontFamily, RelativeSource={RelativeSource AncestorType={x:Type Label}}}"/> 
                                    </Style>
                                </ContentPresenter.Resources>

                            </ContentPresenter>
                        </Border>
                        <Label 
                            x:Name="Ellipses" 
                            Canvas.Left="0" 
                            Canvas.ZIndex="10"
                            FontWeight="{TemplateBinding FontWeight}"
                            FontSize="{TemplateBinding FontSize}"
                            FontFamily="{TemplateBinding FontFamily}"
                            FontStyle="{TemplateBinding FontStyle}"
                            VerticalContentAlignment="Center" 
                            OpacityMask="{StaticResource HeaderBackgroundOpacityMask}" 
                            Background="{TemplateBinding Background}"
                            Foreground="RoyalBlue"
                            Height="{Binding ElementName=Content, Path=ActualHeight}" 
                            Content="...&#160;&#160;&#160;">
                            <Label.Resources>
                                <Style TargetType="Label">
                                    <Style.Triggers>
                                        <DataTrigger Value="true">
                                            <DataTrigger.Binding>
                                                <MultiBinding Converter="{StaticResource GteConverter}">
                                                    <Binding ElementName="Canvas" Path="ActualWidth"/>
                                                    <Binding ElementName="Content" Path="ActualWidth"/>
                                                </MultiBinding>
                                            </DataTrigger.Binding>
                                            <Setter Property="Visibility" Value="Hidden"/>
                                        </DataTrigger>
                                    </Style.Triggers>
                                </Style>
                            </Label.Resources>

                        </Label>
                    </Canvas>

                </Grid>
                <ControlTemplate.Triggers>
                    <Trigger Property="IsEnabled"
                             Value="false">
                        <Setter Property="Foreground"
                                Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}" />
                    </Trigger>
                </ControlTemplate.Triggers>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

There are a couple of utility classes here

GteConverter

<c:GteConverter x:Key="GteConverter"/>

which is

public class RelationalValueConverter : IMultiValueConverter
{
    public enum RelationsEnum
    {
        Gt,Lt,Gte,Lte,Eq,Neq
    }

    public RelationsEnum Relations { get; protected set; }

    public RelationalValueConverter(RelationsEnum relations)
    {
        Relations = relations;
    }

    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
        if(values.Length!=2)
            throw new ArgumentException(@"Must have two parameters", "values");

        var v0 = values[0] as IComparable;
        var v1 = values[1] as IComparable;

        if(v0==null || v1==null)
            throw new ArgumentException(@"Must arguments must be IComparible", "values");

        var r = v0.CompareTo(v1);

        switch (Relations)
        {
            case RelationsEnum.Gt:
                return r > 0;
                break;
            case RelationsEnum.Lt:
                return r < 0;
                break;
            case RelationsEnum.Gte:
                return r >= 0;
                break;
            case RelationsEnum.Lte:
                return r <= 0;
                break;
            case RelationsEnum.Eq:
                return r == 0;
                break;
            case RelationsEnum.Neq:
                return r != 0;
                break;
            default:
                throw new ArgumentOutOfRangeException();
        }
    }

    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

and

public class GtConverter : RelationalValueConverter
{
    public GtConverter() : base(RelationsEnum.Gt) { }
}
public class GteConverter : RelationalValueConverter
{
    public GteConverter() : base(RelationsEnum.Gte) { }
}
public class LtConverter : RelationalValueConverter
{
    public LtConverter() : base(RelationsEnum.Lt) { }
}
public class LteConverter : RelationalValueConverter
{
    public LteConverter() : base(RelationsEnum.Lte) { }
}
public class EqConverter : RelationalValueConverter
{
    public EqConverter() : base(RelationsEnum.Eq) { }
}
public class NeqConverter : RelationalValueConverter
{
    public NeqConverter() : base(RelationsEnum.Neq) { }
}

Here is it working.

enter image description here enter image description here enter image description here

Piscatory answered 9/6, 2015 at 11:45 Comment(5)
For some reason the Label's OpacityMask wasn't working properly for me, but I managed to get the effect I wanted by using the Background property instead - neat solutions, thanks!Primateship
Looks like the cleanest solution. The code didn't work for me though, it does not show any text at all.Assessment
Do you have an example on how to use this? I'm brand new to WPF and XAML. I'm getting an error about namespace prefix "c" not being defined and type 'c:GteConverter' not being found. I assume I either don't have everything in the correct locations/files or I am missing something.Undergird
Hi Chris. I haven't used XAML for over a year now and it's starting to look like black magic to me again. However the code above kind of assumes you know how to deal with XAML namespaces. If not probably reading learn.microsoft.com/en-us/dotnet/framework/wpf/advanced/… might help. If not start another question with as complete as information as you can then someone up to date can probably help.Piscatory
Ah well thanks for the original post anyway. I think I'm getting it figured out. I was thrown off by how Visual Studio displays errors that may go away when the code is actually compiled.Undergird
E
0

I don't know if it's a typo, but you're missing the full stop at the end of your 'desired result'. I'll assume you don't want it. Since you know how many characters should be displayed, you could just get a substring of the whole string and display it. For example,

string origText = "A very long text that requires trimming.";

//15 because the first three characters are replaced
const int MAXCHARACTERS = 15;

//MAXCHARACTERS - 1 because you don't want the full stop
string sub = origText.SubString(origText.Length-MAXCHARACTERS, MAXCHARACTERS-1);

string finalString = "..." + sub;
textBlock.Text = finalString;

If you don't know how many characters you want in advanced, then you can perform a calculation to determine it. In your example, a width of 80 results in 17 characters, you can use that ratio if the width changes.

Et answered 19/1, 2012 at 14:29 Comment(4)
Don't forget - the character width depends on the fontEquipotential
Yes, I assume OP will know the font beforehand. If not, then OP can use this method to determine the text width based on the font: #913553Et
"In your example, a width of 80 results in 17 characters, you can use that ratio if the width changes." - Each character can have (and often has) its individual width. Using a ratio from one sample string is not going to produce any exact results.Maenad
"Since you know how many characters should be displayed..." This assumption is completely unfounded and absolutely absurd. The whole question would be pointless if you knew the desired text length in advance.Belford

© 2022 - 2024 — McMap. All rights reserved.