Im Anhang eine Sammlung von Samples, die OwnerDrawing verwenden.
OwnerDrawing ist ein Prinzip, bei dem im PaintEvent mit dem Graphics des EventArgs die Zeichnung vollführt wird. Um OwnerDrawing zu verstehen muss man sich von der Vorstellung eines fertigzustellenden Bildes verabschieden - bei OD wird kein Bild fertiggestellt.
Sondern die Anwendung muss jederzeit bereitstehen, und die Zeichnung neu vollführen können.
Denn das Paint-Event kann sich jederzeit erneut ereignen, wann immer ein Bildschirm-Ausschnitt ungültig wird: Etwa wenn ein Fenster vergrößert wird, muss ja die hinzukommende Fläche gezeichnet werden. Aber ebenso auch, wenn Daten sich geändert haben.
Das ist vor allem eine Umstellung der Denkweise, ein wirklicher Mehr-Aufwand ist es nicht unbedingt. Denn ob man ein Bild nur einmal zeichnet, und dann ist es fertig, oder ob man es immer zeichnet, wenn das Event feuert - der Zeichnungs-Code muss so ode so geschrieben werden.
Dafür setzt uns das häufige Zeichnen in den Stand, die Darstellung zeitnah zu ändern, also in Interaktion mit dem User zu treten - Beispiel:
Gezieltes OwnerDrawing
WinForms' Grafik ist vergleichsweise langsam, gleicht aber viel aus durch die Möglichkeit, den Bildschirm nur ausschnittsweise zu aktualisieren.
Dazu ein paar Grundsätze:
Auf diese Weise löscht der darauf folgende Zeichnungsvorgang (im Paint-Event) die vorherige Darstellung, und malt dann die Neue hin.
Das erfolgt übrigens im selben Paint-Event, also mehrfaches Invalidieren führt nicht zu vielfach-redundanten ZeichenVorgängen.
Hier ein fast noch simpleres Sample - noch simpler, weil nichtmal ein DrawObjekt existiert, sondern alles findet im Form selbst statt:
labeled Triangle-Draw
(Ups! Dieses Beispiel ist auch deswegen so einfach, weil kein gezieltes OwnerDrawing stattfindet, sondern es wird das ganze Form invalidiert (zeile#21).)
Bildle von Listing#1 und Listing#2:
...
Canvas-Control
Wie man sieht, kann Zeichnungs-Code komplex werden, und noch viel komplexer kann die Interaktions-Logik werden, die auf Maus und evtl. auch Tasten reagiert.
Dann ist geraten, ein eigenes Control zu entwickeln - ich nenne die Dinger immer "Canvas" - Zeichenfläche.
Dieses Control kann man nach Kompilieren aus der Toolbox aufs Form ziehen, und hat so Zuständigkeiten leidlich gut eingeteilt (ein Form hat ja meist noch wesentlich mehr zu tun, als sich um Zeichnungs-Angelegenheiten zu kümmern).
Hier ein Cuboid-(Canvas-)Control, was einen Quader mit einstellbaren Kantenlängen zeichnet - ist schon recht umfangreich, deswegen im Spoiler
Cuboid-Canvas-Class
Hinweisen wollte ich damit auf die Klassen Matrix und GraphicsPath.
Eine Matrix kann geometrische Transformationen speichern, also Drehen, zoomen, verschieben, scheren.
Ein GraphicsPath kann eine Zeichnung speichern, sodass sie entweder als Outlined Figur wiedergegeben werden kann, oder ausgefüllt.
Das Cuboid-Control konstruiert bei Änderungen also einen GraphicsPath, in den die komplette Zeichnung hineingeht.
Die Konstruktion gliedert sich in 2 Teile: Zum einen die richtige Anordnung der 7 Punkte, und die .DrawLine - Befehle, denen die Punkte in richtiger Reihenfolge zu übergeben sind, dass ein Quader draus wird. Zum anderen die Skalierung der Zeichnung, entsprechend dem eingestellten SizeMode.
Sind diese Vorbereitungen getan, so ist das Zeichnen selbst denkbar lapidar:
Zu beachten, dass es sich hier nicht um das Paint-Event handelt, sondern um die OnPaint-Überschreibung. Ist im Endeffekt dasselbe, insbesondere das Eventargs ist dasselbe.
MultiItem-Controls
Oft will man eine Liste von Daten grafisch darstellen. In derlei Fällen empfiehlt es sich, zusätzlich zum Canvas auch noch eine Figur-Klasse anzulegen. Von diesen Figuren kann das Canvas dann eine Liste beinhalten und darstellen.
Dabei ist meist günstig, wenn die Figur-Klasse sich einfach selbst zeichnet. Das Canvas verarbeitet das Paint-Event/Override, und ruft darin alle Figuren auf, sich zu zeichnen.
Card-Figure-Class
Wir haben wieder den Drei-schritt in ApplyChanges, wo der vorherige und der neue Bereich invalidiert werden:
Und wieder ist das Zeichnen recht einfach - diesmal wird hauptsächlich eine Bitmap gedrawt:
Allerdings wird hier ausserdem noch ein Rechteck gezeichnet, um zu markieren, wenn diese Karte selektiert ist.
Ausserausserdem hat ApplyChanges ja evtl. die Bounds vergrößert, sodass die Zeichnung im Draw insgesamt vergrößert ausfällt - als Hover-Indikator.
Das mit den Karten hab ich sogar zweimal programmiert - obiges zeigt die Karten einfach der Reihen nach, und wenn eine Zeile voll ist, die nächste Reihe. Ausserdem kann man auch Karten rauslöschen, dann rücken die anderen Karten entsprechend zusammen.
Sowas ist aber Angelegenheit des Canvas-Controls, und ist nicht ganz trivial, die Verwaltung aller Karten, der selektierten und der gehoverten funktional korrekt zu managen:
Card-Canvas
Wie gut, dass wir ein Canvas haben, was diese Funktionalität zusammenhält - sowas möchte man ja nicht unbedingt in seinem Form rumfahren haben.
Noch komplizierter wird, wenn die Karten nicht (nur) automatisch angeordnet werden, sondern auch mit der Maus zu draggen sind:
Card-Canvas mit Drag und AutoScroll
In diesen Canvas sind auch Scrollbars vorgesehen, daher wird im Code mit AutoScrollMinSize und AutoScrollPosition umgegangen, und die Zeichnungen müssen auch einen Offset berücksichtigen:
Ausserdem wird auch ein Namenszug gezeichnet, auf weissem Untergrund (zeile#21).
Diese Card muss jetzt recht viel vom Canvas wissen, auf dem sie sich zeichnet: Den Font, die AutoScrollPosition, die Invalidate-Methode sowieso. Daher übergebe ich einfach der Figur einen Verweis auf "ihr" Canvas (findet im Canvas statt, im Cards_CollectionChanged-Event).
Die Bildle:
...
Im linken layoutet das Canvas die Cards zeilenweise, wie Platz ist. Die erste Card ist als selekted berahmt (naja, rosa), und die letzte Card ist hovered/vergrößert.
Im rechten Canvas hat der User die Cards durch Dragging angeordnet. "deutschland" ist selected (rosa Rahmen) und "bulgarien" gehovert (blauer Rahmen - nicht vergrößert).
OwnerDrawnGrid
Ist wieder eine andere Anforderung, und wieder muss man OwnerDrawing etwas umstrukturieren.
Bei Grid zeichnet sich die Figur nicht selbst, sondern wird gezeichnet, als Zell-Inhalt, dahin und in der Größe, wie es die Gegebenheiten (Abmasse des Canvas, Anzahl Reihen und Spalten) erfordern.
Ansonsten gibts auch hier wieder die Anforderungen von Selected und Hovered, nur hab ich diesmal MultiSelection (gelber Zell-Background) reingebastelt:
Die Anlage des OwnerDrawnGrid ist recht flexibel, was es angeht, was in die Zellen reinkommt.
OwnerDrawnGrid
zu beachten, dass ich die Zellen nicht nach X und Y addressiere, sondern einfach nach Point - ein Point enthält ja X und Y.
Clock-Control
Zum Schluss noch ein Clock-Control, das arbeitet viel mit Matrix und GraphicsPath.
ZB ein Uhrzeiger ist eine recht komplizierte Figur:
(naja, evtl. wäre einen custom-Pen einzurichten einfacher).
Jedenfalls diesen GraphicsPath kann ich klonen, strecken, und so leiten sich aus dem Stunden-Zeiger auch Minuten- und Sekunden-Zeiger ab.
Aber das Uhr-Sample enthält auch ein "Color-Generator"-Gimmick, womit - wenn aktiviert - der Invalidierte Bereich farblich markiert wird.
Auf diese Weise wird (wörtlich!) vor Augen geführt, dass der invalidierte Bereich auch bei so einer großen Figur wie einem SekundenZeiger doch wesentlich kleiner ist, als die Gesamtfläche. Und dementsprechend weniger Resourcen werden gebraucht.
Bildle ohne und mit ColorGenerator:
.....
Zusammenfassung
OwnerDrawing deckt einen enorm weiten Bereich unterschiedlichster Anforderungen ab.
Knackpunkt ist meist das Umdenken, von der Vorstellung eines Bildes, was man fertigstellt hin zu einer Datenhaltung, die das Bild jederzeit reproduzieren kann.
Je nach Anforderung sind unterschiedliche Architekturen gefragt, einfache Sachen kann man auch in einem Paint-Event abhandeln, ohne ein besonderes Control dafür zu erfinden.
Will man aber User-Interaktion bereitstellen, hovern, selektieren, löschen, draggen, dann wächst die notwendige Logik schnell an, und ein eigenes Control anzulegen wird sinnvoll.
Will man auch eine Mehrzahl von Figuren präsentieren empfiehlt sich meist auch eine besondere Figur-Klasse, die sich ihre Abmasse selbst merkt, und die sich auch selbst zeichnen kann.
Aber eben auch nicht immer, wie das OwnerDrawnGrid-Beispiel zeigt.
Es sind auch noch komplexere Szenarien denkbar, etwa dass verschiedene Figur-Klassen (für Text, Zeichnungen, Bilder) gemeinsam auf einem Canvas präsentiert werden: gezieltes OwnderDrawing
Oder mit Datenverarbeitung verknüpftes OwnerDrawing, sodass man die Gebilde auch abspeichern kann: AdvancedPainting.
Evtl. wird man geneigt sein, sich für OwnerDrawing Pattern anzueignen, oder diese sogar codifizieren durch entsprechende Klassen, Basisklassen, Interfaces. Derlei Ansätze sind für ein Szenario evtl. nützlich, erweisen sich aber oft beim nächsten schon wieder als unzulänglich - die berühmte "eierlegende Wollmilchsau" kann nicht gefunden werden.
Man muss eben vor allem das Prinzip mit dem gezielten Invalidieren kennen, man sollte Matrix und GraphicsPath bedienen können, und dann ist je nach Anforderung zu entwickeln, vorzugsweise natürlich angelehnt an ähnliche Lösungen - wenn vorhanden.
Generell ist WinForms für Dauer-Animationen mit fließenden Bewegungen (Filme, Laufschriften, Chart-Ticker) schlecht geeignet, und je größer die animierte Fläche und je höher Bildwechsel-Frquenz der Animation wird im Grunde unverhältnismäßig viel Cpu-Ressource verbraten.
OwnerDrawing ist ein Prinzip, bei dem im PaintEvent mit dem Graphics des EventArgs die Zeichnung vollführt wird. Um OwnerDrawing zu verstehen muss man sich von der Vorstellung eines fertigzustellenden Bildes verabschieden - bei OD wird kein Bild fertiggestellt.
Sondern die Anwendung muss jederzeit bereitstehen, und die Zeichnung neu vollführen können.
Denn das Paint-Event kann sich jederzeit erneut ereignen, wann immer ein Bildschirm-Ausschnitt ungültig wird: Etwa wenn ein Fenster vergrößert wird, muss ja die hinzukommende Fläche gezeichnet werden. Aber ebenso auch, wenn Daten sich geändert haben.
Das ist vor allem eine Umstellung der Denkweise, ein wirklicher Mehr-Aufwand ist es nicht unbedingt. Denn ob man ein Bild nur einmal zeichnet, und dann ist es fertig, oder ob man es immer zeichnet, wenn das Event feuert - der Zeichnungs-Code muss so ode so geschrieben werden.
Dafür setzt uns das häufige Zeichnen in den Stand, die Darstellung zeitnah zu ändern, also in Interaktion mit dem User zu treten - Beispiel:
VB.NET-Quellcode: Mouse-Line-Draw
- ''' <summary>
- ''' zeichnet eine Linie vom Mouse-Down-Punkt zur aktuellen MousePosition bei gehaltenem LinksButton
- ''' </summary>
- Public Class frmDrawSimple
- Private _Pt0 As Point
- Private _DrawObject As New DrawObject
- Private Sub frmDrawSimple_MouseDown(ByVal sender As Object, ByVal e As MouseEventArgs) _
- Handles Me.MouseDown
- _Pt0 = e.Location
- End Sub
- Private Sub frmDrawSimple_MouseMove(ByVal sender As Object, ByVal e As MouseEventArgs) _
- Handles Me.MouseMove
- If e.Button = Windows.Forms.MouseButtons.Left Then
- Me.Invalidate(_DrawObject.GetRange) 'Neuzeichnen des alten Ranges anstoßen
- _DrawObject.SetLine(_Pt0, e.Location) ' ZeichnungsDaten ändern
- Me.Invalidate(_DrawObject.GetRange) 'Neuzeichnen des neuen Ranges anstoßen
- End If
- End Sub
- Private Sub frmDrawSimple_Paint(ByVal sender As Object, ByVal e As PaintEventArgs) Handles Me.Paint
- _DrawObject.Draw(e.Graphics)
- 'die Debug-Meldung meldet jeden Zeichnungsvorgang, ob nun innerhalb der Anwendung ausgelöst oder durch externe Vorgänge (Verdeckung durch andere Anwendungen)
- 'Später entfernen, denn Debug ist sehr langsam
- Static Counter As Integer = 0
- Debug.WriteLine(String.Concat("Paint", Counter))
- Counter += 1
- End Sub
- End Class
Gezieltes OwnerDrawing
WinForms' Grafik ist vergleichsweise langsam, gleicht aber viel aus durch die Möglichkeit, den Bildschirm nur ausschnittsweise zu aktualisieren.
Dazu ein paar Grundsätze:
- Nur im Paint-Event zeichnen, und nur mit dem dort übergebenen Graphics.
Bei Multi-Items: Immer alle ZeichenObjekte zeichnen - auch die, die garnet sichtbar sind, oder unverändert geblieben.
Die Auswahl, was tatsächlich gezeichnet wird, erfolgt über das Invalidieren. Und Code, der überflüssigerweise an Positionen zeichnet, die nicht zuvor invalidiert wurden, wird intern abgebrochen und kostet (fast) keine Performance. - Zeichnungs-Informationen merken.
- Veränderungen im Dreischritt umsetzen:
- alten Zeichnungsbereich invalidieren
- Zeichnungs-Daten verändern
- neuen Zeichnungsbereich invalidieren
_MouseMove()
, Zeilen #17 - 19: da wird nichts gezeichnet, sondern es werden nur Zeichnungs-Daten geändert und Zeichnungsvorgänge angestoßen, für genau die Bereiche, die von der Änderung betroffen sind - nicht mehr. Betroffen sind genau zwei Bereiche, mämlich die Fläche, wo die Zeichnung vorher war, und die Fläche, wo sie nach der Änderung ist.Auf diese Weise löscht der darauf folgende Zeichnungsvorgang (im Paint-Event) die vorherige Darstellung, und malt dann die Neue hin.
Das erfolgt übrigens im selben Paint-Event, also mehrfaches Invalidieren führt nicht zu vielfach-redundanten ZeichenVorgängen.
Hier ein fast noch simpleres Sample - noch simpler, weil nichtmal ein DrawObjekt existiert, sondern alles findet im Form selbst statt:
VB.NET-Quellcode
- ' zeichnet ein mit Ecken- und Kanten-Beschriftungen versehenes Dreieck
- Public Class Form1
- Private ptA, ptB, ptC As PointF
- Private fFont As New Font("Arial", 10.0)
- Private bBrush As New SolidBrush(Color.Green)
- Public Sub New()
- InitializeComponent()
- DoubleBuffered = True
- Form1_MouseMove(Me, New MouseEventArgs(Windows.Forms.MouseButtons.Left, 0, 20, 20, 0))
- End Sub
- Private Sub Form1_MouseMove(sender As Object, e As MouseEventArgs) Handles Me.MouseMove
- If e.Button <> Windows.Forms.MouseButtons.Left Then Return
- With ClientSize
- ptA = New PointF(.Width / 2.0F, .Height / 2.0F)
- End With
- ptB = New PointF(e.X, e.Y)
- ptC = New PointF(ptB.X, ptA.Y)
- Invalidate()
- End Sub
- Private Sub Form1_Paint(sender As Object, e As PaintEventArgs) Handles Me.Paint
- Dim points() As PointF = {ptA, ptB, ptC}
- e.Graphics.DrawPolygon(Pens.Red, points)
- e.Graphics.DrawString("A", fFont, bBrush, ptA)
- e.Graphics.DrawString("B", fFont, bBrush, ptB)
- e.Graphics.DrawString("C", fFont, bBrush, ptC)
- e.Graphics.DrawString("a", fFont, bBrush, middleSite(ptB, ptC))
- e.Graphics.DrawString("b", fFont, bBrush, middleSite(ptA, ptC))
- e.Graphics.DrawString("c", fFont, bBrush, middleSite(ptA, ptB))
- End Sub
- Private Shared Function middleSite(ByVal a As PointF, ByVal b As PointF) As PointF
- Return New PointF(0.5F * (a.X + b.X), 0.5F * (a.Y + b.Y))
- End Function
- End Class
...
Canvas-Control
Wie man sieht, kann Zeichnungs-Code komplex werden, und noch viel komplexer kann die Interaktions-Logik werden, die auf Maus und evtl. auch Tasten reagiert.
Dann ist geraten, ein eigenes Control zu entwickeln - ich nenne die Dinger immer "Canvas" - Zeichenfläche.
Dieses Control kann man nach Kompilieren aus der Toolbox aufs Form ziehen, und hat so Zuständigkeiten leidlich gut eingeteilt (ein Form hat ja meist noch wesentlich mehr zu tun, als sich um Zeichnungs-Angelegenheiten zu kümmern).
Hier ein Cuboid-(Canvas-)Control, was einen Quader mit einstellbaren Kantenlängen zeichnet - ist schon recht umfangreich, deswegen im Spoiler
VB.NET-Quellcode
- Imports System.Drawing.Drawing2D
- Imports System.ComponentModel
- ''' <summary> Displays the Cuboid of Dimensions: W, H, D as 3d-Drawing </summary>
- <DesignerCategory("code"), DefaultEvent("Paint")> _
- Public Class CuboidControl : Inherits Control
- Private _Gp As New GraphicsPath
- Private _Pen As New Pen(Brushes.AliceBlue, 1)
- Protected Overrides Sub OnHandleCreated(e As EventArgs)
- MyBase.OnHandleCreated(e)
- _Pen.Color = ForeColor
- MyBase.SetStyle(ControlStyles.ResizeRedraw, True)
- UpdateCuboid()
- End Sub
- Protected Overrides Sub OnForeColorChanged(e As EventArgs)
- _Pen.Color = ForeColor
- MyBase.OnForeColorChanged(e)
- End Sub
- Protected Overrides Sub OnPaint(e As PaintEventArgs)
- MyBase.OnPaint(e)
- e.Graphics.DrawPath(_Pen, _Gp)
- End Sub
- Protected Overrides Sub OnSizeChanged(e As EventArgs)
- MyBase.OnSizeChanged(e)
- UpdateCuboid()
- End Sub
- Private Sub UpdateCuboid()
- #If False Then
- __
- /__/|
- |__|/
- die Zeichnung startet mitte links, läuft die 4 Front-Punkte ab, nochmal durch den Startpunkt und dann die 3 Back-Punkte, und Anschluss an pts(2) (front unten-rechts). Dann noch die fehlende Kante oben rechts.
- Da 2 Punkte doppelt durchlaufen werden pts(0), pts(2), sind dem GP insgesamt also 9 pts anzugeben.
- Die 3 BackPoints sind Kopien der ersten 3 frontPoints, aber für Fluchtpunkt-Perspektive nach Strahlensatz etwas verkleinert und um D/2 45° verschoben.
- Die Definition der frontPoints ist relativ zum Front-Mittelpunkt
- #End If
- _Gp.Reset()
- Dim ptsFront = {New PointF(-W, -H), New PointF(W, -H), New PointF(W, H), New PointF(-W, H), New PointF(-W, -H)}
- Dim ptsBack = ptsFront.Take(3).ToArray
- 'Diese Berechnungen stellen v.a. immer eine Perspektive her, bei der 3 Seiten des Quaders sichtbar sind. Die Tiefen-Darstellung ist geometrisch inkorrekt.
- Dim d = _D * 0.5F 'perspektivisch verkürzen
- Dim lFluchtpunktDistanz = Math.Max(W, H) * 2
- lFluchtpunktDistanz = {W, H, d}.Max * 2
- Dim scale = Math.Max(0.0F, 1.0F - d / lFluchtpunktDistanz) ' Strahlensatz-Skalierung, darf aber nicht negativ werden
- Using mtrBackPoints = New Matrix
- mtrBackPoints.Scale(scale, scale, MatrixOrder.Append) 'verkleinern
- mtrBackPoints.Translate(d, -d, MatrixOrder.Append) 'verschieben in Fluchtpunkt-Richtung: -45°
- mtrBackPoints.TransformPoints(ptsBack)
- End Using
- Dim pts(8) As PointF
- Array.Copy(ptsFront, pts, 5)
- Array.Copy(ptsBack, 0, pts, 5, 3)
- pts(8) = pts(2)
- _Gp.AddLines(pts)
- _Gp.AddLine(pts(1), pts(6)) 'die fehlende Kante, die nicht in durchgezogener Linie gezeichnet werden konnte.
- ScaleFigure()
- End Sub
- Private Sub ScaleFigure()
- #If False Then
- Verwendet wird der Matrix-Konstruktor, der ein source-Rectangle auf einen durch 3 Punkte aufgespannten Trapezoid projeziert. Die TrapezPunkte sind so gewählt, dasses genau die Control-Fläche ist.
- Herausforderung ist nun, aus den Bounds des Graphicspaths ein Drumrum-Rechteck zu abzuleiten, dessen Projektion dann die ImageLayout-Bedeutungen (s. Objectbrowser) richtig umsetzt.
- zb .None und .Center erzeugen ein rctFigureSurround, genauso groß wie das Control - sodass keinerlei Vergrößerung/Verkleinerung eintritt.
- Bei .Center ist rctFigureSurround aber eine Vergrößerung in alle Richtungen (Inflate), sodass die Figur mittenzentriert bleibt.
- Auch .Stretch ist ein Inflate, aber auf ein umschließendes Quadrat. Das ist fragwürdig, denn man hätte auch die Original-Bounds belassen können. Nur bei sonem "Real-Stretch" wären überhaupt keine Höhe/Breite-Proportion der Zeichnung mehr erkennbar - daher wird hier der Quadrat-Rahmen als Bezugssystem eingeführt.
- .Zoom inflated die Bounds genau so, dass das Seitenverhältnis (w/h) identisch ist mit dem des Controls. Das ergibt dann eine verzerrungsfreie Vergrößerung.
- Zu beachten auch das 10% Padding, damit die Zeichnung nicht an die Wände stößt.
- #End If
- Dim rctFigureSurround = _Gp.GetBounds
- With rctFigureSurround
- Select Case _ScaleMode
- Case ImageLayout.None 'bnds vergrößern auf Control-Masse
- .Size = New SizeF(Width, Height)
- Case ImageLayout.Center 'bnds inflaten auf Control-Masse
- .Inflate((Width - .Width) * 0.5F, (Height - .Height) * 0.5F)
- Case ImageLayout.Zoom 'bnds inflaten auf ein Rechteck mit gleichem Seitenverhältnis wie das Control
- Dim edgeRatio = Width / CSng(Height)
- If Width / .Width < Height / .Height Then
- .Inflate(0, (.Width / edgeRatio - .Height) * 0.5F)
- Else
- .Inflate((.Height * edgeRatio - .Width) * 0.5F, 0)
- End If
- Case ImageLayout.Tile
- _Gp.Reset()
- _Gp.AddString("ImageLayout.Tile wird nicht unterstützt", Font.FontFamily, Font.Style, Font.Size, rctFigureSurround, StringFormat.GenericTypographic)
- Case ImageLayout.Stretch
- 'bnds inflaten aufs Quadrat-Form. Dieser Stretch nimmt an, dass die Figur als in ein Quadratisches Bezugssystem eingezeichnet gemeint ist
- If .Width > .Height Then
- .Inflate(0, .Width - .Height)
- Else
- .Inflate(.Height - .Width, 0)
- End If
- End Select
- Dim Padding = Math.Max(.Width, .Height) * 0.1F
- .Inflate(Padding, Padding)
- End With
- Using mtr = New Matrix(rctFigureSurround, {Point.Empty, New PointF(Width, 0), New PointF(0, Height)})
- 'die Matrix transformiert rctMatrixSource dann auf Control-Masse
- _Gp.Transform(mtr)
- End Using
- End Sub
- #Region "Public Props W, H, D, Scalemode"
- Private _D As Single = 1
- <RefreshProperties(RefreshProperties.Repaint), Bindable(True)> _
- Public Property D() As Single
- Get
- Return _D
- End Get
- Set(ByVal value As Single)
- If _D = value Then Return
- _D = value
- UpdateCuboid()
- Invalidate()
- End Set
- End Property
- Private _H As Single = 1
- <RefreshProperties(RefreshProperties.Repaint), Bindable(True)> _
- Public Property H() As Single
- Get
- Return _H
- End Get
- Set(ByVal value As Single)
- If _H = value Then Return
- _H = value
- UpdateCuboid()
- Invalidate()
- End Set
- End Property
- Private _W As Single = 1
- <RefreshProperties(RefreshProperties.Repaint), Bindable(True)> _
- Public Property W() As Single
- Get
- Return _W
- End Get
- Set(ByVal value As Single)
- If _W = value Then Return
- _W = value
- UpdateCuboid()
- Invalidate()
- End Set
- End Property
- Private _ScaleMode As ImageLayout = ImageLayout.Stretch
- <RefreshProperties(RefreshProperties.Repaint), Bindable(True)> _
- Public Property ScaleMode() As ImageLayout
- Get
- Return _ScaleMode
- End Get
- Set(ByVal value As ImageLayout)
- If _ScaleMode = value Then Return
- _ScaleMode = value
- UpdateCuboid()
- Invalidate()
- End Set
- End Property
- #End Region 'Public Props W, H, D, Scalemode
- End Class
Eine Matrix kann geometrische Transformationen speichern, also Drehen, zoomen, verschieben, scheren.
Ein GraphicsPath kann eine Zeichnung speichern, sodass sie entweder als Outlined Figur wiedergegeben werden kann, oder ausgefüllt.
Das Cuboid-Control konstruiert bei Änderungen also einen GraphicsPath, in den die komplette Zeichnung hineingeht.
Die Konstruktion gliedert sich in 2 Teile: Zum einen die richtige Anordnung der 7 Punkte, und die .DrawLine - Befehle, denen die Punkte in richtiger Reihenfolge zu übergeben sind, dass ein Quader draus wird. Zum anderen die Skalierung der Zeichnung, entsprechend dem eingestellten SizeMode.
Sind diese Vorbereitungen getan, so ist das Zeichnen selbst denkbar lapidar:
MultiItem-Controls
Oft will man eine Liste von Daten grafisch darstellen. In derlei Fällen empfiehlt es sich, zusätzlich zum Canvas auch noch eine Figur-Klasse anzulegen. Von diesen Figuren kann das Canvas dann eine Liste beinhalten und darstellen.
Dabei ist meist günstig, wenn die Figur-Klasse sich einfach selbst zeichnet. Das Canvas verarbeitet das Paint-Event/Override, und ruft darin alle Figuren auf, sich zu zeichnen.
VB.NET-Quellcode
- Public Class Card
- Private Shared HalfSize As New Size(45, 30)
- Private Shared HoverSize As Integer = 15
- Public Shared ReadOnly CardSize As New Size(HalfSize.Width * 2, HalfSize.Height * 2)
- Public Shared ReadOnly CardRect As New Rectangle(New Point(-HalfSize.Width, -HalfSize.Height), CardSize)
- Public Shared HalfHoverSize As New Size(HalfSize.Width + HoverSize, HalfSize.Height + HoverSize)
- Public Shared ReadOnly CardHoverSize As New Size(HalfHoverSize.Width * 2, HalfHoverSize.Height * 2)
- Public Shared ReadOnly CardHoverRect As New Rectangle(New Point(-HalfHoverSize.Width, -HalfHoverSize.Height), CardHoverSize)
- Private Shared _SelectionPen As New Pen(Color.FromArgb(&HFFFF9999), 4)
- Public ReadOnly Image As Image
- Public ReadOnly Name As String
- Public InvalidateCallback As Action(Of Rectangle)
- Private _isselected As Boolean
- Private _ishovered As Boolean
- Private _Location As Point
- Public Property Location As Point
- Get
- Return _Location
- End Get
- Set(value As Point)
- If _Location = value Then Return
- _Location = value
- ApplyChanges()
- End Set
- End Property
- Private _Bounds As New Rectangle(-2, -2, 2, 2)
- Public ReadOnly Property Bounds() As Rectangle
- Get
- Return _Bounds
- End Get
- End Property
- Private Sub ApplyChanges()
- Dim newBounds = If(_ishovered, CardHoverRect, CardRect)
- newBounds.Offset(Location)
- If newBounds <> _Bounds Then InvalidateCallback(_Bounds)
- _Bounds = newBounds
- InvalidateCallback(_Bounds)
- End Sub
- Public Sub Draw(g As Graphics)
- g.DrawImage(Image, _Bounds)
- If _isselected Then
- Dim rct = _Bounds
- rct.Inflate(-2, -2)
- g.DrawRectangle(_SelectionPen, rct)
- End If
- End Sub
- Public Property Ishovered() As Boolean
- Get
- Return _ishovered
- End Get
- Set(ByVal value As Boolean)
- If _ishovered = value Then Return
- _ishovered = value
- ApplyChanges()
- End Set
- End Property
- Public Property IsSelected() As Boolean
- Get
- Return _isselected
- End Get
- Set(ByVal value As Boolean)
- If _isselected = value Then Return
- _isselected = value
- ApplyChanges()
- End Set
- End Property
- Public Sub New(name As String, image As Image)
- Me.Name = name : Me.Image = image
- End Sub
- End Class
Ausserausserdem hat ApplyChanges ja evtl. die Bounds vergrößert, sodass die Zeichnung im Draw insgesamt vergrößert ausfällt - als Hover-Indikator.
Das mit den Karten hab ich sogar zweimal programmiert - obiges zeigt die Karten einfach der Reihen nach, und wenn eine Zeile voll ist, die nächste Reihe. Ausserdem kann man auch Karten rauslöschen, dann rücken die anderen Karten entsprechend zusammen.
Sowas ist aber Angelegenheit des Canvas-Controls, und ist nicht ganz trivial, die Verwaltung aller Karten, der selektierten und der gehoverten funktional korrekt zu managen:
VB.NET-Quellcode
- Imports System.Collections.ObjectModel
- Imports System.Collections.Specialized
- Imports System.ComponentModel
- <DesignerCategory("code"), DefaultEvent("Paint")> _
- Public Class Canvas : Inherits Control
- Private WithEvents _Cards As New ObservableCollection(Of Card)
- <DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)>
- Public ReadOnly Property Cards() As ObservableCollection(Of Card)
- Get
- Return _Cards
- End Get
- End Property
- <Category("Appearance")> _
- Public Shadows Property DoubleBuffered As Boolean
- Get
- Return MyBase.DoubleBuffered
- End Get
- Set(value As Boolean)
- MyBase.DoubleBuffered = value
- End Set
- End Property
- Private Sub Cards_CollectionChanged(sender As Object, e As NotifyCollectionChangedEventArgs) Handles _Cards.CollectionChanged
- Select Case e.Action
- Case NotifyCollectionChangedAction.Reset
- Case NotifyCollectionChangedAction.Add
- Dim crd = Cards(e.NewStartingIndex)
- crd.InvalidateCallback = AddressOf Invalidate
- Case NotifyCollectionChangedAction.Remove
- Dim crd = DirectCast(e.OldItems(0), Card)
- If crd Is _SelectedCard Then SelectedCard = Nothing
- If crd Is _HoveredCard Then HoveredCard = Nothing
- NewLayout()
- Case NotifyCollectionChangedAction.Move
- Case NotifyCollectionChangedAction.Replace
- End Select
- End Sub
- Public Sub NewLayout()
- Dim pt0 = New Point(Card.HalfHoverSize)
- Dim maxWidth = ClientSize.Width - Card.CardSize.Width * 2
- For i = 0 To _Cards.Count - 1
- _Cards(i).Location = pt0
- If pt0.X < maxWidth Then
- pt0.X += Card.CardSize.Width + 2
- Else
- pt0.X = Card.HalfHoverSize.Width
- pt0.Y += Card.CardSize.Height + 2
- End If
- Next
- End Sub
- Protected Overrides Sub OnPaint(e As PaintEventArgs)
- MyBase.OnPaint(e)
- For Each crd In _Cards
- If Not crd.Ishovered Then crd.Draw(e.Graphics)
- Next
- If _HoveredCard IsNot Nothing Then _HoveredCard.Draw(e.Graphics)
- End Sub
- Protected Overrides Sub OnMouseMove(e As MouseEventArgs)
- MyBase.OnMouseMove(e)
- HoveredCard = _Cards.FirstOrDefault(Function(crd) crd.Bounds.Contains(e.Location))
- End Sub
- Protected Overrides Sub OnMouseDown(e As MouseEventArgs)
- MyBase.OnMouseDown(e)
- If _HoveredCard IsNot Nothing Then SelectedCard = _HoveredCard
- End Sub
- Protected Overrides Sub OnMouseLeave(e As EventArgs)
- MyBase.OnMouseLeave(e)
- HoveredCard = Nothing
- End Sub
- Private _SelectedCard As Card
- Public Property SelectedCard() As Card
- Get
- Return _SelectedCard
- End Get
- Set(ByVal value As Card)
- If _SelectedCard Is value Then Return
- If _SelectedCard IsNot Nothing Then _SelectedCard.IsSelected = False
- _SelectedCard = value
- If _SelectedCard IsNot Nothing Then _SelectedCard.IsSelected = True
- End Set
- End Property
- Private _HoveredCard As Card
- Public Property HoveredCard() As Card
- Get
- Return _HoveredCard
- End Get
- Set(ByVal value As Card)
- If _HoveredCard Is value Then Return
- If _HoveredCard IsNot Nothing Then _HoveredCard.Ishovered = False
- _HoveredCard = value
- If _HoveredCard IsNot Nothing Then _HoveredCard.Ishovered = True
- End Set
- End Property
- End Class
Noch komplizierter wird, wenn die Karten nicht (nur) automatisch angeordnet werden, sondern auch mit der Maus zu draggen sind:
VB.NET-Quellcode
- Imports System.Collections.ObjectModel
- Imports System.Collections.Specialized
- Imports System.ComponentModel
- Imports System.Diagnostics
- <DesignerCategory("code"), DefaultEvent("Paint")> _
- Public Class Canvas : Inherits ScrollableControl
- Private _MouseOffset As Size
- Private WithEvents _Cards As New ObservableCollection(Of Card)
- <DesignerSerializationVisibility(DesignerSerializationVisibility.Hidden)>
- Public ReadOnly Property Cards() As ObservableCollection(Of Card)
- Get
- Return _Cards
- End Get
- End Property
- Protected Overrides Sub OnHandleCreated(e As EventArgs)
- MyBase.OnHandleCreated(e)
- Me.AutoScroll = True
- MyBase.DoubleBuffered = True
- End Sub
- Private Sub Cards_CollectionChanged(sender As Object, e As NotifyCollectionChangedEventArgs) Handles _Cards.CollectionChanged
- Select Case e.Action
- Case NotifyCollectionChangedAction.Reset
- Case NotifyCollectionChangedAction.Add
- Dim crd = Cards(e.NewStartingIndex)
- crd.Canvas = Me
- UpdateAutoScrollMinSize()
- Case NotifyCollectionChangedAction.Remove
- Dim crd = DirectCast(e.OldItems(0), Card)
- If crd Is _SelectedCard Then SelectedCard = Nothing
- If crd Is _HoveredCard Then HoveredCard = Nothing
- crd.Image.Dispose()
- UpdateAutoScrollMinSize()
- Case NotifyCollectionChangedAction.Move
- Case NotifyCollectionChangedAction.Replace
- End Select
- End Sub
- Public Sub NewLayout()
- Dim pt0 = New Point(Card.CardHoverRect.Right, Card.CardHoverRect.Bottom)
- For i = 0 To _Cards.Count - 1
- _Cards(i).Location = pt0
- pt0.X += Card.CardSize.Width + 2
- Next
- UpdateAutoScrollMinSize()
- End Sub
- Protected Overrides Sub OnPaint(e As PaintEventArgs)
- MyBase.OnPaint(e)
- For Each crd In _Cards
- If Not crd.Ishovered Then crd.Draw(e.Graphics)
- Next
- If _HoveredCard IsNot Nothing Then _HoveredCard.Draw(e.Graphics) ' _HoveredCard als alleroberste zeichnen
- End Sub
- Protected Overrides Sub OnMouseMove(e As MouseEventArgs)
- MyBase.OnMouseMove(e)
- Dim pt = e.Location - New Size(AutoScrollPosition)
- Dim newHovered = _Cards.LastOrDefault(Function(crd) crd.Bounds.Contains(pt))
- If _HoveredCard Is Nothing OrElse Not _HoveredCard.Bounds.Contains(pt) Then HoveredCard = newHovered
- If e.Button <> MouseButtons.Left OrElse _SelectedCard Is Nothing Then Return
- _SelectedCard.Location = e.Location - _MouseOffset
- End Sub
- Protected Overrides Sub OnMouseDown(e As MouseEventArgs)
- MyBase.OnMouseDown(e)
- If e.Button <> MouseButtons.Left OrElse _HoveredCard Is Nothing Then Return
- SelectedCard = _HoveredCard
- With _SelectedCard.Location
- _MouseOffset = New Size(e.X - .X, e.Y - .Y)
- End With
- End Sub
- Protected Overrides Sub OnMouseLeave(e As EventArgs)
- MyBase.OnMouseLeave(e)
- HoveredCard = Nothing
- End Sub
- Protected Overrides Sub OnMouseUp(e As MouseEventArgs)
- MyBase.OnMouseUp(e)
- UpdateAutoScrollMinSize()
- End Sub
- Private _SelectedCard As Card
- Public Property SelectedCard() As Card
- Get
- Return _SelectedCard
- End Get
- Set(ByVal value As Card)
- If _SelectedCard Is value Then Return
- If _SelectedCard IsNot Nothing Then _SelectedCard.IsSelected = False
- _SelectedCard = value
- If _SelectedCard IsNot Nothing Then ' _SelectedCard ans Ende von _Cards zu räumen bewirkt, dass sie als oberste gezeichnet wird
- Dim i = _Cards.IndexOf(_SelectedCard)
- _Cards.Move(i, _Cards.Count - 1)
- _SelectedCard.IsSelected = True
- End If
- End Set
- End Property
- Private _HoveredCard As Card
- Public Property HoveredCard() As Card
- Get
- Return _HoveredCard
- End Get
- Set(ByVal value As Card)
- If _HoveredCard Is value Then Return
- If _HoveredCard IsNot Nothing Then _HoveredCard.Ishovered = False
- _HoveredCard = value
- If _HoveredCard IsNot Nothing Then _HoveredCard.Ishovered = True
- End Set
- End Property
- Private Sub UpdateAutoScrollMinSize()
- Dim w = _Cards.Max(Function(c) c.Bounds.Right)
- Dim h = _Cards.Max(Function(c) c.Bounds.Bottom)
- Me.AutoScrollMinSize = New Size(w, h)
- End Sub
- Private Sub CardInvalidate(rct As Rectangle)
- rct.Offset(AutoScrollPosition)
- Invalidate(rct)
- End Sub
- End Class
VB.NET-Quellcode
- Public Sub Draw(g As Graphics)
- Dim offset = Canvas.AutoScrollPosition
- Dim rct = _Bounds
- rct.Offset(offset)
- If _ishovered Then
- rct.Inflate(-2, -2)
- g.DrawRectangle(_HoverPen, rct)
- rct.Inflate(-2, -2)
- End If
- Dim rct2 As RectangleF = rct
- g.DrawImage(Image, rct)
- If _isselected Then
- rct.Inflate(-2, -2)
- g.DrawRectangle(_SelectionPen, rct)
- End If
- Dim sz = g.MeasureString(Name, Canvas.Font, rct.Size, _SF) + New SizeF(2, 2)
- With rct2
- .Offset((.Width - sz.Width) / 2.0F, .Height - sz.Height)
- .Size = sz
- End With
- g.FillRectangle(Brushes.White, rct2)
- g.DrawString(Name, Canvas.Font, Brushes.Black, rct2, _SF)
- End Sub
Diese Card muss jetzt recht viel vom Canvas wissen, auf dem sie sich zeichnet: Den Font, die AutoScrollPosition, die Invalidate-Methode sowieso. Daher übergebe ich einfach der Figur einen Verweis auf "ihr" Canvas (findet im Canvas statt, im Cards_CollectionChanged-Event).
Die Bildle:
...
Im linken layoutet das Canvas die Cards zeilenweise, wie Platz ist. Die erste Card ist als selekted berahmt (naja, rosa), und die letzte Card ist hovered/vergrößert.
Im rechten Canvas hat der User die Cards durch Dragging angeordnet. "deutschland" ist selected (rosa Rahmen) und "bulgarien" gehovert (blauer Rahmen - nicht vergrößert).
OwnerDrawnGrid
Ist wieder eine andere Anforderung, und wieder muss man OwnerDrawing etwas umstrukturieren.
Bei Grid zeichnet sich die Figur nicht selbst, sondern wird gezeichnet, als Zell-Inhalt, dahin und in der Größe, wie es die Gegebenheiten (Abmasse des Canvas, Anzahl Reihen und Spalten) erfordern.
Ansonsten gibts auch hier wieder die Anforderungen von Selected und Hovered, nur hab ich diesmal MultiSelection (gelber Zell-Background) reingebastelt:
Die Anlage des OwnerDrawnGrid ist recht flexibel, was es angeht, was in die Zellen reinkommt.
VB.NET-Quellcode
- #Region "FileHeader"
- #If False Then
- Zur Addressierung der Zellen wird hier nicht X, Y genutzt, sondern Point, was ja X und Y enthält. Darf man halt nicht verwechseln mit tatsächlichen BildschirmPunkten. Also ein pos(0, 3) addressiert die Zelle inne 1.Reihe, 4.Zeile.
- Das GridCanvas hat einstellbare Row-/Column-Count, skaliert die Zeichnung, dass das Gitter das Control auch ausfüllt, hovert die Zelle unter der Maus, bietet MultiSelection.
- Mit wenigen Änderungen - an Sub Draw und anne CellData-Klasse - liesse sich auch ganz was anderes darstellen (etwa Bilder, oder Texte, oder Figuren aller Art) als nur die Durch-Numerierung der Zellen.
- Zu beachten ist, dass ein CellData **nicht** seine Bounds kennt, und sich auch nicht selber zeichnet. Grund ist, dass die Zell-Abmasse überhaupt nicht von der Zelle selbst abhängen, sondern von der Größe des Controls, der Anzahl Reihen/Spalten, und von der Zell-Addresse (dem pos).
- Das sind alles Parameter, die im Control verwaltet werden, nicht in der Zelle, und deswegen bleibt die Zelle "dumm".
- #End If '-- Options, Imports
- #End Region 'FileHeader
- <System.ComponentModel.DesignerCategory("Code")> _
- Public Class GridCanvas : Inherits Control
- Private Shared _SF As New StringFormat With {.Alignment = StringAlignment.Center, .LineAlignment = StringAlignment.Center}
- Private _DrawSize, _CellSize As Size
- Private _Cells()() As CellData
- Private Shared _HoverPen As New Pen(Color.FromArgb(&HFFFF9999), 4)
- Private Shared _NoPoint As New Point(-1, -1)
- Private _HoveredPos As Point = _NoPoint
- Public Property HoveredPos() As Point
- Get
- Return _HoveredPos
- End Get
- Set(ByVal value As Point)
- If _HoveredPos = value Then Return
- If _HoveredPos <> _NoPoint Then Invalidate(GetBounds(_HoveredPos))
- _HoveredPos = value
- If _HoveredPos <> _NoPoint Then Invalidate(GetBounds(_HoveredPos))
- End Set
- End Property
- Public ReadOnly Property Cell(pos As Point) As CellData
- Get
- Return _Cells(pos.Y)(pos.X)
- End Get
- End Property
- Private _Columns As Integer = 7
- Public Property Columns() As Integer
- Get
- Return _Columns
- End Get
- Set(ByVal value As Integer)
- If _Columns = value Then Return
- _Columns = value
- ChangeCellCount()
- End Set
- End Property
- Private _Rows As Integer = 7
- Public Property Rows() As Integer
- Get
- Return _Rows
- End Get
- Set(ByVal value As Integer)
- If _Rows = value Then Return
- _Rows = value
- ChangeCellCount()
- End Set
- End Property
- Private Sub ChangeCellCount()
- ReDim _Cells(_Rows - 1)
- For y = 0 To _Rows - 1
- ReDim _Cells(y)(_Columns - 1)
- For x = 0 To _Columns - 1
- _Cells(y)(x) = New CellData With {.Value = y * _Columns + x + 1}
- Next
- Next
- InitSizes()
- Invalidate()
- End Sub
- Public Function GetSelectedPositions() As IEnumerable(Of Point)
- Return GetPositions.Where(Function(pos) Cell(pos).Checked)
- End Function
- Public Function GetPositions() As IEnumerable(Of Point)
- Return From y In Enumerable.Range(0, _Rows), x In Enumerable.Range(0, _Columns) Select New Point(x, y)
- End Function
- Public Sub New()
- SetStyle(ControlStyles.OptimizedDoubleBuffer Or ControlStyles.ResizeRedraw, True)
- End Sub
- Protected Overrides Sub OnMouseMove(e As MouseEventArgs)
- MyBase.OnMouseMove(e)
- HoveredPos = GetPositionFromPoint(e.Location)
- End Sub
- Protected Overrides Sub OnMouseLeave(e As EventArgs)
- MyBase.OnMouseLeave(e)
- HoveredPos = _NoPoint
- End Sub
- Protected Overrides Sub OnMouseDown(e As MouseEventArgs)
- MyBase.OnMouseDown(e)
- Dim pos = GetPositionFromPoint(e.Location)
- Cell(pos).Checked = Not Cell(pos).Checked
- Invalidate(GetBounds(pos))
- End Sub
- 'Protected InvalidateCell(
- Protected Overrides Sub OnHandleCreated(e As EventArgs)
- MyBase.OnHandleCreated(e)
- ChangeCellCount()
- End Sub
- Private Function GetPositionFromPoint(pos As Point) As Point
- Dim i = 0
- For i = 1 To _Columns - 1
- If pos.X <= i * _CellSize.Width Then Exit For
- Next
- pos.X = i - 1
- For i = 1 To _Rows - 1
- If pos.Y <= i * _CellSize.Height Then Exit For
- Next
- pos.Y = i - 1
- Return pos
- End Function
- Private Function GetBounds(pos As Point) As Rectangle
- Return New Rectangle(New Point(pos.X * _CellSize.Width, pos.Y * _CellSize.Height), _CellSize)
- End Function
- Protected Overrides Sub OnSizeChanged(e As EventArgs)
- MyBase.OnSizeChanged(e)
- InitSizes()
- End Sub
- Private Sub InitSizes()
- _DrawSize = Me.ClientSize - New Size(1, 1)
- _CellSize = New Size(_DrawSize.Width \ _Columns, _DrawSize.Height \ _Rows)
- _DrawSize = New Size(_CellSize.Width * _Columns, _CellSize.Height * _Rows)
- End Sub
- Protected Overrides Sub OnPaint(e As PaintEventArgs)
- MyBase.OnPaint(e)
- For x = 0 To _DrawSize.Width Step _CellSize.Width
- e.Graphics.DrawLine(Pens.Black, x, 0, x, _DrawSize.Height)
- Next
- For y = 0 To _DrawSize.Height Step _CellSize.Height
- e.Graphics.DrawLine(Pens.Black, 0, y, _DrawSize.Width, y)
- Next
- For Each pos In GetPositions()
- Dim rct = GetBounds(pos)
- rct.Inflate(-1, -1)
- Dim cll = Cell(pos)
- If cll.Checked Then e.Graphics.FillRectangle(Brushes.Yellow, rct)
- e.Graphics.DrawString(cll.Value.ToString, Font, Brushes.Black, rct, _SF)
- If pos = _HoveredPos Then
- rct.Inflate(-2, -2)
- e.Graphics.DrawRectangle(_HoverPen, rct)
- End If
- Next
- End Sub
- End Class
- Public Class CellData
- Public Value As Integer, Checked As Boolean = False
- End Class
Clock-Control
Zum Schluss noch ein Clock-Control, das arbeitet viel mit Matrix und GraphicsPath.
ZB ein Uhrzeiger ist eine recht komplizierte Figur:
VB.NET-Quellcode
- 'Uhrzeiger-Figur: langgezogenes Rechteck mit Dreieck als Spitze, Rückseite abgerundet
- Dim HeadWidth As Single = LineWidth * 4
- Length -= HeadWidth
- 'die Y-Werte sind negativ, weil der Zeiger nach oben zeigen soll
- _NormPath.AddLines(New PointF() { _
- New PointF(LineWidth / 2, 0), _
- New PointF(LineWidth / 2, -Length), _
- New PointF(HeadWidth / 2, -Length), _
- New PointF(0, -(Length + HeadWidth)), _
- New PointF(-HeadWidth / 2, -Length), _
- New PointF(-LineWidth / 2, -Length), _
- New PointF(-LineWidth / 2, 0)})
- Dim rctCenter As New RectangleF(-LineWidth / 2, -LineWidth / 2, LineWidth, LineWidth)
- _NormPath.AddArc(rctCenter, 0, 180) 'Rückseite: Halbkreis
Jedenfalls diesen GraphicsPath kann ich klonen, strecken, und so leiten sich aus dem Stunden-Zeiger auch Minuten- und Sekunden-Zeiger ab.
Aber das Uhr-Sample enthält auch ein "Color-Generator"-Gimmick, womit - wenn aktiviert - der Invalidierte Bereich farblich markiert wird.
Auf diese Weise wird (wörtlich!) vor Augen geführt, dass der invalidierte Bereich auch bei so einer großen Figur wie einem SekundenZeiger doch wesentlich kleiner ist, als die Gesamtfläche. Und dementsprechend weniger Resourcen werden gebraucht.
Bildle ohne und mit ColorGenerator:
.....
Zusammenfassung
OwnerDrawing deckt einen enorm weiten Bereich unterschiedlichster Anforderungen ab.
Knackpunkt ist meist das Umdenken, von der Vorstellung eines Bildes, was man fertigstellt hin zu einer Datenhaltung, die das Bild jederzeit reproduzieren kann.
Je nach Anforderung sind unterschiedliche Architekturen gefragt, einfache Sachen kann man auch in einem Paint-Event abhandeln, ohne ein besonderes Control dafür zu erfinden.
Will man aber User-Interaktion bereitstellen, hovern, selektieren, löschen, draggen, dann wächst die notwendige Logik schnell an, und ein eigenes Control anzulegen wird sinnvoll.
Will man auch eine Mehrzahl von Figuren präsentieren empfiehlt sich meist auch eine besondere Figur-Klasse, die sich ihre Abmasse selbst merkt, und die sich auch selbst zeichnen kann.
Aber eben auch nicht immer, wie das OwnerDrawnGrid-Beispiel zeigt.
Es sind auch noch komplexere Szenarien denkbar, etwa dass verschiedene Figur-Klassen (für Text, Zeichnungen, Bilder) gemeinsam auf einem Canvas präsentiert werden: gezieltes OwnderDrawing
Oder mit Datenverarbeitung verknüpftes OwnerDrawing, sodass man die Gebilde auch abspeichern kann: AdvancedPainting.
Evtl. wird man geneigt sein, sich für OwnerDrawing Pattern anzueignen, oder diese sogar codifizieren durch entsprechende Klassen, Basisklassen, Interfaces. Derlei Ansätze sind für ein Szenario evtl. nützlich, erweisen sich aber oft beim nächsten schon wieder als unzulänglich - die berühmte "eierlegende Wollmilchsau" kann nicht gefunden werden.
Man muss eben vor allem das Prinzip mit dem gezielten Invalidieren kennen, man sollte Matrix und GraphicsPath bedienen können, und dann ist je nach Anforderung zu entwickeln, vorzugsweise natürlich angelehnt an ähnliche Lösungen - wenn vorhanden.
Generell ist WinForms für Dauer-Animationen mit fließenden Bewegungen (Filme, Laufschriften, Chart-Ticker) schlecht geeignet, und je größer die animierte Fläche und je höher Bildwechsel-Frquenz der Animation wird im Grunde unverhältnismäßig viel Cpu-Ressource verbraten.
Dieser Beitrag wurde bereits 13 mal editiert, zuletzt von „ErfinderDesRades“ ()