TextEffects is a poorly-documented property that allows you to apply an effect to a portion of text. It’s not the best of name for the property (at least in this first version of WPF), because the only “effects” supported in the current version are Clip, Foreground, and Transform.

There are four important things to know about TextEffects:

  1. They’re really only interesting when you’re working with a substring of text. In other words, if you want to affect the entire content of a TextBlock, you’re better off using properties exposed directly on TextBlock: Clip, Foreground, and RenderTransform (or its cousin, LayoutTransform).
  2. The Clip and Transform effects are always relative to the beginning of the line. In other words, the default CenterX and CenterY of a RotateTransform are at the beginning of the line — not at the beginning of the text being affected (see the screenshots below).
  3. Just like RenderTransform, any size and position changes caused will not affect the layout of text. This is good for animation, since you don’t want to performance penalty caused by re-layout. It does, however, mean that you can easily end up with overlapping text.
  4. They’re a little quirky and frustrating to use (at least in this version of the platform). With the exception of a few narrow scenarios, you’re better off using Clip, Foreground, or RenderTransform.

Let’s start with a basic example and the screenshot:

<Border xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        VerticalAlignment="Center" HorizontalAlignment="Center" BorderThickness="1" BorderBrush="#ccc" Padding="20" SnapsToDevicePixels="True">
  <TextBlock FontFamily="Calibri" FontSize="30" TextWrapping="Wrap">
    <TextBlock.TextEffects>
      <TextEffect PositionStart="5" PositionCount="10">
        <TextEffect.Transform>
          <TranslateTransform Y="-5" />
        </TextEffect.Transform>
      </TextEffect>
    </TextBlock.TextEffects>
    Clap Your Hands Say Yeah!
  </TextBlock>
</Border>

The text 'Clap Your Hands Say Yeah!' with the words 'Your Hands' elevated 10 pixels

The markup is pretty straightforward: TextEffect.PositionStart and TextEffect.PositionCount properties indicate the substring for the effect, while TextEffect.Transform is a standard Transform.

When we resize, notice how the effect applies even across a line break:

The text 'Clap Your Hands Say Yeah!' with the words 'Your Hands' elevated 10 pixels, with a line break between 'Your' and 'Hands'

That was simple enough, now let’s switch to a RotateTransform and see how that works:

<Border xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        VerticalAlignment="Center" HorizontalAlignment="Center" BorderThickness="1" BorderBrush="#ccc" Padding="20" SnapsToDevicePixels="True">
  <TextBlock FontFamily="Calibri" FontSize="30" TextWrapping="Wrap">
    <TextBlock.TextEffects>
      <TextEffect PositionStart="5" PositionCount="4">
        <TextEffect.Transform>
          <RotateTransform Angle="-5" />
        </TextEffect.Transform>
      </TextEffect>
      <TextEffect PositionStart="16" PositionCount="3">
        <TextEffect.Transform>
          <RotateTransform Angle="-5" />
        </TextEffect.Transform>
      </TextEffect>
    </TextBlock.TextEffects>
    Clap Your Hands Say Yeah!
  </TextBlock>
</Border>

Now we have two identical RotateTransform effects being applied to two different portions of the text. Here’s the screenshot:

The text 'Clap Your Hands Say Yeah!' with the words 'Your' and 'Say' rotated 5 degrees counter-clockwise

Is that what you expected? Let’s add some lines to make it clearer:

The text 'Clap Your Hands Say Yeah!' with the words 'Your' and 'Say' rotated 5 degrees counter-clockwise, with guide lines showing the 5 degree angle

As I mentioned earlier, effects are always relative to the beginning of the line. Look what happens when the text wraps:

The text 'Clap Your Hands Say Yeah!' with the words 'Your' and 'Say' rotated 5 degrees counter-clockwise, with a line break between 'Hands' and 'Say'

Notice how “Say” is now rotated with respect to the beginning of the line it occurs on.

Unfortunately, there’s no foolproof way to make the rotation relative to the position of the text. You can manually set the rotation center through the RotateTransform.CenterX and RotateTransform.CenterY properties — but to get the desired effect you’d have to calculate the distance from the word to the beginning of the line (which will end up being incorrect if the text breaks across lines).

<Run>ning into Trouble

Honestly, I had never done much with TextEffects until I started writing this post. I discovered a few tricky issues while constructing the following markup:

<Border xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        VerticalAlignment="Center" HorizontalAlignment="Center" BorderThickness="1" BorderBrush="#ccc" Padding="20" SnapsToDevicePixels="True">
  <TextBlock FontFamily="Calibri" FontSize="30" TextWrapping="Wrap">
    <TextBlock.TextEffects>
      <TextEffect PositionStart="5" PositionCount="4">
        <TextEffect.Transform>
          <RotateTransform Angle="-5" />
        </TextEffect.Transform>
      </TextEffect>
    </TextBlock.TextEffects>
    Clap <Bold>Your</Bold> Hands Say Yeah!
  </TextBlock>
