Foundations

Render Text On A Path With WPF

Charles Petzold

Code download available from the MSDN Code Gallery

Contents

Uniting Paths and Text
Endpoints and Midpoints
Let's Make It a UserControl!
What Size Is It?
Lowering the Level
The Measuring Conundrum
The World of Visual Children
Warping the Text

Text is more than just its literal meaning. Whether printed or displayed on a computer screen, the choice of font can enhance or detract from the impact of the text. Text can be made inviting or forbidding, soothing or stormy. Computer graphics can also release text from the confines of its customary line-by-line progression. By treating text characters as graphical objects, the programmer can make text dance and fly on the screen.

One desirable technique is positioning text characters along a curved line, as shown in Figure 1 . In graphics programming, a collection of straight lines and curves is called a "path," so this task is sometimes described as "text on a path," and that's what I'll tackle in this column.

fig01.gif

Figure 1 Text on a Curved Path

When programming for Windows Presentation Foundation (WPF), a significant bonus accompanies the programming of text on a path: you can animate the individual points defining the path and watch the characters bounce around in response.

As usual in WPF, there is more than one way to do this job, and I'll demonstrate several approaches. As is also common in WPF, the difficult part of the job turns out to be something other than what might originally be assumed. In putting text on a path, the big problem really isn't figuring out how to move and rotate the text characters. That's relatively straightforward. The hard part is properly informing the WPF layout system of the correct size of the resultant graphic.

Uniting Paths and Text

A graphics path is a collection of straight lines and curves. Some of these lines and curves might be connected to each other. Some might form enclosed areas. In WPF, the graphics path is encapsulated in both the PathGeometry and StreamGeometry classes. StreamGeometry offers better performance, but the individual points that define the path become fixed and cannot be animated.

In placing text on a path, it's pretty much essential that the lines and curves comprising the path be connected end to end with each other. You probably don't want to deal with a text string jumping across a disconnection in the path. For this reason, the WPF class I'm really interested in is PathFigure, which is a single collection of connected straight lines and curves. A PathGeometry is a collection of one or more PathFigure objects.

The PathFigure itself defines a StartPoint and contains a collection of segments. The first segment begins at the StartPoint, and each successive segment continues where the last one left off. These segments are all derivatives of the abstract PathSegment class: LineSegment, PolyLineSegment, BezierSegment, PolyBezierSegment, QuadraticBezierSegment, PolyQuadraticBezierSegment, and ArcSegment. In other words, a PathFigure is a connected series of straight lines, Bezier curves, quadratic Bezier curves, and arcs, which are curves on the circumference of an ellipse.

A PathFigure has a geometric length. In the source code that accompanies this column, I refer to this length with the variable pathLength. If the PathFigure consists solely of LineSegment and PolyLineSegment objects, calculating the length is trivial: simply use the Pythagorean Theorem to calculate the length of each line and accumulate them. But an ArcSegment requires more complex math, and calculating the length of a Bezier curve is a positively frightening job.

To simplify the calculation of the PathFigure length, it's convenient to convert the path to a collection of polylines that approximate the curves. This is called flattening the path. It's then easy to calculate the length with repeated application of the Pythagorean Theorem. WPF even makes knowledge of the Pythagorean Theorem unnecessary: subtracting one point from another results in a Vector object, and the Vector object contains a Length property that returns the length between the two points.

The PathFigure class contains a method named GetFlattenedPathFigure that returns another PathFigure containing only Line­Segment and PolyLineSegment objects. And this is ideal for calculating the length of the entire PathFigure.

A text string rendered with a particular font and font size also has a geometric size. If you use the popular TextBlock element to display text, the width of the displayed text is available from the ActualWidth property. During the layout process prior to the actual display, ActualWidth might not be set yet, but TextBlock will set its DesiredSize property to the size of the text.

TextBlock uses the FormattedText object for calculating the size of its text, and this class is also available to the application programmer. The FormattedText constructor requires the text string itself, a font size, and an object of type Typeface, which is built from the desired FontFamily, FontStyle, FontWeight, and FontStretch objects.

