C# WinForms Graphics.DrawString -> verhindern von Flackern

palaber

Captain
Registriert
Juni 2006
Beiträge
3.856
Hi Leute,

ich hab hier eine Aufgabe bei der ich die OnPaint Methode überschreiben soll.
In dieser Methode soll ich das PantEventArgs Objekt verwenden um via
Graphics.DrawString(...) verschiedene Zeichen in verschiedenen Farben zu zeichen.

Soweit funktioniert alles. Da ich aber DrawString() auf eine Matrix mit vielen
Buchstaben (z.B. 10 x 10 Zeichen) anwende, flackert irgendwann die Darstellung.
DrawString() wird in einer geschachtelten Schleife durchlaufen...

Alle Versuche mit "DoubleBuffer = true" haben keine Abhilfe geschafft.

Wenn ich zuerst einen String in der Form der Matrix aufbaue, dann flackert das
Bild nicht. Allerdings kann ich dann die Farben der einzelnen Zeichen nicht beeinflussen.
Oder doch - und ich weiß nicht wie?

Ich dachte an eine Lösung ähnlich zu Graphics.FillRectangle() -> Graphics.DrawRectangle().
Ist so ewas möglich? Habe bisher nichts dazu gefunden.

Hier noch mein Code der OnPaint-Methode:
C#:
protected override void OnPaint(PaintEventArgs e)
{
    var x = 15.0F;
    var y = 15.0F;
    var drawFont = new Font("Arial", 20);
    var drawFormat = new StringFormat()
    {
        Alignment = StringAlignment.Center,
        LineAlignment = StringAlignment.Center
     };

     for (int i = 0; i < this.rows; i++)
     {
         for (int u = 0; u < this.columns; u++)
         {
            var s = playgroundMatrix[u, i].ToString();
            var drawBrush = s == "#" ? new SolidBrush(Color.Black) :
                s == "o" ? new SolidBrush(Color.Blue) : new SolidBrush(Color.Red);

            e.Graphics.DrawString(s, drawFont, drawBrush, x, y, drawFormat);
            x += 30.0F;
           }
        }

        x = 15.0F;
        y += 30.0F;
     }
}

Danke schon mal für eure Hilfe!
 
Kannst du ein Minimalbeispiel bereitstellen?
Auf was zeichnest du denn, also in welcher Klasse wird das OnPaint aufgerufen?
Wo hast du überall DoubleBuffering versucht zu aktivieren und wie hast du es aktiviert?
 
Ich zeichne direkt auf einer Form. Es sind keine weiteren Controls enthalten.
Meine Klasse erbt von Forms...
Ich habe z.B. im Form_Load() folgendes probiert:
C#:
this.DoubleBufferd = true;
this.SetStyle(
    ControlStyles.UserPaint |
    ControlStyles.AllPaintingInWmPaint |
    ControlStyles.OptimizedDoubleBuffer,
    true);

Wie ich auch schon sagte, dass Problem liegt an dem x-Fachen aufruf der DrawString-Methode.
Stelle ich mir in den Schleifen ein String zusammen und führe abschließen einmal Draw.String() aus,
habe ich (so gut wie) kein flackern. Aber eben jedes Zeichen in derselben Farbe...

Setze ich folgendes im Konstruktor meiner Klasse geht es...
C#:
this.SetStyle(
    ControlStyles.UserPaint |
    ControlStyles.AllPaintingInWmPaint |
    ControlStyles.OptimizedDoubleBuffer,
    true);
 
Zuletzt bearbeitet: (Lösung gefunden)
Ich würde einen alternativen Ansatz versuchen, die Strings erstmal in ein Bitmap zu rendern und dieses Ergebniss dann am Ende auf den Bildschirm zeichnen.
 
Oma Erna schrieb:
Ich würde einen alternativen Ansatz versuchen, die Strings erstmal in ein Bitmap zu rendern und dieses Ergebniss dann am Ende auf den Bildschirm zeichnen.
Ja genau, am Anfang der Klasse:
Code:
Bitmap _backBuffer;
In der OnPaint()
Code:
        protected override void OnPaint(PaintEventArgs e) {
            if (_backBuffer == null) {
                _backBuffer = new Bitmap(this.ClientSize.Width, this.ClientSize.Height);
                Graphics.FromImage(_backBuffer).Clear(Color.Black);
             }

            Graphics g = Graphics.FromImage(_backBuffer);

            // ... hier normale DrawString etc

            // Am Ende den Backbuffer zeichnen
            e.Graphics.DrawImageUnscaled(_backBuffer, 0, 0);
        }

Have phun. :)
 
