如何在Windows Phone 7上的后台线程上渲染WriteableBitmap中的文本?

14

我正在尝试在 Windows Phone 7 应用程序中的位图上渲染文本。

当代码运行在主线程时,类似下面的代码看起来是可以正常工作的:

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;
}

现在,由于我需要渲染几个带有更复杂内容的图像,我想要在后台线程上渲染位图,以避免出现无响应的用户界面。

当我使用BackgroundWorker时,构造函数TextBlock抛出UnauthorizedAccessException,声称这是无效的跨线程访问。

我的问题是:如何在不阻塞用户界面的情况下在位图上渲染文本?

  • 请不要建议使用Web服务进行呈现。我需要呈现大量的图像,带宽成本不符合我的需求,并且离线工作的能力是一个主要的要求。
  • 如果有另一种方法来呈现文本,则解决方案不一定必须使用WriteableBitmapUIElements

编辑

另一个想法:有没有人知道是否可以在另一个线程中运行UI消息循环,然后让该线程做这项工作?(而不是使用BackgroundWorker)?

编辑2

为了考虑替代WriteableBitmap的方案,我需要的功能是:

  • 绘制背景图像。
  • 测量给定字体系列和大小(以及最好的样式)的单行字符串的宽度和高度。无需换行。
  • 在给定坐标处绘制具有给定字体系列、大小、样式的1行字符串。
  • 文本呈现应支持透明背景。即在字符之间应该看到背景图像。

你需要哪些文本特性?如果你不需要太多字体或大小的变化,我可以发布一个使用SpriteSheets的方法。但是,如果你需要丰富的格式或想要渲染段落文本,这种方法就不太合适了。 - Kris
@Kris:听起来很有前途,我列出了我需要的功能。谢谢。 - Ran
你找到你需要的所有东西了吗?我正在将一个应用程序移植到WP7,并且我正在寻找完全相同的东西:“绘制背景图像。给定字体系列和大小(最好是样式),测量单行字符串的宽度和高度。不需要换行。在给定坐标处绘制具有给定字体系列、大小和样式的单行字符串。”你能分享一下你的发现吗?谢谢。 - jyavenard
说实话,我没有找到足够好的解决方案。WriteableBitmapEx库可能适合,但对我来说使用它存在问题。特别是,需要为所有字体、样式和大小预先生成精灵,并且由于文本渲染不再基于矢量,因此呈现质量较低。 - Ran
6个回答

15

这种方法是通过从预先制作的图像中复制字母来实现,而不是使用TextBlock,基于我对这个问题的回答。主要限制在于需要为每种所需字体和大小准备不同的图像。20号字体需要大约150kb。

使用SpriteFont2导出所需尺寸的字体和xml度量文件。 代码假设它们被命名为“FontName FontSize”。png和“FontName FontSize”。xml,并将它们添加到您的项目中并将生成操作设置为内容。该代码还需要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;
        }
    }
}

你需要调用一次RegisterFont来加载文件,然后调用DrawString。它使用WriteableBitmapEx.Blit,因此如果您的字体文件具有白色文本和透明背景,则Alpha会被正确处理,您可以重新着色。如果您在没有加载的大小上绘制文本,则代码确实会对其进行缩放,但结果并不理想,可以使用更好的插值方法。

我尝试从不同的线程绘制,这在模拟器中有效,但仍需要在主线程上创建WriteableBitmap。根据我的理解,您想要滚动瓦片,类似于地图应用程序的工作方式,如果是这种情况,请重用旧的WriteableBitmaps而不是重新创建它们。如果不是,则可以更改代码以使用数组代替。


谢谢,Kris!这似乎是我要找的最接近的东西。我会试一试。 - Ran
1
@chendang Spritefont 有一个选项卡,允许您选择要导出的字符,因此如果您这样做并使用适当的字体,它应该可以工作。 - Kris

