Seiten

Dienstag, 1. Februar 2011

How To: Freihandzeichnung mit Path-Erstellung synchronisieren

Sharker Khaleed Mahmut beschreibt in einem Artikel, wie mithilfe eines InkPresenter zur Laufzeit auf ein Image gezeichnet werden kann. Wie das geht, könnt Ihr dort nachlesen. In diesem Artikel geht es darum, wie synchron mit einer Freihandzeichnung auf einen InkPresenter in einem Canvas-Steuerelement Path-Elemente erzeugt werden, die exakt dem entsprechen, was auf den InkPresenter gezeichnet wird. Ein Beispielprojekt mit Quellcode steht zum Download in der Expression Gallery.

Das ist ziemlich einfach und es funktioniert so.

Generelle Vorgehensweise
In den Eregnissen MouseLeftButtonDown, MouseLeftButtonUp und MouseMove werden, dem Beispiel von Sharker Khaleed Mahmut folgend, zunächst die Daten für das Erzeugen der Strokes auf dem InkPresnter erzeugt. Zusätzlich wird eine private Variable "pointList" erstellt, die vom Typ "List(Of StylusPointCollection)" ist. Dieser Variablen werden die gleichen Positionsdaten hinzugefügt, die in den Ereignisbehandlern MouseLeftButtonDown, MouseLeftButtonUp und MouseMove zum Erzeugen der Strokes auf dem InkPresenter verwendet werden. In "pointList" wird jeweils der Rückgabewert vom Typ StylusPointCollection gesammelt. Eine StylusPointCollection hat unter anderem einen X-Koordinatenwert und einen Y-Koordinatenwert. Diese in "pointList" gesammelten Positionsdaten einzelner Punkte werden dann verwendet, um einen Pfad zu erzeugen.

Und so funktioniert es Schritt für Schritt.

Schritt für Schritt
Schritt 1 - Vorbereitungen
Es wird ein neues Silverlight 4-Projekt erzeugt. Der Oberfläche wird ein Canvas (x:Name="cvElements") mit dem "Z-Index" "0" hinzugefügt. Dann wird ein InkPresenter ("inkTest") hinzugefügt mit einem Wert für den "Z-Index" von "1". Der InkPresenter wird mit der gleichen Größe und den gleichen Margin-Werten wie das Canvas-Steuerelement erzeugt.

Schritt 2 - Positionsdaten sammeln
Es wird die private Variable "pointList" erzeugt:

  Private pointList As New List(Of StylusPointCollection)


In den Ereignis-Behandlern von MouseLeftButtonDown (wenn der erste Punkte gezeichnet wird), MouseMove (wenn im Rythmus der MouseEventArgs fortwährend Punkte gezeichnet werden) von MouseLeftButtonUp (wenn der letzte Punkt gezeichnet wird), werden jeweils die einzelnen Punkte der "PointList" hinzugefügt.

Private Sub inkTest_MouseLeftButtonDown(ByVal sender As Object, ByVal e As System.Windows.Input.MouseButtonEventArgs) Handles inkTest.MouseLeftButtonDown


  inkTest.CaptureMouse()

  Dim points As StylusPointCollection = e.StylusDevice.GetStylusPoints(inkTest)

  currentStroke = New Stroke(points)

  currentStroke.DrawingAttributes.Color = Colors.Blue

  currentStroke.DrawingAttributes.Width = 10

  currentStroke.DrawingAttributes.Height = 10

  inkTest.Strokes.Add(currentStroke)


  ' Grab the position to fill the List(Of StylusPointCollection)
  pointList.Add(e.StylusDevice.GetStylusPoints(inkTest))
  '-------------------------------------------------------------


End Sub


