Stellt eine paint()-Methode (und damit paintComponent()) komplexe Zeichnungen dar, die zum Beispiel aus hunderten kleiner Linien und Bögen besteht, so braucht das seine Zeit. Zeichnen wir in paint() immer alles neu, so haben wir große Zeitverzögerungen, wenn sich etwa die Größe des Zeichenbereichs ändert und der Repaint-Manager über ein repaint() wieder zum paint() führt und alles wieder neu gezeichnet wird, ohne dass sich wirklich etwas an der Grafik geändert hat.
Eine einfache und elegante Methode, diesem Problem zu entkommen, ist die Technik der Doppelpufferung (engl. double-buffering). Eine zweite Zeichenebene, so groß wie das Original, wird angelegt und alle Grafikoperationen finden auf diesem Hintergrundbild statt. Immer wenn das zu zeichnende Bild komplett ist, kopieren wir es zur passenden Zeit in den sichtbaren Bereich. Kommt ein Repaint-Ereignis, und hat sich die Grafik bis dahin nicht aktualisiert, so muss nur der entsprechende Teil der Hintergrundgrafik neu gezeichnet werden.
Um ein Programm auf die neue Technik umzustellen, muss zuerst die paint()-Methode umgebaut werden, die direkt die Zeichenbefehle erteilt. Nehmen wir folgende Implementierung an:
private void bigPaint( Graphics g ) { Random r = new Random(); for ( int i = 0; i < 1000; i++ ) { g.drawOval( r.nextInt(getWidth()-100), r.nextInt(getHeight()-100), 100, 100 ); g.setColor( new Color(r.nextInt(255), r.nextInt(255), r.nextInt(255), r.nextInt(255)) ); } } @Override protected void paintComponent( Graphics g ) { bigPaint( g ); }
Um das Programm schon etwas zu vereinfachen, enthält paintComponent() nun schon keine direkten Zeichenbefehle mehr, sondern delegiert an das eigene bigPaint(). Im nächsten Schritt müssen wir das Programm so umbauen, dass es auf das Graphics-Objekt unseres Hintergrundbildes geht. Dazu ist zuerst ein Hintergrundbild nötig. Eine Variante ist, die den Hintergrundpuffer in paintComponent() aufzubauen, denn dann gibt es Zugriff auf die Höhen und Breiten, die sich verändert haben können – natürlich kann auch die Fläche immer gleich groß bleiben.
import java.awt.*; import java.awt.image.BufferedImage; import java.util.Random; import javax.swing.JFrame; import javax.swing.JPanel; public class DoubleBuffering extends JPanel { private void bigPaint( Graphics g ) { g.setColor( Color.WHITE ); g.fillRect( 0, 0, getWidth(), getHeight() ); Random r = new Random(); for ( int i = 0; i < 1000; i++ ) { g.drawOval( r.nextInt(getWidth()-100), r.nextInt(getHeight()-100), 100, 100 ); g.setColor( new Color(r.nextInt(255), r.nextInt(255), r.nextInt(255), r.nextInt(255)) ); } } private final GraphicsConfiguration gfxConf = GraphicsEnvironment .getLocalGraphicsEnvironment().getDefaultScreenDevice().getDefaultConfiguration(); private BufferedImage offImg; @Override protected void paintComponent( Graphics g ) { if ( offImg == null || offImg.getWidth() != getWidth() || offImg.getHeight() != getHeight() ) { offImg = gfxConf.createCompatibleImage( getWidth(), getHeight() ); bigPaint( offImg.createGraphics() ); } g.drawImage( offImg, 0, 0, this ); // bigPaint( g ); } public static void main( String[] args ) { JFrame f = new JFrame(); f.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE ); f.setSize( 800, 600 ); f.add( new DoubleBuffering() ); f.setVisible( true ); } }
Hinweis: Oft fällt ein Flackern bei Grafikoperationen auf. Das Problem ist, dass die Zeichenoperationen so lange dauern, dass die gesamte Zeichnung nicht im Zyklus einer Bildschirmwiederholfrequenz auf den Schirm kommt. Während Teile gezeichnet werden, sendet die Grafikkarte die Teilbilder zum Display und bei jedem Update sehen wir einen aktualisierten Teil unserer Grafik. Bei aufwändigen Zeichenoperationen sind nun einmal viele Durchläufe nötig, bis das Bild komplett ist.
Klingt schlüssig und ergibt Sinn, wenn man es durchdenkt, allerdings ist das Beispiel nicht so sehr geeignet:
Die Variante ohne Double-Buffering wirkt nicht nur viel angenehmer (geringere Zeitverzögerung!), sondern die CPU-Last ist bei mir auch noch um ca. 15% geringer.
Woran kann das liegen? Ist die Berechnung zu simpel?
Auf jeden Fall muss da schon etwas mehr zu tun sein, sonst kann man sich das sparen, ja.
Man sollte noch erwähnen, dass man manche Einstellungen vom originalen Graphics2D Objekt für das neue des BufferedImages übernehmen sollte. Font, BackgroundColor, Color, RenderingHints, evtl. Stroke, Transform usw.
Interessant wäre noch einen Absatz bezüglich der Double Buffering Property von Swing Komponenten.