Warum ist offscreen (mit Image) schneller?

ernst

Top Contributor
Hallo allerseits,
Wenn man zuerst seine Zeichnung (z.B. eine Menge von Geraden) in einen Offscreen-Speicher schreibt (z.B. ein Image) und dann danach alles auf dem Bildschirm, dann soll das schneller sein, als wenn man jede einzelne Gerade hintereinander auf dem Bildschirm ausgibt.
Warum ist das so?

mfg
Ernst
 
... dann soll das schneller sein ...
Warum ist das so?

Wer hat gesagt, dass das schneller sein soll? Und warum fragst du dann, ob das so IST? 😀

Ich gehe davon aus, dass es um diese beiden Alternativen geht: Entweder direkt in der paintComponent zeichnen...
Java:
class Painter extends JPanel
{
    public void paintComponent(Graphics g)
    {
        super.paintComponent(g);
        drawOneMillionLinesOn(g);
    }
}
Oder ungefähr so in ein Bild:
Java:
class Painter extends JPanel
{
    private BufferedImage image = ...

    public Painter()
    {
        Graphics g = image.createGraphics();
        drawOneMillionLinesOn(g);
        g.dispose();
    }

    public void paintComponent(Graphics g)
    {
        super.paintComponent(g);
        g.drawImage(image,0,0,this);
    }
}

Dann kann es gut sein, dass einem das zweite "schneller vorkommt". Wenn im ersten Fall das Panel neu gezeichnet werden muss (z.B. weil das Fenster kurz verdeckt war und jetzt wieder sichtbar ist), dann müssen dort die 1 Million Linien komplett neu gezeichnet werden. Im zweiten Fall würde einfach das fertige Bild mit den 1 Million Linien gezeichnet, und das wäre deutlich schneller.

Auch wenn man z.B. eine Animation hat, bei der sich nur im Vordergrund etwas verändert, und im Hintergrund 1 Million Linien sind, die immer gleich bleiben, wäre es schneller, den Hintergrund in einem Image zu speichern, und in der paintComponent dann nur den "vorberechneten" Hintergrund als Bild zu malen, und darüber dann den sich bewegenden Teil.

Wenn aber ALLES bei jedem neuzeichnen neu in ein Image gemalt werden muss, ist das sicher langsamer. Also... ganz lapidar gesagt: Sowas hier ist Unfug
Java:
class Painter extends JPanel
{
    private BufferedImage image = ...

    public void paintComponent(Graphics g)
    {
        super.paintComponent(g);

        // Unsinnig: Bei jedem neuzeichnen alles ins Bild malen..
        Graphics gBild = image.createGraphics();
        drawOneMillionLinesOn(gBild);
        gBild.dispose();

        // ... und dann erst das Bild "wirklich" malen
        g.drawImage(image,0,0,this);
    }
}
 
Danke für deinen wertvollen Beitrag.
Ich habe selbst etwas experimentiert (bei mir ist bildNormalparabel ein offset-screen).
Doch an einer Stelle klappt es noch nicht (Quelltext siehe unten. Wie üblich habe ich einen Teil mit dem Visual-Editoor von Netbeans gemacht).

Kurzbeschreibung meines Programms :
Jedes Mal, wenn die Maus bewegt wird, wird die Methode maleAlles() aufgerufen.
Diese erzugt ein Bild (bild) des Panels. Dann wird von diesem Bild die
Leinwand (=Grafikkontext) graphics erzeugt.
maleAlles() malt dann genau ein Mal mit erzeugeNormalparabelOffscreenEinmal()
die Parabel in den Offscreen-Image (bildNormalparabel).
Danach wird durch repaint() die Methode paintComponent() aufgerufen.
Diese bringt als Parameter die Leinwand mit, die der Anzeigefläche des
Bildschirms entsricht.
In paintComponent() wird durch die Methode zeichneNormalparabel()
die Normalparabel auf die Leinwand der Anzeigefläche des
Bildschirms gezeichnet. Theoretisch müsste m.M.nach alles funktionieren.
Zur Sicherheit habe ich sogar bildNormalparabel und graphicsNormalparabel als Attribute genommen
(nicht als lokale Variablen. Ist das nötig?).

Leider stimmt das nicht:
Nach der Veränderung der Fenstergröße erscheint keine Parabel mehr.
Wo ist mein Denkfehler??

mfg
Ernst


Java:
package pack1;

public class ZeichnungJFrame extends javax.swing.JFrame {
  private ZeichnungJPanel spielJPanel;
    /** Creates new form ZeichnungJFrame */
    public ZeichnungJFrame() {
        initComponents();
        this.setSize(400, 400);
        spielJPanel = new ZeichnungJPanel();
        jPanel1.add(spielJPanel, "Center");

    }

