2010-11-01 10 views
7

In Wpf (4.0) la mia listbox (che utilizza un VirtualizingStackPanel) contiene 500 articoli. Ogni elemento è di tipo personalizzatoPerché il DrawingContext.DrawText di Wpf è così costoso?

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) 


} 

Ora, quando si sposta la barra di scorrimento della casella di riepilogo avanti e indietro in modo che di visual ogni elemento viene creato almeno una volta l'utilizzo della RAM va fino a 500 Mb, dopo un po 'e poi - dopo un po '- torna a circa 250 Mb ma rimane su questo livello. Perdita di memoria ? Ho pensato che il vantaggio di un VirtualizingStackPanel è che le immagini che non sono necessarie/visibili vengono smaltite ...

In ogni caso, questo uso estremo della ram viene visualizzato solo quando si disegna il testo usando "DrawText". Disegnare altri oggetti come "DrawEllipse" non consuma così tanta memoria.

Esiste un modo più efficiente di disegnare più elementi di testo rispetto all'utilizzo di "DrawText" di Drawing.Context?

Ecco l'esempio completo (basta creare un nuovo progetto WPF Applicazione e sostituire il codice Window1): (so che ci sono FlowDocument e FixedDocument ma sono alternative) 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> 

E il 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

Si potrebbe provare StreamGeometry. Quale è relativamente leggero. http://msdn.microsoft.com/en-us/library/ms742199.aspx D'altra parte. Devo dire. DrawText è una cosa relativamente meno pesata. Non so perché sta prendendo molte risorse. Avete qualche campione per lo scenario sopra? –

+0

DrawingContext.DrawGlyph sembra essere molto più veloce di DrawText. – fritz

risposta

1

Anche se questo non è del tutto utile a voi, la mia esperienza con VirtualizingStackPanel non è che si dispone di oggetti non in vista, ma che permette gli oggetti non in vista di essere d isposed per recuperare la memoria quando l'applicazione ha bisogno di più memoria, il che dovrebbe comportare l'utilizzo della memoria in mongolfiera quando c'è memoria disponibile.

È possibile che dc.DrawText firmi BuildGeometry() per ogni oggetto formattedText e che sia possibile portarlo all'esterno del ciclo? Non so quanto lavoro sia BuildGeometry, ma è possibile che DrawingContext sia solo in grado di accettare la geometria e che la chiamata BuildGeometry venga chiamata inutilmente 999 volte nel campione. Date un'occhiata a:

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

per vedere se ci sono altre ottimizzazioni si può fare.

È possibile emettere alcuni dati del profilo di memoria e alcuni dati di temporizzazione all'interno dei loop per dare un senso di rallentamento o di aumento della memoria in modo non lineare durante il ciclo?

+0

Ecco perché DrawingContext.DrawGlyph è più veloce di DrawText: genera BuildGemeotry una sola volta durante la creazione. Ma il disadvnatage è che i personaggi sembrano più sfocati rispetto a DrawText. – fritz

+1

@fritz, DrawGlyphRun produce testo sfocato perché non si allinea ai pixel del dispositivo. Puoi allinearlo da solo con una combinazione di 'GlyphRun.ComputeAlignmentBox()' e 'DrawingContext.PushGuidelineSet()'. –

6

Un grande contributo è il fatto (basato sulla mia esperienza con GlyphRun che penso venga utilizzata dietro le quinte) che utilizza almeno 2 ricerche di dizionario per carattere per ottenere l'indice e la larghezza del glifo. Una modifica che ho usato nel mio progetto è stata la determinazione dell'offset tra il valore ASCII e l'indice glifo per caratteri alfanumerici per il carattere che stavo usando. Poi l'ho usato per calcolare gli indici di glifo per ogni carattere piuttosto che cercare il dizionario. Questo mi ha dato una discreta velocità. Inoltre, il fatto di poter riutilizzare il glifo lo sposta spostandolo con una trasformazione di traduzione senza ricalcolare tutto o quelle ricerche del dizionario. Il sistema non può fare questo hack da solo perché non è abbastanza generico da essere usato in ogni caso. Immagino che un simile hack potrebbe essere fatto per altri tipi di carattere. Ho provato solo con Arial, altri caratteri potrebbero essere indicizzati in modo diverso. Potrebbe essere in grado di andare ancora più velocemente con un font con spaziatura singola poiché potresti essere in grado di assumere che le larghezze degli glifi sarebbero tutte uguali e fare solo una ricerca piuttosto che una per carattere, ma non l'ho ancora testata.

L'altro contributore di rallentamento è questo piccolo codice, non ho ancora capito come modificarlo. typeface.TryGetGlyphTypeface (out glyphTypeface);

Ecco il mio codice alfanumerico per il mio mod Arial (compatibilità con altri personaggi sconosciuti)

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

Bel codice per iniziare. Grazie! – LionAM

1

ho trovato la soluzione di user638350 essere molto utile; nel mio caso uso solo una dimensione del font in modo che le seguenti ottimizzazioni riducano il tempo a meno di 0,0000 oltre 20.000 fotogrammi da 0,0060 ms ogni fotogramma. La maggior parte del rallentamento proviene da "TryGetGlyphTypeface" e "AdvanceWidths", quindi questi due sono memorizzati nella cache. Inoltre, aggiunto il calcolo di una posizione di offset e il tracciamento di una larghezza totale.

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