Why is Wpf DrawingContext.DrawText so expensive?

In Wpf (4.0), my list (using VirtualizingStackPanel) contains 500 items. Each element has its own type.

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

Now, when you move the scroll bar of the list back and forth so that each visual of an element is created at least once, when the use of the ram-server increases to 500 MB after a while, and then - after a while - it returns to 250 MB but remains at that level. Memory leak? I thought the advantage of VirtualizationStackPanel is that visual effects that are not needed / visible are removed ...

In any case, this extreme use of ram only appears when drawing text using "DrawText". Drawing other objects, such as "DrawEllipse", does not consume so much memory.

Is there a more efficient way to draw many text elements than using Drawing.Context "DrawText"?

Here is a complete sample (just create a new Wpf application project and replace the window1 code): (I know there are FlowDocument and FixedDocument, but they are not an 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> 

And 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; } } } 
+7
c # wpf drawing
source share
3 answers

Although this is not entirely useful for you, my experience with VirtualizationStackPanel is not that it has objects that are not displayed, but that it allows objects that cannot be disposed of to recover memory when the application needs more memory, which should result in your memory usage being used when memory is available.

Is it possible that dc.DrawText triggers BuildGeometry () for each formattedText object and can take this out of the loop? I don’t know how much BuildGeometry works, but it is possible that a DrawingContext is capable of perceiving geometry, and a call to BuildGeometry is called 999 times unnecessarily in your example. Take a look:

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

to find out if there are any other optimizations you can do.

Can you output some memory profile data and some temporary data in your loops to let you know if it slows down, or does the memory grow non-linearly during the loop?

+1
source share

A big contributor is a fact (based on my experience with GlyphRun, which I think is being used behind the scenes), it uses at least 2 vocabulary dictionaries for each character to get the index and glyph width. One hack that I used in my project, I found out the offset between the ASCII value and the glyph index for the alphanumeric characters for the font that I used. Then I used this to calculate glyph indices for each character, and not to look up a dictionary. It gave me a decent speed. Also the fact that I could reuse the glyph run, moved it with the help of translational conversion without recounting everything or searching for a dictionary. The system cannot crack this because it is not common enough to use in each case. I guess a similar hack can be done for other fonts. I tested only with Arial, other fonts could be indexed differently. You might be able to go even faster with a monolayer, since you can assume that the width of the glyphs will be the same, and only one look up, not one per character, but I have not tested this.

Another slow contributor is a small code, I have not figured out how to crack it yet. typeface.TryGetGlyphTypeface (out glyphTypeface);

Here is my code for my alphanumeric Arial hack (compatible with other unknown characters)

 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; } 
+6
source share

I found the solution user638350 very useful; in my case, I use only one font size, so the following optimizations reduced the time to less than 0.0000 per 20 000 frames with 0.0060ms of each frame. Most of the slowdown comes from "TryGetGlyphTypeface" and "AdvanceWidths", and therefore the two are cached. In addition, the calculation of the offset position and tracking of the total width has been added.

  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
source share

All Articles