Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

WPF-Drawing Canvas Control

0.00/5 (No votes)
5 Oct 2012 1  
A drawing tool program that can create simplified XAML code

Introduction

I have been play around for quite some time with a custom Drawing canvas that could do some simple but yet useful stuff, a program that had some of the functionality of normal drawing program, and could be used to generate some XAML content. For writing code that looks interesting and useful to others, we often want to use "real world" data as input. Meaning that we might have a picture of, let’s say, a race track, but it’s a pain to construct the XAML code from that alone, just to code some other fancy stuff in WPF.

As always, the first thing I did was some searching on the CodeProject site and elsewhere, and I did find a WPF drawing tool here on this very site. The article I found was WPF-DrawTools, which covers all the basic needs of a drawing program. I had some different ideas for implementing some stuff, like zooming and panning, loading images and other things, that would require quite a lot of redesigning of the program, and I needed a challenge, so I decided to write my own drawing program.

As I looked around the web for interesting ideas, I came upon a couple of articles on this site that gave me some ideas; there were especially two that really stood out: 

  • WPF: A* search by Sacha Barber. The code in the article had a really nice structure to it, and was easy to read. One could easily maintain the code and expand it, so I tried to follow his example, but that was not easy.   
  • WPF Diagram Designer: Part 1 by sukram. The way he designed the resize using an Adorner was brilliant, and I used his code nearly without making any changes.   

The control I would describe here would inherit two custom controls and a control form the framework itself. The only control that is fully custom is my Drawing canvas, that inherits the Canvas control with some simple modifications. 

Background

I have, at my previous work places, done quite a lot of editing digital maps, and have witnessed how vital a real good editing tool for digital drawing program is. I have also had some experience in how difficult it can be if you lack those tools, they can litterally mean the difference between swearing and not. Unfortunately, a real good all purpose drawing program, are huge, and normally requires a whole programming team to be implemented in an easy to use way. So this program will eventually have short comings, but I hope its a good start.

Based on the other programs I could find the easiest thing seem to be to create a custom user control that inherited the Canvas. This canvas would hold all the drawn elements, and it would also be necessary to override some of the default properties. The standard canvas also has some problem, in that it is not intended, as it is, to have any scrolling or limits. This would have to be solved by using some additional outer controls that could remedy the situation.

I would also have to create a control for resizing the drawing area, as I don’t want it to be infinite large. This was perhaps the easiest thing, as I implemented the WPF Diagram Designer items, tweaked the design slightly and voila.

Last I would also need to create a customized control that inherits the Scroll View control. This control would be responsible for panning and zooming the main drawing area, it would also have to be the outer most control, meaning that it would house the Adorner resize and the custom canvas.

About bubble and tunnel events

A drawing program like this would have to handle quite a lot of very complex events. Events would have to be stopped, tunneled, and routed, based on the actions that are going to be performed. I will start with the Zoom and panning event. After some thinking I eventually came to the conclusion that the panning would have to be handled by the scroll control, and the zooming would have to be initiated from the same control as well, as both the two lower controls would depend on this.

I would also have to in part, exclude the resize control from the zooming event, as it would become invisible if I zoomed out far enough. Its size, meaning height and width, would have to change according to the changes in the height and width of the canvas though.

However, dragging and resizing of the actual drawings on the canvas, would have to take place on the canvas control itself, as it would have all the necessary information of the custom framework elements that I would create my own custom classes in. This meant that my main custom control would consist of three distinct parts:

  • A custom designed Scroll viewer  
  • A custom resize control, which stems from this project here on the Codeproject site.
  • A custom control that inherits the Canvas and adds zoom features.   

This would also, however mean that I would have to design tall the controls that did the actual drawing, to also be able to find out whether or not you clicked a point or the line, meaning they would have to be implemented inheriting a FrameworkElement and using DrawingContext to hold the visual objects shown on the Canvas control. I call all of these controls Basic Controls, as they form the underlying for the elements to be drawn, while the outer controls just are housing blocks and organizers of the content inside them. The bad news is that some information would have to be routed cross several controls, making the program much harder to read and reuse for other, but I really didn’t know how to implement it another way.  

Creating basic drawing controls