A couple of different approaches come to mind for uniting the PathFigure and the text string. You might want to specify a particular font size for the text and display the text starting at the beginning of the path. But the text might not fit the whole path, and (worse) it might exceed the length of the path. What then?

To avoid those problems, I took a different approach: I decided to scale the size of the text to make it exactly the length of the path from beginning to end. The text is first created with an arbitrary font size, which is 100 in the sample programs. The width of the text is stored in a variable named textLength.

The ratio of pathLength to textLength is a number I call scalingFactor. This scaling factor must be applied as a transform to the individual text characters so they fit on the path from beginning to end.

Each individual character in the text string is subjected not only to this scaling transform but also to two other transforms: a translation transform moves the character to a particular position on the path, and a rotation transform turns the character so its baseline is tangent to the path.

PathGeometry defines a method named GetPointAtFractionLength that is extremely useful for positioning text on the path. The method requires an argument ranging from 0 to 1 indicating a fractional length of the path. The method returns the corresponding point on the path. This fractional length is easy to provide. Each character occupies a fractional length of the path equal to the width of the individual character times the scaling factor divided by pathLength. The point on the path returned by the method is useful for translating the character to the path. As an extra bonus, the GetPointAtFractionLength method returns a second point representing the tangent of the path at that point. Passing the X and Y properties of this second point to the Math.Atan2 method gives you the angle necessary to rotate the text character.

Although GetPointAtFractionLength is defined by PathGeometry and not PathFigure, it's easy to make a PathGeometry from a single PathFigure for purposes of using GetPointAtFractionLength.

Endpoints and Midpoints

Even when the general technique is established, some questions remain: what part of each character should be aligned with the path? Should it be the top of the character, the bottom (below the descenders), or the baseline? I chose the baseline as the most natural approach and figured that a switch to either the top or bottom would then be trivial. FontFamily has a Baseline property suitable for this purpose. It's based on a font size of 1 unit, so the distance from the top of a character to its baseline is simply the product of the Baseline property, the font size (100 in my code), and scalingFactor.

Figure 2 shows two different ways to place text characters on a simple circular path. Look at the h, x, and n characters in the graphic on the left. You'll see that the left and right endpoints of each character's baseline touch the path. In the graphic on the right, look at the e and o characters to see that the midpoint of each character's baseline touches the path.

fig02.gif

Figure 2 Two Ways to Align Text on a Path

At first this seems like a trivial distinction. If the circles themselves were absent, you probably wouldn't even see a difference between the two. However, I felt that the approach on the left offers slightly better visual continuity between the characters.

I was disappointed when I realized how much more difficult it is (algorithmically speaking) than the approach on the right. It's not hard to position the left end of the character's baseline on the path, but the right end requires finding a point on the path that is a straight-line distance from the first point equal to the character's scaled width. Moreover, after all the characters have been positioned, you'll discover that the last character goes beyond the end of the path. This happens because the characters have been scaled based on the curved path but positioned based on straight-line shortcuts through the path. This approach requires multiple passes with successive refinements of the scaling factor.

Although I hacked together a simple version of this algorithm for producing Figure 2 , all of the downloadable code uses the much simpler midpoint approach. Each character requires a fraction of the path equal to its fraction of the total text width.

Let's Make It a UserControl!

Deriving from UserControl is an extremely popular technique in WPF programming, rather like an erector set for quickly constructing sturdy controls. UserControl derives from ContentControl so the visual tree defining the control is generally defined in XAML as the control's Content property. The codebehind file might define a couple more custom properties and handles events and interactions.

It's possible to implement text on a path using the everyday WPF building blocks of UserControl, Canvas, and TextBlock. UserControl is doubly convenient for this particular job because it already has many of the required properties to define the text: FontFamily, FontStyle, FontWeight, FontStretch, and Foreground. (The FontSize property will be ignored because the text size will be scaled based on the path length.) If TextBlock elements are descendents of the UserControl, the settings of these properties on the UserControl will be inherited by TextBlock. It will be necessary to override the metadata for these properties to supply additional property-changed event handlers, but that's about it. The only required new properties are Text (of type string) and PathFigure (of type PathFigure).

