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.

Posted By: Ayende Rahien

Published at

Originally posted at

Comments

Ian Yates
03/26/2012 02:35 AM by
Ian Yates

Looks like a great idea and a very straightforward implementation. I'm going to give it a go - just started work on a WPF-based reworking of our app. Is it ok to use this in a commercial application (with attribution of some sort)?

Ayende Rahien
03/26/2012 06:30 AM by
Ayende Rahien

Ian, Yes, you can use the code here in your app, if you have proper attribution.

Pavel K.
06/07/2012 07:13 AM by
Pavel K.

Hello. There is bug in SL4 which is causing the "Silverlight Catastrophic failure (Exception from HRESULT: 0x8000FFFF (E_UNEXPECTED))" exception. The exception thrown when PropertyPath class is used. This is connected with this issue http://connect.microsoft.com/VisualStudio/feedback/details/674309/silverlight-unable-bind-with-custom-attached-properties-in-code-behind. Under SL5 FadeTrimming is working perfectly.

Could you please update FadeTrimming class to get it work under SL4?

And thanks for sharing this class. It is very handful. I'm tried to implement similar with OpacityMask, but your idea is better. I'm going to use your class in my application with proper attribution. Thanks again.

Samuel Jack
06/08/2012 10:51 AM by
Samuel Jack

Hi Pavel, I'm glad you're finding the FadeTrimming code useful. And thanks for reporting the bug in SL4. I've just pushed an update to GitHub which removes the bindings that I was creating in code.

This helps to improve performance, and it should also fix the problem for SL4 (though I haven't tried it myself).

Let us know how you get on.

Pavel K.
06/13/2012 02:47 AM by
Pavel K.

Everything is working fine now.

Anatoly
06/18/2012 02:55 PM by
Anatoly

Any thoughts on how to get this to work for Windows 8 Metro (WinRT)? I updated the references and added a couple ifdefs in the code to solve the build issues for the .cs file, but VS2012 designer complains that FadeTrimming doesn't exist in the SilverlightEffects namespace. Would be happy to send you the updated file.

Samuel Jack
06/19/2012 08:09 AM by
Samuel Jack

I've not tried it myself yet, unfortunately, so I can't say. I presume you have updated the xmlns syntax in the XAML? WinRT uses "using:MyNamespace" rather than "clr-namespace:MyNamespace"

Anatoly
06/19/2012 07:20 PM by
Anatoly

Thanks, "using" was the trick to getting it built at least. Now I just have to figure out how to get the correct clipping rectangle for the TextBlock, since WinRT doesn't have LayoutInformation.GetLayoutClip().

Comments have been closed on this topic.