Thursday, March 29, 2007

textbox for high-contrast very small text

 The most common question about text capabilities in WPF is about small fonts. By default all texts rendered with presentation layer are anti-aliased,  wich means, that we will get great text experience with large fonts, and blurry texts with small and extra small fonts. 

As you, probably know, the common usage of big fonts is in entertainment-related application, however most of business-related applications using small fonts. There is impossible to solve "blurry" problem with WPF texts, so today, we'll solve it with WPF geometry. Today, we'll create custom textblock, that holds texts, which visible and readable on any background, even with very small fonts. 


 So, let the show start. First of all, we'll create new control, derived from FrameworkElement. We'll add a couple of dependency properties for Text, Font, FontSize, fore and background color. But, were the trick, you'll ask... Instead of drawing text, onRender event, we'll draw geometry of the text and stroke it with contrast color.


protected override void OnRender(DrawingContext drawingContext)
           drawingContext.DrawGeometry(Fill, m_pen, m_textg);

The next question is - where to get the geometry of the text? FormattedText class exposes method named BuildGeomerty, that converts text into geometrical representation. So, in order to build a text and it's geometry, we'll build internal method, for text generation, based on control's DPs


private void GenerateText()
            if (Font == null)
                Font = new FontFamily("Arial");
            FormattedText fText = new FormattedText(
               new Typeface(
            m_textg = fText.BuildGeometry(new Point(0, 0)).GetAsFrozen() as Geometry;

So now, after compilation, we'll get the text stroked with contrast pen, but it will be anti-aliased. What to do? Framework, can not cancel anti-aliasing for text, but it can do it for geometry. In order to do it, just call the framework to cancel it.

RenderOptions.SetEdgeMode(this, EdgeMode.Aliased);

There are a couple of other tips you'll have to do. One is snap your graphics to device pixels. Why to do it? Read MSDN :)

this.SnapsToDevicePixels = true;

Additional tip is set linejoin to Round and MeterLimit to 1 for line "pike" prevention. One other trick is to freeze the pen after generation, because, you wont going to change it while rendering, so save the system resources.


private static void OnTextInvalidated(DependencyObject d, DependencyPropertyChangedEventArgs e)
            m_pen = new Pen(((HiResTextBlock)d).Stroke, ((HiResTextBlock)d).StrokeThickness);
            m_pen.LineJoin = PenLineJoin.Round;
            m_pen.MiterLimit = 1;
            m_pen = m_pen.GetAsFrozen() as Pen;

Now, tell the framework to call all those methods by doing this and you're done


public static readonly DependencyProperty TextProperty = DependencyProperty.Register(
            new FrameworkPropertyMetadata(
                 new PropertyChangedCallback(OnTextInvalidated),

Now, let's see what we get. Pretty nice isn't it? 

 Source code for this article

No comments: