Dienstag, 13. Juli 2010

Double Buffering mit C#

Die Standardklasse für Grafikoperationen bei Windows Forms-Anwendungen Graphics ist für umfangreichere Grafikaufgaben leider schlichtweg zu langsam.
Bei Anwendungen, bei denen viel gezeichnet wird, fängt das Formular an zu flimmern und zu ruckeln, die Anwendung läuft stockend.
Das höchster der Gefühle für die Grafikprogrammierung sind externe Programmbibliotheken wie DirectX oder XNA, soweit möchte ich heute zwar nicht ausholen, dafür euch aber eine andere nützliche Technik vorstellen: Das (Double) Buffering. Bei "normalen" Zeichenmethoden wird jedes zu zeichnende Element einzeln auf das Formular gezeichnet, der Benutzer sieht, wie das Bild sich langsam aufbaut. Dadurch kann es vorkommen, dass das Formular sogar mitten im Prozess neugezeichnet werden muss, der Prozess verlangsamt sich noch mehr. Beim Buffering wird ein sogenannter Buffer verwendet. Dieser speichert die zu zeichnenden Elemente zwischen, statt direkt auf das Formular, werden sie erst einmal auf den Buffer gezeichnet, abschließend wird der komplette Buffer auf das Formular übertragen. So werden alle Elemente quasi als ein Bild gleichzeitig übertragen, der Benutzer sieht direkt das volle Bild und keinen langsamen Aufbau, die Anwendung läuft flüssig. Wird sogar der Buffer nocheinmal gebuffert, spricht man von DoubleBuffering.
Um den Effekt dieser Technik zu demonstrieren, habe ich ein kleines Beispielprogramm geschrieben.
Das Programm zeichnet nach einem Klick auf einen Button viele Kreise und Schriftzüge zufällig auf das Formular, die erste Version benutzt nur Standard Graphics - Methoden:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Drawing;

namespace WindowsFormsApplication1
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        private void Form1_Load(object sender, EventArgs e)
        {
            this.Bounds = Screen.PrimaryScreen.Bounds;
        }

        private void button1_Click(object sender, EventArgs e)
        {
            Graphics G = this.CreateGraphics();
            Random Rnd = new Random();

            for (int i = 0; i < 1000; i++)
            {
                G.DrawEllipse(new System.Drawing.Pen(Color.Red), Rnd.Next(this.Width), Rnd.Next(this.Height), Rnd.Next(100), Rnd.Next(100));
            }

            for (int i = 0; i < 1000; i++)
            {
                G.DrawString("Test test test", new System.Drawing.Font("Arial", 12), new SolidBrush(Color.Green), new PointF(Rnd.Next(this.Width), Rnd.Next(this.Height)));
            }
        }
    }
}

Der Zeichenprozess dauert seine Zeit, man sieht, wie die einzelnen Elemente nacheinander auf das Formular gezeichnet werden.

Eine automatische Möglichkeit, DoubleBuffering zu aktivieren, ist es, dem Formular dementsprechende Styles hinzuzufügen, die folgende Zeile kann z.B. in die Funktion Form1_Load() kopiert werden:
this.SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint | ControlStyles.DoubleBuffer, true);

Sie bewirkt, dass wie oben beschrieben ein Buffer verwendet wird.
Der Zeichenprozess wird schon deutlich beschleunigt.
Wer aber mehr Kontrolle über die verwendeten Buffers etc. haben möchte, muss diese manuell programmieren.
Die letzte Version des Programms verwendet manuell angelegte Buffer Objekte. Die Zeichenelemente werden zuerst in den Buffer gezeichnet und nach Abschluss der Zeichenprozedur geschlossen auf das Formular:

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;

namespace WindowsFormsApplication1
{
    public partial class Form1 : Form
    {
        public Form1()
        {
            InitializeComponent();
        }

        // benötigte Bufferobjekte
        BufferedGraphicsContext currentContext;
        BufferedGraphics myBuffer;

        private void Form1_Load(object sender, EventArgs e)
        {
            this.Bounds = Screen.PrimaryScreen.Bounds;
      
            // Buffer auf Formular initialisieren
            currentContext = BufferedGraphicsManager.Current;
            myBuffer = currentContext.Allocate(this.CreateGraphics(), this.DisplayRectangle);
        }

        private void button1_Click(object sender, EventArgs e)
        {
            Random Rnd = new Random();

            // Elemente zuerst auf den Buffer zeichnen

            for (int i = 0; i < 1000; i++)
            {
                myBuffer.Graphics.DrawEllipse(new System.Drawing.Pen(Color.Red), Rnd.Next(this.Width), Rnd.Next(this.Height), Rnd.Next(100), Rnd.Next(100));
            }

            for (int i = 0; i < 1000; i++)
            {
                myBuffer.Graphics.DrawString("Test test test", new System.Drawing.Font("Arial", 12), new SolidBrush(Color.Green), new PointF(Rnd.Next(this.Width), Rnd.Next(this.Height)));
            }

            // abschließend den kompletten Buffer "rendern", d.h. auf das Formular zeichnen
            myBuffer.Render();
        }
    }
}

Kommentare: