2010-11-01 5 views
7

En Wpf (4.0) mi listbox (usando VirtualizingStackPanel) contiene 500 elementos. Cada elemento es de un tipo de encargo¿Por qué es tan costoso DrawingContext.DrawText de Wpf?

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) 


} 

Ahora cuando se mueve la barra de desplazamiento del cuadro de lista de ida y vuelta para que la visual de cada elemento se crea al menos una vez el uso de memoria RAM sube a 500 Mb después de un tiempo y luego - después un tiempo - vuelve a ca 250 Mb, pero se mantiene en este nivel. Pérdida de memoria ? Pensé que la ventaja de un VirtualizingStackPanel es que los elementos visuales que no son necesarios/visibles quedan eliminados ...

De todos modos, este uso extremo del ram solo aparece cuando se dibuja texto con "DrawText". Dibujar otros objetos como "DrawEllipse" no consume tanta memoria.

¿Hay una manera más eficiente de dibujar muchos elementos de texto que usar Drawing.Context's "DrawText"?

Aquí está la muestra completa (basta con crear un nuevo proyecto de aplicación de WPF y reemplazar el código window1): (Sé que hay FlowDocument y FixedDocument pero son otra alternativa) 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> 

Y la 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; 

     } 

    } 

} 
+0

Puede probar StreamGeometry. Que es relativamente liviano http://msdn.microsoft.com/en-us/library/ms742199.aspx Por otro lado. Debo decir. DrawText es algo relativamente menos pesado. No sé por qué está tomando tanto recurso. ¿Tiene alguna muestra para el escenario anterior? –

+0

DrawingContext.DrawGlyph parece ser mucho más rápido que DrawText. – fritz

Respuesta

1

Si bien esto no es del todo útil para usted, mi experiencia con VirtualizingStackPanel no es que no dispone de objetos a la vista, sino que permite que los objetos no en vista que se va re Se propone recuperar la memoria cuando la aplicación necesita más memoria, lo que debería hacer que el uso de la memoria se dispare cuando hay memoria disponible.

¿Es posible que dc.DrawText esté ejecutando BuildGeometry() para cada objeto FormattedText, y que puede sacarlo del bucle? No sé cuánto trabajo BuildGeometry es, pero es posible que DrawingContext solo sea capaz de aceptar geometría, y la llamada BuildGeometry se llama innecesariamente 999 veces en su muestra. Echar un vistazo a:

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

para ver si hay otras optimizaciones que puede hacer.

¿Puede generar algunos datos de perfil de memoria y algunos datos de temporización dentro de sus bucles para dar una idea de si se está desacelerando, o la memoria está aumentando de forma no lineal durante el ciclo?

+0

Es por eso que DrawingContext.DrawGlyph es más rápido que DrawText: dispara BuildGemeotry solo una vez en la creación. Pero la desventaja es que los personajes se ven más borrosos que con DrawText. – fritz

+1

@fritz, DrawGlyphRun produce texto borroso porque no se alinea con los píxeles del dispositivo por usted. Puede alinearlo usted mismo con una combinación de 'GlyphRun.ComputeAlignmentBox()' y 'DrawingContext.PushGuidelineSet()'. –

6

Un gran contribuyente es el hecho (basado en mi experiencia con GlyphRun que creo que se usa entre bastidores) que usa al menos 2 búsquedas de diccionario por carácter para obtener el índice y el ancho del glifo. Un truco que utilicé en mi proyecto fue averiguar el desfase entre el valor ASCII y el índice de glifo para caracteres alfanuméricos para la fuente que estaba usando. Luego lo usé para calcular los índices de glifos para cada personaje en lugar de hacer la búsqueda del diccionario. Eso me dio una velocidad decente. También el hecho de que podía reutilizar el glifo se ejecuta moviéndolo con una transformación de traducción sin volver a calcular todo o las búsquedas de diccionario. El sistema no puede hacer este truco por sí mismo porque no es lo suficientemente genérico como para ser utilizado en todos los casos. Me imagino que se podría hacer un truco similar para otras fuentes. Solo probé con Arial, otras fuentes podrían indexarse ​​de manera diferente. Puede ir aún más rápido con una fuente monoespaciada, ya que puede suponer que los anchos de los glifos serán todos iguales y solo hacer una búsqueda en lugar de uno por carácter, pero no lo he probado.

El otro colaborador lento es este pequeño código, todavía no he descubierto cómo hackearlo. tipo de letra.TryGetGlyphTypeface (sale de glyphTypeface);

Aquí está mi código para mi truco Arial alfanumérica (compatibilidad con otros personajes desconocidos)

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

Buen código para comenzar. ¡Gracias! – LionAM

1

encontré la solución de user638350 a ser muy útil; en mi caso, solo uso un tamaño de fuente, por lo que las siguientes optimizaciones redujeron el tiempo a menos de 0.0000 en 20.000 fotogramas desde 0.0060ms en cada fotograma. La mayor parte de la ralentización es de 'TryGetGlyphTypeface' y 'AdvanceWidths', por lo que estos dos se almacenan en caché. Además, se agregó el cálculo de una posición de desplazamiento y el seguimiento de un ancho total.

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