The program has in all 5 different controls that implement some drawing capabilities. They are however all implemented using the same logic, although the Canvas Point is just an element used by the other controls, as I needed hit testing on the points on a line and polygon, so it’s just used indirectly. It’s the only control that I will show the complete code from, as it is the smallest of them, and it has the following structure: 

Imports System.Globalization
 
Public Class CanvasPoint
    Inherits FrameworkElement
 
    Implements System.ComponentModel.INotifyPropertyChanged
 
    ' Create a collection of child visual objects.
    Private _children As VisualCollection
 
#Region "Constructors"
    Public Sub New()
        _children = New VisualCollection(Me)
        _children.Add(CreateDrawingVisualCircle())
 
    End Sub
 
    Public Sub New(p_position As Point)
        PositionOnCanvas = p_position
 
        _children = New VisualCollection(Me)
        _children.Add(CreateDrawingVisualCircle())
    End Sub
#End Region
 
#Region "Properties"
 
    Private p_PositionOnCanvas As New Point
    Public Property PositionOnCanvas() As Point
        Get
            Return p_PositionOnCanvas
        End Get
        Set(ByVal value As Point)
            p_PositionOnCanvas = value
            INotifyChange("PositionOnCanvas")
        End Set
    End Property
#End Region
 
#Region "Overided properties"
 
    ' Provide a required override for the VisualChildrenCount property.
    Protected Overrides ReadOnly Property VisualChildrenCount() As Integer
        Get
            Return _children.Count
        End Get
    End Property
 
    ' Provide a required override for the GetVisualChild method.
    Protected Overrides Function GetVisualChild(ByVal index As Integer) As Visual
        If index < 0 OrElse index >= _children.Count Then
            Throw New ArgumentOutOfRangeException()
        End If
 
        Return _children(index)
    End Function
 
#End Region
 
#Region "Drawing"
    ' Create a DrawingVisual that contains a rectangle.
    Private Function CreateDrawingVisualCircle() As DrawingVisual
        Dim drawingVisual As New DrawingVisual()
 
        ' Retrieve the DrawingContext in order to create new drawing content.
        Dim drawingContext As DrawingContext = drawingVisual.RenderOpen()
 
        ' Create a circle and draw it in the DrawingContext.
        drawingContext.DrawEllipse(Brushes.Red, New Pen(Brushes.Red, 1.0), PositionOnCanvas, 4, 4)
 
        ' Persist the drawing content.
        drawingContext.Close()
 
        Return drawingVisual
    End Function
 
#End Region
 
#Region "Events"
    Public Sub INotifyChange(ByVal info As String)
        RaiseEvent PropertyChanged(Me, New System.ComponentModel.PropertyChangedEventArgs(info))
    End Sub
 
    Public Event PropertyChanged(sender As Object, _
           e As System.ComponentModel.PropertyChangedEventArgs) _
           Implements System.ComponentModel.INotifyPropertyChanged.PropertyChanged
#End Region
 
End Class

The four other controls that are implemented are given below:

  • CustomLine
  • CustomPolygon
  • Measurement (a custom point with text)
  • CustomPictureControl
They consist of more elaborate handling, as they inevitable would have more elements to hold. You should also notice that I could create a whole set of properties that would make you able to control all aspects of the design. For simplicity these are not implemented in this program.

The DrawingCanvas control

This is by far the most complex control in the program, as it handles most of the drawing functions, and stops and initiates nearly all the functions.

The beginning of the DrawingCanvas includes a whole set of private variables that are used for preview drawing. The way I allowed the preview drawings was to register the entire mouse down events in the PointCollection called MouseLeftButtonDownOnCanvas. When I mouse move event occurred, I simply cloned the mouse down PointCollection and added the MouseMove current mouse position as the last element to DottedLineForPreviewDrawing. When I clicked on mouse right button the preview drawing and solid drawing for the PolyLines were cleared.

Public Class DrawingCanvas
    Inherits Canvas
    Implements ComponentModel.INotifyPropertyChanged