    /** This method is called from within the constructor to
     * initialize the form.
     * WARNING: Do NOT modify this code. The content of this method is
     * always regenerated by the Form Editor.
     */
    @SuppressWarnings("unchecked")
    // <editor-fold defaultstate="collapsed" desc="Generated Code">                          
    private void initComponents() {

        jPanel1 = new javax.swing.JPanel();

        setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE);

        jPanel1.setBackground(new java.awt.Color(255, 255, 204));
        jPanel1.setBorder(javax.swing.BorderFactory.createLineBorder(new java.awt.Color(0, 0, 0)));
        jPanel1.setLayout(new java.awt.BorderLayout());

        javax.swing.GroupLayout layout = new javax.swing.GroupLayout(getContentPane());
        getContentPane().setLayout(layout);
        layout.setHorizontalGroup(
            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGroup(layout.createSequentialGroup()
                .addContainerGap()
                .addComponent(jPanel1, javax.swing.GroupLayout.DEFAULT_SIZE, 380, Short.MAX_VALUE)
                .addContainerGap())
        );
        layout.setVerticalGroup(
            layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
            .addGroup(layout.createSequentialGroup()
                .addContainerGap()
                .addComponent(jPanel1, javax.swing.GroupLayout.DEFAULT_SIZE, 278, Short.MAX_VALUE)
                .addContainerGap())
        );

        pack();
    }// </editor-fold>                        

    /**
    * @param args the command line arguments
    */
    public static void main(String args[]) {
        java.awt.EventQueue.invokeLater(new Runnable() {
            public void run() {
                ZeichnungJFrame zjf = new ZeichnungJFrame();
                zjf.setVisible(true);
                zjf.spielJPanel.maleAlles();
            }
        });
    }

    // Variables declaration - do not modify                     
    private javax.swing.JPanel jPanel1;
    // End of variables declaration                   

}


Java:
package pack1;

import java.awt.Color;
import java.awt.Graphics;
import java.awt.Image;
import java.awt.Point;
import java.awt.event.MouseEvent;

public class ZeichnungJPanel extends javax.swing.JPanel {
    private Graphics graphics=null;
    private Image bild;
    private Graphics graphicsNormalparabel;
    private Image bildNormalparabel;
    private int statusNormalparabel=0;

    public ZeichnungJPanel() {
        initComponents();
    }

    public void paintComponent(Graphics g) {
        super.paintComponent(g);
        zeichneNormalparabel(g, Color.black);
    }

    public void erzeugeNormalparabelOffscreenEinmal(Color farbe){
        int sx, sy;
        int x;

        sx = getSize().width;
        sy = getSize().height;

        if(bildNormalparabel==null || bildNormalparabel.getWidth(null)!=sx || bildNormalparabel.getHeight(null)!=sy){
            if(sx!=0 && sy!=0){
                bildNormalparabel = createImage(sx, sy);
            }
        }

        if(sx!=0 && sy!=0 && statusNormalparabel == 0){
            graphicsNormalparabel = bildNormalparabel.getGraphics();
            graphicsNormalparabel.setColor(getBackground());
            graphicsNormalparabel.fillRect(0,0,sx,sy);
            graphicsNormalparabel.setColor(farbe);
            for(x=1; x<200; x++){
                graphicsNormalparabel.drawLine(x, x*x, x+1, (x+1)*(x+1));
            }
        }
        statusNormalparabel=1;
    }

    public void zeichneNormalparabel(Graphics g, Color farbe){
        g.drawImage(bildNormalparabel,0,0,null);
    }

    public void maleAlles() {
        int sx, sy;
        Graphics graphics=null;

        sx = this.getSize().width;
        sy = this.getSize().height;

        if(bild==null || bild.getWidth(this)!=sx || bild.getHeight(this)!=sy){
            if(sx!=0 && sy!=0){
                bild = createImage(sx, sy);
            }
        }

        if(sx!=0 && sy!=0){
            graphics = bild.getGraphics();
            graphics.setColor(getBackground());
            graphics.fillRect(0,0,sx,sy);
            // wird genau ein Mal gemacht.
            erzeugeNormalparabelOffscreenEinmal(Color.black);
        }
        repaint();
    }

