# Optimiertes Zeichnen - wie?



## Antimon (17. Mrz 2009)

Hallo zusammen,

folgende Problematik besteht bei mir: Ich zeichne verschiedene Objekte auf ein Panel über die paintComponent()-Methode. Dabei kann das Panel ziemlich groß werden, deswegen wird es in einer JScrollPane untergebracht. Das Ganze ist übrigens ähnlich der Zeichenfläche in einem Grafikprogramm, durch die Scrollpane kann man einen anderen Auschnitt einer Zeichnung betrachten.

Nun ist es so, dass bei einer Änderung (Verschieben von Grafikelementen, Verschieben des Bildausschnitts) das Panel neu gezeichnet werden müsste. Wenn aber beim Verschieben eines Elements mit ein paar Pixeln Größe jedes Mal das komplette Panel neu gezeichnet wird, sinkt die Performance ziemlich in den Keller.

Gut, nun kann man der repaint-Methode Koordinaten mitgeben, die neu gezeichnet werden sollen.
Allerdings: Bezieht sich das auf die Koordinaten des Panels, auf dem gezeichnet wird, oder auf die Koordinaten des sichtbaren Bildausschnitts der Scrollpane?

Das weitere Problem ist, dass mein Koordinatensystem nicht links oben mit (0,0) beginnt, sondern links unten. Um das zu erreichen, wird das Graphics-Objekt in der paintComponent-Methode mit 


```
AffineTransform trafo = new AffineTransform();
trafo.scale(1.0, -1.0);
trafo.translate(-scrollpane.getViewport().getViewPosition().getX(), -panel.getHeight() + scrollpane.getViewport().getViewPosition().getY());
g2d.setTransform(trafo);
```

gespiegelt.

Beziehen sich jetzt die Koordinaten für repaint() auf die Bildschirmkoordinaten, oder werden die durch die Transformation auch gleich mit umgerechnet? Ich habe zwar versucht, mir den repaint-Bereich einzeichnen zu lassen, aber da kam nur Müll dabei raus...

Und zu guter Letzt: Wird nur ein Bereich per repaint neu gezeichnet, muss ich den in der paintComponent()-Methode abfragen und dann nur die Elemente neu zeichnen, die in diesen Bereich hineinragen, sehe ich das richtig?

Und wenn der Ausschnitt der Scrollpane verschoben wird, muss ich dann auch dafür sorgen, dass der neue sichtbare Ausschnitt neu gezeichnet wird? Oder wird da ein bereits erzeugtes Graphics-Objekt von Swing intern verwendet und nur der neue Ausschnitt angezeigt? Irgendwas mache ich da wohl noch falsch, denn beim Verschieben entstehen einige Grafikfehler...

Mit den ganzen Paint-Geschichten blicke ich leider noch nicht so durch, ich habe mir zwar die Paint-Tutorials von Sun durchgelesen, aber die beantworten leider noch nicht alle Fragen - wenn Ihr Tips für mich habt, bin ich Euch sehr dankbar!


----------



## Ebenius (17. Mrz 2009)

So ziemlich alle Koordinaten und Größen in Component und deren Ableitungen beziehen sich mit 0×0 auf die linke obere Ecke der Komponente, nach rechts und unten ansteigend. Das trifft zum Beispiel auf getSize(), getLocation(), repaint(Rectangle), repaint(int, int, int, int), und getVisibleRect() zu. Letztere Methode sagt Dir, welches Rechteck der Komponente sichtbar ist, zum Beispiel in einer JScrollPane.

Wenn Du zeichnest, kannst Du alle anderen Bereiche außen vor lassen und nur innerhalb des VisibleRects zeichnen. Dieses musst Du natürlich ebenfalls durch die selbe Transformation schicken, um es in Deinem View-Koordinatensystem nutzen zu können.

Hilft das soweit?

Ebenius


----------



## Antimon (17. Mrz 2009)

Hm okay das hilft mir schon mal weiter - ich muss also die Koordinaten selbst umrechnen.

Was mir allerdings noch nicht ganz klar ist, ist wie genau ich zeichnen muss/soll. Vielleicht verdeutlicht das ein Beispiel: Angenommen das Panel auf dem ich zeichne hat ein Raster mit Gitternetzlinien von 10 Pixeln Abstand und ist 800x600 Pixel groß. Die sichtbare Fläche ist 400x300 Pixel groß. Das bedeutet nun dass ich beim Zeichnen erst berechnen muss wo sich der Ausschnitt befindet und dort dann nur das sichtbare Gitter zeichne und nicht die kompletten 800x600 Pixel?