#Region "Private variables and properties"

    'Get the Zoom area 
    Private RectZoom As New Rectangle

    'Temporary lines used while drawing 
    Private SolidLineForDrawing, DottedLineForPreviewDrawing As New Polyline

    'Stores all the mouse button Left button clicks, used in mouse move
    Private MouseLeftButtonDownOnDrawingCanvas As New PointCollection

    'Variables used for dragging elements
    Private startPoint As Point
    Private selectedElementOrigins As Point
    Private isDragging As Boolean
#End Region

#Region "Constructor"
    Sub New()
        DefaultStyleKeyProperty.OverrideMetadata(GetType(DrawingCanvas), _
               New FrameworkPropertyMetadata(GetType(DrawingCanvas)))

        'Create and add an empty polyline to the DrawingCanvas
        SolidLineForDrawing.StrokeThickness = 2
        SolidLineForDrawing.Stroke = Brushes.Black
        Me.Children.Add(SolidLineForDrawing)

        'Create and add an empty polyline to the DrawingCanvas
        DottedLineForPreviewDrawing.StrokeThickness = 1
        DottedLineForPreviewDrawing.Stroke = Brushes.Black
        Dim d As New DoubleCollection
        d.Add(3.5)
        d.Add(1.5)
        DottedLineForPreviewDrawing.StrokeDashArray = d
        Me.Children.Add(DottedLineForPreviewDrawing)

        'Enables a rectangle of the zoom in on DrawingCanvas
        RectZoom.Fill = Brushes.Transparent
        RectZoom.Stroke = Brushes.Black
        RectZoom.StrokeThickness = 2
        RectZoom.StrokeDashArray = d
        RectZoom.Height = 0
        RectZoom.Width = 0
        Me.Children.Add(RectZoom)

        'Set clip to bounds true, as elements outside
        'the Decorator should not be visible unless you increas the size
        Me.ClipToBounds = True
    End Sub
#End Region

Some dependency properties follow, as well as the Type of SelectedDrawingEvent, given below as an Enum:

Public Enum SelectedDrawingEvent
    SelectCursor
    Hand
    ZoomInRect
    PlacePoint
    DrawLine
    DrawClosedPolygon
    AddPoints
End Enum

If you hold in the Ctrl button and turn the mouse Wheel you can also resize just the selected element:

