2D-Grafik Kreis "von Hand" zeichnen -> jedesmal repaint()?

thousands

Mitglied
Hi,

ich versuche derzeit in Java ein Paint-ähnliches Zeichenprogramm umzusetzen und bin dabei auf ein Problem gestoßen, bei dem ich aktuell nicht weiter komme. Bei meiner google-Recherche hab ich dieses Forum hier entdeckt und mir gedacht ich schildere mein Problem hier einfach mal, in der Hoffnung, dass mir evtl. jmd. weiterhelfen oder den ein oder anderen nützlichen Tipp geben kann.

Zu meinem Problem: Nach erfolgreicher Umsetzung des "Freihand"-Zeichnens widme ich mich momentan dem Zeichnen von Shapes wie Kreisen und Rechtecken.
Mir ist dabei bekannt, wie das grundsätzliche Erstellen und Einfügen der jeweiligen Shape funktioniert, das Problem ist das "dynamische" Zeichnen der Shape während der Benutzereingabe (also das "ziehen" der Kreis- oder Rechteckgröße per Maus).

Ein Beispiel: Der Benutzer will einen Kreis zeichnen. Das soll analog zu (z.B.) MS Paint funktionieren, der Benutzer klickt also auf den gewünschten Ausgangspunkt für den Kreis und "zieht" (=dragged) dann die Maus, wobei sich die Kreisgröße entsprechend anpasst. Bei mouse-release soll der Kreis dann (endgültig) auf die Zeichenfläche übernommen werden.
Die Realisierung habe ich mir folgendermaßen vorgestellt: bei jedem MouseDragged-Event zeichne ich einen Kreis entsprechend den aktuellen Koordinaten; sollte dies nicht der erste Kreis sein, so wird der vorherige entfernt (sonst zeichne ich ja eine "3D-Röhre" aus vielen Kreisen).

Das Problem dabei ist nun folgendes: es gibt scheinbar keine Möglichkeit von meiner Zeichenfläche (Canvas oder JPanel, beides bereits versucht) das jeweils zuletzt (per .draw()) gezeichnete Objekt (hier: der obsolete Kreis der des alten MouseDragged-Events) wieder zu löschen. Das gezeichnete Objekt "verschmilzt" gewissermaßen mit der Zeichenfläche und lässt sich nachträglich nicht removen. Das bedeutet, dass ich bei jedem MouseDragged-Event ein repaint() ohne die Kreise aufrufen müsste, um den gezeichneten Kreis wieder zu "löschen", also den "ursprünglichen" Bildinhalt wiederherzustellen.
Dies würde zwar von der reinen Programmier-Logik her funktionieren (denk ich... 🙂), aber da MouseDragged ja mehrmals pro Sekunde aufgerufen wird dürfte dies (gerade wenn das bestehende Bild aufwendig ist) zu massiven Performanceproblemen führen.

Dafür muss es doch irgendwie eine wesentlich elegantere Variante geben, nur komm ich momentan scheinabar nicht von alleine drauf.

Schonmal vielen Dank für jegliche Hilfestellung!

Gruß,
thousands
 
Zuletzt bearbeitet:
Nachtrag: hier der Code der View eines kleines Testprogrammes zum Zeichnen von Kreisen. Den Controller poste ich der Übersichtlichkeit halber nicht mit, nur soviel: bei jedem MouseDragged-Event wird drawCircle aufgerufen, notFirst ist dabei beim ersten aufruf der aktuellen "Ziehbewegung" false, sonst true.

Anzumerken ist vielleicht noch, dass das ständige repaint() hier kein größeres Performanceproblem ist, da das zu "repaintente" Bild nicht sehr komplex ist. Sollte ich aber später z.B. die Funktion implementieren ein "richtiges" Bild (jpg, bmp etc..) zu importieren, dann dürften mehrere repaint()s pro Sekunde wohl zum Problem werden.

Java:
public class View extends JPanel
{
    private Point2D.Double pMerker;
    private Shape oval;
    private ArrayList<Shape> shapeList;

