Laden...

"Gezieltes OwnerDrawing" - schnelles Zeichnen bewegter Objekte

Erstellt von ErfinderDesRades vor 16 Jahren Letzter Beitrag vor 15 Jahren 19.929 Views
Information von herbivore vor 16 Jahren

Dies ist ein Thread, auf den aus der FAQ verwiesen wird. Bitte keine weitere Diskussion, sondern nur wichtige Ergänzungen und diese bitte knapp und präzise. Vielen Dank!

ErfinderDesRades Themenstarter:in
5.299 Beiträge seit 2008
vor 16 Jahren
"Gezieltes OwnerDrawing" - schnelles Zeichnen bewegter Objekte

Ausgehend von Herbivores [Tutorial]Zeichnen in Windows-Programmen (Paint/OnPaint, PictureBox),
weitergeführt zu proggers [Tutorial] Gezeichnete Objekte mit der Maus verschieben
habe ich quasi eine Art "Tutorial-Erweiterung" verzapft, die an folgenden Code-Kommentar aus proggers Tut anknüpft:

            // Hier könnte man noch optimieren, indem man immer nur den Bereich
            // neuzeichnet, in dem das Objekt bewegt wurde.
            this.Invalidate();

nur den Bereich zeichnen, in dem das Objekt bewegt wurde - ist hier Thema. (Das KnowHow der vorgenannten Tuts wird vorrausgesetzt.)
Der Grundgedanke ist einfach: Jedes zu zeichnende Objekt (bei mir heißt die Klasse "Figure") muß wissen, welchen Bereich es braucht, um sich zu zeichnen. Ich denke, "Bounds" ist die angemessene Bezeichnung dieses Bereiches.
Bei kleinen Objekten auf großen Zeichenflächen verringert sich die zu zeichnende Fläche schnell um Faktor 20, wenn nur noch die Bounds gezeichnet werden, nicht mehr die Gesamtfläche.

**Eine Bewegung oder sonstige Veränderung der Figur stellt sich nun so dar:**1.Eigenschaften der Figur werden verändert (Größe, Position, Rotation...) 1.Das vorherige Bounds-Rectangle wird für ungültig erklärt (Control.Invalidate(Bounds) aufrufen), damit die Figur an der alten Position gelöscht wird 1.Das neue Bounds wird berechnet 1.Das neue Bounds wird ebenfalls invalidiert, damit die Figur an der neuen Position gezeichnet wird

Control.Invalidate(Rectangle) bewirkt, daß irgendwann kurz darauf die CLR im Control die OnPaint()-Überschreibung aufruft, und mit e.ClipRectangle ein beide Bounds vereinigendes Rechteck übergibt.
** Auch wenn viele Figuren verändert werden, werden alle Invalidierungen in einen Aufruf von OnPaint() zusammengeführt. **
Im override OnPaint() (bzw. _Paint()-Event) ist dann der Code zu schreiben, der Figure.Draw() aufruft

Die Schritte 2 - 4 lassen sich bequem in einer Funktion zusammenfassen, die aufzurufen ist, nachdem Eigenschaften geändert wurden:


   public void ApplyChanges() {
      // ...
      // (Berechnung der neuen Figur, anhand geänderter Eigenschaften)
      // (Berechnung von RectangleF NewBounds)
      // ...
      
      _Control.Invalidate(_Bounds);
      _Bounds = Rectangle.Ceiling(NewBounds);
      _Control.Invalidate(_Bounds);
   }

Wohlgemerkt: Das zeichnet noch nichts. Es wird nur von der CLR ein Zeichenvorgang angefordert.

Bestimmung der Bounds
...einer auszufüllenden Figur ist einfach:

[FONT]RectangleF NewBounds = GraphicPath.GetBounds();[/FONT]

Probleme machen Linien und Umriss-Figuren. Die Zeichnung überragt die theoretische Linie des GraphicPaths um mindestens die halbe Stiftbreite, an spitzen Ecken noch erheblich mehr. Und bei aktivierter Kantenglättung verbleiben gelegentlich Glättungspixel außerhalb der mit _pthOutline.GetBounds() ermittelten Bounds.
Es gibt GetBounds()-Überladungen, welche die Stift-Art berücksichtigen sollen, die sind aber fehlerhaft, und ermitteln bis zu vierfach zu große Flächen.
Daher folgender Workaround:


         _pthWidened.Reset();
         _pthWidened.AddPath(_pthOutline, false);
         _pthWidened.Widen(_Pen);
         RectangleF NewBounds = _pthWidened.GetBounds();

