Почему WConf DrawingContext.DrawText так дорого? - PullRequest
9 голосов
/ 01 ноября 2010

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

        }

    }

}

Ответы [ 3 ]

8 голосов
/ 13 августа 2012

Большим вкладом является тот факт (основываясь на моем опыте работы с GlyphRun, который, я думаю, используется за кулисами), что он использует по крайней мере 2 просмотра словаря на символ для получения индекса глифа и ширины. Один хак, который я использовал в своем проекте, заключался в том, что я выяснил смещение между значением ASCII и индексом глифа для буквенно-цифровых символов для шрифта, который я использовал. Затем я использовал это для вычисления индексов глифов для каждого символа, а не поиска словаря. Это дало мне приличное ускорение. Кроме того, тот факт, что я мог повторно использовать прогон глифа, перемещая его с помощью преобразования translate, без пересчета всего или тех поисков по словарю. Система не может сделать это самостоятельно, потому что она не достаточно универсальна для использования в каждом случае. Я полагаю, что аналогичный хак может быть сделано для других шрифтов. Я тестировал только с 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;
    }
1 голос
/ 08 июня 2015

Я нашел решение user638350 очень полезным;в моем случае я использую только один размер шрифта, поэтому следующие оптимизации сократили время до менее 0,0000 на 20000 кадров по сравнению с 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;
    }
1 голос
/ 11 января 2011

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

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

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

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

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

...