为什么Wpf的DrawingContext.DrawText如此昂贵?

时间:2010-11-01 09:27:59

标签: c# wpf drawing

在Wpf(4.0)中,我的列表框(使用VirtualizingStackPanel)包含500个项目。每个项目都是自定义类型

class Page : FrameworkElement
...
protected override void OnRender(DrawingContext dc)
{
   // Drawing 1000 single characters to different positions
   //(formattedText is a static member which is only instantiated once and contains the string "A" or "B"...)
   for (int i = 0; i < 1000; i++)
     dc.DrawText(formattedText, new Point(....))


  // Drawing 1000 ellipses: very fast and low ram usage
    for (int i = 0; i < 1000; i++)     
    dc.DrawEllipse(Brushes.Black, null, new Point(....),10,10)


}

现在,来回移动列表框的滚动条,以便至少一次创建每个项目的视觉效果,一段时间内ram的使用量达到500 Mb,然后 - 一段时间后 - 恢复到250 Mb左右但保持不变在这个层面上。内存泄漏 ?我认为VirtualizingStackPanel的优点是不需要/可见的视觉效果被处理......

无论如何,只有在使用“DrawText”绘制文本时才会出现此极端ram用法。绘制像“DrawEllipse”这样的其他对象不会消耗太多内存。

绘制多个文本项是否比使用Drawing.Context的“DrawText”更有效?

这是完整的示例(只需创建一个新的Wpf应用程序项目并替换window1代码):(我知道有FlowDocument和FixedDocument但它们别无选择) XAML:

<Window x:Class="WpfApplication1.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1" Height="900" Width="800">
<Grid Background="Black">
    <ListBox Name="lb" ScrollViewer.CanContentScroll="True"   Background="Black">
        <ListBox.ItemsPanel>
            <ItemsPanelTemplate>
                <VirtualizingStackPanel Orientation="Horizontal" />
            </ItemsPanelTemplate>
        </ListBox.ItemsPanel>
    </ListBox>
</Grid>
</Window>

Window1.xaml.cs:

public partial class Window1 : Window
{
    readonly ObservableCollection<FrameworkElement> collection = new ObservableCollection<FrameworkElement>();

  public Window1()
    {
        InitializeComponent();

        for (int i = 0; i < 500; i++)
        {
            collection.Add(new Page(){ Width = 500, Height = 800 });
        }

        lb.ItemsSource = collection;
    }
}

 public class Page : FrameworkElement
{
    static FormattedText formattedText = new FormattedText("A", CultureInfo.GetCultureInfo("en-us"),
                                              FlowDirection.LeftToRight,
                                              new Typeface(new FontFamily("Arial").ToString()),
                                              12,Brushes.Black);
    protected override void OnRender(DrawingContext dc)
    {
        dc.DrawRectangle(Brushes.White, null, new Rect(0, 0, Width, Height));
        double yOff = 0;
        for (int i = 0; i < 1000; i++) // draw 1000 "A"s 
        {
            dc.DrawText(formattedText, new Point((i % 80) * 5, yOff ));
            if (i % 80 == 0) yOff += 10;

        }

    }

}

3 个答案:

答案 0 :(得分:8)

一个重要的贡献者是(基于我对GlyphRun的经验,我认为在幕后使用),它每个字符使用至少2个字典查找来获得字形索引和宽度。我在我的项目中使用的一个黑客是我找出了ASCII值和我使用的字体的字母数字字符的字形索引之间的偏移量。然后我用它来计算每个字符的字形索引,而不是查找字典。这给了我一个体面的加速。此外,我可以重复使用字形运行,使用转换变换移动它,而无需重新计算所有内容或字典查找。系统无法自行完成此操作,因为它不够通用,无法在每种情况下使用。我想可以为其他字体做类似的黑客攻击。我只用Arial测试过,其他字体的索引可能不同。因为你可以假设字形宽度都是相同的而且每个字符只能查找一个而不是一个字符,所以可以更快地使用单倍间距字体,但我没有测试过这个。

