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:
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
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"
Protected Overrides ReadOnly Property VisualChildrenCount() As Integer
Get
Return _children.Count
End Get
End Property
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"
Private Function CreateDrawingVisualCircle() As DrawingVisual
Dim drawingVisual As New DrawingVisual()
Dim drawingContext As DrawingContext = drawingVisual.RenderOpen()
drawingContext.DrawEllipse(Brushes.Red, New Pen(Brushes.Red, 1.0), PositionOnCanvas, 4, 4)
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"
Private RectZoom As New Rectangle
Private SolidLineForDrawing, DottedLineForPreviewDrawing As New Polyline
Private MouseLeftButtonDownOnDrawingCanvas As New PointCollection
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)))
SolidLineForDrawing.StrokeThickness = 2
SolidLineForDrawing.Stroke = Brushes.Black
Me.Children.Add(SolidLineForDrawing)
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)
RectZoom.Fill = Brushes.Transparent
RectZoom.Stroke = Brushes.Black
RectZoom.StrokeThickness = 2
RectZoom.StrokeDashArray = d
RectZoom.Height = 0
RectZoom.Width = 0
Me.Children.Add(RectZoom)
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:
Public Sub ChangeSizeOfElement(ByVal e As MouseWheelEventArgs)
Dim scalefactor As Double
Dim scale As Double = 1
If e.Delta > 0 Then
scalefactor = 0.1
ElseIf e.Delta < 0 Then
scalefactor = -0.1
End If
If scale + scalefactor < 0 Then
Exit Sub
End If
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
If MouseLeftButtonDownOnDrawingCanvas.Count > 1 Then
If CanvasEvent = SelectedDrawingEvent.DrawClosedPolygon Then
Dim NewCustomPolygon As New CustomPolygon(MouseLeftButtonDownOnDrawingCanvas.Clone)
Me.Children.Add(NewCustomPolygon)
ClearTempVariables()
ElseIf CanvasEvent = SelectedDrawingEvent.DrawLine Then
Dim NewCustomLine As New CustomLine(MouseLeftButtonDownOnDrawingCanvas.Clone)
Me.Children.Add(NewCustomLine)
ClearTempVariables()
Else
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"
Private scrollStartPoint As Point
Private scrollStartOffset As Point
#End Region
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
scrollStartPoint = e.GetPosition(Me)
scrollStartOffset.X = HorizontalOffset
scrollStartOffset.Y = VerticalOffset
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
Dim point As Point = e.GetPosition(Me)
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)))
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
Protected Overrides Sub OnScrollChanged(e As System.Windows.Controls.ScrollChangedEventArgs)
MyBase.OnScrollChanged(e)
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:
<!---->
<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:
- Click on Add Picture, and add the overview picture of the track.
- 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.
- 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.
- 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.
- 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.
- 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.
- 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: