Hibernating Rhinos

Zero friction databases

Fade-Trimming TextBlocks in Silverlight and WPF

This is a guest post by Samuel Jack, who had done a lot of work on the new UI for RavenFS.

Using ellipsis is so last year. When did you last see the cool HTML 5 kids writing … when they couldn’t fit all their text in a column? They’ve got this fancy new feature, powered by CSS3, where text that doesn’t quite fit simply fades out as it reaches the edge of the text block. Like this (with thanks to the QuickUI Catalog)

image

Subtle! It looks much prettier, and it means you can fit in an extra three characters of real text instead of the …s.

Silverlight’s future might be uncertain, but I want to show you that it isn’t ready to roll over and die just yet. We can have those fancy fade-trimming effects too!

As part of the work I’m doing on the UI for RavenFS I implemented Fade-trimming to make our DataGrids look smarter, and I thought it would be nice to share. So I’ve packaged up all you need it into an easy-to-use class called FadeTrimming.cs which works in Silverlight and WPF, and given you two demo projects to show you how to get started.

How to use it

Include the FadeTrimming.cs file in your project. Add a namespace reference in the root element of your XAML file like this:

<Window x:Class="FadeTrimmingDemo.Wpf.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
        
        xmlns:b="clr-namespace:SilverlightEffects"
        
        Title="MainWindow" Height="350" Width="525">

You can then enable Fade Trimming on individual TextBlocks like this:

<TextBlock b:FadeTrimming.IsEnabled="True">
    Lorem ipsum dolor sit amet, consectetur adipiscing elit. 
</TextBlock>

or using a Style, like this:

<Style x:Key="Style_TextBlock_FadeTrimming" TargetType="TextBlock">
    <Setter Property="b:FadeTrimming.IsEnabled" Value="True"/>
</Style>

If you want to use it in a column of a DataGrid, try this:

<sdk:DataGridTemplateColumn Header="Name" Width="*">
     <sdk:DataGridTemplateColumn.CellTemplate>
         <DataTemplate>
             <TextBlock Text="{Binding Item.Name}" 
                        b:FadeTrimming.IsEnabled="True" 
                        b:FadeTrimming.ShowTextInToolTipWhenTrimmed="True"
                        VerticalAlignment="Center"
                        Margin="4"/>
         </DataTemplate>
     </sdk:DataGridTemplateColumn.CellTemplate>
 </sdk:DataGridTemplateColumn>

This last example shows another useful feature. Set b:FadeTrimming.ShowTextInToolTipWhenTrimmed="True", and when the text is trimmed, the TextBlock will automatically start showing the full text in its tool tip.

A couple of things to note:

  • If TextBlock.TextWrapper is set to NoWrap, then the TextBlock will be fade-trimmed on the left. Otherwise, when text wrapping is switched on, it will be fade-trimmed at the bottom.
  • You’ll probably want to set TextOptions.TextHintingMode=”Animated”. Otherwise there’s a strange rendering glitch in Silverlight where the font appears darker when it is fade-trimmed. I don’t think my code’s to blame: it seems to happen whenever text is rendered using anything other than a SolidColorBrush. TextHintingMode is an inherited property, so you can set it on a root element and it will flow to the children.
  • When TextBlocks are used in certain places (e.g. as direct children of Grids, with GridSplitters involved) they don’t seem to get the events they need to know when to update the Fade Trimming. The solution is to wrap the TextBlock in another element, usually a Border, and then everything works as it should.

How it works

There are two key ingredients to this fade-trimming trick:

  1. Knowing when and where the TextBlock has been clipped
  2. Setting the TextBlock’s Foreground brush to a LinearGradientBrush that fades to transparent just before the clip boundary.

Finding out if and how an element has been clipped in Silverlight turns out to be very easy. You just call

var layoutClip = LayoutInformation.GetLayoutClip(_textBlock);

layoutClip will be null if the element is not clipped. Otherwise its Bounds property will be set to a rectangle outlining the visible area of the element.

Fading at the edges

Setting up the TextBlock’s Foreground Brush is slightly more involved. Here’s how we do it for a horizontally clipped TextBlock:

private LinearGradientBrush GetHorizontalClipBrush(double visibleWidth)
{
    return new LinearGradientBrush
    {
        // set MappingMode to absolute so that
        // we can specify the EndPoint of the brush in
        // terms of the TextBlock's actual dimensions
        MappingMode = BrushMappingMode.Absolute,
        StartPoint = new Point(0, 0),
        EndPoint = new Point(visibleWidth, 0),
        GradientStops =
            {
                new GradientStop()
                    {Color = _foregroundColor, Offset = 0},
                new GradientStop()
                    {
                        Color = _foregroundColor,
                        // Even though the mapping mode is absolute,
                        // the offset for gradient stops is always relative with
                        // 0 being the start of the brush, and 1 the end of the brush
                        Offset = (visibleWidth - FadeWidth)/visibleWidth
                    },
                new GradientStop()
                    {
                        Color = Color.FromArgb(0, _foregroundColor.R, _foregroundColor.G, _foregroundColor.B),
                        Offset = 1
                    }
            }
    };
}

First we create a LinearGradientBrush and set its MappingMode to BrushMappingMode.Absolute. This allows us to set the StartPoint and EndPoint in terms of coordinates on the TextBlock itself. We set StartPoint to the top left corner of the TextBlock, and EndPoint to the edge of the visible region.

Then we set three GradientStops, the first two in the solid foreground color, and the last one, at the edge of the clipping region, completely transparent. One oddity with GradientStops is that even though the brush mapping mode is Absolute their values are still relative – but relative to the StartPoint and EndPoint of the brush. So Offset = 0 maps to the StartPoint of the Brush, and Offset = 1 maps to the end point of the brush. We want the fade to start a few pixels in from the edge of the visible area, so we set the Offset of the second GradientStop to (visibleWidth - FadeWidth)/visibleWidth.

Other points of interest

There are a couple of other points of interest, if you fancy inspecting the code.

One is that the clipping requirements of the TextBlock can change for many reasons. It could change because the length of the text changing, or because the font size or style changes, or because the size of the text block itself changes (imagine it is in a column of a DataGrid that the user is expanding or contracting). Whenever any of these changes occur, we need to update our special Foreground brush so the gradient fades out at the right point.

It turns out that we can handle all of these cases just by listening for the SizeChanged event on the TextBlock and of its visual parent. Note that the visual parent is not the one you obtain by calling TextBlock.Parent. That gets you the logical parent (see here to understand the difference). To get the Visual parent you call VisualTreeHelper.GetParent(_textBlock).

Before I hit on the trick of using the Foreground brush, I tried implementing this using OpacityMasks. That worked too, but it was horribly slow once you enabled fade-trimming on any significant number of TextBlocks. But that is only to be expected when you remember how OpacityMasks work. An OpacityMask is a Brush from which Silverlight extracts just the Alpha channel. To display an element with an OpacityMask, Silverlight first has to render everything behind the element, then it has to render the element (and each of its children), then finally it has to composite the two pixel-by-pixel, giving each pixel of the element the transparency determined by the corresponding pixel in the OpacityMask brush. And none of that work is hardware accelerated. OpacityMasks are great, but only when used sparingly.

Give it a go. I’d love to hear how it works out for you.

Published at

Originally posted at

Comments (8)