Abgerundete Freihandlinie zeichnen (mit Vorschau)

Beschreibung
Eine einfache Linie zu zeichnen ist sicherlich kein Problem. Aber wie zeichnet man eine Freihandlinie? Man könnte das MouseMove-Ereignis nutzen und jeden Pixel zeichnen. Hierbei wird man aber schnell merken, dass bei schnellen Mausbewegungen keine vollständige Linie gezeichnet wird. Hier ist ein Tipp, der dieses Problem beseitigt.
Dafür erstellen wir uns eine neue Klasse, die von System.Windows.Forms.Panel erben soll.
VBC#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
''' <summary>
''' Stellt ein Steuerelement zum Zeichnen von Objekten dar.
''' </summary>
''' <remarks></remarks>
Public Class PaintBox
   Inherits System.Windows.Forms.Panel
 
#Region " Deklarationen - Felder "
 
#End Region
 
#Region " Konstruktoren "
 
#End Region
 
#Region " Eigenschaften "
 
#End Region
 
#Region " Methoden - Function "
 
#End Region
 
#Region " Methoden - Events "
 
#End Region
 
End Class
1
Kein Code vorhanden
Wir wollen natürlich auch verschiedene Eigenschaften bereitstellen, die als Parameter der zu zeichnenden Linie dienen sollen. Dafür brauchen wir zunächst folgende Felder:
VBC#
1
2
3
4
Private pLineAccuracy As Integer
Private pLineBending As Single
Private pLineColor As System.Drawing.Color
Private pLineSize As Integer
1
Kein Code vorhanden

Dazu die entsprechenden Eigenschaften:

VBC#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
''' <summary>
''' Ruft einen Wert für die Genauigkeit der zu zeichnenden Kurve ab oder legt diesen fest.
''' </summary>
''' <value></value>
''' <returns>Die Genauigkeit der zu zeichnenden Kurve.</returns>
''' <remarks></remarks>
Public Property LineAccuracy() As Integer
    Get
        Return pLineAccuracy
    End Get
    Set(ByVal value As Integer)
        If value < 1 Then
            Throw New ArgumentOutOfRangeException("Der Wert darf nicht kleiner 1 sein!")
        End If
 
        pLineAccuracy = value
    End Set
End Property
 
''' <summary>
''' Ruft einen Wert für den Grad der Krümmung der zu zeichnenden Kurve ab oder legt diesen fest.
''' </summary>
''' <value></value>
''' <returns>Der Grad der Krümmung der zu zeichnenden Kurve.</returns>
''' <remarks></remarks>
Public Property LineBending() As Single
    Get
        Return pLineBending
    End Get
    Set(ByVal value As Single)
        pLineBending = value
    End Set
End Property
 
''' <summary>
''' Ruft die Farbe der zu zeichnenden Kurve ab oder legt diese fest.
''' </summary>
''' <value></value>
''' <returns>Die Farbe der zu zeichnenden Kurve.</returns>
''' <remarks></remarks>
Public Property LineColor() As System.Drawing.Color
    Get
        Return pLineColor
    End Get
    Set(ByVal value As System.Drawing.Color)
        If value = Color.Transparent Then
            Throw New ArgumentException("Transparent wird nicht unterstützt!")
        End If
 
        pLineColor = value
    End Set
End Property
 
''' <summary>
''' Ruft die Breite der zu zeichnenden Kurve ab oder legt diese fest.
''' </summary>
''' <value></value>
''' <returns>Die Breite der zu zeichnenden Kurve.</returns>
''' <remarks></remarks>
Public Property LineSize() As Integer
    Get
        Return pLineSize
    End Get
    Set(ByVal value As Integer)
        If value < 1 Then
            Throw New ArgumentOutOfRangeException("Der Wert darf nicht kleiner 1 sein!")
        End If
 
        pLineSize = value
    End Set
End Property
1
Kein Code vorhanden
Mit diesen Eigenschaften können wir sowohl die Farbe und Dicke der Linie als auch die Krümmung und die Genauigkeit festlegen. Was die Genauigkeit festlegt wird später noch geklärt.

