Laden...

[Artikel] Flackernde Controls und flackerndes Zeichnen vermeiden

Erstellt von gelöschtem Konto vor 15 Jahren Letzter Beitrag vor 15 Jahren 49.204 Views
Gelöschter Account
vor 15 Jahren
[Artikel] Flackernde Controls und flackerndes Zeichnen vermeiden

Flackern eines Controls kann im Wesentlichen in 2 voneinander unabhängigen Fällen auftreten.

Fall 1:

Wenn man OnPaint(...) überschreibt, viel selbst zeichnet und auch noch im z.B. MouseMove-Ereignis eine komplette Neuzeichnung (.Invalidate()) anstößt, kann es vorkommen, dass sich das Steuerelement in hässliches Flackern hüllt.

Fall 2:

Ebenso kann es beim Befüllen eines DataGridViews, ListView, ListBox, beim Hinzufügen von vielen Controls zu einer Form oder einem anderen Control, dazu kommen, dass es hässlich flackert, bis das Befüllen abgeschlossen ist.

Zum Fall 1:

  1. Nie in einem MouseMove-Ereignis ein komplettes .Invalidate() ohne Angabe einer eingegrenzten Region aufrufen.

Wenn man z.B. ein Gummiband (Rubberband) zeichnen möchte, damit dem User visuell angezeigt wird, dass er gerade einen Rahmen um etwas zieht, dann ist es besser ein Invalidate(...) auf die Größe des Rahmens zu beschränken. Natürlich würde bei Selektion in Größe des Controls ein komplettes Neuzeichnen verlangen, aber in den meisten Fällen selektiert der User einen kleineren Bereich. (Alternativ kann man für jede Seite des alten und des neuen Rechtecks nur den Bereich .Invalidate(...) machen. Also achtmal insgesamt.)

  1. Setzen der richtigen style-flags mit der SetStyle(...) Methode für das Form und für alle flackernden Controls.* die .DoubleBuffered Eigenschaft des Controls

Das bevorzugte Verfahren zum Aktivieren der Doppelpufferung mit gleichem Ergebnis besteht jedoch darin, die DoubleBuffered-Eigenschaft für das Steuerelement auf true festzulegen.

Soll heißen: bevor man mit SetStyle arbeitet um Flimmern zu vermeiden, soll man die DoubleBuffered-Eigenschaft auf true setzen und schauen, ob das Problem immer noch auftritt.

DoubleBuffered-Eigenschaft ist "protected" und kann daher nur bei eigenen oder abgeleiteten Controls auf "true" gesetzt werden außer bei einer "Form" und da gilt: wenn die "Form" DoubleBuffered ist, Flackern die Controls auch weiterhin, da ausschließlich die Form buffert, nicht aber die Controls. DoubleBuffered muss also auch für alle flackernden Controls gesetzt werden.

  • SetStyle(ControlStyles.DoubleBuffer,true)

Wenn true, wird der Zeichenvorgang in einem Puffer ausgeführt und das Ergebnis nach Beendigung auf dem Bildschirm ausgegeben. Durch Doppelpufferung wird Flimmern durch das Neuzeichnen des Steuerelements verhindert.

Besser gesagt... es verringert das Flimmern. Manchmal reicht es nicht aus.

Hinweis: Dieses Flag ist für die Intellisence ausgeblendet, da es nur noch aus Kompatibilitätsgründen zu .net 1.x enthalten sein muss. Statt diesem Flag sollte man lieber "ControlStyles.OptimizedDoubleBuffer" setzen.

  • SetStyle(ControlStyles.OptimizedDoubleBuffer,true)

Siehe Punkt a.

  • SetStyle(ControlStyles.CacheText,true)

Wenn true, bewahrt das Steuerelement eine Kopie des Textes auf, sodass dieser nicht jedes Mal, wenn er benötigt wird, aus Handle abgerufen werden muss. Dieser Stil hat den Standardwert false. Dieses Verhalten verbessert die Leistung, erschwert jedoch die Textsynchronisierung.

  • SetStyle(ControlStyles.AllPaintingInWmPaint,true)

Wenn true, ignoriert das Steuerelement die WM_ERASEBKGND-Fenstermeldung, um das Flimmern zu verringern. Dieses Format sollte nur angewendet werden, wenn das UserPaint-Bit auf true festgelegt wurde.

  • SetStyle(ControlStyles.UserPaint,true)

Wenn true, zeichnet sich das Steuerelement selbst, sodass es nicht vom Betriebssystem gezeichnet werden muss. Wenn false, wird das Paint-Ereignis nicht ausgelöst. Dieser Stil wird nur auf von Control abgeleitete Klassen angewendet.

