Prestazioni terribili del controllo personalizzato

Sto facendo un semplice controllo grafico in wpf . E non posso spiegare né correggere il problema delle prestazioni: è troppo lento rispetto alle winform. Forse sto facendo qualcosa di sbagliato.

Preparo una demo per dimostrare il problema.

Ecco il controllo di prova:

 public class Graph : FrameworkElement { private Point _mouse; private Point _offset = new Point(500, 500); public Graph() { Loaded += Graph_Loaded; } private void Graph_Loaded(object sender, RoutedEventArgs e) { // use parent container with background to receive mouse events too var parent = VisualTreeHelper.GetParent(this) as FrameworkElement; if (parent != null) parent.MouseMove += (s, a) => OnMouseMove(a); } protected override void OnRender(DrawingContext context) { // designer bugfix if (DesignerProperties.GetIsInDesignMode(this)) return; Stopwatch watch = new Stopwatch(); watch.Start(); // generate some big figure (try to vary that 2000!) var radius = 1.0; var figures = new List(); for (int i = 0; i  { Window.GetWindow(this).Title = string.Format("{0:000}ms; {1:000}ms", time, watch.ElapsedMilliseconds); }, DispatcherPriority.Loaded); } protected override void OnMouseMove(MouseEventArgs e) { base.OnMouseMove(e); var mouse = e.GetPosition(this); if (e.LeftButton == MouseButtonState.Pressed) { // change graph location _offset.X += mouse.X - _mouse.X; _offset.Y += mouse.Y - _mouse.Y; InvalidateVisual(); } // remember last mouse position _mouse = mouse; } } 

Ecco come usarlo in xaml:

      

Alcune osservazioni: il controllo disegnerà la figura, che può essere spostata con il mouse:

inserisci la descrizione dell'immagine qui

Visualizzerà 2 misure nel titolo: prima è il tempo impiegato da OnRender() per completare e il secondo è il tempo impiegato dal rendering effettivo (prima invocazione dopo il rendering).

Prova a variare il 2000 : l’impostazione 1000 rende lo spostamento confortevole, 3000 è come un ritardo di mezzo secondo prima che la figura venga ridisegnata (sul mio PC).

Domande:

  1. È utile usare InvalidateVisual() per aggiornare l’offset del grafico in MouseMove ? E se male, qual è la tecnica giusta da invalidare?
  2. Si blocca, ce ne sono molti senza alcun effetto evidente. Devo usarli o no?
  3. Sembra che 5ms solo 5 5ms per completare il rendering, ma il movimento soggettivo richiede molto più tempo (200 5ms +). Perché?

E la domanda principale è ovviamente la prestazione, perché è così terribile? Potrei disegnare poche centinaia di migliaia di linee in controllo delle forms vere finché non diventerà sciatto, come fa il controllo del mio wpf con appena 1000 … = (


Ho trovato una risposta sull’ultima domanda. Misurare il tempo di rendering non funziona correttamente quando si sposta con il mouse. Ma se la finestra viene ridimensionata, la seconda volta diventa 300ms (sul mio PC con 2000 cifre). Quindi non è un topo sbagliato invalidare (prima domanda), ma in effetti il ​​rendering molto lento.

Questo è un tipo di compito che WPF non è molto bravo in. Intendo la grafica vettoriale in generale. Grazie alla modalità mantenuta. È utile per il rendering dei controlli, ma non per i grafici occupati che vengono aggiornati molto. Ho faticato con lo stesso problema cercando di visualizzare tracce GPS su una mappa WPF.

Suggerirei di usare direct2d e di ospitarlo in WPF. Qualcosa del genere: http://www.codeproject.com/Articles/113991/Utente-Direct-D-with-WPF

Questo ti darà alte prestazioni.

PS Non fraintendermi. Non c’è niente di male con WPF. È progettato per risolvere problemi specifici. È molto facile comporre i controlli e creare interfacce utente impressionanti. Diamo molto per scontato dal sistema di layout automatico. Ma non può essere intelligente in ogni situazione ansible e Microsoft non ha fatto un ottimo lavoro spiegando le situazioni, dove non è una buona opzione. Lasciate che vi faccia un esempio. IPad è performante perché ha la risoluzione fissa e un layout assoluto. Se correggi le dimensioni della finestra di WPF e utilizzi il pannello Canvas otterrai la stessa esperienza.

ecco una riscrittura del tuo codice usando StreamGeometry questo può darti un incremento del 5% -10%

  protected override void OnRender(DrawingContext context) { // designer bugfix if (DesignerProperties.GetIsInDesignMode(this)) return; Stopwatch watch = new Stopwatch(); watch.Start(); // generate some big figure (try to vary that 2000!) var radius = 1.0; StreamGeometry geometry = new StreamGeometry(); using (StreamGeometryContext ctx = geometry.Open()) { Point start = new Point(radius * Math.Sin(0) + _offset.X, radius * Math.Cos(0) + _offset.Y); ctx.BeginFigure(start, false, false); for (int i = 1; i < 2000; i++, radius += 0.1) { Point current = new Point(radius * Math.Sin(i) + _offset.X, radius * Math.Cos(i) + _offset.Y); ctx.LineTo(current, true, false); } } //var geometry = new PathGeometry(new[] { new PathFigure(figures[0].Point, figures, false) }); geometry.Freeze(); Pen pen = new Pen(Brushes.Black, 5); pen.Freeze(); context.DrawGeometry(null, pen, geometry); // measure time var time = watch.ElapsedMilliseconds; Dispatcher.InvokeAsync(() => { Window.GetWindow(this).Title = string.Format("{0:000}ms; {1:000}ms", time, watch.ElapsedMilliseconds); }, DispatcherPriority.Loaded); } 

MODIFICA 2

ecco una riscrittura completa della tua class, questo implementa il caching per evitare il ridisegno e tradurre la trasformazione per eseguire i movimenti via mouse invece di ridisegnare di nuovo. utilizzato anche UIElement come base per l’elemento che è leggermente leggero, quindi FrameworkElement

 public class Graph : UIElement { TranslateTransform _transform = new TranslateTransform() { X = 500, Y = 500 }; public Graph() { CacheMode = new BitmapCache(1.4); //decrease this number to improve performance on the cost of quality, increasing improves quality this.RenderTransform = _transform; IsHitTestVisible = false; } protected override void OnVisualParentChanged(DependencyObject oldParent) { base.OnVisualParentChanged(oldParent); if (VisualParent != null) (VisualParent as FrameworkElement).MouseMove += (s, a) => OnMouseMoveHandler(a); } protected override void OnRender(DrawingContext context) { // designer bugfix if (DesignerProperties.GetIsInDesignMode(this)) return; Stopwatch watch = new Stopwatch(); watch.Start(); // generate some big figure (try to vary that 2000!) var radius = 1.0; StreamGeometry geometry = new StreamGeometry(); using (StreamGeometryContext ctx = geometry.Open()) { Point start = new Point(radius * Math.Sin(0), radius * Math.Cos(0)); ctx.BeginFigure(start, false, false); for (int i = 1; i < 5000; i++, radius += 0.1) { Point current = new Point(radius * Math.Sin(i), radius * Math.Cos(i)); ctx.LineTo(current, true, false); } } //var geometry = new PathGeometry(new[] { new PathFigure(figures[0].Point, figures, false) }); geometry.Freeze(); Pen pen = new Pen(Brushes.Black, 5); pen.Freeze(); context.DrawGeometry(null, pen, geometry); // measure time var time = watch.ElapsedMilliseconds; Dispatcher.InvokeAsync(() => { Application.Current.MainWindow.Title = string.Format("{0:000}ms; {1:000}ms", time, watch.ElapsedMilliseconds); }, DispatcherPriority.Loaded); } protected void OnMouseMoveHandler(MouseEventArgs e) { var mouse = e.GetPosition(VisualParent as FrameworkElement); if (e.LeftButton == MouseButtonState.Pressed) { _transform.X = mouse.X; _transform.Y = mouse.Y; } } } 

nell’esempio sopra ho usato 5000 per testare e posso dire che è abbastanza fluido.

Poiché ciò consente movimenti fluidi tramite il mouse ma il rendering effettivo potrebbe richiedere un po ‘più di tempo per creare la cache (solo per la prima volta). Posso dire una spinta del 1000% nello spostamento dell’object tramite il mouse, il rendering rimane abbastanza vicino al mio precedente approccio con un piccolo sovraccarico di cache. prova questo e condividi ciò che senti


MODIFICA 3

ecco un esempio usando DrawingVisual l’approccio più leggero disponibile in WPF

 public class Graph : UIElement { DrawingVisual drawing; VisualCollection _visuals; TranslateTransform _transform = new TranslateTransform() { X = 200, Y = 200 }; public Graph() { _visuals = new VisualCollection(this); drawing = new DrawingVisual(); drawing.Transform = _transform; drawing.CacheMode = new BitmapCache(1); _visuals.Add(drawing); Render(); } protected void Render() { // designer bugfix if (DesignerProperties.GetIsInDesignMode(this)) return; Stopwatch watch = new Stopwatch(); watch.Start(); using (DrawingContext context = drawing.RenderOpen()) { // generate some big figure (try to vary that 2000!) var radius = 1.0; StreamGeometry geometry = new StreamGeometry(); using (StreamGeometryContext ctx = geometry.Open()) { Point start = new Point(radius * Math.Sin(0), radius * Math.Cos(0)); ctx.BeginFigure(start, false, false); for (int i = 1; i < 2000; i++, radius += 0.1) { Point current = new Point(radius * Math.Sin(i), radius * Math.Cos(i)); ctx.LineTo(current, true, false); } } geometry.Freeze(); Pen pen = new Pen(Brushes.Black, 1); pen.Freeze(); // measure time var time = watch.ElapsedMilliseconds; context.DrawGeometry(null, pen, geometry); Dispatcher.InvokeAsync(() => { Application.Current.MainWindow.Title = string.Format("{0:000}ms; {1:000}ms", time, watch.ElapsedMilliseconds); }, DispatcherPriority.Normal); } } protected override Visual GetVisualChild(int index) { return drawing; } protected override int VisualChildrenCount { get { return 1; } } protected override void OnMouseMove(MouseEventArgs e) { if (e.LeftButton == MouseButtonState.Pressed) { var mouse = e.GetPosition(VisualParent as FrameworkElement); _transform.X = mouse.X; _transform.Y = mouse.Y; } base.OnMouseMove(e); } } 

È strano e nessuno menzionato qui, ma è ansible usare gdi draw in wpf in modo nativo (senza contenitore di hosting ).

Ho trovato prima questa domanda, che diventa un normale grafico basato sul rendering (usa InvalidateVisuals() per ridisegnare).

 protected override void OnRender(DrawingContext context) { using (var bitmap = new GDI.Bitmap((int)RenderSize.Width, (int)RenderSize.Height)) { using (var graphics = GDI.Graphics.FromImage(bitmap)) { // use gdi functions here, to ex.: graphics.DrawLine(...) } var hbitmap = bitmap.GetHbitmap(); var size = bitmap.Width * bitmap.Height * 4; GC.AddMemoryPressure(size); var image = Imaging.CreateBitmapSourceFromHBitmap(hbitmap, IntPtr.Zero, Int32Rect.Empty, BitmapSizeOptions.FromEmptyOptions()); image.Freeze(); context.DrawImage(image, new Rect(RenderSize)); DeleteObject(hbitmap); GC.RemoveMemoryPressure(size); } } 

Questo approccio è in grado di disegnare centinaia di migliaia di linee. Molto reattivo.

svantaggi:

  • non così liscio, come pure un grafico di DrawImage , DrawImage verifica alcune volte dopo, tremolerà un po ‘.
  • necessario convertire tutti gli oggetti wpf in gdi (a volte è imansible): penne, pennelli, punti, rettangoli, ecc.
  • nessuna animazione, il grafico stesso può essere animato (ad esempio, trasformato), ma i disegni non lo sono.