myCSharp.de - DIE C# und .NET Community
Willkommen auf myCSharp.de! Anmelden | kostenlos registrieren
 
 | Suche | FAQ

» Hauptmenü
myCSharp.de
» Startseite
» Forum
» FAQ
» Artikel
» C#-Snippets
» Jobbörse
» Suche
» Regeln
» Wie poste ich richtig?
» Forum-FAQ

Mitglieder
» Liste / Suche
» Wer ist wo online?

Ressourcen
» openbook: Visual C#
» openbook: OO
» Microsoft Docs

Team
» Kontakt
» Übersicht
» Wir über uns

» myCSharp.de Diskussionsforum
Du befindest Dich hier: Community-Index » Diskussionsforum » Gemeinschaft » .NET-Komponenten und C#-Snippets » "Gezieltes OwnerDrawing" - schnelles Zeichnen bewegter Objekte
Letzter Beitrag | Erster ungelesener Beitrag Druckvorschau | Thema zu Favoriten hinzufügen

Antwort erstellen
Zum Ende der Seite springen  

"Gezieltes OwnerDrawing" - schnelles Zeichnen bewegter Objekte

 
Autor
Beitrag « Vorheriges Thema | Nächstes Thema »
myCSharp.de
Moderationshinweis von herbivore (11.03.2008 14:38):

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
myCSharp.de-Poweruser/ Experte

avatar-3151.jpg


Dabei seit: 31.01.2008
Beiträge: 5.287


ErfinderDesRades ist offline

"Gezieltes OwnerDrawing" - schnelles Zeichnen bewegter Objekte

Beitrag: beantworten | zitieren | editieren | melden/löschen       | Top

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:

C#-Code:
            // 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...)
  2. 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
  3. Das neue Bounds wird berechnet
  4. 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:

C#-Code:
   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:

RectangleF NewBounds = GraphicPath.GetBounds();

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:

C#-Code:
         _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().


Dateianhang:
unknown DrawObject.zip (19 KB, 1.258 mal heruntergeladen)

Dieser Beitrag wurde 7 mal editiert, zum letzten Mal von ErfinderDesRades am 11.03.2008 13:41.

01.03.2008 22:36 E-Mail | Beiträge des Benutzers | zu Buddylist hinzufügen
ErfinderDesRades
myCSharp.de-Poweruser/ Experte

avatar-3151.jpg


Dabei seit: 31.01.2008
Beiträge: 5.287

Themenstarter Thema begonnen von ErfinderDesRades

ErfinderDesRades ist offline

Beitrag: beantworten | zitieren | editieren | melden/löschen       | Top

Zitat:
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).



Dateianhang:
unknown DrawObjects02.zip (74 KB, 940 mal heruntergeladen)

Dieser Beitrag wurde 3 mal editiert, zum letzten Mal von ErfinderDesRades am 11.03.2008 14:18.

11.03.2008 13:59 E-Mail | Beiträge des Benutzers | zu Buddylist hinzufügen
Zwischen diesen beiden Beiträgen liegen mehr als 8 Monate.
DFDotNet DFDotNet ist männlich
myCSharp.de-Mitglied

Dabei seit: 17.08.2007
Beiträge: 201
Entwicklungsumgebung: MS VS 2008 /.NET 2.0/3.5


DFDotNet ist offline

Beitrag: beantworten | zitieren | editieren | melden/löschen       | Top

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:

C#-Code:
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?

DFDotNet hat dieses Bild (verkleinerte Version) angehängt:
curve.jpg
Volle Bildgröße

Dieser Beitrag wurde 2 mal editiert, zum letzten Mal von DFDotNet am 26.11.2008 13:55.

26.11.2008 13:46 E-Mail | Beiträge des Benutzers | zu Buddylist hinzufügen
ErfinderDesRades
myCSharp.de-Poweruser/ Experte

avatar-3151.jpg


Dabei seit: 31.01.2008
Beiträge: 5.287

Themenstarter Thema begonnen von ErfinderDesRades

ErfinderDesRades ist offline

Beitrag: beantworten | zitieren | editieren | melden/löschen       | Top

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 Augenzwinkern )
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.
26.11.2008 15:52 E-Mail | Beiträge des Benutzers | zu Buddylist hinzufügen
Baumstruktur | Brettstruktur       | Top 
myCSharp.de | Forum Der Startbeitrag ist älter als 11 Jahre.
Der letzte Beitrag ist älter als 11 Jahre.
Antwort erstellen


© Copyright 2003-2020 myCSharp.de-Team | Impressum | Datenschutz | Alle Rechte vorbehalten. | Dieses Portal verwendet zum korrekten Betrieb Cookies. 23.02.2020 02:30