@lynxx
Danke für den Ansatz mit dem Bitmap, auch mir hat es geholfen das Flackern zu beseitigen.
Allerdings steigt bei mir mit jeder Bewegung der Speicherbedarf an. So dass ich schnell mehrere hundert MegaByte belege. Ich hatte versucht ein Dispose nach dem Zeichnen zu machen, aber er scheint nach dem Entladen nicht direkt aufzuräumen.

Hat noch Jemand eine Idee den Speicher klein zu halten?

P.S.:
Ich habe den Code in einer eigenen DrawField-Methode, da ich die OnPaint nicht getriggert bekommen habe. Nach einem Key-Event mache ich dann Refresh(), wodurch das DrawField wieder ausgeführt wird.
 
Vermutlich verwendest du noch mehr was Dispose bedürftigt ist. Graphics z.B. mag auch ein Dispose haben. (Auch bei Brush/Pen/etc. ggfls. darauf achten)
 
Oma Erna schrieb:
Vermutlich verwendest du noch mehr was Dispose bedürftigt ist. Graphics z.B. mag auch ein Dispose haben. (Auch bei Brush/Pen/etc. ggfls. darauf achten)
Ich mache auf alles was da ist ein Dispose.
_backBuffer,drawFormat,formGraphics

Fonts und Brushes instanziere ich an anderer Stelle in der Klasse und weise beim Zeichnen nur andere Werte zu (also ändern der Farbe z.B.). Dass es an denen liegt, kann ich aber ausschließen. Kommentiere ich das Bitmap aus und erstelle die "Grafik" live bleibt der Speicher wo er ist, aber dann flackert es wieder.
Also der Speicher wächst definitiv wegen den Bitmaps bzw. obigen Code.
 
Ist ohne Code schwer zu beurteilen. Du kannst den Garage Collector manuell anstupsen oder Du erstellt das Zwischenbitmap nicht bei jedem zeichnen neu sondern löschst einfach den vorherigen Inhalt. Ggfls. musst du prüfen ob sich die Größe der Zeichenfläche geändert hat und dann natürlich das Bitmap neu erstellen.
 
@Oma Erna,

ich habe genau den Code aus Posting #5 von @lynxx implementiert. Aber hier nochmal als Abriss:

C#:
 protected override void OnPaint(PaintEventArgs e)
        {
            Bitmap _backBuffer = null;
            this.DoubleBuffered = true;
            if (_backBuffer == null)
            {
                _backBuffer = new Bitmap(this.ClientSize.Width, this.ClientSize.Height);
                Graphics.FromImage(_backBuffer);
            }

            Graphics formGraphics = Graphics.FromImage(_backBuffer);
             
                 //Schleifen mit DrawString je Druchlauf:  (formGraphics.DrawString(...);)
         
            e.Graphics.DrawImageUnscaled(_backBuffer, 0, 0);

            e.Dispose();
            _backBuffer.Dispose();
            drawFormat.Dispose();
            formGraphics.Dispose();
          }

Das ist der momentane Versuch.
Mache ich es dagegen so, flackert es aber der Speicher bleibt bei wenigen MB.

C#:
 protected override void OnPaint(PaintEventArgs e)

            Graphics formGraphics = createGraphics();
             
                 //Schleifen mit DrawString je Druchlauf:  (formGraphics.DrawString(...);)

         
            drawFormat.Dispose();
            formGraphics.Dispose();
          }

P.S.:
Wenn ich Bitmap _backBuffer aus der Methode vor in die Klasse ziehe, dann bekomme ich beim zweiten Durchlauf eine Exeption auf Graphics formGraphics = Graphics.FormImage(_backBuffer);.

1.PNG


2.PNG
 
Zuletzt bearbeitet:
Im ersten Snippet scheint mir Zeile 8 das Problem zu sein. Der Code wird immer ausgeführt, das If Statement in Zeile 5 ist imho unnötig. In Zeile 8 ziehst du eine weitere nutzlose Graphics Instanz, die du nirgends speicherst, verwendest und natürlich auch nicht mehr freigibst.
 
  • Gefällt mir
Reaktionen: steppi
Oma Erna schrieb:
Im ersten Snippet scheint mir Zeile 8 das Problem zu sein. Der Code wird immer ausgeführt, das If Statement in Zeile 5 ist imho unnötig. In Zeile 8 ziehst du eine weitere nutzlose Graphics Instanz, die du nirgends speicherst, verwendest und natürlich auch nicht mehr freigibst.
Du hast natürlich recht, ich hatte oben von Posting #5 den Code so übernommen.
Da er mir die Exception geliefert hatte, hatte ich das Bitmap dann mit NULL initialisiert, was den If-Block obsolet gemacht hat. Das in der IF nochmal eine sinnlose Grafik drin steckt, habe ich aber glatt übersehen bzw. ist es in Posting #5 ja eine Clear-Anweisung, die ich bei mir nicht brauchte und leider nur teilweise entfernt habe.