The resultant class is called TextOnPathControl, and Figure 3 shows an excerpt. The rest of the file is available in the code download, of course. The source code for this column consists of a single solution named TextOnPath, which contains a DLL project named Petzold.TextOnPath that contains all the classes that implement the various techniques for arranging text on a path. The other projects in the TextOnPath solution are demonstration programs.

Figure 3 Excerpt from TextOnPathControl.cs

static void OnTextPropertyChanged(DependencyObject obj,
  DependencyPropertyChangedEventArgs args) 
{

  TextOnPathControl ctrl = obj as TextOnPathControl;
  ctrl.mainPanel.Children.Clear();

  if (String.IsNullOrEmpty(ctrl.Text))
    return;

  foreach (Char ch in ctrl.Text) 
{
    TextBlock textBlock = new TextBlock();
    textBlock.Text = ch.ToString();
    textBlock.FontSize = FONTSIZE;
    ctrl.mainPanel.Children.Add(textBlock);
  }

  ctrl.OrientTextOnPath();
}

void OrientTextOnPath() 
{
  double pathLength = 
    TextOnPathBase.GetPathFigureLength(PathFigure);
  double textLength = 0;

  foreach (UIElement child in mainPanel.Children) 
{
    child.Measure(new Size(Double.PositiveInfinity,
      Double.PositiveInfinity));
    textLength += child.DesiredSize.Width;
  }

  if (pathLength == 0 || textLength == 0)
    return;

  double scalingFactor = pathLength / textLength;
  PathGeometry pathGeometry =
    new PathGeometry(new PathFigure[] { PathFigure });
  double baseline =
    scalingFactor * FONTSIZE * FontFamily.Baseline;
  double progress = 0;

  foreach (UIElement child in mainPanel.Children) 
{
    double width = scalingFactor * child.DesiredSize.Width;
    progress += width / 2 / pathLength;
    Point point, tangent;

    pathGeometry.GetPointAtFractionLength(progress, 
      out point, out tangent);

    TransformGroup transformGroup = new TransformGroup();

    transformGroup.Children.Add(
      new ScaleTransform(scalingFactor, scalingFactor));
    transformGroup.Children.Add(
      new RotateTransform(Math.Atan2(tangent.Y, tangent.X)
      * 180 / Math.PI, width / 2, baseline));
    transformGroup.Children.Add(
      new TranslateTransform(point.X - width / 2,
        point.Y - baseline));

    child.RenderTransform = transformGroup;
    progress += width / 2 / pathLength;
  }
}

The OnTextPropertyChanged method is the property-changed handler for the Text property; it is responsible for creating all the Text­Block children. The mainPanel variable is a Canvas object set to the Content property of the UserControl. OnTextPropertyChanged concludes by calling OrientTextOnPath, as do the other property-changed handlers for the font-related properties and the Path­Figure property.

OrientTextOnPath basically applies transforms to all the TextBlock elements. It obtains the total length of the path (TextOnPathBase is another class in the DLL) and the total width of the TextBlock objects, and calculates the all-important scalingFactor. The method concludes with a large loop through all the TextBlock children of the Canvas. Notice that the variable "progress" passed to the GetPointAtFractionLength method is based on half the scaled width of the character, so the point returned from the method corresponds to the character's midpoint.

The loop concludes with the definition of three transforms: the first one scales the size of the TextBlock based on scalingFactor. The TextBlock is then rotated based on the tangent numbers returned from GetPointAtFractionLength. Notice the center of rotation is the midpoint of the character's baseline. Finally, the TranslateTransform moves the scaled and rotated character to the desired point on the path. This group of three transforms becomes the RenderTransform property of the TextBlock.

The TextOnPathControlDemo1.xaml file shown in Figure 4 was responsible for creating the image in Figure 1 . Notice that the Text­OnPathControl class doesn't itself draw the PathFigure used to orient the text. If you want it drawn, you'll have to provide for it yourself. However, as the TextOnPathControlDemo1.xaml file shows, you can use the same PathGeometry for both tasks.

Figure 4 TextOnPathControl