Erläuterung: Für eine Kopie des Umrisses (Outline) wird .Widen(Pen) aufgerufen - Das generiert einen Umriss um die Fläche des Striches, mit dem Pen die Zeichnung ausführen würde.
Und davon .GetBounds() - das ist korrekt.

So, damit wäre "gezieltes OwnerDrawing" im Kern eigentlich schon abgehandelt.

Die eierlegende Woll-Milch-Sau
Aber dann bin ich noch sehr von Herbivores Design (viele spezialisierten Zeichnungs-Objekte) abgegangen, und habe die berühmte eierlegende Woll-Milch-Sau implementiert.
Hierbei der Grundgedanke: Wenn die Klasse GraphicsPath so viel kann (das ist nämlich die eigentliche WollMilchSau), und ohnehin alle statischen Zeichnungsinformationen in einem GraphicsPath gespeichert werden - dann brauche ich den ja nur offenzulegen, und schon kann der User in einer Figure alle Elemente: Kreise, Rechtecke, Splines, Strings, ... nach Belieben anlegen (und kombinieren!).

Ich empfehle sehr, bei Anlage der Figuren im (bei mir so genannten) "TemplatePath" innerhalb des Größenbereiches von +-1.0 zu bleiben. Die tatsächliche Vergrößerung wird dann über die "Scale"-Property angegeben.

Diese Vorgehensweise erweist sich als sehr nützlich, etwa wenn es gilt, eine Uhr zu gestalten: Der Minutenzeiger sei einfach eine Vergrößerung des Stundenzeigers; außerdem kann man schon am Code die relativen Proportionen von Zeiger, Ziffernblatt und Ziffern recht gut abschätzen.
Auch kann man Resizing-Code einfügen, der die Scalierung auf die jeweiligen Abmaße des Controls abstimmt (Auf rechteckigen Controls die Uhr oval machen).

Strings
Die Darstellung von Strings erfordert leider eine Sonderbehandlung, weil man an GraphicsPath.AddString() keinen so mikroskopischen Font übergeben kann, daß die String-Darstellung innerhalb des Größenbereiches von +-1.0 bliebe. Die Sonderbehandlung besteht in der Figure.Normalize()-Funktion, die den "TemplatePath" auf eine Größe bringt, in der er grade in mein "Norm-Koordinatensystem" (X: -1/+1,Y: -1/+1) hineinpasst. So kann man Strings mit beliebigen Font-Größen eingeben - Normalize() schrumpelt sie dann auf Norm-Maße.

Das ist natürlich kein sehr schönes Design, diese "ExtraWurst für Strings", aber das Klassen-Design, was ich eigentlich anstrebe:


               DrawPath
              /
DrawFigureBase -- DrawImage
              \
               DrawString

... hier vollständig zu entwickeln, ist mir zu ausführlich.

**In die Beispiel-Solution habe ich einige Testmöglichkeiten eingebaut:***Setzen eines Clips umschaltbar *Visualisierung des Clip-Rectangles *Ausschalten des "gezielten Invalidates" *Doppelpufferung umschaltbar *AutoRun

AutoRun: die Figur rotiert, so schnell sie kann. Dabei werden die Geschwindigkeitsunterschiede der verschiedenen Zeichnungs-Modi sehr gut sichtbar.

Aufschlußreich auch die
Visualisierung des Clip-Rectangles

Volle Bildgröße

Ein schnell über das Form gezogenes kleines Fenster veranlaßt eine Folge von Zeichenvorgängen, mit einem ClipRectangle in Maßen des kleinen Fensters.
Letzteres zieht quasi eine "Spur von Invalidierungen" hinter sich her.
Dieses Verhalten geht von der CLR aus, das Beispiel-Programm hat ja gar nicht den Focus.
Durch die OnPaint-Überschreibung hat es sich aber in den Zeichenvorgang "eingeklinkt", und neuzeichnet seine Figuren in die ClipRectangles, sodaß sie hinter dem kleinen Fenster wieder zutage treten, anstatt "weggewischt" zu sein.

Denselben "Spur-Effekt" erhält man, wenn man eines der Objekte schnell draggt.

Volle Bildgröße

Hierbei ist das Beispiel-Programm aktiv, indem es die Invalidierungen selbst auslöst, nämlich durch
_Control.Invalidate(_Bounds) in Figure.ApplyChanges().

Der frühe Apfel fängt den Wurm.

ErfinderDesRades Themenstarter:in
5.299 Beiträge seit 2008
vor 16 Jahren

