# Canvas-Inhalt in Laufzeit ändern



## .SyS (18. Jul 2010)

Hallo,
ich wende mich in allerhöchster Not an euch. Bis morgen soll ein Projekt fertig werden, und es sieht aus als würde es an einer Kleinigkeit scheitern. 
Das Wesentliche skizziere ich grob: Ein mit NetBeans erstelltes Fenster(MainFrame) enthält ein simCanvas(siehe unten) und einige Steuerelemente. Für eine Simulation soll sich in diesem Canvas immer die aktuelle Situation darstellen. Im Konstruktor des MainFrame wird ein simCanvas erstellt.

Das ist der Code der simCanvas-Klasse:

```
import java.awt.*;
 
public class simCanvas extends Canvas {

    public void paint(final Graphics g){
        super.paint(g);
        
        g.setColor(Color.RED);
        g.fillRect(0, 0, 500, 500); 
    }
}
```

Wenn ich die main()-Methode des MainFrame aufrufe, dann erhalte ich auch eine rote Fläche, allerdings entzieht sich trotz intensiver Durchsicht hunderter Foren und Tutorials die Funktionsweise von repaint() bzw. update() meinem Verständnis. 

*Meine Frage:* Wie kann ich in Laufzeit (z.B. durch Druck auf einen Button) von MainFrame aus ein Rechteck in mein Canvas malen und zwar so, dass alles außenrum erhalten bleibt.

Ich glaube es ist wirklich nur eine Kleinigkeit, aber ich komme nicht dahinter.

Vielen vielen dank für schnelle Antworten!
.SyS


----------



## Marco13 (18. Jul 2010)

Was heißt dass "alles außenrum erhalten bleibt"?


```
public class simCanvas extends Canvas 
{
    private boolean malDasDing = false;

    public void setMalDasDing(boolean b)
    {
        this.malDasDing = b;
        repaint();
    }
 
    public void paint(final Graphics g){
        super.paint(g);
        if (malDasDing)
        {        
            g.setColor(Color.RED);
            g.fillRect(0, 0, 500, 500); 
        }
    }
}
```


----------



## .SyS (18. Jul 2010)

Vielen Dank für deine Antwort!

Aber ich glaube ich habe meine Frage falsch formuliert:
Wie kann ich ein neues Rechteck (dessen Parameter ich von außen übergeben kann) über das bereits vorhandene drüber malen?

Aber eine neue Erkenntniss habe ich jetzt gewonnen: repaint() ruft also paint() nochmal auf. Wie kann ich mir das also zu meinem Zweck zunutze machen?

*EDIT: Ich habe jetzt folgenden Code:*

```
import java.awt.*;
 
public class simCanvas extends Canvas {
    int x = 0, y = 0, w = 500, h = 500;
    Color color = Color.RED;
    
    public void paint(final Graphics g){
        super.paint(g);
        
        g.setColor(color);
        g.fillRect(x, y, w, h); 
    }
    
    
    
    public void newRect(int x_new, int y_new, int w_new, int h_new, Color color_new)
    {
        x = x_new;
        y = y_new;
        w = w_new;
        h = h_new;
        color = color_new;
        
        repaint();
    }
}
```

Vielleicht ist es umständlich und geht einfacher, aber zumindest erscheint jetzt auf Knopfdruck das gewünschte Rechteck. Nur leider verschwindet das rote "Standard"-Rechteck. Wie kann ich das neue darüber malen?


----------



## Marco13 (18. Jul 2010)

Oh ja.

ALLES, was gezeichnet wird, muss von der paint-Methode aus gezeichnet werden.

(soo, erstmal setzen lassen...)


Jetzt die Details: 
_repaint() ruft also paint() nochmal auf._ 
Das ist so nicht ganz richtig. "repaint()" ist nur ein Hinweis: "Zeichne mal so bald wie möglich neu". Das eigentliche Aufrufen von "paint" macht dann sozusagen(!!!) das Betriebssystem (GANZ grob gesagt..).

In diesem Fall wäre die Lösung demnach (auch ganz grob) sowas wie

```
public class simCanvas extends Canvas 
{
    private List<Rectangle> rectangles = new ArrayList<Rectangle>();
    private List<Color> colors = new ArrayList<Color>();

    void addNewRectangle(int x, int y, int w, int h, Color color)
    {
        rectangles.add(new Rectangle(x,y,w,h));
        colors.add(color);
        repaint();
    }
 
    public void paint(final Graphics gr){
        super.paint(gr);
        Graphics2D g = (Graphics2D)gr;

        g.setColor(Color.RED);
        g.fillRect(0, 0, 500, 500); 

        for (int i=0; i<rectangles.size(); i++) 
        {
            g.setColor(colors.get(i));
            g.fill(rectangles.get(i));
        }
    }
}
```