<!-- TextOnPathControlDemo1.xaml by Charles Petzold, September 2008 -->
<Page 
  xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
  xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
  xmlns:textonpath=
    "clr-namespace:Petzold.TextOnPath;assembly=Petzold.TextOnPath"
  Title="TextOnPathControl Demo #1"
  WindowTitle="TextOnPathControl Demo #1"
  FontFamily="Times New Roman"
  Foreground="Blue">

  <Page.Resources>
    <PathGeometry x:Key="path" 
      Figures="M 100 100 C 200 150 300 0 400 100" />
  </Page.Resources>

  <Grid>
    <Path 
      Data="{StaticResource path}" 
      Stroke="Red" />

    <textonpath:TextOnPathControl 
      Text="Hello, Path!" 
      PathFigure="{Binding Source={StaticResource path}, 
      Path=Figures[0]}" />
  </Grid>
</Page>

What Size Is It?

The TextOnPathControl class shows some of the versatility of UserControl, but also its limitations. The control uses a Canvas as parent to all the TextBlock objects, and a Canvas not given an explicit height or width has no dimensions for layout purposes. If you were to put this control in a StackPanel, for example, it wouldn't be visible at all.

You could replace the Canvas in TextOnPathControl with a single-cell Grid. That would give the control a non-zero size for layout purposes, but that size would not be correct. The size of the control would be calculated as if all the individual unscaled TextBlock children were displayed on top of each other—which is precisely what the control does before applying a RenderTransform. As you know, the RenderTransform has no effect on layout.

You might consider replacing the RenderTransform with a LayoutTransform so the transformed TextBlock elements would be recognized by the WPF layout system. But that won't work either, because LayoutTransform ignores translation transforms.

Normally, a UserControl is populated with a visual tree that contains other controls and elements that report their sizes to the WPF layout system. The UserControl then has a composite size encompassing all of its children. But if you set the RenderTransform property of these children, then there's no guarantee they will be inside the element's boundaries.

Let's face it: although the TextOnPathControl class demonstrates the versatility of UserControl, it's not quite the right approach. Creating a TextBlock for each individual character is overkill and only makes sense if you want each character to be associated with its own mouse processing or perhaps its own tooltip.

Lowering the Level

Let's drop down a couple of levels and derive from FrameworkElement, which is the same base class for TextBlock itself and many other elements including Control. A class that derives from FrameworkElement generally draws the element's visual contents in an override of the OnRender method. The OnRender method is called with an object of type DrawingContext, a class that defines a complete set of drawing methods, including DrawText. This is the lowest-level graphics output you can do and still call yourself a full-fledged WPF application.

Unfortunately, FrameworkElement doesn't have any of the FontFamily, FontStyle, FontWeight, FontStretch, or Foreground properties you need, to say nothing of Text and PathFigure. Because other classes in this column will need these same properties, I put all seven of them in a class I called TextOnPathBase. This class also defines four abstract property-changed methods named OnFontPropertyChanged, OnForegroundPropertyChanged, OnTextPropertyChanged, and OnPathPropertyChanged that descendent classes override to implement measuring and drawing code. The TextOnPathElement class derives from TextOnPathBase. Figure 5 shows an excerpt from the class.

Figure 5 Excerpt from TextOnPathElement.cs

protected override void OnTextPropertyChanged(
  DependencyPropertyChangedEventArgs args) 
{

  formattedChars.Clear();
  textLength = 0;

  foreach (char ch in Text) 
{
    FormattedText formattedText =
      new FormattedText(ch.ToString(),
      CultureInfo.CurrentCulture,
      FlowDirection.LeftToRight, typeface, 100,
      Foreground);

    formattedChars.Add(formattedText);
    textLength +=
      formattedText.WidthIncludingTrailingWhitespace;
  }

  InvalidateMeasure();
  InvalidateVisual();
}

protected override void OnPathPropertyChanged(
  DependencyPropertyChangedEventArgs args) 
{

  pathLength = GetPathFigureLength(PathFigure);

  InvalidateMeasure();
  InvalidateVisual(); 
}

