How can I truncate a text string in quotes and preserve the quotes?

Viewed 183

I have limited space to display a title in quotes:

'This is a short title'

so if the title is too long it needs to be truncated and the missing characters indicated by an ellipsis:

'This is too long so is trun...'

I originally had a converter that counted the characters and returned a quoted string but that produces inconsistent results due to the use of proportional fonts. The best I have come up with so far is:

<TextBlock Grid.Column="3"
           Margin="5"
           Text="{Binding Title, Mode=OneWay, StringFormat='{}\'{0}\''}"
           TextTrimming="CharacterEllipsis">

but this just displays:

'This is too long so is trun...

with the closing ' missing. I've tried negative margins and setting the Padding on the TextBlock but all that does is move where the string is truncated and still leaves off the closing '.

The TextBlock is in a Grid that is stretched across the width of the window and there are other UI elements to the right of the title that can't be moved.

I can't use a StackPanel to display the text in three parts (the opening ', the text, the closing ') as that foils the truncation completely as it now thinks it has enough space to display the whole text. I tried using a DockPanel:

<DockPanel Grid.Column="3"
           Margin="5">
    <TextBlock Text="'"/>
    <TextBlock Text="{Binding AlbumName, Mode=OneWay}"
               TextTrimming="CharacterEllipsis"/>
    <TextBlock Text="'"/>
</DockPanel>

Which worked some of the time. Depending on the title I'd sometimes get the closing ' if the truncated text ended up that little bit shorter. However, I can't use this because if the title is short enough not to need truncating I get:

'This is short '

with a space between the title and the closing '

I've looked into the possibility of passing the column width into the converter but haven't discovered a way of doing that - unless I've been searching using the wrong terms.

Is there any way I can get the effect I desire?

3 Answers

If you really need to find the maximum-text (taking FontSize/Family/etc into account), that fits into a given available space, you can calculate it by creating a temporary new TextBox and adjust/measure it until the assigned text is short enough:

  1. create a new TextBox and assign the text to it

    => You want to include leading and trailing " ' " and " ... " into the tested text

  2. measure the size of the new TextBox, to know how much space it would need to display the text, with respect to FontFamily/FontSize/etc

    => To measure the size of a control, see: Measuring controls created at runtime in wpf

However, another way is to ask the control to calculate what size it wants to be. To do that, you call Measure, and pass it a suggested size. In this case, you want to pass an infinite size (meaning the control can be as large as it likes), since that's what Canvas is going to do in the layout pass:

lb.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity)); Debug.WriteLine(lb.DesiredSize.Width);

The call to Measure doesn't actually change the control's Width, RenderSize, or ActualWidth. It just tells the Label to calculate what size it wants to be, and put that value in its DesiredSize (a property whose sole purpose is to hold the result of the last Measure call).

  1. if it's bigger, than the available-space (eg columnSize), remove 1 char and measure again

    => To pass the column-width and the text into a converter, you can can use a MultiBinding: one Binding for the text and another one for the ColumnDefinition.

xaml

<Window.Resources>
    <local:MyConverter x:Key="MyConv" />
</Window.Resources>

<Grid>
    <Grid.ColumnDefinitions>
        <ColumnDefinition x:Name="Col1" Width="*" />
        <ColumnDefinition Width="*" />
    </Grid.ColumnDefinitions>

    <TextBox>
        <TextBox.Text>
            <MultiBinding Converter="{StaticResource MyConv}">
                <Binding Path="MyText" Mode="OneWay"/>
                <Binding ElementName="Col1" Mode="OneWay"/>
            </MultiBinding>
        </TextBox.Text>
    </TextBox>
</Grid>

converter

public class MyConverter : IMultiValueConverter
{
    public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
    {
        var text = (string)values[0];
        var colDefinition = (ColumnDefinition)values[1];
            
        /// 
        /// implement your code
        /// 
        var colWidth = colDefinition.Width;

        return text;
    }

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

You can do all of that in the view (=converter), but maybe it's better placed in a VM. Since the ColumnDefinition's Width is a DependencyProperty, you can just bind it to a property on the VM and do the measuring before assigning the final (fitting) text to the property bound to TextBox.Text.

This seems to work for me:

<Grid HorizontalAlignment="Left">
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="Auto"/>
        <ColumnDefinition/>
        <ColumnDefinition Width="Auto"/>
    </Grid.ColumnDefinitions>

    <TextBlock Text="'"/>
    <TextBlock Grid.Column="1" Text="Some long dynamic text that could be truncated" TextTrimming="CharacterEllipsis"/>
    <TextBlock Grid.Column="2" Text="'"/>
</Grid>

The left and right quotes are always present, while the text in the center trims if it's too long.

You can do this with the DockPanel as well, if you set DockPanel.Dock correctly:

<DockPanel HorizontalAlignment="Left">
    <TextBlock DockPanel.Dock="Left" Text="'"/>
    <TextBlock DockPanel.Dock="Right" Text="'"/>
    <TextBlock Grid.Column="1" Text="Some long dynamic text that could be truncated" TextTrimming="CharacterEllipsis"/>
</DockPanel>

For DockPanel you just have to keep in mind:

If you set the LastChildFill property to true, which is the default setting, the last child element of a DockPanel always fills the remaining space, regardless of any other dock value that you set on the last child element.

Just as an alternative you could use this approach:

<TextBlock>'<TextBlock TextTrimming="CharacterEllipsis" Text="{Binding AlbumName, Mode=OneWay}"/>'</TextBlock>  

I have tested this and it seems that if the text block is too long only the inner text block seems to have ellipses.
I am not particularly happy with this (TextBlock within a TextBlock) but it seems to work.

Related