另一个减速贡献者是这个小代码,我还没弄明白如何破解它。 typeface.TryGetGlyphTypeface(out glyphTypeface);

这是我的字母数字Arial hack的代码(与其他未知字符的兼容性)

public  GlyphRun CreateGlyphRun(string text,double size)
    {
        Typeface typeface = new Typeface("Arial");
        GlyphTypeface glyphTypeface;
        if (!typeface.TryGetGlyphTypeface(out glyphTypeface))
            throw new InvalidOperationException("No glyphtypeface found");          

        ushort[] glyphIndexes = new ushort[text.Length];
        double[] advanceWidths = new double[text.Length];

        for (int n = 0; n < text.Length; n++) {
            ushort glyphIndex = (ushort)(text[n] - 29);
            glyphIndexes[n] = glyphIndex;
            advanceWidths[n] = glyphTypeface.AdvanceWidths[glyphIndex] * size;
        }

        Point origin = new Point(0, 0);

        GlyphRun glyphRun = new GlyphRun(glyphTypeface, 0, false, size, glyphIndexes, origin, advanceWidths, null, null, null,
                                         null, null, null);
        return glyphRun;
    }

答案 1 :(得分:1)

虽然这对你来说并不完全有用,但我对VirtualizingStackPanel的体验并不是它处理不在视图中的对象,而是当应用程序需要更多内存时它允许不在视图中的对象来恢复内存,当有可用内存时,这会导致内存使用量膨胀。

dc.DrawText是否可能为每个formattedText对象触发BuildGeometry(),并且你可以将它带到循环之外?我不知道BuildGeometry有多少工作,但是DrawingContext可能只能接受几何,并且你的样本中不必要地调用了BuildGeometry调用999次。看看:

http://msdn.microsoft.com/en-us/library/system.windows.media.formattedtext.aspx

查看是否还有其他优化措施。

你能在循环中输出一些内存配置文件数据和一些定时数据,以便了解它是否在减速,或者在循环期间内存是否以非线性方式增加?

答案 2 :(得分:1)

我发现user638350的解决方案非常有用;在我的情况下,我只使用一种字体大小,因此以下优化将时间从每帧0.0060毫秒减少到超过20,000帧的0.0000以下。大多数减速来自'TryGetGlyphTypeface'和'AdvanceWidths',所以这两个都是缓存的。此外,还计算了偏移位置并跟踪总宽度。

    private static Dictionary<ushort,double> _glyphWidths = new Dictionary<ushort, double>();
    private static GlyphTypeface _glyphTypeface;
    public static GlyphRun CreateGlyphRun(string text, double size, Point position)
    {
        if (_glyphTypeface == null)
        {
            Typeface typeface = new Typeface("Arial");
            if (!typeface.TryGetGlyphTypeface(out _glyphTypeface))
                throw new InvalidOperationException("No glyphtypeface found");                
        }

        ushort[] glyphIndexes = new ushort[text.Length];
        double[] advanceWidths = new double[text.Length];

        var totalWidth = 0d;
        double glyphWidth;

        for (int n = 0; n < text.Length; n++)
        {
            ushort glyphIndex = (ushort)(text[n] - 29);
            glyphIndexes[n] = glyphIndex;

            if (!_glyphWidths.TryGetValue(glyphIndex, out glyphWidth))
            {
                glyphWidth = _glyphTypeface.AdvanceWidths[glyphIndex] * size;
                _glyphWidths.Add(glyphIndex, glyphWidth);
            }
            advanceWidths[n] = glyphWidth;
            totalWidth += glyphWidth;
        }

        var offsetPosition = new Point(position.X - (totalWidth / 2), position.Y - 10 - size);

        GlyphRun glyphRun = new GlyphRun(_glyphTypeface, 0, false, size, glyphIndexes, offsetPosition, advanceWidths, null, null, null, null, null, null);

        return glyphRun;
    }