Text On A Path With Alignment

Click here to view the source code

In past WPF projects, design specifications given by artists are sometimes challenging to attempt to implement. One scenario I have ran into multiple times was trying to display custom text around a curved object. Most of the time this problem was avoided by the artist providing the content as images, but, fortunately, this was only feasible because the text was finite and static. Recently I came across this same scenario again except this time the incoming text could be dynamic. The immediate reaction was to simply read the text as images stored in a folder provided by the customer. This solution tends to create problems in the future because:

  • The user must be familiar with some image editing tool
  • If not accustomed to any, then the user has to be trained
  • The user must be aware of the exact font, weight, and color the artist intended
  • The user must also have the same eye for lining up text as the original artist intended (which many tools can aid in)

With all these issues, it would be better to assume that if a user could get something wrong then they will, which will result in an application that seems off. Not happy with this path I decided to look into creating text on a path.

Fortunately, a majority of the work had already been developed by Charles Petzold described in his article Render Text On A Path With WPF.

The article provides a comprehensive description of his algorithm for placing text on a path. It is worth a read to get an in-depth understanding of how the algorithm works. To try and sum it up though, the algorithm uses a PathFigure object to shape the path the text will render and the class comes equipped with a method to help simplify calculations. In sizing the text font onto the path, the text is scaled to fit entirely on the length of the path. For the best performance, we render the text as DrawingVisual objects so they only need to be created once when the text changed. The result comes out as this:

Result1

 

Fantastic! This worked well for what I wanted, except for one problem; the text scaling. I knew the path I wanted my text to render, but not the length of the text since that could change dynamically. When attempting to apply this algorithm to a path I had in mind, the results were less than pleasing (red curve represents the path for the text):

Result2

 

I needed the text to render the same size despite the length of the path, but the current algorithm would require me to resize the path dynamically based on the size of the text. This did not seem reasonable, especially since I would prefer the text to rest on the center of the path.

So, in order to accomplish this the first thing I needed was a Dependency Property of type HorizontalAlignment to set the content alignment. Setting the alignment to Stretch will keep the original behavior of the algorithm by resizing the text to fit the path. Then we need another Dependency Property for font size, which will only get applied if the content alignment was not Stretch. Now, when calculating the text length we need to substitute our font size over the default 100

private void OnTextPropertyChanged()
{
    _formattedChars.Clear();
    _textLength = 0;
    if (!String.IsNullOrEmpty(Text))
    {
        foreach (char ch in Text)
        {
            var fontSize = ContentAlignment == HorizontalAlignment.Stretch ? 100 : FontSize;
            var formattedText =
                new FormattedText(ch.ToString(), CultureInfo.CurrentCulture,
                    FlowDirection.LeftToRight, _typeface, fontSize, Foreground);
            _formattedChars.Add(formattedText);
            _textLength += formattedText.WidthIncludingTrailingWhitespace;
        }
        GenerateVisualChildren();
    }
}

Then we move onto our TransformVisualChildren method and make sure the scaling factor stays at 1 since we no longer want the text to rescale to the path

private void TransformVisualChildren()
{
    ...
    var scalingFactor = ContentAlignment == HorizontalAlignment.Stretch ? _pathLength / _textLength : 1;
    ...
}

Since content alignment can be set as Center, or Right aligned, the initial progress will need adjusting to push the text further along the path

private void TransformVisualChildren()
{
    ...
    double progress = 0;
    switch (ContentAlignment)
    {
        case HorizontalAlignment.Left:
        case HorizontalAlignment.Stretch:
            progress = 0;
            break;
        case HorizontalAlignment.Center:
            progress = Math.Abs(_pathLength - _textLength) / 2 / _pathLength;
            break;
        case HorizontalAlignment.Right:
            progress = Math.Abs(_pathLength - _textLength) / _pathLength;
            break;
    }
    ...
}

And that is all we have to do since the scaling factor will be set to 1 if content is not stretched. Here is the result with content alignment set to Center. Also, you can see all the various alignments applied:

Result31

 

Although I was very satisfied with the results, a change in the scope of our project made this solution obsolete. Just another day in the life of a programmer.