Jetzt, wo wir alle Eigenschaften festgelegt haben, können wir einen parameterlosen Konstruktor erstellen.
VBC#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
''' <summary>
''' Initialisiert eine neue Instanz von PaintBox.
''' </summary>
''' <remarks></remarks>
Public Sub New()
    ' Hintergrundfarbe setzen
    MyBase.BackColor = Color.White
 
    ' Eigenschaften setzen
    pLineAccuracy = 5
    pLineBending = 0.5
    pLineColor = Color.Red
    pLineSize = 2
End Sub
1
Kein Code vorhanden
Als nächstes kommt der Kern des Steuerelementes, nämlich das Zeichnen der Linie. Dafür brauchen wir das MouseDown-Event. Doch zuvor brauchen wir noch diese Felder:
VBC#
1
2
3
4
Private AllowDrawing As Boolean
Private LastPoint As System.Drawing.Point
 
Private pCurrentCurve As New List(Of Point)
1
Kein Code vorhanden
VBC#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Protected Overrides Sub OnMouseDown(e As System.Windows.Forms.MouseEventArgs)
    ' Überprüfe, ob mit Links geklickt.
    If e.Button = Windows.Forms.MouseButtons.Left Then
        ' Zeichnen erlauben
        AllowDrawing = True
        LastPoint = e.Location
 
        ' Punkte erzeugen
        pCurrentCurve.Clear()
        pCurrentCurve.Add(e.Location)
    End If
 
    MyBase.OnMouseDown(e)
End Sub
1
Kein Code vorhanden
Das Zeichnen einer Linie soll nur möglich sein, wenn die linke Maustaste gedrückt wurde. Dafür sorgt die erste Abfrage. Danach soll das Zeichnen der einzelnen Punkte, über die wir mit der Maus zeigen, ermöglicht werden. LastPoint speichert den zuletzt aufgenommen Punkt der Liste, diesen brauchen wir später noch. Danach sollen alle Punkte aus der Liste entfernt werden und der erste Punkt, nämlich die aktuelle Cursorposition, aufgenommen werden.

Als nächstes müssen alle weiteren Punkte in die Liste aufgenommen werden, wenn wir mit der Maus über dem Steuerelement sind. Dazu brauchen wir das MouseMove-Event.
VBC#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
Protected Overrides Sub OnMouseMove(e As System.Windows.Forms.MouseEventArgs)
    ' Überprüfe, ob Zeichnen erlaubt und ein neuer Punkt gezeichnet werden soll
    If AllowDrawing AndAlso e.Location <> LastPoint Then
        ' true -> Graphics-Objekt erzeugen
        Using g As Graphics = Me.CreateGraphics
            ' Rechteck erzeugen
            Dim rect As New Rectangle(LastPoint.X - LineSize \ 2, LastPoint.Y - pLineSize \ 2, _
                                      pLineSize, pLineSize)
 
            ' Ellipse und Linie zeichnen um unsaubere Effekte zu vermeiden!
            g.FillEllipse(New SolidBrush(pLineColor), rect)
            g.DrawLine(New Pen(pLineColor, pLineSize), LastPoint, e.Location)
 
            ' Punkt der Liste hinzufügen
            LastPoint = e.Location
            pCurrentCurve.Add(e.Location)
        End Using
    End If
 
    MyBase.OnMouseMove(e)
End Sub
1
Kein Code vorhanden
In der ersten Abfrage wird geprüft, ob überhaupt neue Punkte hinzugefügt werden sollen und sich der aktuelle Punkt von dem zuletzt aufgenommenen unterscheidet. Ist das der Fall, dann wollen wir den Punkt, auf den die Maus zeigt, zeichnen, also in die Liste aufnehmen. Zum zeichnen der Punkte werden wir die FillEllipse und DrawLine-Methode des Graphics-Objekt nutzen. Die DrawLine-Methode alleine reicht nicht aus. Es treten kleine Unschönheiten beim Zeichnen auf. Die zusätzliche FillEllipse-Methode kompensiert diese Unschönheit. Anschließend wird auch hier wieder der letzte Punkt gesetzt und in die Liste aufgenommen.