Vielen Dank für deine Mühe und Hilfe.
 
Das
Code:
Bitmap _backBuffer;
muss wie ich oben schon schrieb an den Anfang der Klasse, nicht in die OnPaint(), die .Dispose() können alle weg da die Objekte ja immer wieder verwendet werden.
 
lynxx schrieb:
Das
Code:
Bitmap _backBuffer;
muss wie ich oben schon schrieb an den Anfang der Klasse, nicht in die OnPaint(), die .Dispose() können alle weg da die Objekte ja immer wieder verwendet werden.
Ich musste jetzt nochmal meine "Entwicklung" dieser Methode nachvollziehen.

Im ersten Schritt hatte ich den Code so wie von dir gezeigt, also den _backBuffer in der Klasse und nicht in der Methode. In diesem Fall zieht aber meine Spielfigur eine Spur, also die Bilder werden einfach überlagert und nicht ausgestauscht. Also hatte ich die Dispose() aufgenommen. Damit hatte ich dann aber Exceptions und die konnte ich nur auflösen, indem ich die Initialisierung in die OnPaint genommen habe.
Bei mir ist jetzt quasi das IF-Statement komplett entfernt. Die Bitmap wird zu Beginn der OnPaint neu instanziert, dann wird alles gezeichnet und am Ende mache ich das Dispose auf die Bitmap und die formGraphics. Letzteres hat den Grund, dass sonst der Arbeitsspeicher ins unermessliche anwächst mit jeder Bewegung.

Würde ich jetzt tatsächlich zeichnen z.B. ein Diagramm, eine Linie oder irgendwas anderes fortlaufendes wäre deine Implementierung ideal gewesen. Hier müssen die Sachen vom "Bild" davor aber auch wieder verschwinden/ sich verändern.
 
Zuletzt bearbeitet:
Der Sinn dahinter ist ja eben das dass Hintergrund-System möglichst wenig machen muss, erzeugt man immer wieder neue Instanzen wird der Speicherverbrauch ansteigen auch wenn man fleissig die Objekte.Dispose() nutzt, das kommt einfach daher das der Garbage-Kollektor diese Objekte wirklich nur entfernt wenn das Ram knapp wird - in dem moment wird die Animation ruckelt, weil dann gleich hunderte Objekte gleichzeitig zerstört/freigeben werden. Das "überlagern" bzw das dass Image so bleibt wie es war ist gewollt, das System soll mit dem Image nichts machen ausser es zu zeichnen wenn alle formGraphics-Aufrufe durch sind.
Man muss dann natürlich bevor man seine "normalen" Aufrufe macht entweder die Bitmap mit einer einzelnen Farbe füllen (formGraphics.Clear(Color.Black)) oder z.B ein Hintergrund-Bild in die Bitmap zeichnen - das kann man unter Umständen auch noch optimieren wenn z.B die animierte Figur nur im unteren Drittel animiert wird (noch besser natürlich wenn man sich in Variablen merkt von welcher X/Y inkl. Breite und Höhe sich auf dem Bild etwas geändert hat), dann muss man auch nur da die Hintergrund-Grafik neuzeichnen oder diesen Bereich mit einem Gefüllten Rechteck der Hintergrund-Farbe überzeichnen.
Ich hoffe das war jetzt ausführlich genug erklärt. :p
 
  • Gefällt mir
Reaktionen: steppi
Danke, dass du dir nochmal Zeit für so eine ausführliche Antwort genommen hast. Das Bild vorher nochmal mit einem neutralen Hintergrund zu überzeichnen ist natürlich eine super Idee, das werde ich nochmal ausprobieren. Theoretisch habe ich die Koordinaten im Sinne der Anfangspunkte. Steht die Figur z.B. auf (5, 9) ist das in der Zeichenfläche bei 5*28.0F und 9*28.0F. (28.0F ist meine Schrittweite zwischen zwei Zeichen.)
Ich werde das trotzdem nochmal durchdenken.

Speicherverbrauch liegt übrigens bei 16MB zu Beginn und bei einem 13x19 Spielfeld lande ich wenn alle Items gesammelt sind bei ~20MB, danach steigt der Speicher nicht mehr an, egal wieviel ich die Figuar noch bewege oder eben so minimal, dass ich es nicht beobachten konnte.
 
Zuletzt bearbeitet:
Zurück
Oben