Oder würde es da vielleicht sogar Sinn machen, das komplette Gitter zu zeichnen, bevor erst ewig berechnet wird wo es denn nun beginnen muss - und vielleicht sogar Grafikfehler durch falsche Berechnungen entstehen?

Und Elemente die aus der Arbeitsfläche rausragen werden vermutlich besser gleich komplett gezeichnet anstatt großartig zu berechnen welche Ecken davon berechnet werden müssen und welche nicht, sehe ich das richtig?

Dass ich das komplette Bild einmal zeichne und dann nur noch wenn sich am Bildinhalt etwas ändert neu aufbaue, funktioniert nicht? Denn theoretisch könnte die komplette Grafik ja fertig gezeichnet im Speicher liegen und nur der sichtbare Ausschnitt angezeigt werden, solange sich an dem Inhalt der Grafik nichts ändert, müsste diese ja nicht neu gezeichnet werden?


----------



## Wildcard (17. Mrz 2009)

Ein pragmatischer Ansatz ist ein Bounding Box Test. Berührt die Bounding Box eines Objekt den Clip des Graphics Objects, dann zeichnest du es, ansonsten nicht. Damit fällt dir schon einiges weg.
Ob es Sinn macht diese Objekte dann evtl. nur partiell zu Zeichnen, hängt von der Komplexität deiner Objekte ab.


----------



## 0x7F800000 (18. Mrz 2009)

1) zu jedem der zu zeichnenden Objekte eine bounding box berechnen (das ist schonmal eine sehr gute idee)
2) je nach dem wie speicherintensiv das ist: entweder alles in ein großes regelmäßiges Gitter packen (wobei in jedes Kästchen... kA... 20-30-50-100? Objekte reinpassen)
Oder ein Quadtree über das ganze bild legen, und alle Objekte anhand der bounding box dareinpacken (hat den vorteil, dass die Feinheit der aufteilung variabel ist)
3) beim zeichnen schaust du, welcher bildausschnitt neugezeichnet werden muss, und zeichnest einfach alle sichtbare leafs (dank des Quadtrees musst du da nicht *jedes* objekt prüfen, ob es sich mit dem sichtbaren bereich schneidet, sondern kannst direkt gigantische äste des Quadtrees komplett ignorieren)
Dann brauchst du nicht O(n), sondern nur noch O(ln(n)), was bei vielen objekten ziemliche ersparnis ist (wenn die Objekte dumm verteilt sind, und du dennoch alle siehst, dann bringt es nichts, im worst case ist das immer noch O(n))

Wenn der zuletzt angesprochene fall eintreten kann, kannst du mit demselben Quadtree noch eine weitere optimierung anstellen:
-Beim ändern (verschieben, skalieren der Objekte) markierst du alle "betroffene" leafs des Quadtrees, d.h diejenigen, in den die Bounding box vorher gewesen war (dort wurden evtl bereiche offengelegt) sowohl alle diejenigen leafs, die von der bounding box nach der transformation betroffen sind (dort wurde evtl etwas verdeckt)
NUR DIESE leafs musst du jetzt neuzeichnen, alles andere kann unberührt bleiben, was wieder eine große ersparnis sein kann.

Noch eine weitere optimierungsstufe wäre es natürlich, große objekte (etwa Bilder) in kleinere rechtecke zu zersägen, damit man da auch nicht alles neuzeichnen muss...

Also, da sind der phantasie keine grenzen gesetzt.


----------



## Antimon (23. Mrz 2009)

Okay, super - danke für die Tips schon mal, die werd ich jetzt mal Stück für Stück umsetzen.

Jetzt ist mir nur noch was aufgefallen, was mir nicht so gefällt:

Durch das Spiegeln des Koordinatensystems werden Texte, die ich per drawString() zeichne, auf den Kopf gestellt gezeichnet. Das ist natürlich eher suboptimal... muss ich nun für jeden Text den ich zeichnen möchte eine neue Spiegelung per scale(1.0, -1.0) plus benötigter Verschiebung ausführen oder geht das Ganze einfacher?

Also mein Ziel ist es, ein Koordinatensystem zu haben, das nach rechts positive x-Koordinaten hat und nach oben positive y-Koordinaten. Der Ursprung soll (verschiebbar) links unten im Fenster dargestellt werden. Gibt es einen sinnvolleren Ansatz als den von mir oben verwendeten?


----------



## 0x7F800000 (23. Mrz 2009)

