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

fri*_*itz 9 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)


}
Run Code Online (Sandbox Code Playgroud)

现在,当来回移动列表框的滚动条时,每个项目的视觉效果至少创建一次,一段时间内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>
Run Code Online (Sandbox Code Playgroud)

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

        }

    }

}
Run Code Online (Sandbox Code Playgroud)

小智 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;
    }
Run Code Online (Sandbox Code Playgroud)


小智 2

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

dc.DrawText 是否有可能为每个 formattedText 对象触发 BuildGeometry(),并且您可以将其带到循环之外?我不知道 BuildGeometry 有多少工作量,但 DrawingContext 可能只能接受几何图形,并且在您的示例中 BuildGeometry 调用被不必要地调用了 999 次。看一下:

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

看看是否还有其他可以进行的优化。

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

  • @fritz,DrawGlyphRun 会生成模糊文本,因为它不与您的设备像素对齐。您可以使用“GlyphRun.ComputeAlignmentBox()”和“DrawingContext.PushGuidelineSet()”的组合自行对齐它。 (3认同)