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

6

14

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.
Winifield answered 14/4, 2011 at 16:56 Comment(4)
What text features do you require? I can post a method using SpriteSheets that would be suitable if you don't need many variations of the font or size. However if you do need rich formatting or want to render paragraphs of text it's not really appropriate.Lodged
@Kris: it sounds promising, I listed the features I need. Thanks.Winifield
Did you manage to find all the stuff you were looking for? I'm in the process of porting an application to WP7, and I'm looking for the exact same stuff: "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.". Could you share your findings? ThanksPhlegmy
Truth be told, I did not find a good enough solution. The WriteableBitmapEx library might be suitable, but for me there were problems with using it. Especially, having to pre-generate sprites for all the fonts, styles and sizes, and having a lower rendering quality due to the fact that the text rendering was no longer vector-based.Winifield
L
15

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.

Lodged answered 15/4, 2011 at 14:57 Comment(2)
Thanks, Kris! This seems to be closest to what I was looking for. I will give it a try.Winifield
@chendang Spritefont has a tab that allows you to choose what characters to export so if you do that and use an appropriate font it should work.Lodged
A
2

I'm not sure if this will fully resolve your issues, but there are 2 tools that I use in my comic book reader (I won't shamelessly plug it here, but I'm tempted.. a hint if you are searching for it.. it is "Amazing"). There are times where I need to stitch together a bunch of images. I use Rene Schulte's (and a bunch of other contributors) WriteableBitmapExtensions (http://writeablebitmapex.codeplex.com/). I have been able to offload rendering/stitching of an image to a background thread and then set the resulting WriteableBitmap as the source of some image on the UI thread.

Another up and comer in this space is the .NET Image Tools (http://imagetools.codeplex.com/). They have a bunch of utilities for saving/reading various image formats. They also have a few of the low levels, and I wish there were an easy way to use both (but there isn't).

All of the above work in WP7.

I guess the major difference is with these tools you won't be using XAML you will be writing directly to your image (so you may need to do size detection of your text and stuff like that).

Autoplasty answered 14/4, 2011 at 19:31 Comment(1)
Thanks. I don't really need XAML, I already do all sorts of calculations to place the texts on the image in code. I was aware of WriteableBitmapExtensions but AFAIK it only deals with drawing all sorts of geometries, and I need to draw strings using fonts.Winifield
U
0

First off, are you sure about rendering this as a bitmap? How about generating a Canvas with an image and TextBlock?

I need to render a large number of images

I have a feeling that this generating will kill phone performance. Generally, for bitmap mainupulation, the best way is to use XNA. Some parts of the XNA framework do a great job Silverlight projects. (BTW the refreshed Windows Phone Developer Tools will allow Silverlight and XNA coexist in the same project)

I would step back and think about this feature. Developing something like this for a week and then end up with unacceptable performance would make me a sad panda.

EDIT

As far I understand you need some kind of popup with image as a background and the message.

Make a Canvas with TextBlock but hide it.

<Canvas x:Name="userInfoCanvas"  Height="200" Width="200" Visibility="Collapsed">
    <Image x:Name="backgroundImage"> </Image>
    <TextBlock x:Name="messageTextBlock" Canvas.ZIndex="3> </TextBlock> <!--ZIndex set the order of elements  -->
</Canvas>

When you got the new message, show the Canvas to the user (a opacity animation would be nice), when you finish rendering on the background thread.

messageTextBlock.Text = message;
backgroundImage.Source = new BitmapImage(renderedImage);

Obviouslly, here is a problem with update. UIelements can be updated only form the UI Thread, hence update must be queue with Dispatcher

Dispatcher.BeginInvoke(DispatcherPriority.Background, messageUpdate);  //messageUpdate is an Action or anthing that can be infered to Delegate

PS. didn't compile, this is more pseudocode.

Unused answered 14/4, 2011 at 17:26 Comment(3)
The performance is fine for my needs. I would usually only have to generate 1-3 images at a time, and only in response to a user action that usually happens once a minute or so. The thing is that rendering the images causes the UI to hang for a very short time (maybe 200ms or so), and I would like to avoid that too.Winifield
I wouldn't mind generating the Canvas instead of the image, and then only rendering it at the UI, or attaching it to the visual tree, but I can't even do that in the background.Winifield
Thanks, but I think this is not very relevant to my scenario. I'm developing a viewer of sorts. When the user scrolls to a new location, I would like to render the "surrounding areas" on the background, so that when the user scrolls again, I will have the images ready. If I understand correctly, your suggestion is to create a Canvas with a TextBlock child. In reality, I can have lots and lots of TextBlocks on each image in coordinates that I calculate in code. I would like to perform this work in another thread.Winifield
S
0

The very nature of UI elements requires interaction with them on the UI thread. Even if you could create them on a background thread, when you came to try to render them into the WriteableBitmap you'd get a similar exception, and even then if it allowed you to do that, the elements wouldn't actually have a visual representation until they were added into the visual tree. You might need to use a generic image manipulation library instead of using UI elements.

Perhaps you could describe your scenario on a wider basis, we might have a better solution for you :)

Saiff answered 14/4, 2011 at 17:28 Comment(1)
Do you know of such a generic image manipulation library for Silverlight? I only resorted to using WriteableBitmap and TextBlock because System.Drawing and Graphics are not available in Silverlight.Winifield
H
0

You can draw on WriteableBitmap in thread, but You have to

  1. create WriteableBitmap in main UI thread
  2. do draw work in background thread
  3. assign BitmapSource in main UI thread
Halfmoon answered 18/9, 2011 at 14:8 Comment(0)
S
-3

i'll agree with Derek's answer: you're trying to use UI controls without a UI.

If you want to render a bitmap you need to stick to classes for drawing text on bitmaps.

i presume Windows Phone 7 has the .NET Compact Framework.

psudeo-code:

public Bitmap RenderText(string text, double x, double y)
{
   Bitmap bitmap = new Bitmap(400, 400);

   using (Graphics g = new Graphics(bitmap))
   {
      using (Font font = SystemFonts....)
      {
         using (Brush brush = new SolidColorBrush(...))
         {
            g.DrawString(text, font, brush, new Point(x, y));
         }
      }
   }

   return bitmap;
}
Solfa answered 14/4, 2011 at 17:46 Comment(2)
No, sadly it does not. I wish it did. Windows Phone 7 supports Silverlight and XNA. System.Drawing is not available. You don't have Bitmap, Graphics, or DrawString. I'm actually porting an application from Windows Mobile to Silverlight, and was surprised to find Graphics gone. The only alternative I found so far was this awkward WriteableBitmap technique.Winifield
Ahhh. i knew Silverlight was originally WPF/e (Windows Presentation Foundation Everywhere) in development. But in my head i took that to mean ".NET Compact Framework).Solfa

© 2022 - 2024 — McMap. All rights reserved.