Die bisher gezeichnete Linie ist eine eckige und verpixelte Linie. Daher brauchen wir noch das MouseUp-Ereignis, in dem die Linie mit den weiteren Parametern nachgezeichnet werden soll. Die bisherige Linie geht dabei verloren. Sie wird durch die neue ersetzt.
VBC#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
Protected Overrides Sub OnMouseUp(e As System.Windows.Forms.MouseEventArgs)
    If AllowDrawing Then
        ' Zeichnen verbieten
        AllowDrawing = False
 
        ' Liste mit der Genauigkeit der Punkte erzeugen
        pCurrentCurve = GetCurvePoints()
 
        ' Neuzeichnen
        Call Invalidate()
    End If
 
    MyBase.OnMouseUp(e)
End Sub
1
Kein Code vorhanden
Das Zeichnen von weiteren Punkten wird nun verboten. Durch den Aufruf von GetCurvePoints wird eine Methode aufgerufen, die eine Liste von Punkten zurück gibt, die die angegebene Genauigkeit beinhaltet. Diese Methode wird etwas später erstellt. Danach soll der Inhalt neugezeichnet werden. Die eckige, verpixelte Linie wird entfernt und die abgerundete Linie gezeichnet, sofern wir das Paint-Ereignis angepasst haben.
VBC#
1
2
3
4
5
6
7
8
9
10
11
12
Protected Overrides Sub OnPaint(e As System.Windows.Forms.PaintEventArgs)
    ' Kurve zeichnen, wenn mindestens 3 Punkte vorhanden sind
    If pCurrentCurve.Count > 2 Then
        ' AntiAlias verwenden
        e.Graphics.SmoothingMode = Drawing2D.SmoothingMode.AntiAlias
 
        ' Kurve zeichnen
        e.Graphics.DrawCurve(New Pen(pLineColor, pLineSize), pCurrentCurve.ToArray, pLineBending)
    End If
 
    MyBase.OnPaint(e)
End Sub
1
Kein Code vorhanden
Zuerst wird geprüft, ob mindestens 3 Punkte enthalten sind. Ist das der Fall, wird die DrawCurve-Methode des Graphics-Objekts aufgerufen und die abgerundete Linie über alle Punkte der Liste gezeichnet.

Kommen wir nun zur GetCurvePoints-Funktion.
VBC#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
''' <summary>
''' Gibt eine Auflistung der tatsächlich zu zeichnenden Punkte mit der festgelegten Genauigkeit 
''' zurück.
''' </summary>
''' <returns>Die tatsächlich zu zeichnenden Punkte.</returns>
''' <remarks></remarks>
Private Function GetCurvePoints() As List(Of Point)
    Dim p As New List(Of Point)
 
    ' Iteriere durch alle Punkte in Schrittweite der Genauigkeit
    For i As Integer = 0 To pCurrentCurve.Count - 1 Step pLineAccuracy
        ' aktuellen Punkt zur Liste hinzufügen
        p.Add(pCurrentCurve(i))
    Next
 
    Return p
End Function
1
Kein Code vorhanden
Diese Methode gibt eine Auflistung von den Punkten zurück, die als Grundlage zum Zeichnen der abgerundeten Linie dienen sollen. Je dichter die Punkte aneinanderliegen, desto eckiger sieht die Linie aus. Die Genauigkeit gibt hier an, wieviele Punkte verwendet werden sollen. Je höher die Genauigkeit ist, desto mehr Punkte werden verwendet und desto eckiger erscheint die Linie. Es sollten allerdings auch nicht zu wenig Punkte verwendet werden, da sonst die Linie zu abstrakt gezeichnet wird und somit nicht mehr der ursprünglichen Strecke folgt. Ein guter Wert für die Genauigkeit liegt bei 5. Es wird also jeder 5. Wert verwendet.

Damit ist es nun möglich eine abgerundete Freihandlinie mit AntiAlias zu zeichnen.