Private Sub inkTest_MouseMove(ByVal sender As Object, ByVal e As System.Windows.Input.MouseEventArgs) Handles inkTest.MouseMove


  If currentStroke IsNot Nothing Then

    pointList.Add(e.StylusDevice.GetStylusPoints(inkTest))

    currentStroke.StylusPoints.Add(points)

    ' Grab the position to fill the List(Of StylusPointCollection)
    Dim points As StylusPointCollection = e.StylusDevice.GetStylusPoints(inkTest)
    '-------------------------------------------------------------

  End If


End Sub


Private Sub inkTest_MouseLeftButtonUp(ByVal sender As Object, ByVal e As System.Windows.Input.MouseButtonEventArgs) Handles inkTest.MouseLeftButtonUp


  Dim points As StylusPointCollection = e.StylusDevice.GetStylusPoints(inkTest)
  currentStroke = New Stroke(points)

  inkTest.ReleaseMouseCapture()

  currentStroke = Nothing

  ' Grab the position to fill the List(Of StylusPointCollection)
  pointList.Add(e.StylusDevice.GetStylusPoints(inkTest))


  ' Create the path in the canvas
  If pointList.Count > 0 Then

    DrawPath()

  End If
  '---------------------------------------------------------


End Sub

Schritt 3 - Positionsdaten auswerten und den Pfad erzeugen
Im Ereignisbehandler des MouseLeftButtonUp events wird als letztes die Routine "DrawPath" aufgerufen. Hier ist der Quellcode dieser Routine:

Private Sub DrawPath()

  Dim pathToDraw As Path = New Path()

  Dim pathGeometry As PathGeometry = New PathGeometry()

  Dim pathFigure As PathFigure = New PathFigure()

  Dim pathSegments As PathSegmentCollection = New PathSegmentCollection()


  pathFigure.StartPoint = New Point(CreatePointFromStylusPointCollection(pointList.ElementAt(0)).X,
                                    CreatePointFromStylusPointCollection(pointList.ElementAt(0)).Y)

  For Each member As StylusPointCollection In pointList

    pathSegments.Add(CreateLineSegment(CreatePointFromStylusPointCollection(member)))

  Next

  pathFigure.Segments = pathSegments

  pathGeometry.Figures.Add(pathFigure)

  With pathToDraw

    .Data = pathGeometry

    .Stroke = New SolidColorBrush(Colors.Orange)

    .StrokeThickness = 10

    .StrokeLineJoin = PenLineJoin.Round

    .StrokeStartLineCap = PenLineCap.Round

    .StrokeEndLineCap = PenLineCap.Round

  End With

  Canvas.SetZIndex(pathToDraw, 0)

  cvElements.Children.Add(pathToDraw)

  pointList.Clear()

End Sub


Mit der Variablen "pathToDraw" wird ein neues Path-Element erzeugt. Außerdem wird die Variable "pathGeometry" vom Typ "PathGeometry" erzeugt. Diese wird später der Data-Eigenschaft des Pfads zugewiesen. Die Variable "pathFigure" vom Typ "PathFigure" wird später der "PathFigureCollection" von "pathGeometry" zugewiesen. Und der Eigenschaft "Segments" von "pathFigure" wird die "PathSegmentCollection", erzeugt in der Variablen "pathSegments", zugewiesen.

Das eigentliche Arbeitstier der Routine ist der folgende Quellcode-Ausschnitt:

  pathFigure.StartPoint =
    New Point(CreatePointFromStylusPointCollection(pointList.ElementAt(0)).X,
              CreatePointFromStylusPointCollection(pointList.ElementAt(0)).Y)

  For Each member As StylusPointCollection In pointList

    pathSegments.Add(CreateLineSegment(CreatePointFromStylusPointCollection(member)))

  Next


Es wird zunächst der Startpunkt des Pfads ermittelt und zugewiesen. Dazu wird die Funktion "CreatePointFromStylusPointCollection" verwendet. Der Quellcode dieser Funktion sieht so aus:

Private Function CreatePointFromStylusPointCollection(ByVal value As StylusPointCollection) As Point

  Return New Point With {.X = value.Item(0).X, .Y = value.Item(0).Y}