Antimon hat gesagt.:


> Durch das Spiegeln des Koordinatensystems werden Texte, die ich per drawString() zeichne, auf den Kopf gestellt gezeichnet. Das ist natürlich eher suboptimal... muss ich nun für jeden Text den ich zeichnen möchte eine neue Spiegelung per scale(1.0, -1.0) plus benötigter Verschiebung ausführen oder geht das Ganze einfacher?


Hmm, willst du denn das wirklich zulassen? Ich kenne kein 2D-Bildbearbeitungs Programm o.ä. , wo man das Koordinatensystem Spiegeln kann. Normalerweise beschränken sich alle diese Programme auf Verschieben und Zoomen. Mehr ist imho auch nicht nötig, weil der Benutzer ansonsten ganz schnell nicht mehr weiß, wo oben und unten ist...

[edit]
Achso, jetzt raff ich das, du willst einfach nur das Koordinatensystem "klassisch herum" zeichnen, und ärgerst dich, dass sich die Achsenbeschriftungen auch umdrehen... Nja, das ist ärgerlich. Ich denke nicht, dass du es dir wesentlich einfacher machen kannst, als den Punkt, wo der text sein soll, vorher mit der aktuell gesetzten AffinenTransformation in Bildschirmkoordinaten zu transformieren, und anschließend mit der Identitäts-Transformation normal den text an dieser Stelle hinzeichnen.


----------



## Antimon (24. Mrz 2009)

Ähm jo genau, sorry wenn das ein wenig diffus erklärt war von mir 

Okay... das gefällt mir irgendwie gar nicht, denn die Problematik wird dadurch nicht einfacher.

Aber gut, was wäre dann die angenehmere Variante - ein Image im Speicher zu zeichen, dessen Höhe und Breite ich kenn und das ich dann einfach spiegle und an die Stelle zeichne wo es hinkommen soll? Oder den ursprünglichen Graphics-Kontext zu "Identitäts-transformieren"?

Die letztere Variante, also mit den ursprünglichen Bildschirm-Koordinaten zu arbeiten gefällt mir aus mehreren Gründen nicht:
- Alle anderen Zeichenoperationen geschehen auf dem neuen, internen Koordinatensystem (Zoom, Verschiebung und so läuft dann alles nur über die Trafo)
- Wenn ich den Text zeichnen möchte, muss ich die Texthöhe kennen bzw. vorher umständlich berechnen, damit der Text an die richtige Position kommt
- Wenn ich auf Bildschirmkoordinaten umrechnen möchte, muss ich immer die Höhe des Panels, Position des Ursprungs etc. kennen und umrechnen

Ich weiss schon, ich habe mir keinen Gefallen getan indem ich gesagt habe, ich verwende das übliche Koordinatensystem-Schema - aber im Nachhinein ist man immer schlauer und für den Benutzer ist die Geschichte doch intuitiver...


----------



## 0x7F800000 (24. Mrz 2009)

Antimon hat gesagt.:


> - Alle anderen Zeichenoperationen geschehen auf dem neuen, internen Koordinatensystem (Zoom, Verschiebung und so läuft dann alles nur über die Trafo)


Da soll aber eben nicht alles durch diese zoom-verschiebungs-trafo (ich nenn die mal T. T transformiert also die "wahren Koordinaten" in "Bildschirmkoordinaten".)
Der Text soll ja immer eine Konstante Größe behalten und nicht mitskaliert werden, richtig? Also musst du die Form des Textes an der Transformation T vorbeischmuggeln, während seine Position aber stets transformiert wird, sodass er im endgültigen Bild an der richtigen Stelle landet.

Dieses "teilweise Vorbeischmuggeln" realisierst du eben wie folgt:
[highlight=Java]
String text="blahblahblah";
Point2D.Double realTextPosition=new Point2D.Double(x,y);

AffineTransform T=g.getTransform(); // T wird rausgenommen
g.setTransform(new AffineTransform()); // stattdessen wird Trafo eingesetzt, die gar nichts tut
Point2D.Double screenTextPosition;
T.transform(realTextPosition,screenTextPosition); //position des Textes wird transformiert, seine Form aber nicht!
g.drawString(text, screenTextPosition.x , screenTextPosition.x); //zeichnen
g.setTransform(T); //transform zurücksetzen
[/highlight]
Klarer geworden, was ich meine? [der code oben ist natürlich als "pseudocode" zu verstehen, da sollten wenigstens ein paar float-casts noch hin]


----------