''' <summary>
''' Resize the selected element only
''' </summary>
''' <param name="e"></param>
''' <remarks>Should be called from Parant control</remarks>
Public Sub ChangeSizeOfElement(ByVal e As MouseWheelEventArgs)

    'Ued to translate mouse wheel argument to zoom factor
    Dim scalefactor As Double

    'Creating And Storing The scalefactor
    Dim scale As Double = 1
    'Transform the MouseWheel to the scalefactor
    If e.Delta > 0 Then
        scalefactor = 0.1
    ElseIf e.Delta < 0 Then
        scalefactor = -0.1
    End If

    'The scale cannot be negative
    If scale + scalefactor < 0 Then
        Exit Sub
    End If

    'Adjusting the old scale
    scale = scale + scalefactor

    If TypeOf (SelectedElement) Is CustomPolygon Then
        Dim NewPointCollection As New PointCollection
        Dim OldCenterPoint As New Point
        Dim NewCenterPoint As New Point
        Dim PointerCustomPolygon As CustomPolygon = DirectCast(SelectedElement, CustomPolygon)
        For Each p As Point In PointerCustomPolygon.Points
            OldCenterPoint.X += p.X
            OldCenterPoint.Y += p.Y
        Next

        OldCenterPoint.X /= PointerCustomPolygon.Points.Count
        OldCenterPoint.Y /= PointerCustomPolygon.Points.Count

        NewCenterPoint.X = OldCenterPoint.X * scale
        NewCenterPoint.Y = OldCenterPoint.Y * scale

        Dim correction As New Point
        correction.X = OldCenterPoint.X - NewCenterPoint.X
        correction.Y = OldCenterPoint.Y - NewCenterPoint.Y

        For Each p As Point In PointerCustomPolygon.Points
            NewPointCollection.Add(New Point(p.X * scale + correction.X, p.Y * scale + correction.Y))
        Next

        PointerCustomPolygon.Points = NewPointCollection
        PointerCustomPolygon.ReDraw()

    ElseIf TypeOf (SelectedElement) Is CustomLine Then
        Dim PointerCustomLine As CustomLine = DirectCast(SelectedElement, CustomLine)

        Dim NewPointCollection As New PointCollection
        Dim OldCenterPoint As New Point
        Dim NewCenterPoint As New Point

        For Each CustomLinePoint As Point In PointerCustomLine.Points
            OldCenterPoint.X += CustomLinePoint.X
            OldCenterPoint.Y += CustomLinePoint.Y
        Next

        OldCenterPoint.X /= PointerCustomLine.Points.Count
        OldCenterPoint.Y /= PointerCustomLine.Points.Count

        NewCenterPoint.X = OldCenterPoint.X * scale
        NewCenterPoint.Y = OldCenterPoint.Y * scale

        Dim MassCenterCorrection As New Point
        MassCenterCorrection.X = OldCenterPoint.X - NewCenterPoint.X
        MassCenterCorrection.Y = OldCenterPoint.Y - NewCenterPoint.Y

        For Each p As Point In PointerCustomLine.Points
            NewPointCollection.Add(New Point(p.X * scale + MassCenterCorrection.X, _
                                   p.Y * scale + MassCenterCorrection.Y))
        Next

        PointerCustomLine.Points = NewPointCollection
        PointerCustomLine.ReDraw()
    End If

End Sub

You can also move the complete Line or Polygon by holding down the mouse on the lines. If you Right click on one of the points, you would just move the selected point and not the complete element.

The mouse down event on the DrawingCanvas is given in code blow. Becouse of the complex dragging events, applying to points and lines, I would have to control both the Preview MouseDown and the MouseDown event.

Private Sub CanvasDraw_MouseDown(sender As System.Object, _
           e As System.Windows.Input.MouseButtonEventArgs) Handles Me.PreviewMouseDown
    Dim Actual_position, Modified_position As New Point
    Actual_position = Mouse.GetPosition(Me)
    Modified_position = Mouse.GetPosition(Me)

    MousePosition = "X: " & CInt(Actual_position.X) & " Y: " & CInt(Actual_position.Y)

    If CanvasEvent = SelectedDrawingEvent.SelectCursor Then
        If Mouse.RightButton = MouseButtonState.Pressed Then
            CreateContextMenu()
        End If
    ElseIf CanvasEvent = SelectedDrawingEvent.AddPoints Then
        AddPointToCustomDrawingObject(Actual_position)
    ElseIf CanvasEvent = SelectedDrawingEvent.ZoomInRect Then
        RectangleZoom(Actual_position, e)
    Else
        If Mouse.LeftButton = MouseButtonState.Pressed Or Mouse.MiddleButton = MouseButtonState.Pressed Then
            If Not CanvasEvent = SelectedDrawingEvent.PlacePoint Then
                DrawCustomObjectWithLines(Actual_position, Modified_position)
            Else
                Dim NewCustomPoint As New CustomPoint("FileAttr", Actual_position)
                Me.Children.Add(NewCustomPoint)
                ClearTempVariables()
            End If
        ElseIf Mouse.RightButton = MouseButtonState.Pressed Then
            'Check if the Lines or Polygons could be ended or closed:
            If MouseLeftButtonDownOnDrawingCanvas.Count > 1 Then
                If CanvasEvent = SelectedDrawingEvent.DrawClosedPolygon Then
                    'Add a new polygon
                    Dim NewCustomPolygon As New CustomPolygon(MouseLeftButtonDownOnDrawingCanvas.Clone)
                    Me.Children.Add(NewCustomPolygon)
                    ClearTempVariables()
                ElseIf CanvasEvent = SelectedDrawingEvent.DrawLine Then
                    'Add a new line
                    Dim NewCustomLine As New CustomLine(MouseLeftButtonDownOnDrawingCanvas.Clone)
                    Me.Children.Add(NewCustomLine)
                    ClearTempVariables()
                Else
                    'The program should not have come this far
                    ClearTempVariables()
                    MessageBox.Show("You have not selected type of drawing")
                End If
                e.Handled = True
            Else
                ClearTempVariables()
            End If
        End If
    End If
End Sub

I also registered two events. A INotifyPropertyChanged and a ChangeRectangelZoom. The last one I implemented to send the Select zoom to the custom DrawingScrollbar. This control handles all the zooming done on the DrawingCanvas.

#Region "Events"
    Public Event PropertyChanged(sender As Object, e
 As System.ComponentModel.PropertyChangedEventArgs) Implements 
System.ComponentModel.INotifyPropertyChanged.PropertyChanged

    Private Sub NotifyPropertyChanged(ByVal propertyName As String)
        RaiseEvent PropertyChanged(Me, New ComponentModel.PropertyChangedEventArgs(propertyName))
    End Sub

    Public Event NewRectangleZoom(ByVal TopLeft As Point, ByVal BottomRight As Point)

    Private Sub ChangeRectangleZoom(ByVal TopLeft As Point, ByVal BottomRight As Point)
        RaiseEvent NewRectangleZoom(TopLeft, BottomRight)
    End Sub
#End Region

As you noticed the Control has a host of preview subs that is primarily used were you need to check what kind of event it is, and you have the need to stop it from bubbling and tunneling past this point. The preview events are nearly always followed by the normal event, were all the necessary implementation won’t happen in any other control.

There is also one other feature that I didn’t explain, if you hold down the Shift key, you'll get a 90 degree bend on the lines, whether they are Polygon or Line drawings.

Zooming on the canvas

When you are zooming you usually want to keep the focus on the spot where your cursor is, meaning that the position on the canvas remains on the same spot after zooming in with a mouse wheel event. This requires some coding on the custom Scrollviewer to function like that, and the code is taken from MSDN WPF Forum site and was an answer given in this thread.

All that is needed it to take advantage of the ScaleTransform to zoom in. I do however have the ability to set the render area, which means that I would have to use ScaleTransform on the DrawingCanas alone, and calculate the new resize border around the actual drawing area. This is done by registering when the mouse it over the resize icon, and if you are, you cannot zoom in with the mouse wheel. The complete class for DrawingScrollViewer is given below:

Public Class DrawingScrollViewer
    Inherits ScrollViewer

#Region "Data"
    ' Used when manually scrolling
    Private scrollStartPoint As Point
    Private scrollStartOffset As Point

#End Region

    ' Since Im going to have clicable elements I want to turn off the possibility of moving the canvans and 
    ' stopping an event on it. I called e.Handle = True in these functions, as I wanted to move around freely 
    ' without thinking about getting hits from other elements.
    Public Shared ReadOnly HandProperty As DependencyProperty = _
       DependencyProperty.Register("Hand", _
       GetType([Boolean]), GetType(DrawingScrollViewer), New PropertyMetadata())

    Private _Hand As Boolean = False
    Public Property Hand() As Boolean
        Get
            Return _Hand
        End Get
        Set(ByVal value As Boolean)
            _Hand = value
        End Set
    End Property

#Region "Mouse Events"
    Protected Overrides Sub OnPreviewMouseDown(e As MouseButtonEventArgs)
        If Hand Then
            If IsMouseOver Then
                ' Save starting point, used later when determining how much to scroll.
                scrollStartPoint = e.GetPosition(Me)
                scrollStartOffset.X = HorizontalOffset
                scrollStartOffset.Y = VerticalOffset

                ' Update the cursor if can scroll or not.
                Me.Cursor = If((ExtentWidth > ViewportWidth) OrElse _
                    (ExtentHeight > ViewportHeight), Cursors.ScrollAll, Cursors.Arrow)

                Me.CaptureMouse()
            End If
            MyBase.OnPreviewMouseDown(e)
            e.Handled = True
        End If
    End Sub

    Protected Overrides Sub OnPreviewMouseMove(e As MouseEventArgs)
        If Hand Then
            If Me.IsMouseCaptured Then
                ' Get the new scroll position.
                Dim point As Point = e.GetPosition(Me)

                ' Determine the new amount to scroll.
                Dim delta As New Point(If((point.X > Me.scrollStartPoint.X), -(point.X - Me.scrollStartPoint.X), _
                   (Me.scrollStartPoint.X - point.X)), If((point.Y > Me.scrollStartPoint.Y),_
                    -(point.Y - Me.scrollStartPoint.Y), (Me.scrollStartPoint.Y - point.Y)))

                ' Scroll to the new position.
                ScrollToHorizontalOffset(Me.scrollStartOffset.X + delta.X)
                ScrollToVerticalOffset(Me.scrollStartOffset.Y + delta.Y)
            End If

            MyBase.OnPreviewMouseMove(e)
            e.Handled = True
        End If
    End Sub

    Protected Overrides Sub OnPreviewMouseUp(e As MouseButtonEventArgs)
        If Hand Then
            If Me.IsMouseCaptured Then
                Me.Cursor = Cursors.Arrow
                Me.ReleaseMouseCapture()
            End If
            MyBase.OnPreviewMouseUp(e)
            e.Handled = True
        End If
    End Sub

    Public Sub ScrollFromCode(ByVal CenterPoint As Point)
        ScrollFromRectangleZoom = True
        OldSenterPoint = CenterPoint
    End Sub

    Private OldSenterPoint As New Point
    Private ScrollFromRectangleZoom As Boolean = False

    ' This assunes that you want to keep the same center in your picture while zooming in
    Protected Overrides Sub OnScrollChanged(e As System.Windows.Controls.ScrollChangedEventArgs)
        MyBase.OnScrollChanged(e)

        'http://social.msdn.microsoft.com/Forums/en-US/wpf/thread/c48379ca-a3e1-4930-a2a1-a7bf5d5f5cf9
        If e.Source Is Me Then
            If e.ExtentHeightChange <> 0 Or e.ExtentWidthChange <> 0 Then
                Dim mousePosition As Point
                If ScrollFromRectangleZoom Then
                    mousePosition = OldSenterPoint
                    ScrollFromRectangleZoom = False
                Else
                    mousePosition = Mouse.GetPosition(Me)
                End If


                Dim offsetx As Double = e.HorizontalOffset + mousePosition.X
                Dim offsety As Double = e.VerticalOffset + mousePosition.Y

                Dim oldExtentWidth As Double = e.ExtentWidth - e.ExtentWidthChange
                Dim oldExtentHeight As Double = e.ExtentHeight - e.ExtentHeightChange

                Dim relx As Double = offsetx / oldExtentWidth
                Dim rely As Double = offsety / oldExtentHeight

                offsetx = Math.Max(relx * e.ExtentWidth - mousePosition.X, 0)
                offsety = Math.Max(rely * e.ExtentHeight - mousePosition.Y, 0)

                Me.ScrollToHorizontalOffset(offsetx)
                Me.ScrollToVerticalOffset(offsety)

            End If
        End If
    End Sub
#End Region

End Class

All zoom events are hosed in the DrawingCanvasControl, and it is this that forms the actual user control whitch you implement in the main program.  It was however not easy to implement becouse I wanted to attach a specific zooming that did the following:

Changed the size of the DrawingCanvas only, meaning that I could call LayoutTransfrom on this object, but at the same time not to change the layout of the Resize control.   

About the Style

The only thin I'll say about how the styles is implemented, and especially the RadioButtons for selecting the type of drawing elements you select. They are implemented as an own MainViewModel class that consists of a ObservableCollection with three variables. How its connected to the Window is also given below, and you could read more about the approch here.

There is however one thing that you should know about  adding resources to your project. If you do it on the main form like this: 

<Window.Resources>
    <ResourceDictionary>
        <ResourceDictionary.MergedDictionaries>
            <ResourceDictionary Source="Resources/AppStyles.xaml"/>
        </ResourceDictionary.MergedDictionaries>
        <local:MainViewModel x:Key="ViewModel" />
        <ImageBrush x:Key="Ocean" ImageSource="Images/OceanWaves.jpg" Stretch="Fill"  />
    </ResourceDictionary> 
</Window.Resources>  

The styles won't be found in other forms that you have stored in your project.  You can add them as a resource to those projects also but that seems to be a little cumbersome way to do it. The easier thing to do is to add the styles to the  main Application.XAML file like this:

    <Application.Resources>
        <ResourceDictionary>
            <ResourceDictionary.MergedDictionaries>
                <ResourceDictionary Source="Resources/AppStyles.xaml"/>
            </ResourceDictionary.MergedDictionaries>
        </ResourceDictionary>
    </Application.Resources> 

 This would mean that all your styles are available in all your windows, regardless where  they are stored.  You would of course have to set your local resources in your main window though:

     <Window.Resources> 
            <local:MainViewModel x:Key="ViewModel" />
            <ImageBrush x:Key="Ocean" ImageSource="Images/OceanWaves.jpg" Stretch="Fill"  /> 
    </Window.Resources> 

The styling in the main window, regarding the Radiobuttons are done the following way: 

<StackPanel Orientation="Horizontal" Background="Black" 
        x:Name="rbtn" DataContext="{StaticResource ViewModel}" Height="47">
    <ItemsControl  VerticalAlignment="Center" 
                Margin="5" ItemsSource="{Binding Intersections}">
        <ItemsControl.Template>
            <ControlTemplate>
                <WrapPanel  Width="{TemplateBinding Width}" 
                     Height="{TemplateBinding Height}" 
                     FlowDirection="LeftToRight" IsItemsHost="true"/>
            </ControlTemplate>
        </ItemsControl.Template>
        <ItemsControl.ItemTemplate>
            <DataTemplate>
                <WrapPanel>
                    <RadioButton GroupName="Intersections" Margin="0" 
                             Height="30" Style="{StaticResource toggleStyle}" 
                             IsChecked="{Binding IsChecked, Mode=TwoWay}">
                        <RadioButton.Content>
                            <TextBlock Text="{Binding Text}" VerticalAlignment="Center" 
                               FontSize="14" Foreground="White" Margin="5" />
                        </RadioButton.Content>
                    </RadioButton>
                </WrapPanel>
            </DataTemplate>
        </ItemsControl.ItemTemplate>
    </ItemsControl>
</StackPanel>

And the style, which is converted from a Silverlight template by Michael Sync, and its implemented and is set in the AppStyles.XAML: 

<!--Glass Styled Radiobutton-->
<Style x:Key="toggleStyle" 
         BasedOn="{StaticResource {x:Type ToggleButton}}" 
         TargetType="{x:Type RadioButton}">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type ToggleButton}">
                <Border BorderBrush="#FFFFFFFF" BorderThickness="1,1,1,1" >
                    <Border.Triggers>
                    <EventTrigger RoutedEvent="Border.MouseEnter">
                        <EventTrigger.Actions>
                            <BeginStoryboard>
                                    <Storyboard>
                                        <DoubleAnimationUsingKeyFrames BeginTime="00:00:00" 
                                                Storyboard.TargetName="glow" 
                                                Storyboard.TargetProperty="(UIElement.Opacity)">
                                            <SplineDoubleKeyFrame KeyTime="00:00:00.3000000" Value="1"/>
                                        </DoubleAnimationUsingKeyFrames>
                                    </Storyboard>
                                </BeginStoryboard>
                        </EventTrigger.Actions>
                    </EventTrigger>
                        <EventTrigger RoutedEvent="Border.MouseLeave">
                            <EventTrigger.Actions>
                                <BeginStoryboard>
                                    <Storyboard>
                                        <DoubleAnimationUsingKeyFrames BeginTime="00:00:00" 
                                                Storyboard.TargetName="glow" 
                                                Storyboard.TargetProperty="(UIElement.Opacity)">
                                            <SplineDoubleKeyFrame KeyTime="00:00:00.3000000" Value="0"/>
                                        </DoubleAnimationUsingKeyFrames>
                                    </Storyboard>
                                </BeginStoryboard>
                            </EventTrigger.Actions>
                        </EventTrigger>
                    </Border.Triggers>
                    <Border x:Name="border" Background="#7F000000" 
                          BorderBrush="#FF000000" BorderThickness="1,1,1,1" 
                          CornerRadius="4,4,4,4">
                        <Grid>
                            <Grid.RowDefinitions>
                                <RowDefinition Height="0.507*"/>
                                <RowDefinition Height="0.493*"/>
                            </Grid.RowDefinitions>
                            <Border Opacity="0" HorizontalAlignment="Stretch" 
                                    x:Name="glow" Width="Auto" 
                                    Grid.RowSpan="2" CornerRadius="4,4,4,4">
                                <Border.Background>
                                    <RadialGradientBrush>
                                        <RadialGradientBrush.RelativeTransform>
                                            <TransformGroup>
                                                <ScaleTransform ScaleX="1.702" ScaleY="2.243"/>
                                                <SkewTransform AngleX="0" AngleY="0"/>
                                                <RotateTransform Angle="0"/>
                                                <TranslateTransform X="-0.368" Y="-0.152"/>
                                            </TransformGroup>
                                        </RadialGradientBrush.RelativeTransform>
                                        <GradientStop Color="#B28DBDFF" Offset="0"/>
                                        <GradientStop Color="#008DBDFF" Offset="1"/>
                                    </RadialGradientBrush>
                                </Border.Background>
                            </Border>
                            <ContentPresenter HorizontalAlignment="Center" 
                              VerticalAlignment="Center" Width="Auto" Grid.RowSpan="2"/>
                            <Border HorizontalAlignment="Stretch" 
                                  Margin="0,0,0,0" x:Name="shine" Width="Auto" >
                                <Border.Background>
                                    <LinearGradientBrush EndPoint="0.494,0.889" StartPoint="0.494,0.028">
                                        <GradientStop Color="#99FFFFFF" Offset="0"/>
                                        <GradientStop Color="#33FFFFFF" Offset="1"/>
                                    </LinearGradientBrush>
                                </Border.Background>
                            </Border>
                        </Grid>
                    </Border>
                </Border>
   
                <ControlTemplate.Triggers>
                    <Trigger Property="IsChecked" Value="true">
                        <Setter Property="Background" 
                               TargetName="border" Value="Blue"/>
                    </Trigger>
                </ControlTemplate.Triggers>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

How to use it

Let’s say you want to draw a Race car track, and you could get a hold of the satellite photo of the complete track. And you want to incorporate the track in XAML code in your program. You would simply do this:

  1. Click on Add Picture, and add the overview picture of the track.
  2. Zoom in by either using the mouse wheel, while the Pointer button is blue. If you want to Pan the image a little you simply click on the Hand for dragging, and drag the image into position.
  3. Select the Draw Polygon (assuming the track is circular). Left Click on the points that represent the track and end with a mouse right button to close the polygon. You don’t have to be very accurate, as you could adjust the track afterwards. You do this by selecting the Pointer, and click on the Polygon to get it selected. Click on the Add point’s button. And place the mouse were you want to add points.
  4. That would complete the Race track, but you want to scale the track up. You click on the pointer button, select the polygon. Hold down the Ctrl button and start the mouse Wheel and you’ll see that is either a scale the track up or down, the track is now the right size but some of the lanes are outside the boundaries.
  5. You move your mouse to the lower left corner; make sure the Pointer button I clicked. It's located just outside the gray square. Hold the Left mouse button down, and simply drag into the empty gray area and you'll see it would get bigger. Now you want to move the entre circuit.
  6. Again make sure the Pointer button is blue and click and hold (Left button down on one of the line segments. Start dragging it, into the right position.
  7. Click on export to XAML and a window would open to show you all the lines that make up the Race track circuit.

Tip: You complete each drawing or zoom event by RightMouseButtonDown.

And yes, I was thinking of a possible editor for WPF Grand Prix program by Marcelo Ricardo de Oliveira 

So, whats missing?

Well.... Quite a lot actually. There are some things I did not include, as I though the program code was enough for my purposes. The most pressing needs are probably:

  • A properties editor that binds to the selected element, were you could set the properties of the different shapes that is showed on the canvas. This would involve creating a lot of dependency properties on each custom user control. I could however use the properties editor from this project at the CodePlex site.
  • An undo history so that you could go back.
  • Saving the drawings to file, and of course loading from file.
  • Some basic elements are missing: Circles, Ellipses, Bezier segments etc. Have not included those, but they are definitely useful in a drawing program.   

History

Well, that was the whole run through. There are some styling issues that I did not mention, but I assume that they are not so difficult to find out, and I left those out of the article and would refer you to the source code to check it out.

Anyways, I hope you'll find the tool useful. I might revisit this program in the future and expand the functionality much more.

References

A couple of CodeProject articles are used in the creation of the program:

My own article of Adding a point to a Polyline is also used in the program:

The GlassButton style is taken from the blog entery by Michael Sync: 

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here