    public View()
    {
        this.setBackground(Color.WHITE);
        pMerker = new Point2D.Double();
        shapeList = new ArrayList<Shape>();
        this.setDoubleBuffered(true);
    }

    @Override
    public void paintComponent(Graphics g)
    {
        super.paintComponent(g);
        Graphics2D g2 = (Graphics2D) g;
		
        for (Shape elem : shapeList)
        {
            g2.draw(elem);
        }
    }


    public void drawCircle(Point p, boolean notFirst)
    {
        this.repaint();

        if (notFirst == false) // ist dies der erste Punkt der Ziehbewegung? ...
        {
            this.pMerker.setLocation(p.getX(), p.getY()); // ... dann speichern ...
        }
        else
        {
            shapeList.remove(shapeList.size() - 1); // ... sonst zuletzt gespeicherten Kreis löschen
        }

        oval = new Ellipse2D.Double(pMerker.getX(), pMerker.getY(), p.getX()-pMerker.getX(), p.getY()-pMerker.getY());
        shapeList.add(oval);
    }  
}

P.S.: sorry für den Doppelpost, ich wollte vermeiden, dass mein erster Post allzu riesig wird!
 
Zuletzt bearbeitet:
Hmja, also so ganz hab' ich das Problem nicht verstanden. Ein paar Stichpunkte
- Ja, man kann das Objekt nicht löschen. (Mit PaintMode XOR könnte man tricksen, ist aber aufwändig und meistens unnötig)
- Richtig: Alles, was gezeichnet wird, muss von der paintComponent aus gezeichnet werden - und zwar IMMER ALLES. Das ist eben so. (Einschränkung: Man könnte bei "repaint" den Bereich angeben, der neu gezeichnet werden muss, und hoffen, dass der Zeichnende das berücksichtigt....)
- Ein Hintergrundbild zu malen ist (speziell (oder "nur") wenn es keine Transparenz enthält) schnell. U.u. ist es schneller und einfacher als ein "komplexes" Shape: Bei einem Shape muss wirklich gerechnet werden. Ein Bild malen bedeutet im besten Fall: Kopier' einen Block Daten von hier nach da. Das geht so weit, dass man bei "vielen" Shapes (also wenn shapeList "viele" Einträge hat) das erstmal in ein BufferedImage malen könnte, damit beim Zeichnen eines neuen Elements nur das Bild und nicht die vielen komplexen Shapes neu gezeichnet werden muss
 
Hmja, also so ganz hab' ich das Problem nicht verstanden. Ein paar Stichpunkte
- Ja, man kann das Objekt nicht löschen. (Mit PaintMode XOR könnte man tricksen, ist aber aufwändig und meistens unnötig)
- Richtig: Alles, was gezeichnet wird, muss von der paintComponent aus gezeichnet werden - und zwar IMMER ALLES. Das ist eben so. (Einschränkung: Man könnte bei "repaint" den Bereich angeben, der neu gezeichnet werden muss, und hoffen, dass der Zeichnende das berücksichtigt....)
- Ein Hintergrundbild zu malen ist (speziell (oder "nur") wenn es keine Transparenz enthält) schnell. U.u. ist es schneller und einfacher als ein "komplexes" Shape: Bei einem Shape muss wirklich gerechnet werden. Ein Bild malen bedeutet im besten Fall: Kopier' einen Block Daten von hier nach da. Das geht so weit, dass man bei "vielen" Shapes (also wenn shapeList "viele" Einträge hat) das erstmal in ein BufferedImage malen könnte, damit beim Zeichnen eines neuen Elements nur das Bild und nicht die vielen komplexen Shapes neu gezeichnet werden muss

Danke, gerade der dritte Punkt war sehr hilfreich. Ich hab nun einfach mal ein relativ großes bmp (gewissermaßen als "Stresstest") in meine Zeichenfläche eingefügt und das ständige repaint() macht tatsächlich überhaupt keine Performanceprobleme. Mein ursprüngliches Problem wäre damit denke ich gelöst!

