Die Community zu .NET und Classic VB.
Menü

Tipp-Upload: VB.NET 0018: OwnerDrawing - Control mit beweglicher Figur darstellen

 von 

Ü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.

Zurück zur Übersicht

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

Schwierigkeitsgrad 3

Verwendete API-Aufrufe:

Download:

Download des Beispielprojektes [15,93 KB]

' 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.