ทำไม DrawingContext.DrawText ของ Wpf ถึงมีราคาแพงมาก

ใน 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)


}

ตอนนี้เมื่อย้ายแถบเลื่อนของกล่องรายการไปมาเพื่อให้วิชวลของทุกรายการถูกสร้างขึ้นอย่างน้อยหนึ่งครั้งการใช้งาน ram สูงถึง 500 Mb หลังจากนั้นไม่นาน - หลังจากนั้นไม่นาน - กลับไปที่ประมาณ 250 Mb แต่ยังคงอยู่ที่ระดับนี้ . หน่วยความจำรั่ว? ฉันคิดว่าข้อดีของ VirtualizingStackPanel คือภาพที่ไม่จำเป็น/มองเห็นได้จะถูกกำจัดไป...

อย่างไรก็ตาม การใช้ RAM ที่รุนแรงนี้จะปรากฏเฉพาะเมื่อวาดข้อความโดยใช้ "DrawText" เท่านั้น การวาดวัตถุอื่นๆ เช่น "DrawEllipse" ไม่ใช้หน่วยความจำมากนัก

มีวิธีที่มีประสิทธิภาพในการวาดรายการข้อความจำนวนมากมากกว่าการใช้ "DrawText" ของ Drawing.Context หรือไม่

นี่คือตัวอย่างที่สมบูรณ์ (เพียงสร้างโครงการ Wpf Application ใหม่และแทนที่โค้ด 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() สำหรับแต่ละวัตถุที่จัดรูปแบบข้อความ และคุณสามารถนำสิ่งนั้นออกไปนอกวงได้ ฉันไม่รู้ว่า 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: มันจะยิง BuildGemeotry เพียงครั้งเดียวในการสร้าง แต่ข้อเสียคือตัวละครดูพร่ามัวมากกว่า DrawText - person fritz; 23.01.2011
comment
@fritz, DrawGlyphRun สร้างข้อความที่พร่ามัวเนื่องจากไม่สอดคล้องกับพิกเซลของอุปกรณ์สำหรับคุณ คุณสามารถจัดตำแหน่งด้วยตนเองโดยใช้การผสม GlyphRun.ComputeAlignmentBox() และ DrawingContext.PushGuidelineSet() - person Brian Reichle; 27.11.2013

ผู้มีส่วนร่วมรายใหญ่คือข้อเท็จจริง (จากประสบการณ์ของฉันกับ GlyphRun ซึ่งฉันคิดว่ามีการใช้เบื้องหลัง) ที่ใช้การค้นหาพจนานุกรมอย่างน้อย 2 รายการต่ออักขระเพื่อรับดัชนีสัญลักษณ์และความกว้าง แฮ็คอย่างหนึ่งที่ฉันใช้ในโครงการของฉันคือฉันหาค่าออฟเซ็ตระหว่างค่า ASCII และดัชนีสัญลักษณ์สำหรับอักขระตัวอักษรและตัวเลขสำหรับแบบอักษรที่ฉันใช้ จากนั้นฉันใช้สิ่งนั้นเพื่อคำนวณดัชนีสัญลักษณ์สำหรับอักขระแต่ละตัวแทนที่จะค้นหาในพจนานุกรม นั่นทำให้ฉันเร่งความเร็วได้พอสมควร ความจริงที่ว่าฉันสามารถนำสัญลักษณ์มาใช้ซ้ำได้โดยการเคลื่อนย้ายมันไปรอบ ๆ ด้วยการแปลงการแปลโดยไม่ต้องคำนวณทุกอย่างหรือการค้นหาพจนานุกรมเหล่านั้นใหม่ ระบบไม่สามารถทำการแฮ็กนี้ได้ด้วยตัวเองเนื่องจากไม่ได้เป็นแบบทั่วไปเพียงพอที่จะใช้ในทุกกรณี ฉันคิดว่าการแฮ็กที่คล้ายกันสามารถทำได้กับแบบอักษรอื่น ฉันทดสอบกับ Arial เท่านั้น แบบอักษรอื่นๆ สามารถจัดทำดัชนีแตกต่างออกไปได้ อาจทำงานได้เร็วยิ่งขึ้นด้วยแบบอักษรที่เว้นระยะโมโนเนื่องจากคุณอาจถือว่าความกว้างของสัญลักษณ์ทั้งหมดจะเท่ากันและทำการค้นหาเพียงครั้งเดียวแทนที่จะเป็นหนึ่งรายการต่ออักขระ แต่ฉันยังไม่ได้ทดสอบสิ่งนี้

ผู้สนับสนุนที่ช้าลงอีกคนคือโค้ดเล็กๆ นี้ ฉันยังไม่รู้ว่าจะแฮ็กมันอย่างไร แบบอักษร TryGetGlyphTypeface (ออก 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.0060ms ทุกเฟรม การชะลอตัวส่วนใหญ่มาจาก '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