How can I render text on a WriteableBitmap on a background thread, in Windows Phone 7?

Ran picture Ran · Apr 14, 2011 · Viewed 19.4k times · Source

I am trying to render text on a bitmap in a Windows Phone 7 application.

Code that looks more or less like the following would work fine when it's running on the main thread:

public ImageSource RenderText(string text, double x, double y)
{
    var canvas = new Canvas();

    var textBlock = new TextBlock { Text = text };
    canvas.Children.Add(textBloxk);
    Canvas.SetLeft(textBlock, x);
    Canvas.SetTop(textBlock, y);

    var bitmap = new WriteableBitmap(400, 400);
    bitmap.Render(canvas, null);
    bitmap.Invalidate();
    return bitmap;
}

Now, since I have to render several images with more complex stuff, I would like to render the bitmap on a background thread to avoid an unresponsive UI.

When I use a BackgroundWorker to do so, the constructor for TextBlock throws an UnauthorizedAccessException claiming that this is an invalid cross-thread access.

My question is: how can I render text on a bitmap without blocking the UI?

  • Please don't suggest using a web service to do the rendering. I need to render a large number of images and the bandwidth cost is not acceptable for my needs, and the ability to work offline is a major requirement.
  • The solution doesn't necessarily has to use WriteableBitmap or UIElements, if there is another way to render text.

EDIT

Another thought: does anyone know if it should be possible to run a UI message loop in another thread, and then have that thread do the work? (instead of using a BackgroundWorker)?

EDIT 2

To consider alternatives to WriteableBitmap, the features I need are:

  • Draw a background image.
  • Measure the width and height of a 1-line string, given a font familiy and size (and preferably style). No need for word wrapping.
  • Draw a 1-line string, with given font family, size, style, at a given coordinate.
  • Text rendering should support a transparent background. I.e. you should see the background image between the characters.

Answer

Kris  picture Kris · Apr 15, 2011

This method copies the letters from an pre-made image instead of using TextBlock, it's based on my answer to this question. The main limitation is requiring a different image for each font and size needed. A size 20 Font needed about 150kb.

Using SpriteFont2 export the font and the xml metrics file in the sizes you require. The code assumes they're named "FontName FontSize".png and "FontName FontSize".xml add them to your project and set the build action to content. The code also requires WriteableBitmapEx.

public static class BitmapFont
{
    private class FontInfo
    {
        public FontInfo(WriteableBitmap image, Dictionary<char, Rect> metrics, int size)
        {
            this.Image = image;
            this.Metrics = metrics;
            this.Size = size;
        }
        public WriteableBitmap Image { get; private set; }
        public Dictionary<char, Rect> Metrics { get; private set; }
        public int Size { get; private set; }
    }

    private static Dictionary<string, List<FontInfo>> fonts = new Dictionary<string, List<FontInfo>>();
    public static void RegisterFont(string name,params int[] sizes)
    {
        foreach (var size in sizes)
        {
            string fontFile = name + " " + size + ".png";
            string fontMetricsFile = name + " " + size + ".xml";
            BitmapImage image = new BitmapImage();

            image.SetSource(App.GetResourceStream(new Uri(fontFile, UriKind.Relative)).Stream);
            var metrics = XDocument.Load(fontMetricsFile);
            var dict = (from c in metrics.Root.Elements()
                        let key = (char) ((int) c.Attribute("key"))
                        let rect = new Rect((int) c.Element("x"), (int) c.Element("y"), (int) c.Element("width"), (int) c.Element("height"))
                        select new {Char = key, Metrics = rect}).ToDictionary(x => x.Char, x => x.Metrics);

            var fontInfo = new FontInfo(new WriteableBitmap(image), dict, size);

            if(fonts.ContainsKey(name))
                fonts[name].Add(fontInfo);
            else
                fonts.Add(name, new List<FontInfo> {fontInfo});
        }
    }

    private static FontInfo GetNearestFont(string fontName,int size)
    {
        return fonts[fontName].OrderBy(x => Math.Abs(x.Size - size)).First();
    }

    public static Size MeasureString(string text,string fontName,int size)
    {
        var font = GetNearestFont(fontName, size);

        double scale = (double) size / font.Size;

        var letters = text.Select(x => font.Metrics[x]).ToArray();

        return new Size(letters.Sum(x => x.Width * scale),letters.Max(x => x.Height * scale));
    }

    public static void DrawString(this WriteableBitmap bmp,string text,int x,int y, string fontName,int size,Color color)
    {
        var font = GetNearestFont(fontName, size);

        var letters = text.Select(f => font.Metrics[f]).ToArray();

        double scale = (double)size / font.Size;

        double destX = x;
        foreach (var letter in letters)
        {
            var destRect = new Rect(destX,y,letter.Width * scale,letter.Height * scale);
            bmp.Blit(destRect, font.Image, letter, color, WriteableBitmapExtensions.BlendMode.Alpha);
            destX += destRect.Width;
        }
    }
}

You need to call RegisterFont once to load the files then you call DrawString. It uses WriteableBitmapEx.Blit so if your font file has white text and a transparent background alpha is handled correctly and you can recolour it. The code does scale the text if you draw at a size you didn't load but the results aren't good, a better interpolation method could be used.

I tried drawing from a different thread and this worked in the emulator, you still need to create the WriteableBitmap on the main thread. My understanding of your scenario is that you want to scroll through tiles similar to how mapping apps work, if this is the case reuse the old WriteableBitmaps instead of recreating them. If not the code could be changed to work with arrays instead.