    /** This method is called from within the constructor to
     * initialize the form.
     * WARNING: Do NOT modify this code. The content of this method is
     * always regenerated by the Form Editor.
     */
    @SuppressWarnings("unchecked")
    // <editor-fold defaultstate="collapsed" desc="Generated Code">
    private void initComponents() {

        setBackground(new java.awt.Color(204, 255, 204));
        setBorder(javax.swing.BorderFactory.createLineBorder(new java.awt.Color(0, 0, 0)));
        setPreferredSize(new java.awt.Dimension(200, 200));
        addMouseMotionListener(new java.awt.event.MouseMotionAdapter() {
            public void mouseDragged(java.awt.event.MouseEvent evt) {
                ZeichnungJPanel.this.mouseDragged(evt);
            }
        });
        setLayout(new java.awt.BorderLayout());
    }// </editor-fold>

    private void mouseDragged(java.awt.event.MouseEvent evt) {
        // TODO add your handling code here:
        maleAlles();
    }


    // Variables declaration - do not modify
    // End of variables declaration

}
 
OK, wenn ich das richtig sehe ist das dann der Fall den ich so beschrieben hatte:

Auch wenn man z.B. eine Animation hat, bei der sich nur im Vordergrund etwas verändert, und im Hintergrund 1 Million Linien sind, die immer gleich bleiben, wäre es schneller, den Hintergrund in einem Image zu speichern, und in der paintComponent dann nur den "vorberechneten" Hintergrund als Bild zu malen, und darüber dann den sich bewegenden Teil.

Dabei geht es quasi um eine "Animation" (d.h. Veränderung des "Vordergrunds") der durch mouseDragged ausgelöst werden soll. Der Hintergrund (die Parabel) soll immer gleich bleiben.

In diesem Fall lag das Problem wohl hauptsächlich an diesem 'statusNormalParabel' - der wurde EINmal auf 1 gesetzt, aber danach nie wieder auf 0. Das wäre ggf. schon behoben, wenn man das so machen würde:
Code:
        if(bildNormalparabel==null || bildNormalparabel.getWidth(null)!=sx || bildNormalparabel.getHeight(null)!=sy){
            if(sx!=0 && sy!=0){
                bildNormalparabel = createImage(sx, sy);
                [b]statusNormalparabel=0; // status=0 bedeutet: Muss neu gezeichnet werden[/b]
                System.out.println("BildNormal "+sx+" "+sy);
            }
        }

        if(sx!=0 && sy!=0 && statusNormalparabel == 0){
            graphicsNormalparabel = bildNormalparabel.getGraphics();
            graphicsNormalparabel.setColor(getBackground());
            graphicsNormalparabel.fillRect(0,0,sx,sy);
            graphicsNormalparabel.setColor(farbe);
            System.out.println("Male normal...");
            [b]statusNormalparabel=1; // status=1 bedeutet: Ist auf dem aktuellen stand[/b]
            for(x=1; x<200; x++){
                graphicsNormalparabel.drawLine(x, x*x, x+1, (x+1)*(x+1));
            }
        }

Allerdings ist dieser Ablauf ein bißchen ... unsinnig. Der Status wird ja dort nur lokal verwendet, d.h. man könnte den zweiten Teil auch einfach in eine Methode packen, und diese Methode dann dort, wo [c]statusNormalparabel=0;[/c] steht, einfach aufrufen...


Ich hab' das, was dort abläuft hier nochmal etwas aufgeräumt und anders strukturiert zusammengefasst. Allerdings ohne Netbeans-Editor, sondern von Hand...

Java:
import java.awt.*;
import java.awt.event.*;
import java.awt.image.BufferedImage;

import javax.swing.*;

public class ZeichnungJFrame2 extends JFrame
{
    public static void main(String args[])
    {
        SwingUtilities.invokeLater(new Runnable()
        {
            public void run()
            {
                ZeichnungJFrame2 zjf = new ZeichnungJFrame2();
                zjf.pack();
                zjf.setVisible(true);
            }
        });
    }
    
    
    private ZeichnungJPanel spielJPanel;

    public ZeichnungJFrame2()
    {
        setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);

        JPanel centerPanel = new JPanel();
        centerPanel.setBackground(new Color(255, 255, 204));
        centerPanel.setBorder(
            BorderFactory.createCompoundBorder(
                BorderFactory.createEmptyBorder(10,10,10,10),
                BorderFactory.createLineBorder(new Color(0, 0, 0))));
        centerPanel.setLayout(new BorderLayout());
        
        spielJPanel = new ZeichnungJPanel();
        centerPanel.add(spielJPanel);
        
        getContentPane().add(centerPanel);
    }

}



class ZeichnungJPanel extends javax.swing.JPanel
{
    private BufferedImage bildKonstanterTeil;
    private Point etwasVeränderliches = new Point();
    
    public ZeichnungJPanel()
    {
        setBackground(new Color(204, 255, 204));
        setBorder(BorderFactory.createLineBorder(new Color(0, 0, 0)));
        setPreferredSize(new Dimension(200, 200));
        addMouseMotionListener(new MouseMotionAdapter()
        {
            public void mouseDragged(MouseEvent evt)
            {
                mouseUpdate(evt);
            }
            public void mouseMoved(MouseEvent evt)
            {
                mouseUpdate(evt);
            }
        });
        setLayout(new BorderLayout());
    }

    public void paintComponent(Graphics g)
    {
        super.paintComponent(g);
        
        System.out.println("Zeichne...");
        
        zeichneKonstantenTeil(g);
        zeichneVeränderlichenTeil(g);
    }


    public void zeichneKonstantenTeil(Graphics g)
    {
        aktualisiereKonstantenTeilFallsNotwendig();
        g.drawImage(bildKonstanterTeil, 0, 0, null);
    }

    public void aktualisiereKonstantenTeilFallsNotwendig()
    {
        int sx = getWidth();
        int sy = getHeight();

        if (bildKonstanterTeil == null ||
            bildKonstanterTeil.getWidth() != sx ||
            bildKonstanterTeil.getHeight() != sy)
        {
            bildKonstanterTeil = new BufferedImage(
                sx, sy, BufferedImage.TYPE_INT_ARGB);

            System.out.println("Größe hat sich geändert");
            System.out.println("Neue Größe: "+getSize());
            System.out.println("aktualisiereKonstantenTeil!");
            
            Graphics g = bildKonstanterTeil.createGraphics();
            aktualisiereKonstantenTeil(g, Color.black);
            g.dispose();
        }
    }
    
    private void aktualisiereKonstantenTeil(Graphics g, Color farbe)
    {
        int sx = getWidth();
        int sy = getHeight();

        g.setColor(getBackground());
        g.fillRect(0, 0, sx, sy);
        g.setColor(farbe);
        for (int x = 1; x < 200; x++)
        {
            g.drawLine(x, x * x, x + 1, (x + 1) * (x + 1));
        }
    }
    
    
    public void zeichneVeränderlichenTeil(Graphics g)
    {
        int x = etwasVeränderliches.x;
        int y = etwasVeränderliches.y;
        g.drawLine(x-10, y-10, x+10, y+10);
        g.drawLine(x+10, y-10, x-10, y+10);
    }

    private void mouseUpdate(MouseEvent evt)
    {
        etwasVeränderliches = evt.getPoint();
        repaint();
    }


}


Man könnte sich überlegen, das noch ausgefeilter zu machen: Z.B. mehrere Ebenen anzubieten, bei denen man auf Wunsch jede einzelne neu zeichnen kann (so dass es mehrere konstante Teile gibt) aber... da müßte man sich erstmal genauer Gedanken über die Struktur machen...
 
Wer hat gesagt, dass das schneller sein soll? Und warum fragst du dann, ob das so IST? 😀

Dann kann es gut sein, dass einem das zweite "schneller vorkommt". Wenn im ersten Fall das Panel neu gezeichnet werden muss (z.B. weil das Fenster kurz verdeckt war und jetzt wieder sichtbar ist), dann müssen dort die 1 Million Linien komplett neu gezeichnet werden. Im zweiten Fall würde einfach das fertige Bild mit den 1 Million Linien gezeichnet, und das wäre deutlich schneller.
1)
Das eine Mal muss eine Million mal eine drawLine Anweisung gemacht werden.
Wenn dies 10 Mal gemacht werden muss, braucht man 10 Mal diese Zeit.

2)
Das andere Mal wurden diese eine Million Anweisungen schon ausgeführt (und damit in ein Offset-Scree geschrieben).
Das wird nur _genau ein Mal_ gemacht und ist deswegen vernachlässigbar.
Jetzt müssen diese eine Million Linien nur noch in den Bildschirmspeicher kopiert werden.
(Wenn dies 10 Mal gemacht werden muss, braucht man 10 Mal diese Zeit.)
D.h. es muss ein Teil des Arbeitsspeichers in den Bildschirmspeicher kopiert werden.
(vermutlich auf Assemblerebene mit mov). Das geht vermutlich schneller.
Habe ich dich damit richtig verstanden?

mfg
Ernst
 