protected override Size MeasureOverride(Size availableSize) 
{
  if (PathFigure == null)
    return MeasureOverride(availableSize);

  Rect rect = new PathGeometry(
    new PathFigure[] { PathFigure }).Bounds;
  return (Size)rect.BottomRight;
}

protected override void OnRender(DrawingContext dc) 
{
  if (pathLength == 0 || textLength == 0)
    return;

  double scalingFactor = pathLength / textLength;
  double progress = 0;
  PathGeometry pathGeometry = 
    new PathGeometry(new PathFigure[] { PathFigure });

  foreach (FormattedText formText in formattedChars) 
{
    double width = scalingFactor * 
      formText.WidthIncludingTrailingWhitespace;
    double baseline = scalingFactor * formText.Baseline;
    progress += width / 2 / pathLength;
    Point point, tangent;

    pathGeometry.GetPointAtFractionLength(progress, 
      out point, out tangent);
    dc.PushTransform(
      new TranslateTransform(point.X - width / 2, 
      point.Y - baseline));
    dc.PushTransform(
      new RotateTransform(Math.Atan2(tangent.Y, tangent.X)
      * 180 / Math.PI, width / 2, baseline));
    dc.PushTransform(
      new ScaleTransform(scalingFactor, scalingFactor));

    dc.DrawText(formText, new Point(0, 0));
    dc.Pop();
    dc.Pop();
    dc.Pop();

    progress += width / 2 / pathLength;
  }
}

Whenever the Text property or one of the Font properties change, the class creates FormattedText objects for each character in the string. The width of these characters is accumulated in the textLength field. Whenever the PathFigure property changes, the length of the path is stored in the pathLength field. Most of the real work occurs in the OnRender override.

OnRender makes a call to the DrawText method of DrawingContext for each character in the text string. But notice the three PushTransform calls. These are the same transforms that you saw in the TextOnPathControl class, but they must be pushed on the DrawingContext stack in the opposite order that they are applied. Following the DrawText call, the three transforms are removed from the DrawingContext stack with calls to Pop.

The Measuring Conundrum

When you're working on the level of FrameworkElement, you can also inform WPF how large your element is by overriding the MeasureOverride method. Your responsibility is to return a Size object that is required by the element. MeasureOverride provides an availableSize argument that you can use to scale your graphic or completely ignore.

Here's the catch: the WPF layout system assumes that the element has been rendered in the OnRender method with its upper-left corner at the point (0, 0). If that's not the case, then you'll have a little problem. For example, suppose you want to write a derivative of FrameworkElement devoted solely to drawing a line from the point (-50, 100) to the point (100, 50). The OnRender method probably contains the call:

dc.DrawLine(pen, new Point(-50, 100), new Point(100, 50));

But what does MeasureOverride return? The standard approach is to return a Size object based on the maximum X and Y coordinates of the rendered object, which means the body of MeasureOverride is simply:

return new Size(100, 100);

Actually, MeasureOverride should take the width of the pen used to draw the line into account, but let's ignore refinements like that for this discussion.

If you now put that element in a Grid cell with a Background color of cyan, where both the column and row width are set to GridLength.Auto, the result is shown in the first graphic of Figure 6 .

fig06.gif

Figure 6 Different Ways to Draw a Line and Specify Its Size

Vertically, the grid cell allows for coordinates going down to zero, but the size does not accommodate the line going beyond the left border of the cell. You might try to compensate for this by returning a value from MeasureOverride that equals the real rendered width and height of the graphic:

return new Size(150, 50);

But doing that only makes it worse! The line still is not within the cell, and the cell is much too large, as illustrated in the second graphic of Figure 6 .

You might then decide to be very clever and return a size of (150, 150) from MeasureOverride, but adjust the coordinates in the OnRender method so they have an origin of (0, 0). Just add 50 to the X coordinates and subtract 50 from the Y coordinates:

dc.DrawLine(pen, new Point(0, 50), new Point(150, 0));

Now the graphic fits snugly in the Grid cell, as shown in the third graphic of Figure 6 . However, if you want to mix this graphic with other lines in that same Grid cell or on a Canvas, it won't be rendered properly because the DrawLine call has adjusted the coordinate points. If you use one of the text-on-a-path classes to display text on the path, the text and path won't be aligned.

We must reluctantly conclude that the first graphic of Figure 6 shows the correct approach. That's how the regular Line element behaves, for example.

Now let's put some text on the path, as shown in the fourth graphic of Figure 6 . How should the size of the element change to accommodate the text? In this particular case, not at all. The text doesn't go beyond the maximum horizontal coordinate of the line or below the maximum vertical coordinate. While there will be other paths where the text will make a difference, it usually won't make much of a difference. That is why the TextOnPathElement method implements the MeasureOverride method based entirely on the dimensions of the PathFigure, as shown in Figure 5 . The next class I'll describe will do something just a little more sophisticated.

The World of Visual Children

Any class that derives from FrameworkElement also derives from Visual and thus can maintain a collection of visual children. In a practical sense, these children are usually of type DrawingVisual. And in some measure, these DrawingVisual objects are similar to elements—that is, they have a visual appearance and they can be used for hit testing—but they are a much lighter weight. They do not receive keyboard, mouse, or stylus events, and they do not directly participate in layout.

After first creating an object of type DrawingVisual, you generally call the RenderOpen method to obtain a DrawingContext object. You use the same drawing methods with this DrawingContext object that you use in the OnRender override. Finish up with calling the Close method. The graphic rendered with the drawing commands are now stored in the DrawingVisual.

You don't directly render DrawingVisual objects on the screen. Instead, the class that creates and stores the DrawingVisual objects must override the VisualChildrenCount property to indicate the number of visual children and also override the GetVisualChild method to return a visual child based on an index.

Classes that use visual children should also call AddVisualChild and RemoveVisualChild so the visual children properly participate in event routing. This overhead is handled for you if you store the visual children in a VisualCollection object.

For displaying text on a path, it makes sense to create a DrawingVisual object for each character of the text string. Just as when drawing in the OnRender method, you can call the PushTransform method to apply a transform to the graphic. However, for putting text on a path, there's a better approach: the DrawingVisual itself has a Transform property, which means that the creation of the DrawingVisual objects and the application of the Transform can be separated just as in the earlier TextOnPathControl class. This separation allows the PathFigure to be animated without causing the DrawingVisual objects to be recreated.

The code in Figure 7 shows two methods from the TextOnPathVisuals class. The GenerateVisualChildren method is called whenever the Text property or a font property changes, and it concludes by calling TransformVisualChildren. The TransformVisualChildren method is called whenever the PathFigure changes. Notice how the Generate­VisualChildren method prepares the DrawingVisual by creating a TransformGroup with three transforms and setting that to the DrawingVisual's Transform property. These transforms are filled with the proper values in the TransformVisualChildren method.

Figure 7 Excerpts from TextOnPathVisuals

protected virtual void GenerateVisualChildren() {
  visualChildren.Clear();

  foreach (FormattedText formText in formattedChars) 
{
    DrawingVisual drawingVisual = new DrawingVisual();

    TransformGroup transformGroup = new TransformGroup();
    transformGroup.Children.Add(new ScaleTransform());
    transformGroup.Children.Add(new RotateTransform());
    transformGroup.Children.Add(new TranslateTransform());
    drawingVisual.Transform = transformGroup;

    DrawingContext dc = drawingVisual.RenderOpen();
    dc.DrawText(formText, new Point(0, 0));
    dc.Close();

    visualChildren.Add(drawingVisual);
  }

  TransformVisualChildren();
}

