Well, I know it’s been a while since I’ve posted… although I’m going to be paying a LOT more attention to this moving forward. I’m hoping though that my first post back will be worth it.
We’ve been talking a lot to designers about what they want, and one thing I keep hearing is a desire for Tracking and Kerning. Currently, Silverlight doesn’t really support those, and there really isn’t an easy way to do it.
So, on the bus this morning, I started thinking, and this is what I came up with. As always, feel free to use any code here that you like, but use at your own risk. I’m certainly up for hearing possible improvements on it. Right now, this only does Tracking, but I’m trying to figure out how to do the kerning as well.
So, naturally, I’m doing this in Blend (not a huge surprise), but I want to do this in Silverlight. So, I’m creating a new Silverlight project, and I’m going to be creating a custom control. Start off by drawing a TextBlock, selecting it, and choose “Make User Control”. Blend will ask you for a name, and I chose “KerningTextBlock”.
Here’s my strategy. I’m going to use a StackPanel to take care of lining up all my characters, and create TextBlocks for EACH character. Then, set the padding on those TextBlocks to increase the space uniformly between consecutive characters.
So, get rid of the TextBlock that’s already there, and drop in a StackPanel. Rename that StackPanel since we’re going to be interacting with it in code. I named it “TextDisplay”, since that made sense to me. Also, set the Width and Height to Auto and set the Orientation to Horizontal.
Cool, now I want to create two DependencyProperties on our control, and that means code. So, open up the Solution in VS, and let’s get cracking. I want to implement “ActualText” as a string, so I can set it in one place, and have the StackPanel create it’s TextBlocks, and then a “Tracking” as a double that I can set and see the results.
First, the ActualText:
public static readonly DependencyProperty ActualTextProperty = DependencyProperty.Register("ActualText", typeof(string), typeof(KerningTextBlock), new PropertyMetadata(ActualTextValueChanged));
public string ActualText
private static void ActualTextValueChanged(DependencyObject caller, DependencyPropertyChangedEventArgs e)
This is all pretty much “boilerplate” code. I can use “ActualText” to get and set the property, and every time it changes, ActualTextValueChanged will get called. I’ll use that later to update the contents of my StackPanel.
Next, let’s go ahead and get Tracking:
public static readonly DependencyProperty TrackingProperty = DependencyProperty.Register("Tracking", typeof(double), typeof(KerningTextBlock), new PropertyMetadata(TrackingValueChanged));
public double Tracking
private static void TrackingValueChanged(DependencyObject caller, DependencyPropertyChangedEventArgs e)
All right, we’ve got our DPs, now let’s hook them up. For starters, let’s do the ActualText. Whenever it gets changed, we want to clear out the children of the TextDisplay StackPanel, and add new TextBlocks for each character. Here’s the code, and I’ll go through it after:
StackPanel textDisplay = ((KerningTextBlock)caller).TextDisplay;
foreach (char thisChar in e.NewValue.ToString().ToCharArray())
TextBlock newTextBlock = new TextBlock();
newTextBlock.Text = thisChar.ToString();
newTextBlock.Padding = new Thickness(0, 0, ((KerningTextBlock)caller).Tracking, 0);
So, what am I doing here? First, I’m grabbing the TextDisplay control. Since the method is static, I can’t simply go to “this.TextDisplay”. But, “caller” is the my KerningTextBlock, and all I need to do is cast it to KerningTextBlock and get my control.
Once I’ve got the TextDisplay, I’m going to empty it, with the Children.Clear command.
Finally, I’m going to iterate through each character in the new value, and create a new TextBlock for it. Those TextBlocks are going to get the character as their text, and set the Padding on it to my Tracking property. If you follow the logic, let’s say ActualText is “Dante” and Tracking is 4.0. We clear out the TextDisplay StackPanel, so it’s empty. We then create a TextBlock with only the letter “D”, and give it a 4 pixel padding (to the right). We then add it to the TextDisplay, and continue on with the next letter. We move on to the “a”, add the TextBlock, and so on.
I realize that this could be optimized to only add additional TextBlocks as needed, and changing the text on the characters instead of destroying and recreating them, but I wanted to start simple. When the handler is called, the “e” argument contains both the OldValue and the NewValue, so writing an optimizer wouldn’t be impossible.
So, if I wanted to, I could just tell the Tracking property to call the same method as ActualText. But, I decided that I wanted to clean it up just a bit.
foreach (UIElement thisCharacter in textDisplay.Children)
((TextBlock)thisCharacter).Padding = new Thickness(0, 0, ((KerningTextBlock)caller).Tracking, 0);
In this case, we’re just iterating through all of the children and resetting the padding. Much less destructive then going through and destroying all the children and rebuilding them every time.
The cool part about all of this is that you can set properties like the Foreground or FontFamily on the UserControl, and they end up getting inherited. So, once you’ve put all this in, rebuild the project in Blend, and instantiate one of our controls in Blend.
Once you’ve got it in Blend, go down to the Misc category, and you’ll see that both Tracking and ActualText show up as properties. You can set them there and see the effects immediately. Plus, like I said, go ahead and change the Foreground, Font Family, Size, etc… and they’ll update immediately.
I don’t know how to make Blend think that the control is a Text control, so F2 isn’t going to work like a normal TextBlock, but the concept is there.
Now, the part that I’m trying to figure out, is a good way to do the Kerning. Tracking is easy since it’s a uniform spacing between all the characters, but to be able to adjust the positions of individual pairs of characters would be cool. It would be even cooler to automatically detect it, and since you technically can get the geometry of text, it SHOULD be calculatable, but making that performant may be tricky.
Well, hope ya’ll enjoy J.