Das ist natürlich kein sehr schönes Design, diese "ExtraWurst für Strings", aber das Klassen-Design, was ich eigentlich anstrebe:

  
               DrawPath  
              /  
DrawFigureBase -- DrawImage  
              \  
               DrawString  

... hier vollständig zu entwickeln, ist mir zu ausführlich.

Na, hab ich jetzt doch gemacht.
Es ist ein etwas anderes KlassenDesign dabei herausgekommen:


                    ImageDraw 
                   /
(abstract) DrawBase 
                   \
                    FillDraw
                            \
                             OutlineDraw

FillDraw zeichnet einen GraphicsPath ausgefüllt, OutlineDraw die mit einem Pen ausgeführte Linie.
DrawString zu entwerfen erübrigte sich, Textdarstellungen sind ausgefüllte GraphicsPathes, die mit einem String beschickt wurden.

Da jetzt auch Bilder gezeichnet werden, macht sich das Prinzip: "nur minimal erforderlichen Bereich zeichnen" recht deutlich bemerkbar.
Weitere Optimierung im strengen Sinne (Optimierung als nicht wiederverwendbare Laufzeit-Verbesserung im Einzelfall) wäre, Bitmaps zu verwenden, deren Auflösung die des Bildschirms nicht übertrifft (das kostet nämlich, ohne sichtbar zu werden).

Der frühe Apfel fängt den Wurm.

D
201 Beiträge seit 2007
vor 15 Jahren

Hallo, ich habe zu diesem Thema noch eine Anmerkung/Frage:

Man kann meiner Meinung nach noch effektiver und "flackerfreier" zeichnen, wenn man nicht nur rechteckige regionen mit Invalidate() behandelt, sondern exaktere Regionen. Das geht eigentlich recht einfach:


pControl.Invalidate(new Region(invalidatePath));

(pControl ist das Control, invalidatePath ist ein GraphicPath, der das neu zu zeichnende Objekt umschliesst)

Mit dieser Methode wird nicht ein (evtl. viel zu großes) Rechteck für ungültig erklärt, sondern (fast) nur die Region, in der auch wirklich neu gezeichnet werden muss. Die Region hat also auch die Form des Objektes, das neu gezeichnet werden soll.
Bei Kurven (z.b. die im Screenshot) macht das performance-mäßig doch sicherlich etwas aus, oder?

ErfinderDesRades Themenstarter:in
5.299 Beiträge seit 2008
vor 15 Jahren

Ja, genau, damit habich auch 'ne Weile rumprobiert.

Ist aber sone Sache. Regions sind Annäherungs-Objekte, und je nachdem, wie die Figur aussieht, unterschiedlich aufwändig. Intern stellichs mir als Ansammlung von verschieden großer Rechtecke vor, die im Randbereich sehr klein sein müssen, um die vorgegebene Randlinie hinreichend genau anzunähern.
Und Kurven würden einen recht hohen Berechnungs und SpeicherVerbrauch haben: Man müsste die Kurve in eine sie umschließende einhüllen (Einfach: GraphicsPath.Widen), weil Regions brauchen flächige Berechnungsgrundlagen.
Ich glaub, ein Kreis, mit Strichpunkt-Linie gezogen, ergab damals eine Region mit über 10000 Datenpunkten, wennichmichrechterinner, und der Konstruktor war so langsam, dass für normale Figuren die Simpel-Lösung: einfach das umschließende Rechteck neu zeichnen, wesentlich schneller war. (Aber Strichpunktlinie ist auch wirklich gemein zu de Regions 😉 )
Wirklich günstig wären vermutlich vereinfachte Regions, die ihre Randlinien zwar nur schlecht annähern, dafür aber mit hoher Erstell-Geschwindigkeit und wenigen Datenpunkten.

Ja, Tatsache: Region hat auch einen Konstruktor, wo man das RegionData gleich mit angeben kann. Also mit einem "schlampigeren" Region-Algorithmus könnte man son Region-Data sehr schnell erzeugen, und dann daraus eine Region erstellen.
Der eigene Algo könnte auch speziell die umschließende Region generieren, ich glaub, die derzeitige Region ist die eingeschriebene.

Aber mit Diagrammen könnte sichs wirklich lohnen, auf Regions aufzubauen.
Weil bei einer Kurve auffm Diagramm "nur" das umschließende Rechteck zu neuzeichnen, läuft meist drauf hinaus, die _gesamte _Diagrammfläche neu zu machen, also dieser Ansatz hier bringt für Diagramme eigentlich nix.

Der frühe Apfel fängt den Wurm.