2
我不确定这是否能完全解决你的问题,但是在我的漫画阅读器中有两个工具(我不会在这里进行自我宣传,但我很想...如果你正在搜索它,提示一下,它是“Amazing”)。有时我需要拼接一堆图像。我使用Rene Schulte(和其他许多贡献者)的WriteableBitmapExtensions(http://writeablebitmapex.codeplex.com/)。我已经能够将图像的渲染/拼接卸载到后台线程,然后将生成的WriteableBitmap设置为UI线程上某个图像的源。

在这个领域的另一个新秀是.NET Image Tools(http://imagetools.codeplex.com/)。他们有许多用于保存/读取各种图像格式的实用程序。他们还有一些低级别的东西,我希望有一种简单的方法来同时使用两者(但没有)。

以上所有内容均适用于WP7。

我想主要区别在于,使用这些工具时,您将不会使用 XAML,而是直接编写到您的图像中(因此您可能需要对文本进行大小检测等操作)。

谢谢。我不需要XAML,我已经在代码中进行各种计算来放置图像上的文本。我知道WriteableBitmapExtensions,但据我所知它只处理绘制各种几何图形,而我需要使用字体绘制字符串。 - Ran

0

首先,您确定要将其呈现为位图吗?如何使用图像和生成

我需要渲染大量的图像

我有一种感觉,这种生成方式会降低手机性能。通常,对于位图操作,最好的方法是使用XNA。 XNA框架的某些部分在Silverlight项目中表现出色。(顺便说一句,{{link1:刷新的Windows Phone Developer Tools}}将允许Silverlight和XNA共存于同一项目中)

我会退后一步,考虑一下这个功能。开发类似这样的功能一周,然后最终得到无法接受的性能,这会让我很难过。

编辑

据我所知,您需要一些带有图像背景和消息的弹出窗口。

制作一个Canvas和TextBlock,但将其隐藏。

<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>

当您收到新消息时,应在后台线程上完成渲染后向用户显示画布(透明度动画效果会更好)。
messageTextBlock.Text = message;
backgroundImage.Source = new BitmapImage(renderedImage);

显然,这里有一个更新问题。UI元素只能从UI线程更新,因此更新必须排队使用Dispatcher。

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

附注:没有编译,这只是伪代码。


性能对我的需求来说还不错。我通常只需要一次生成1-3张图像,而且只有在用户每分钟左右的操作响应中才需要生成。问题是渲染图像会导致界面卡顿很短的时间(可能200毫秒左右),我也想避免这种情况。 - Ran
我不介意生成Canvas而不是图像,然后只在UI上呈现它,或将其附加到可视树中,但我甚至无法在后台执行该操作。 - Ran
谢谢,但我认为这与我的场景不太相关。我正在开发一种查看器。当用户滚动到新位置时,我希望在背景上呈现“周围区域”,以便当用户再次滚动时,我将拥有准备好的图像。如果我理解正确,您的建议是创建具有TextBlock子元素的Canvas。实际上,我可以在每个图像上计算出的坐标中拥有很多很多的TextBlock。我想在另一个线程中执行此工作。 - Ran

0

UI元素的本质要求在UI线程上与它们进行交互。即使您可以在后台线程上创建它们,当您尝试将它们呈现到WriteableBitmap中时,您会得到类似的异常,即使允许您这样做,这些元素也不会实际上具有视觉表示,直到它们被添加到可视树中。您可能需要使用通用图像处理库,而不是使用UI元素。

也许您可以更广泛地描述您的情况,我们可能会为您提供更好的解决方案 :)


你知道是否有适用于Silverlight的通用图像处理库吗?我只能使用WriteableBitmapTextBlock,因为在Silverlight中不支持System.DrawingGraphics - Ran

0

你可以在线程中绘制WriteableBitmap,但是你必须:

  1. 在主UI线程中创建WriteableBitmap
  2. 在后台线程中进行绘制工作
  3. 在主UI线程中分配BitmapSource

-3
我同意Derek的答案:你试图在没有UI的情况下使用UI控件。
如果你想渲染位图,你需要使用绘制文本的类。
我假设Windows Phone 7有.NET Compact Framework。
伪代码:
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;
}

不幸的是,它并没有支持。但我真希望它能够支持。Windows Phone 7 支持 Silverlight 和 XNA,但不支持 System.Drawing 库。这就意味着你无法使用 Bitmap、Graphics 或 DrawString 等类。事实上,我正在将一个应用程序从 Windows Mobile 移植到 Silverlight,而发现 Graphics 类已经被删除让我感到惊讶。目前我所找到的唯一替代方案是使用 WriteableBitmap 技巧,但感觉使用起来有些棘手。 - Ran
啊。我知道Silverlight最初是作为WPF/e(Windows Presentation Foundation Everywhere)开发的。但是在我脑海中,我理解成了“.NET Compact Framework”。 - Ian Boyd

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接