Ja, zugegeben, als ich mir diesen Abschnitt dann ein zweites mal druchgelesen hatte, fand ich auch, dass er etwas wiedersprüchlich oder unklar klang. Ich hätte das wohl dahingehend präzisieren (oder betonen) sollen, dass das eigentliche Zeichnen natürlich nicht schneller geht. Wie dann im letzten Codeschnipsel der ersten Antwort angedeutet:
10 Millionen Linien direkt zeichnen
ist nicht langsamer (sondern theoretisch sogar schneller) als
10 Millionen Linien in ein Bild packen und dann das Bild zeichnen.

Die Aussage, dass es einem "schneller vorkommt" bzw. auch tatsächlich "deutlich schneller" sein kann, wenn man so etwas in ein Bild zeichnet, bezog sich nicht auf den ersten Satz des Beitrags, sondern wirklich nur auf den nachfolgend beschriebenen Fall, dass man z.B. im Hintergrund 10 Millionen Linien hat, die immer gleich bleiben, und darüber 1 Linie malt, die sich bewegt. Dann würde es sich lohnen, das ganze in ein Bild zu zeichnen - genau aus dem Grund, den du beschrieben hast: Für diesen Teil muss dann nichts mehr berechnet werden, sondern es wird einfach "der Speicher vom Bild auf den Bildschirm kopiert".

Sorry falls die zitierten Teile oben für Verwirrung gesorgt haben 😳
 
public void aktualisiereKonstantenTeilFallsNotwendig()
{
int sx = getWidth();
int sy = getHeight();

if (bildKonstanterTeil == null ||
bildKonstanterTeil.getWidth() != sx ||
bildKonstanterTeil.getHeight() != sy)
{
bildKonstanterTeil = new BufferedImage(
sx, sy, BufferedImage.TYPE_INT_ARGB);
...
Wäre es nicht besser, noch das folgende hinzuzufügen?
Was meinst du?
Java:
    public void aktualisiereKonstantenTeilFallsNotwendig()
    {
        int sx = getWidth();
        int sy = getHeight();
 
        if (bildKonstanterTeil == null ||
            bildKonstanterTeil.getWidth() != sx ||
            bildKonstanterTeil.getHeight() != sy)
        {
           if(sx!=0 && sy!=)){                        // diesen Teil hinzufügen
            bildKonstanterTeil = new BufferedImage(
                sx, sy, BufferedImage.TYPE_INT_ARGB);

mfg
Ernst
 
Jein ... an sich bringt das IMHO nicht wirklich was. Der Fall, dass der Zeichenbereich 0x0 Pixel groß ist, tritt in der Praxis fast nicht auf - außer wenn man das Fenster so klein macht, dass man ohnehin nichts mehr sieht, und dann ist's eigentlich wurscht, ob er da nun so ein Bild erstellt oder nicht.
In diesem Fall würde es zwar "etwas bringen", wegen der Schleife die bei dir IMMER bis 200 läuft, aber in einem richtigen Anwendungsfall würde man das ja vermutlich nicht machen...
 
Jein ... an sich bringt das IMHO nicht wirklich was. Der Fall, dass der Zeichenbereich 0x0 Pixel groß ist, tritt in der Praxis fast nicht auf - außer wenn man das Fenster so klein macht, dass man ohnehin nichts mehr sieht, und dann ist's eigentlich wurscht, ob er da nun so ein Bild erstellt oder nicht.
In diesem Fall würde es zwar "etwas bringen", wegen der Schleife die bei dir IMMER bis 200 läuft, aber in einem richtigen Anwendungsfall würde man das ja vermutlich nicht machen...
Warum bringt es in meinen Fall etwas bzw. was hat der Pixelbereich 0x0 mit der Schleife bis 200 zu tun?

mfg
Ernst
 
In deinem Fall würde es nur "etwas bringen", weil die Berechnung der Funktion ja aich von dieser If-Abfrage abhängt. D.h. wenn sx==0 && sy==0 ist, dann wird der Teil mit
Code:
for (int x = 1; x < 200; x++)
{
     g.drawLine(x, x * x, x + 1, (x + 1) * (x + 1));
}
auch NICHT durchgeführt, d.h. er spart sich diese Berechnungen.

Wenn das ein "richtiger" Funktionsplotter werden soll(te), würde man diesen Teil ohnehin etwas anders machen (das hast du aber glaub' ich schon mit deiner "Transformation"-Klasse im "richtigen" Programm) und diese Schleife würde eher sowas machen wie
Code:
[code]
for (int x = 1; x < [b]sx[/b]; x++)
{
     g.drawLine(x, x * x, x + 1, (x + 1) * (x + 1));
}
In diesem Fall wäre die if-Abfrage dann wieder weitgehend überflüssig, weil er ja bei sx==0 auch die Schleife nicht durchlaufen würde.
 

Zurück
Oben