</Border>

This is the markup used in our rotation example, with the second effect removed and a Bold tag added around the word “Your.” If you load this XAML, you’ll see the following:

The text 'Clap Your Hands Say Yeah!' with the word 'Your' in bold.

It was a little tricky to realize what’s going on here. First, it turns out that TextEffects only works with a simple string of content. Let’s try fix this by applying the TextEffect to the Bold instead of the TextBlock (note that we also change the PositionStart to 0):

<Border xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        VerticalAlignment="Center" HorizontalAlignment="Center" BorderThickness="1" BorderBrush="#ccc" Padding="20" SnapsToDevicePixels="True">
  <TextBlock FontFamily="Calibri" FontSize="30" TextWrapping="Wrap">
    Clap
    <Bold>
      <Bold.TextEffects>
        <TextEffect PositionStart="0" PositionCount="4">
          <TextEffect.Transform>
            <RotateTransform Angle="-5" />
          </TextEffect.Transform>
        </TextEffect>
      </Bold.TextEffects>
      Your
    </Bold>
    Hands Say Yeah!
  </TextBlock>
</Border>

Load this up and we get:

The text 'Clap Your Hands Say Yeah!' with the word 'Your' in bold.

Well, we’re missing yet another thing! As you may already know, Run elements are used behind the scenes when we’re constructing element trees with text. It seems that the effect isn’t applying to the Run inside the Bold. Let’s switch the Bold to a Run and set the FontWeight property:

<Border xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        VerticalAlignment="Center" HorizontalAlignment="Center" BorderThickness="1" BorderBrush="#ccc" Padding="20" SnapsToDevicePixels="True">
  <TextBlock FontFamily="Calibri" FontSize="30" TextWrapping="Wrap">
    Clap
    <Run FontWeight="Bold">
      <Run.TextEffects>
        <TextEffect PositionStart="0" PositionCount="4">
          <TextEffect.Transform>
            <RotateTransform Angle="-5" />
          </TextEffect.Transform>
        </TextEffect>
      </Run.TextEffects>
      Your
    </Run>
    Hands Say Yeah!
  </TextBlock>
</Border>

Drumroll please …

The text 'Clap Your Hands Say Yeah!' with the word 'Your' in bold.

Argh! This last one took a while, but I finally figured it out by playing with PositionStart and PositionEnd. Counter-intuitively, the PositionStart property isn’t relative to the beginning of the Run, it seems to be relative to the beginning of the containing TextBlock — but not quite. Originally, we used a PositionStart of 5, but in order to work as expected, I had to use a value of 8! Removing the whitespace between “Clap” and the Run lowered this to 7, but I never figured out how to get it to 5 (which, although counter-intuitive, I could at least understand).

Bleh. My recommendation is to set PositionStart to 0 and PositionCount to something large, like 100. Doing so, we get our final markup here:

<Border xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        VerticalAlignment="Center" HorizontalAlignment="Center" BorderThickness="1" BorderBrush="#ccc" Padding="20" SnapsToDevicePixels="True">
  <TextBlock FontFamily="Calibri" FontSize="30" TextWrapping="Wrap">
    Clap
    <Run FontWeight="Bold">
      <Run.TextEffects>
        <TextEffect PositionStart="0" PositionCount="100">
          <TextEffect.Transform>
            <RotateTransform Angle="-5" />
          </TextEffect.Transform>
        </TextEffect>
      </Run.TextEffects>
      Your
    </Run>
    Hands Say Yeah!
  </TextBlock>
</Border>

Finally, we get the expected result!

The text 'Clap Your Hands Say Yeah!' with the word 'Your' in bold and rotated 5 degrees.

In the end …

In most cases, you’re better off using RenderTransform if you can get away with it. Hopefully, future version of WPF will add some more interesting effects and fix these weird issues. Meanwhile, I hope this post helps people who are struggling with TextEffects.

Viewing 1 Comment

    • ^
    • v
    Hi,
    Just looked at your page and it solved a problem for me when doing the same thing in code.

    Dim run As New Run
    Dim translateTransform As New TranslateTransform
    translateTransform.Y = -10
    translateTransform.X = 0
    Dim textEffect As New TextEffect
    textEffect.PositionStart = 0
    textEffect.PositionCount = 100

    textEffect.Transform = translateTransform
    run.TextEffects.Add(textEffect)

    If you ever work out why the PositionCount seems to need to be such a large number then I'd be grateful if you let me know.
    Best wishes
    Sean

Trackbacks

close Reblog this comment
blog comments powered by Disqus