A Centered and Resizable Text Header
Tweeted by @Pete_Brown recently:
Attention WPF (and SL4) Devs: Get back to blogging. You’re getting lost in the amazing amount of #wp7dev content 🙂
Well, when Pete says “Jump!”… I’ve actually been meaning to post this for a few months, so thanks to Pete for giving me a push 🙂
The Problem
A friend of mine is learning Silverlight and in prototyping a simple app he wanted to just use a TextBlock for his header. When the application has a fixed size, it works fine, but when the size is flexible he ran into one of the issues with TextBlock: it doesn’t resize or scale. When you set the properties for a TextBlock, you set the size of the font and it never changes.
Here is the default layout we are discussing:
And here it is expanded to full screen on a large resolution:
The text stays centered, but the size stays static. It makes the header seem small and out of proportion. And similarly when the window is much smaller the text seems too large and out of proportion. If you get extreme you can even see some weird results:
Some Ideas
One way to solve this would be to listen to the UserControl’s SizeChanged event and do some math to calculate the new FontSize, but that just feels so WinForm. I’d much rather find a way to do this without code behind.
You could try to bind the FontSize of the TextBlock to a property in the ViewModel, but you still have to find a way to trigger the action. If you bound the UserControl width and height to the ViewModel you could have its Set method raise the PropertyChanged event for the FontSize property. And of course, you’d still have to write all the code to calculate it, which I’m sure would include measuring the text, calculating buffer zones and margins, etc.
These are just ideas, I haven’t tried either approach. You may find a situation where you need to do one of these things or come up with something different, but honestly, these ideas just somehow feel wrong in a XAML world. In this particular case, where the Text is static, I have a better solution.
Convert Text To A Path
The solution starts with taking advantage of the vector graphic nature of XAML. While Text may not expand and contract as desired, a Path certainly will, so the first step is to convert the Text to a Path.
In Blend, select the TextBlock item and right click, select Path –> Convert to Path (or go to Object –> Path –> Convert to Path). This will convert the text into a Path object (outlined in Red in the screen shot below). You’ll also notice the Horizontal and Vertical alignments have both been changed to Stretch and the Margins have been set (outlined in Yellow).
If you reset the Margins to 0, you will see the Text take up the entire space. If you change both the alignments to Center it will look OK in Blend, but when you execute the application you’ll see we actually get the same behavior as the Stretch. This is because of Width and Height are set to Auto, which is what we want: if we set these to fixed sizes we are right back where we started.
The good news is that if you resize the window now, either bigger or smaller, you’ll see the header resize itself, so we must be on the right track!
Margins and Proportions
At least in this case, we don’t want the text bumping up against the edges of its Border: it’s distracting and not very clean. Instead, we’d like a little space surrounding it on all sides.
You might be thinking “No big deal, I’ll just add a Margin” and you wouldn’t be totally wrong. The problem is that hard coding the Margin, like hard coding the Text’s FontSize, means it can never change. So a Margin that looks good when the window is small doesn’t necessarily look good when the window is large.
What we want is the effect of a Margin, but we want that Margin to be proportional to the available space. We really can’t solve this with the Margin property, at least not without a lot of work and calculation, which I’m just too lazy to figure out. So the real solution is not Margins, or even in the Text (now Path) itself: the real solution is in Layout.
Solving the Problem Using Layout
One of the things I see developers new to XAML struggling with is the power of layout. I’ve started labeling my approach “Container Driven Design” which really relies on the containers to manage the size and spacing of it’s child elements. It frequently involves nested containers, which is what we are going to use to solve this problem.
What we really want is for our Margins to float and resize in proportion to their parent container. Fortunately we have a container type built in that is perfect for this: the Grid. With a Grid, we can specify percentage based sized rows and columns. (NOTE: Yes, I know they are not *really* percentage based, but an explanation of the Star system is beyond the scope of this article.)
So to solve this problem using layout we are going to wrap our header in a 9-celled Grid: three rows and three columns, with the center cell holding our header. Right click the Path and select Group Into –> Grid. If you look at your Objects and Timelines panel you will see the Path is now the child of a Grid:
With Grid selected, you can use the blue bars along the top and left to position the rows and columns:
While I avoid editing XAML, there are a few times that it is simply faster and easier: editing Grid row and column sizes is one of those times. In the screen shot below, you’ll see that I’ve effectively created floating margins by defining star sizes for the top and bottom rows and right and left columns. The center row and center column have no size definition, so they will take up the remaining available space.
Execute this and you’ll find that as you resize the window the margins will resize themselves proportionally, the text will remain nicely centered and will also resize itself proportionally.
Wrapping it Up
So there are a couple of lessons I would want you to take away from this exercise. First, the problem we were having was with static text, so we solved that by turning that text into something else. We found a graphical solution to our graphical problem!
Second, we had a problem with Margins, so we used grid rows and columns instead of the Margin property. We solved that issue by relying on a Layout Container instead of a single property.
In both cases, we found simple and elegant solutions by thinking outside the box. I’ll grant that this example is not overly complex, but it does illustrate the power of XAML to solve design problems. And of course, a chance to play around in Blend is always welcome!
An alternative, which removes the need to convert it to a path, would be to use a converter on a binding to the fontsize. This means it could work with any text.
Basically you pass in the current width of the window, or container, and the textblock you are resizing and then these are used to resize it to a certain percentage of the width of the container. The text block is passed in so it can be used for the current text settings.
Note, this is not necessarily a very efficient approach and is something I knocked up in about 10 minutes. I am sure there are better approaches (behaviours anyone?) and assumptions are made (take the first typeface). However it may be useful to someone as a starting point.
The same approach could be used for the margins.
Anyway here is the code I used:
…
…
And then the following converter:
public class PercentageConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
double percentage;
double windowWidth;
if (values == null || values.Length != 2)
return values;
var textBlock = values[1] as TextBlock;
if (textBlock == null || !double.TryParse(parameter.ToString(), out percentage) || !double.TryParse(values[0].ToString(), out windowWidth))
return values;
var formattedText = new FormattedText(textBlock.Text, Thread.CurrentThread.CurrentUICulture, textBlock.FlowDirection,
textBlock.FontFamily.GetTypefaces().First(), textBlock.FontSize, textBlock.Foreground);
var ratio = windowWidth / formattedText.Width;
return ratio * textBlock.FontSize * percentage;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
return new object[] { value };
}
}
The last comment removed the XAML for the textblock so in english …
Bind the TextBlock.FontSize to a MultiBinding which has:
– a converter instance of the percentage converter, Converter=”{StaticResource percentageConverter}”
– and a converter parameter which is the percentage of the overall width, ConverterParameter=”0.5″
The first binding parameter is the actualwidth of the container, Path=”ActualWidth” RelativeSource=”{RelativeSource AncestorType={x:Type Window}}”
The second binding parameter is the text block,
RelativeSource=”{RelativeSource Self}”
Hoepfully you can work it out from this (shame it removed the XAML).
Unfortunately my WordPress is not very friendly with tags in comments. It’s not all that friendly with Tags at all, but I can work around it in the article body.
I had a mention of using a Value Converter in the original draft where I talk about possibly doing this with Binding but I must have cut it out at some point. This could certainly work but would be overkill for static text. I had thought about writing a UserControl that would take a Text property and convert it to a Path, so you could do this on any text, I just haven’t had the time to work it out.
I really like the Behavior idea, I might have to give that a go. As for binding to Margins, I find the Grid approach much easier.
Thanks!
Sorry, my comment about margins was to use the same approach you did. Using the converter for the margins is certainly overkill 🙂