End Function


Die Funktion erhält als Parameter eine "StylusPointCollection" und gibt einen "Point" zurück. Um den Startpunkt des Pfads zu ermitteln, wird das erste Item aus der "List(Of SytlusPointCollection)", also von "pointList" genommen, und mit dem X-Wert sowie dem Y-Wert dieses Items der Rückgabewert der Funktion erzeugt.

In der "For Each"-Schleife wird dann mithilfe der Funktionen "CreateLineSegment" und "CreatePointFromStylusPointCollection" die einzelnen "LineSegments" erzeugt und der "PathSegmentsCollection" "pathSegments" hinzugefügt. Die Funktion "CreateLineSegment" sieht so aus:

Private Function CreateLineSegment(ByVal value As Point) As LineSegment

  Return New LineSegment With {.Point = value}

End Function

Um ein LineSegment zu definieren, benötigt man nur einen Punkt. Der eigentliche Pfad, der erzeugt wird, bewegt sich vom Startpunkt zum nächsten Punkt und von dort aus zum nächsten Punkt, usw. Jeder Punkt eines LineSegment steht für einen Punkt des Pfades. Das letzte LineSegment stellt den Endpunkt des Pfades dar. Und so hangelt sich die "For Each"-Schleife durch die "pointList" und erzeugt, nachdem der Startpunkt gesetzt wurde, die einzelnen Punkte (LineSegment.Point), die den endgültigen Pfad definieren.

Um die perfekte visuelle Übereinstimmung mit den Strokes, die auf den InkPresenter gezeichnet werden, zu bewirken, ist die der folgende Quellcode-Ausschnitt wichtig:

  With pathToDraw

    .Data = pathGeometry

    .Stroke = New SolidColorBrush(Colors.Orange)

    '!!!!!!!!!!!!!!!!!!!!!
    .StrokeThickness = 10

    .StrokeLineJoin = PenLineJoin.Round

    .StrokeStartLineCap = PenLineCap.Round

    .StrokeEndLineCap = PenLineCap.Round
    '!!!!!!!!!!!!!!!!!!!!!
  End With


Im obigen Quellcode-Ausschnitt werden alle Ecken sowie der Startpunkt und der Endpunkt des Pfads abgerundet. Das ist wichtig, weil auch die Strokes rund sind. Außerdem wird die "StrokeThickness" des Pfads auf den Wert "10" gesetzt. Das entspricht der Höhe und der Breite der Strokes:

Private Sub inkTest_MouseLeftButtonDown(ByVal sender As Object, ByVal e As System.Windows.Input.MouseButtonEventArgs) Handles inkTest.MouseLeftButtonDown

  inkTest.CaptureMouse()
  Dim points As StylusPointCollection = e.StylusDevice.GetStylusPoints(inkTest)
  currentStroke = New Stroke(points)
  currentStroke.DrawingAttributes.Color = Colors.Blue

  '!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
  currentStroke.DrawingAttributes.Width = 10

  currentStroke.DrawingAttributes.Height = 10
  '!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

  inkTest.Strokes.Add(currentStroke)
  ' Grab the position for the List(Of StylusPointCollection)
  pointList.Add(e.StylusDevice.GetStylusPoints(inkTest))
  '---------------------------------------------------------

End Sub


Abschließend wird der erzeugte Pfad dem Canvas hinzugefügt und mit pointList.Clear() die "pointList" geleert. Das ist wichtig, weil bei der nächsten Pfad-Zeichnung andernfalls auch die alten Werte noch verarbeitet werden würden.

Das war's!

Das Beispielprojekt, das hier heruntergeladen werden kann, enthält noch deutlich mehr Quellcode. Der dient aber nur dazu, die Anwendung einigermaßen ansehnlich zu gestalten. Die grundlegende Technik hat dieser Artikel aufgezeigt.

Viel Spass damit.

Keine Kommentare:

Kommentar veröffentlichen