Tipp-Upload: VB.NET 0018: OwnerDrawing - Control mit beweglicher Figur darstellen
von Spatzenkanonier
Über den Tipp
Dieser Tippvorschlag wird übernommen.
Der Vorschlag ist in den folgenden Kategorien zu finden:
- Grafik
- Steuerelemente
Dem Tippvorschlag wurden folgende Schlüsselwörter zugeordnet:
OwnerDrawing, OwnerDrawn, Paint, Matrix, GraphicsPath, Nullable, IDisposable, Dispose, DoubleBuffer
Der Vorschlag wurde erstellt am: 30.07.2007 04:05.
Die letzte Aktualisierung erfolgte am 18.03.2016 17:42.
Beschreibung
Zum Zeichnen eigener Figuren in .Net muß man sich von der Vorstellungen eines Bildes als statisches Objekt trennen.
Ownerdrawing klinkt sich in die Fensterverwaltung der CLR ein und erfordert, daß die Zeichnung jedesmal komplett neu erstellt wird, sobald das Fenster oder Teile davon in den Bildschirm-Vordergrund treten.
Es gilt also, die kompletten Zeichnungs-Anweisungen in einem Objekt zu speichern, welches die Zeichnung jederzeit neu ausführen kann.
"Jederzeit" bedeutet konkret: im Paint-Event des Controls, welches die Zeichnung anzeigen soll (hier: ein Form).
Bei der Umsetzung wird von zwei sehr nützliche Klassen des Namespaces System.Drawing.Drawing2D ausgiebig Gebrauch gemacht:
GraphicsPath kann beliebige Figuren speichern
Matrix kann Skalierung, Rotation und Positionierung speichern, und auf GraphicsPath anwenden; bildet somit die Basis für Bewegungs-Darstellungen.
Interessant vllt. auch der Einsatz der generischen Nullable - Struktur, sowie die Ressourcen-Bereinigung
Schwierigkeitsgrad |
Verwendete API-Aufrufe: |
Download: |
' Dieser Source stammt von http://www.activevb.de ' und kann frei verwendet werden. Für eventuelle Schäden ' wird nicht gehaftet. ' Um Fehler oder Fragen zu klären, nutzen Sie bitte unser Forum. ' Ansonsten viel Spaß und Erfolg mit diesem Source! ' ' Beachten Sie, das vom Designer generierter Code hier ausgeblendet wird. ' In den Zip-Dateien ist er jedoch zu finden. ' ----------- Anfang Projektgruppe DrawObject.sln ----------- ' ---------- Anfang Projektdatei DrawObject.vbproj ---------- ' -------------- Anfang Datei ColorGenerator.vb -------------- ' IDE-Voreinstellungen: ' Option Strict On ' Option Explicit On ' Imports System.Windows.Forms ' Imports System.Drawing Public Class ColorGenerator ' Jede Farbkomponente läuft den Farb-Bereich in eigener Schrittweite auf und ab. ' Die Kombination der Komponenten ergibt eine sehr hohe Anzahl an Farb-Permutationen Private Shared _Red As Integer = 127 Private Shared _RedStep As Integer = 17 Private Shared _Green As Integer = 127 Private Shared _GreenStep As Integer = 13 Private Shared _Blue As Integer = 127 Private Shared _BlueStep As Integer = 23 Private Shared Sub MoveNext(ByRef ColorPart As Integer, ByRef StepSize As Integer) ColorPart += StepSize If ColorPart > 255 OrElse ColorPart < 50 Then StepSize *= -1 ' Laufrichtung umkehren ColorPart += StepSize End If End Sub Public Shared Function GetNextColor() As Color MoveNext(_Red, _RedStep) MoveNext(_Green, _GreenStep) MoveNext(_Blue, _BlueStep) Return Color.FromArgb(_Red, _Green, _Blue) End Function End Class ' --------------- Ende Datei ColorGenerator.vb --------------- ' ---------------- Anfang Datei DrawObject.vb ---------------- Imports System.Drawing.Drawing2D Imports System.Collections.Generic Public Class DrawObject Implements IDisposable Private _TemplatePath As New Drawing2D.GraphicsPath Private _pthLine As New Drawing2D.GraphicsPath Private _pthWidenedLine As New GraphicsPath Private _Pen As New Pen(Color.Red, 1) Private _Matrix As New Drawing2D.Matrix Private _Bounds As Rectangle Private _Control As Control Private _Rotation As Double = 0.0# Private _Location As New Point(100, 100) Private _Scale As New Size(100, 100) ' Bei aktivierter Kantenglättung liegen einzelne Punkte außerhalb der exakt berechneten Figur ' ApplyChanges() berücksichtigt dieses, um Zeichnungs-Fehler zu vermeiden Private _IsSmoothing As Boolean = True Private _SmoothModes As IList(Of SmoothingMode) = New SmoothingMode() { _ SmoothingMode.AntiAlias, SmoothingMode.HighQuality} Public Sub New(ByVal Control As Control) _Control = Control End Sub ''' <summary> Offenlegung des Stiftes, mit dem gezeichnet wird </summary> Public ReadOnly Property Pen() As Pen Get Return _Pen End Get End Property ''' <summary> ''' Zur Aufnahme des Musters der Figur. Verwenden Sie Größenangaben im Bereich von +-1.0, ''' und legen Sie mit Public Property Scale() die Größe der Darstellung fest. ''' </summary> Public ReadOnly Property TemplatePath() As GraphicsPath Get Return _TemplatePath End Get End Property ''' <summary> Vergrößerungs-Faktor in X / Y - Richtung </summary> Public Property Scale() As Size Get Return _Scale End Get Set(ByVal NewValue As Size) _Scale = NewValue End Set End Property ''' <summary> Winkel zur X-Achse in Degree (360°) </summary> Public Property Rotation() As Double Get Return _Rotation End Get Set(ByVal NewValue As Double) _Rotation = NewValue Mod 360 End Set End Property ''' <summary> Position des Nullpunktes </summary> Public Property Location() As Point Get Return _Location End Get Set(ByVal NewValue As Point) _Location = NewValue End Set End Property '''<summary> ''' wendet die aktuellen Einstellungen auf die Figur an, stößt Neuzeichnen an ''' </summary> Public Sub ApplyChanges() ' tückisch: wird bei Matrix-Befehlen die jeweilige Überladung ohne Parameter "Order As ' MatrixOrder" gewählt, erhält man i.A. unerwartete Ergebnisse, denn die Voreinstellung ' ist "MatrixOrder.Prepend" _Matrix.Reset() _Matrix.Scale(_Scale.Width, _Scale.Height, MatrixOrder.Append) _Matrix.Translate(_Location.X, _Location.Y, MatrixOrder.Append) _Matrix.RotateAt(CSng(_Rotation), _Location, MatrixOrder.Append) _pthLine.Reset() _pthLine.AddPath(_TemplatePath, False) _pthLine.Transform(_Matrix) ' Invalidate-Rectangle ermitteln, dabei Stift und ggfs. Kantenglättung berücksichtigen ' Die Überladungen von GraphicsPath.GetBounds(), die den Stift berücksichtigen ' sollen, sind buggy, und liefern viel zu große Bounds. Einzig GetBounds() für ' auszufüllende Pfade arbeitet korrekt. ' Das bedeutet, daß Graphics.DrawPath(Path, Pen) nicht benutzt werden kann, sondern nur ' Graphics.FillPath(Path). ' Zum Glück generiert GraphicsPath.Widen(Pen) genau die Umrißlinie der ' **Fläche des Striches**, mit dem Pen die Linie zeichnen würde. Diesen mit Pen geweiteten ' Pfad auszufüllen ist also äquivalent dazu, den eigentlichen Pfad mit Pen zu zeichnen. ' Beachte: für Zeichnungen ausgefüllter Figuren ist dieser Workaround unnötig. _pthWidenedLine.Reset() _pthWidenedLine.AddPath(_pthLine, False) _pthWidenedLine.Widen(_Pen) Dim NewBounds As RectangleF = _pthWidenedLine.GetBounds If _IsSmoothing Then NewBounds.Inflate(0.5F, 0.5F) ' Invalidate(Rectangle) wird für zwei verschiedene Bereiche aufgerufen: ' 1) bisheriger Zeichnungsbereich der Figur (Löschen) ' 2) neuer Zeichnungsbereich der Figur ' Invalidate() veranlasst die CLR, kurze Zeit darauf ein Paint-Event auszulösen (nur ' eines!), mit einem Clip-Bereich, welcher **alle** invalidierten Bereiche vereint. ' Dieses erhellt, wie extrem der ganze Vorgang darauf optimiert, das zeitkritische ' Zeichnen so selten wie möglich auszuführen, und auch nur die minimal erforderliche ' Fläche zu "bemalen". _Control.Invalidate(_Bounds) _Bounds = Rectangle.Ceiling(NewBounds) _Control.Invalidate(_Bounds) End Sub Public ReadOnly Property Contains(ByVal Pt As PointF) As Boolean Get Return _pthLine.IsVisible(Pt) End Get End Property ''' <summary> zeichnet die Figur </summary> ''' <remarks> ''' wird letztlich von der CLR aufgerufen, **nicht** in direkter Reaktion auf Benutzereingaben ''' </remarks> Public Sub Draw(ByVal G As Graphics) _IsSmoothing = _SmoothModes.Contains(G.SmoothingMode) G.FillPath(_Pen.Brush, _pthWidenedLine) End Sub #Region " IDisposable Support " ''' <summary> ''' Ressourcen-Freigabe: für alle Objekte, die IDisposable implementieren, Dispose() aufrufen ''' </summary> Protected Overridable Sub Dispose(ByVal disposing As Boolean) If Not disposing Then Return Static IsDisposed As Boolean = False If IsDisposed Then Return IsDisposed = True For Each D As IDisposable In New IDisposable() { _TemplatePath, _pthLine, _ _pthWidenedLine, _Matrix, _Pen} D.Dispose() Next End Sub Public Sub Dispose() Implements IDisposable.Dispose Dispose(True) GC.SuppressFinalize(Me) End Sub #End Region ' IDisposable Support End Class ' ----------------- Ende Datei DrawObject.vb ----------------- ' -------------- Anfang Datei frmDrawObject.vb -------------- Imports System.Drawing.Drawing2D ''' <summary> ''' Auf diesem Form wird eine verschiebbare Pfeil-Figur gezeichnet, die sich ''' zusätzlich auf die Mausposition ausrichtet ''' </summary> Public Class frmDrawObject Private _Arrow As New DrawObject(Me) ''' <summary>Differenz zw. Nullpunkt und Drag-Anfasspunkt</summary> ''' <remarks> ''' die Nullable-Struktur tranportiert zusätzlich die Information, ''' ob _GrabOffset überhaupt gesetzt ist ''' </remarks> Private _GrabOffset As Nullable(Of Size) Public Sub New() InitializeComponent() ' Pfeil-Figur aufsetzen (mit allem Drum und Dran) _Arrow.Pen.Width = 10 _Arrow.Pen.LineJoin = LineJoin.Round _Arrow.TemplatePath.AddLines(New PointF() { New PointF(0, -0.2), New PointF(0.6, _ -0.2), New PointF(0.6, -0.5), New PointF(1, 0), New PointF(0.6, 0.5), New PointF( _ 0.6, 0.2), New PointF(0, 0.2)}) _Arrow.TemplatePath.AddArc(New RectangleF(-0.2, -0.2, 0.4, 0.4), 90, 180) _Arrow.Location = New Point(250, 200) _Arrow.Scale = New Size(100, 100) _Arrow.ApplyChanges() End Sub ''' <summary>Draggen des _Arrows starten</summary> Protected Overrides Sub OnMouseDown(ByVal e As MouseEventArgs) MyBase.OnMouseDown(e) If _Arrow.Contains(e.Location) Then _GrabOffset = New Size(e.Location - New Size( _ _Arrow.Location)) End Sub ''' <summary> Figur entweder draggen, oder auf die Maus ausrichten </summary> Protected Overrides Sub OnMouseMove(ByVal e As MouseEventArgs) MyBase.OnMouseMove(e) With _Arrow If _GrabOffset.HasValue Then .Location = e.Location - _GrabOffset.Value Else .Rotation = GetAngle(.Location, e.Location) * 180 / Math.PI ' in Degree! End If .ApplyChanges() End With End Sub ''' <summary>Draggen beenden</summary> Protected Overrides Sub OnMouseUp(ByVal e As MouseEventArgs) MyBase.OnMouseUp(e) _GrabOffset = Nothing End Sub Protected Overrides Sub OnPaint(ByVal e As PaintEventArgs) MyBase.OnPaint(e) If ckShowClip.Checked Then ' ggfs. ClipBereich visualisieren ' e.Graphics.Clear() färbt nur den Clipbereich neu e.Graphics.Clear(ColorGenerator.GetNextColor) End If e.Graphics.SmoothingMode = SmoothingMode.HighQuality _Arrow.Draw(e.Graphics) If ckAutoRun.Checked Then ' Ereigniskette: ApplyChanges() löst das nächste Paint-Ereignis aus ' So zeigt sich die maximale Zeichnungs-Geschwindigkeit With _Arrow .Rotation += 2 .ApplyChanges() End With End If End Sub ''' <summary>Ressourcen-Freigabe</summary> Protected Overrides Sub Dispose(ByVal disposing As Boolean) ' IMHO gehört der Dispose-Override nicht in den Designer-Code (wurde da herausgeholt) If disposing Then If _Arrow IsNot Nothing Then _Arrow.Dispose() _Arrow = Nothing End If If components IsNot Nothing Then components.Dispose() End If MyBase.Dispose(disposing) End Sub Private Sub ckAutoRun_CheckedChanged(ByVal sender As Object, ByVal e As EventArgs) _ Handles ckAutoRun.CheckedChanged If ckAutoRun.Checked Then Me.Invalidate() End Sub Private Sub ckDoubleBuffer_CheckedChanged(ByVal sender As Object, ByVal e As EventArgs) _ Handles ckDoubleBuffer.CheckedChanged ' reduziert flackern, indem der Zeichenvorgang in einen Puffer umgeleitet wird. ' Die Performance-Kosten machen sich erst bemerkbar, wenn extrem schnelle ' Bewegungen darzustellen sind. ' Beachte: DoubleBuffered ist Protected. Beim Malen auf fremde Controls also kein Zugriff. ' Zusatz-Info DoubleBuffered-unveränderliche Einstellungen: Picturebox True, Panel: False MyBase.DoubleBuffered = ckDoubleBuffer.Checked End Sub ''' <summary>berechnet den Winkel eines Vektors zur X-Achse</summary> Private Function GetAngle(ByVal ptFrom As PointF, ByVal ptTo As PointF) As Double ptTo -= New SizeF(ptFrom) ' ptTo in Relation zu ptFrom setzen With ptTo Dim Amount As Double = Math.Sqrt(.X ^ 2 + .Y ^ 2) If Amount > 0 Then ' Nulldivision vermeiden If .X > 0 Then Return Math.Asin(.Y / Amount) Else Return Math.PI - Math.Asin(.Y / Amount) End If End If End With End Function End Class ' --------------- Ende Datei frmDrawObject.vb --------------- ' ----------- Ende Projektdatei DrawObject.vbproj ----------- ' ------------ Ende Projektgruppe DrawObject.sln ------------
Diskussion
Diese Funktion ermöglicht es, Fragen, die die Veröffentlichung des Tipps betreffen, zu klären, oder Anregungen und Verbesserungsvorschläge einzubringen. Nach der Veröffentlichung des Tipps werden diese Beiträge nicht weiter verlinkt. Allgemeine Fragen zum Inhalt sollten daher hier nicht geklärt werden.
Um eine Diskussion eröffnen zu können, müssen sie angemeldet sein.