Wenn das AllPaintingInWmPaint-Bit auf true festgelegt ist, wird die WM_ERASEBKGND-Fenstermeldung ignoriert, und sowohl die OnPaintBackground-Methode als auch die OnPaint-Methode werden direkt aus der WM_PAINT-Fenstermeldung aufgerufen. Dadurch wird das Flimmern i. d. R. verringert, sofern keine anderen Steuerelemente die WM_ERASEBKGND-Fenstermeldung an das Steuerelement senden. Sie können die WM_ERASEBKGRND-Fenstermeldung senden, um einen pseudotransparenten Effekt, ähnlich wie SupportsTransparentBackColor, zu erzeugen. Dies wird z. B. bei ToolBar in flacher Darstellung verwendet.

Damit die Doppelpufferung vollständig aktiviert wird, können Sie auch die Bits OptimizedDoubleBuffer und AllPaintingInWmPaint auf true festlegen. Das bevorzugte Verfahren zum Aktivieren der Doppelpufferung mit gleichem Ergebnis besteht jedoch darin, die DoubleBuffered-Eigenschaft für das Steuerelement auf true festzulegen.


Zum Fall 2:

Gerade unter Windows Forms ist die Anzahl von Controls, die in einem Form / einer Anwendung ohne Performance-Einbußen und ohne Flackern dargestellt werden kann, stark begrenzt. Unter Umständen sind schon 100 Controls zu viel. Wenn das der Grund für das Flackern ist, hilft es nur, die Anzahl der Controls zu reduzieren. Dazu gibt es viele Möglichkeiten, vom simplen Entschlacken der Form, über das Verwenden von Listen-Controls statt von eigenen Controls für die einzelnen Listeneinträge bis zum Selberzeichnen von Controls und Inhalten (siehe [Tutorial] Zeichnen in Windows-Forms-Programmen (Paint/OnPaint, PictureBox)).

Wenn es nur beim Hinzufügen/Umordnen der Controls flackert: Jedes dieser Steuerelemente hat eine Methode SuspendLayout() und eine ResumeLayout().

Die Layoutlogik des Steuerelement wird unterbrochen, bis die ResumeLayout-Methode aufgerufen wird.

Die SuspendLayout-Methode und die ResumeLayout-Methode werden zusammen verwendet, um mehrere Layout-Ereignisse zu unterdrücken, während mehrere Attribute des Steuerelements angepasst werden. Beispiel: die SuspendLayout-Methode wird aufgerufen, dann werden die Eigenschaften Size, Location, Anchor und/oder Dock des Steuerelements festgelegt und schließlich wird die ResumeLayout-Methode aufgerufen, damit die Änderungen wirksam werden.

Das bedeutet, das man vor dem Befüllen einfach ein SuspendLayout(...) macht und nach dem Befüllen ein ResumeLayout(...). Schon wird das Flackern vermieden.

Selbiges gilt beim Hinzufügen mehrerer Controls zu einem Control:

Wenn Sie mehrere Steuerelemente einem übergeordneten Steuerelement hinzufügen, empfiehlt es sich, vor dem Initialisieren der hinzuzufügenden Steuerelemente die SuspendLayout-Methode aufzurufen. Rufen Sie die ResumeLayout-Methode auf, nachdem die Steuerelemente dem übergeordneten Steuerelement hinzugefügt wurden. Dies erhöht bei vielen Steuerelementen die Leistung der Anwendung.

Manche Controls haben dennoch Spezialitäten:
(dieser teil ist von winSharp93)

Oft findet man Code, der folgendem ähnlich sieht:

for (int i = 0; i < 10000; i++)
    this.listBox1.Items.Add("Item " + i);  

Während der Ausführung (die quälend langsam verläuft) kann man auch hier oft ein unangenehmes Flackern wahrnehmen. Abhilfe schaffen die zusätzlichen Methoden Begin- und EndUpdate von CheckedListBox, ListBox sowie ListView. Folgender Code wird sogar um Faktor sechs (!) schneller ausgeführt - das Flackern ist weg.

this.listBox1.BeginUpdate();
for (int i = 0; i < 10000; i++)
   this.listBox1.Items.Add("Item " + i);
this.listBox1.EndUpdate();  

Noch schneller geht es übrigens, wenn man die Items.AddRange Methode verwendet.
Auch hier ein kurzes Beispiel:

List<string> lst = new List<string>();
for (int i = 0; i < 10000; i++)
   lst.Add(Item " + i);
this.listBox1.Items.AddRange(lst);

An dieser Stelle vielen Dank an Maddy für den Hinweis mit AddRange.

Beginnen wir auch hier mit einem Codebeispiel:

for (int i = 0; i < 500; i++)
   this.textBox1.Text += "Item " + i + Environment.NewLine;  

Auf den ersten Blick scheint der Code keine großen Optimierungsmöglichkeiten zu bieten. Stöbert man etwas in der Doku zur Textbox, stößt man schnell auf die AppendText Methode:

for (int i = 0; i < 500; i++)
   this.textBox1.AppendText("Item " + i + Environment.NewLine);  

Das Ergebnis kann sich schon jetzt sehen lassen - etwa zwei Drittel der ursprünglichen Dauer lassen sich einsparen.
Doch das Optimum ist noch lange nicht erreicht. Die Verwendung eines StringBuilders beschleunigt den Code noch einmal um Faktor 40 (!!!):

StringBuilder sb = new StringBuilder();
for (int i = 0; i < 500; i++)
   sb.Append("Item " + i + Environment.NewLine);
this.textBox1.Text = sb.ToString();  

Vergleicht man nun den ursprünglichen mit dem letzten Code, stellt man fest, dass der letzte Code beinahe 200 mal schneller läuft. Außerdem flackert nichts mehr.

Zu guter letzt noch ein paar Links zum Thema:

	[[Einführung] Zeichnen Optimieren / Schnelles zeichnen  ](http://www.mycsharp.de/wbb2/thread.php?threadid=28527)  
	[GetPixel und SetPixel um Längen geschlagen. 800 mal schneller ](http://www.mycsharp.de/wbb2/thread.php?threadid=29667)  
	[Bitmap-Manipulation (MemBitmap)](http://www.mycsharp.de/wbb2/thread.php?threadid=59354)  

[FAQ] Flackernde Controls vermeiden / Schnelles, flackerfreies Zeichnen
[Tutorial] Zeichnen in Windows-Programmen (Paint/OnPaint, PictureBox)
[Tutorial] Gezeichnete Objekte mit der Maus verschieben
GDI+ & OutOfMemoryException
[Artikel] Performant Strings verketten

Ein gutes Tutorial zum Einstieg in das Zeichnen mit GDI+ und C#:
Professional C# - Graphics with GDI+

Und die GDI+ Bibel:
GDI+ FAQ

Vielen Dank an herbivore und an winSharp93 für die engagierte Unterstützung.

P
82 Beiträge seit 2007
vor 15 Jahren

Hallo,

hab mich gerade bisschen mit Optimierung beschäftigt und, glaube, bin auf einem Fehler bei dir draufgekommen:

Noch schneller geht es übrigens, wenn man die Items.AddRange Methode verwendet.
Auch hier ein kurzes Beispiel:

  
List<string> lst = new List<string>();  
for (int i = 0; i < 10000; i++)  
   lst.Items.Add(Item " + i);  
this.listBox1.Items.AddRange(lst);  

Sollte das net so sein


List<string> lst = new List<string>();
for (int i = 0; i < 10000; i++)
   lst.Add(Item " + i);
this.listBox1.Items.AddRange(lst.ToArray());

? Weil dein Beispiel geht so nicht ^^ (finde kein Item Propertie in einer List und keine möglichkeit AddRange mit einer List standardmäßig zu befüllen)

Lg

Gelöschter Account
vor 15 Jahren

finde kein Item Propertie in einer List

du hast recht, list hat kein "Item" property aber in diesem beispiel wir auch eine "ListBox" verwendet und die hat eine "Item" property.

keine möglichkeit AddRange mit einer List standardmäßig zu befüllen

alle "AddRange" methoden haben eine überladung, die ein IEnumerable<T> als parameter annehmen. dieses interface ist in allen generischen collections (also auch in List<T> implementiert. Daher kann man an die addrange methode ohne weiteres jede iterierbare generische collection übergeben.

P
82 Beiträge seit 2007
vor 15 Jahren

Hallo,

will nicht klguscheissen, aber du verwendest in dem von mir zitierten Beispiel die List<string> lst in der for schleife .. und die hat kein Item prop ..

und bei AddRange(lst) kommt immer:

Argument '1': cannot convert from 'System.Collections.Generic.List<string>' to 'object[]'

AddRange() hat 2 Überladungen: object[] und ListBox.ObjectCollection

lg

Gelöschter Account
vor 15 Jahren

ah ja hab das überlesen. du hast recht, sry. -> korrigiert

5.742 Beiträge seit 2007
vor 15 Jahren

Oha - mein Fehler...
Ich fordere eine Zwischenablage mit C# Syntaxprüfungen 😁