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:
- Eigenschaften der Figur werden verändert (Größe, Position, Rotation...)
- 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
- Das neue Bounds wird berechnet
- 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);
}
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();
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().