Ich hätte aber noch eine weitere Frage: wo genau liegt der Unterschied zwischen der Verwendung von JPanel und Canvas als Zeichenfläche, was sind die jeweiligen Vor- und Nachteile?
Soweit ich gesehen hab hat Canvas keine paintComponent() Methode, ersatzweise kann man scheinbar sowohl paint(), als auch update() verwenden (funktioniert beides). Welche Methode ist die "richtige"?

Gruß,
thousands
 
Ich hätte aber noch eine weitere Frage: wo genau liegt der Unterschied zwischen der Verwendung von JPanel und Canvas als Zeichenfläche, was sind die jeweiligen Vor- und Nachteile?
Soweit ich gesehen hab hat Canvas keine paintComponent() Methode, ersatzweise kann man scheinbar sowohl paint(), als auch update() verwenden (funktioniert beides). Welche Methode ist die "richtige"?
Canvas ist AWT und JPanel/JComponent ist Swing. In den meisten Fällen sollte ein überschreiben der paint() [AWT] bzw. paintComponent() [Swing] ausreichen.
 
Versteh ich das richtig, dass Du nur zwei Layer hast? Das Bild und das aktuell zeichnende Objekt?
Dann besteht der Ablauf beim Zeichnen eigentlich nur aus dem Zeichnen von Hintergrund und Vordergrund.
Sobald der Kreis fertig ist, wird er in den Hintergrund übertragen.
In der paintComponent() wird dann bei jedem Durchlauf zuerst das Hintergrundbild (BufferedImage) gezeichnet und danach das aktuelle Zeichenobjekt (Kreis, etc.)
Sobald der Kreis fertig ist, zeichnest Du ihn dann fix auf das Hintergrundbild. In diesem Durchlauf ist der zweite Layer komplett leer.

Der zweite Layer ist kein echtes Bild, sondern nur eine Funktion, die eben das zeichnet, was gerade "getan" wird.
Wenn Du mit mehreren Ebenen arbeitest musst Du da ein wenig mit Masken umgehen.
Solange Du die gezeichneten Elemente nicht wieder "anklicken" willst, ist es doch gar kein Problem, wenn Du sie alle in das gleiche Bild zusammenpresst.
 
Versteh ich das richtig, dass Du nur zwei Layer hast? Das Bild und das aktuell zeichnende Objekt?
Dann besteht der Ablauf beim Zeichnen eigentlich nur aus dem Zeichnen von Hintergrund und Vordergrund.
Sobald der Kreis fertig ist, wird er in den Hintergrund übertragen.
In der paintComponent() wird dann bei jedem Durchlauf zuerst das Hintergrundbild (BufferedImage) gezeichnet und danach das aktuelle Zeichenobjekt (Kreis, etc.)
Sobald der Kreis fertig ist, zeichnest Du ihn dann fix auf das Hintergrundbild. In diesem Durchlauf ist der zweite Layer komplett leer.

Der zweite Layer ist kein echtes Bild, sondern nur eine Funktion, die eben das zeichnet, was gerade "getan" wird.
Wenn Du mit mehreren Ebenen arbeitest musst Du da ein wenig mit Masken umgehen.
Solange Du die gezeichneten Elemente nicht wieder "anklicken" willst, ist es doch gar kein Problem, wenn Du sie alle in das gleiche Bild zusammenpresst.

Alles klar, danke. Ich dachte nur ursprünglich, dass der repaint() des unteren Layers (also des bisherigen Bildes) bei jedem einzelnen MouseDragged-Event ziemliche Performanceprobleme bereiten würde. Ich hab mittlerweile durch testen heraus gefunden, dass dem nicht so ist, selbst dann nicht, wenn meine Zeichenfläche nicht doubleBuffered ist.

Das Problem hat sich damit denke ich erledigt, danke nochmal an alle für die Hilfe!
 

Neue Themen


Zurück
Oben