protected virtual void TransformVisualChildren() 
{
  boundingRect = new Rect();

  if (pathLength == 0 || textLength == 0)
    return;

  if (formattedChars.Count != visualChildren.Count)
    return;

  double scalingFactor = pathLength / textLength;
  PathGeometry pathGeometry = 
    new PathGeometry(new PathFigure[] { PathFigure });
  double progress = 0;
  boundingRect = new Rect();

  for (int index = 0; index < visualChildren.Count; index++) 
{
    FormattedText formText = formattedChars[index];
    double width = scalingFactor * 
      formText.WidthIncludingTrailingWhitespace;
    double baseline = scalingFactor * formText.Baseline;
    progress += width / 2 / pathLength;
    Point point, tangent;
    pathGeometry.GetPointAtFractionLength(progress, 
      out point, out tangent);

    DrawingVisual drawingVisual = 
      visualChildren[index] as DrawingVisual;
    TransformGroup transformGroup = 
      drawingVisual.Transform as TransformGroup;
    ScaleTransform scaleTransform = 
      transformGroup.Children[0] as ScaleTransform;
    RotateTransform rotateTransform = 
      transformGroup.Children[1] as RotateTransform;
    TranslateTransform translateTransform = 
      transformGroup.Children[2] as TranslateTransform;

    scaleTransform.ScaleX = scalingFactor;
    scaleTransform.ScaleY = scalingFactor;
    rotateTransform.Angle = 
      Math.Atan2(tangent.Y, tangent.X) * 180 / Math.PI;
    rotateTransform.CenterX = width / 2;
    rotateTransform.CenterY = baseline;
    translateTransform.X = point.X - width / 2;
    translateTransform.Y = point.Y - baseline;

    Rect rect = drawingVisual.ContentBounds;
    rect.Transform(transformGroup.Value);
    boundingRect.Union(rect);

    progress += width / 2 / pathLength;
  }
  InvalidateMeasure();
}

The result is slightly better performance than the TextOnPathElement class and the ability to indicate a layout size that takes account of the characters. The TransformVisualChildren method begins by creating a Rect object named boundingRect. As transforms are applied to each visual child, the ContentBounds property (which represents the untransformed visual) is modified by the composite transform, and then the union is taken of that rectangle and boundingRect:

Rect rect = drawingVisual.ContentBounds;
rect.Transform(transformGroup.Value);
boundingRect.Union(rect);

The result is a rectangle that encompasses all the transformed characters. The body of MeasureOverride is quite simply:

return (Size)boundingRect.BottomRight;

Figure 8 shows the TextOnPathVisualsDemo program running with text following the hills and valleys of an animated sine curve. The pink background indicates the layout size, which changes as the animation moves different characters to the right and bottom. Notice how the rectangle has expanded to accommodate the y on the right and the descender of the g at the bottom.

fig08.gif

Figure 8 The TextOnPathVisualsDemo Program

Previously Published

To read more about my adventures with text, animation, and the Microsoft .NET Framework, check out these previous installments of the Foundations column:

Extending the WPF Animation Classes

3D Text in WPF

Vector Graphics and the WPF Shape Class

Warping the Text

A very different approach to putting text on a path is implemented in the TextOnPathWarped class. The baseline of each character is actually curved to lie on the path; each vertical line in the character remains a straight line but becomes perpendicular to the path at that point. If a path is curved, the character will fan out on the convex side but become narrower on the concave side.

This approach doesn't require separating the text into the individual characters; the preliminary step is calling the BuildGeometry method of FormattedText for the entire text string and then flattening the geometry. No transforms are involved, although a little trigonometry is required. As usual, when converting text to a path, the text loses its rendering hints and at small sizes might be difficult to read. You can animate the path, but for long text strings you might see a significant performance hit.

TextOnPathWarped also defines a ShiftToOrigin property. When set to true, the class offsets all the points in the warped geometry so the upper-left corner is (0, 0). This allows the class to report its actual dimensions in the MeasureOverride method, but the result can't be successfully mixed with other graphics. In some cases, such as the screenshot in Figure 9 , you can really see the distortion of the characters.

fig09a.gif

Figure 9 The TextOn-PathWarpedDemo3 Program

While it's always interesting to see several different solutions to the same problem, which one is best? Overall, I think the best solution involves the visual children in the Text­OnPathVisuals class, particularly if you will also be animating the path.

Send your questions and comments to mmnet30@microsoft.com .

Charles Petzold is a longtime Contributing Editor to MSDN Magazine and the author of The Annotated Turing: A Guided Tour through Alan Turing's Historic Paper on Computability and the Turing Machine (Wiley, 2008). His Web site is charlespetzold.com .