Eine Alternative, die etwas näher an dem ist, was du vielleicht meintest, aber selten in dieser Form wirklich sinnvoll (außer z.B. bei Dingen, die in Richtung eines "Malprogramms" gehen) wäre, alles in ein BufferedImage zu zeichnen, und NUR dieses BufferedImage in der paint-Methode zu malen. In diesem BufferedImage würde natürlich "alles erhalten bleiben", aber man muss sich überlegen, ob das für den jeweiligen Anwendungsfall wirklich das beste ist...


----------



## .SyS (18. Jul 2010)

Vielen vielen tausendfachen Dank! Es funktioniert! Und es sieht genauso aus, wie ich es mir vorgestellt habe!
Jetzt noch eine kleine Frage: Wie sind die Performanceunterschiede zwischen der Lösung mit den ArrayLists und dem BufferedImage für den Fall dass bis zu 30.000 kleine Rechtecke verwaltet werden sollen?
Falls in dem Fall BufferedImage sinnvoller wäre, wie wende ich das ganze dann wieder auf paint() an?


----------



## Marco13 (18. Jul 2010)

OK, das ist dann schon was anderes ... bei 30000 kleinen Rechtecken dürfte das mit dem BufferedImage schon allein wegen der Performance sinnvoll sein, allein wenn man davon ausgeht, dass in jedem Schritt nur wenig neu gezeichnet wird (und 29999 Rechtecke gleich bleiben).

Wieder GANZ grob anskizziert

```
class SimCanvas
{
    private BufferedImage bufferedImage = null;

    private List<Rectangle> rectangles = new ArrayList<Rectangle>();
    private List<Color> colors = new ArrayList<Color>();
 
    void addNewRectangle(int x, int y, int w, int h, Color color)
    {
        Rectangle rectangle = new Rectangle(x,y,w,h);
        rectangles.add(rectangle);
        colors.add(color);

        paintIntoImage(rectangle, color);
        repaint();
    }
    
    // Malt ein einzelnes Rechteck ins Bild
    private void paintIntoImage(Rectangle rectangle, Color color)
    {
        Graphics2D g = bufferedImage.createGraphics();
        g.setColor(color);
        g.fill(rectangle);
        g.dispose();
    }

    // Malt NUR das Bild    
    public void paint(Graphics g)
    {
        super.paint(g);
        if (bufferedImage == null)
        {
            bufferedImage = new BufferedImage(getWidth(), getHeight(), BufferedImage.TYPE_INT_RGB);
            updateImage();
        }
        g.drawImage(bufferedImage, 0, 0, null);
    }

    // Malt ALLE Rechtecke ins bild
    private void updateImage()
    {
        Graphics2D g = bufferedImage.createGraphics();

        g.setColor(Color.WHITE);
        g.fillRect(0,0,bufferedImage.getWidth(), bufferedImage.getHeight());

        g.setColor(Color.RED);
        g.fillRect(0, 0, 500, 500); 
 
        for (int i=0; i<rectangles.size(); i++) 
        {
            g.setColor(colors.get(i));
            g.fill(rectangles.get(i));
        }
        g.dispose();
    }
}
```

Da muss man sich dann halt noch überlegen ob man die Rechtecke und Farben wirklich nochmal speichern muss, oder ob's reicht, wenn sie einmal ins Bild gemalt wurden. Dabei sollte man beachten, dass evtl. ein neues BufferedImage angelegt werden muss, wenn die Größe des Canvas sich ändert (das würde man dann zusätzlich zur Abfrage "if (bufferedImage == null)" noch testen). Dann hat man die Möglichkeit, dort das alte (ggf. kleinere) Bild reinzumalen, oder ein komplettes "update" zu machen, wie oben angedeutet. Wenn man das aber nicht braucht, muss man auch die Listen nicht unbedingt speichern...


----------



## .SyS (18. Jul 2010)

Perfekt! Du warst die Rettung des Projekts. Alles sieht so aus, wie es soll, und nebenbei zumindest ein Stück weit in die Funktionsweise von Canvas eingestiegen... Irgenwann versteh ichs vielleicht mal 
DANKE!


----------

