Почему Wpf DrawingContext.DrawText такой дорогой?

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


}

Теперь при перемещении полосы прокрутки списка назад и вперед, чтобы визуальный элемент каждого элемента создавался по крайней мере после того, как через некоторое время использование оперативной памяти увеличилось до 500 МБ, а затем - через некоторое время - возвращается примерно к 250 МБ, но остается на этом уровне . Утечка памяти ? Я думал, что преимущество VirtualizingStackPanel заключается в том, что визуальные эффекты, которые не нужны/видимы, удаляются...

В любом случае, это экстремальное использование оперативной памяти проявляется только при рисовании текста с помощью «DrawText». Рисование других объектов, таких как «DrawEllipse», не требует много памяти.

Есть ли более эффективный способ рисования большого количества текстовых элементов, чем использование "DrawText" Drawing.Context?

Вот полный пример (просто создайте новый проект приложения 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;

        }

    }

}

person fritz    schedule 01.11.2010    source источник
comment
Вы можете попробовать StreamGeometry. Что относительно легкий вес. msdn.microsoft.com/en-us/library/ms742199.aspx С другой стороны. Я должен сказать. DrawText относительно менее весомая штука. Не знаю, почему это занимает так много ресурсов. У вас есть образцы для описанного выше сценария?   -  person Prince Ashitaka    schedule 01.11.2010
comment
DrawingContext.DrawGlyph кажется намного быстрее, чем DrawText.   -  person fritz    schedule 03.11.2010


Ответы (3)


Хотя это не совсем полезно для вас, мой опыт работы с VirtualizingStackPanel заключается не в том, что он удаляет объекты, не находящиеся в поле зрения, а в том, что он позволяет удалять объекты, не находящиеся в поле зрения, для восстановления памяти, когда приложению требуется больше памяти, что должно привести к в вашем использовании памяти, когда есть доступная память.

Возможно ли, что dc.DrawText запускает BuildGeometry() для каждого объекта formattedText, и что вы можете вывести это из цикла? Я не знаю, сколько работы требует BuildGeometry, но возможно, что DrawingContext способен принимать только геометрию, а вызов BuildGeometry вызывается в вашем образце 999 раз без необходимости. Посмотри на:

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

чтобы увидеть, есть ли какие-либо другие оптимизации, которые вы можете сделать.

Можете ли вы вывести некоторые данные профиля памяти и некоторые временные данные в ваших циклах, чтобы дать представление о том, замедляется ли он или память увеличивается нелинейным образом во время цикла?

person David Hagan    schedule 11.01.2011
comment
Вот почему DrawingContext.DrawGlyph работает быстрее, чем DrawText: он запускает BuildGmeotry только один раз при создании. Но недостатком является то, что символы выглядят более размытыми, чем с DrawText. - person fritz; 23.01.2011
comment
@fritz, DrawGlyphRun создает размытый текст, потому что он не выравнивается для вас по пикселям устройства. Вы можете выровнять его самостоятельно комбинацией GlyphRun.ComputeAlignmentBox() и DrawingContext.PushGuidelineSet(). - person Brian Reichle; 27.11.2013

Большой вклад вносит тот факт (исходя из моего опыта работы с GlyphRun, который, я думаю, используется за кулисами), что он использует как минимум 2 поиска в словаре для каждого символа, чтобы получить индекс и ширину глифа. Один хак, который я использовал в своем проекте, заключался в том, что я выяснил смещение между значением ASCII и индексом глифа для буквенно-цифровых символов для шрифта, который я использовал. Затем я использовал это для вычисления индексов глифов для каждого символа, а не для поиска в словаре. Это дало мне приличный прирост скорости. Также тот факт, что я мог повторно использовать запуск глифа, перемещая его с помощью преобразования перевода, не пересчитывая все или эти поиски в словаре. Система не может сделать этот взлом самостоятельно, потому что она недостаточно универсальна, чтобы ее можно было использовать в каждом случае. Я предполагаю, что подобный хак можно было бы сделать и для других шрифтов. Я тестировал только с Arial, другие шрифты могут быть проиндексированы по-другому. Возможно, вы сможете работать еще быстрее с моноширинным шрифтом, поскольку вы можете предположить, что ширина всех глифов будет одинаковой, и выполнять только один поиск, а не один для каждого символа, но я не проверял это.

Другим фактором замедления является этот небольшой код, я еще не понял, как его взломать. typeface.TryGetGlyphTypeface(out glyphTypeface);

Вот мой код для моего буквенно-цифрового хака Arial (совместимость с другими символами неизвестна)

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;
    }
person user638350    schedule 13.08.2012

Я нашел решение user638350 очень полезным; в моем случае я использую только один размер шрифта, поэтому следующие оптимизации сократили время до менее чем 0,0000 на 20 000 кадров по сравнению с 0,0060 мс на каждый кадр. Большая часть замедления связана с «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;
    }
person jv_    schedule 08.06.2015