Silverlight Ux Musings: Providing Panning Functionality for a Canvas of Objects - Part 2 [Corrina Barber]
I’m back with part 2 of the blog on panning functionality (part 1 is here), and, to quickly recap, we’re creating a region in a web site that can be panned rather than scrolled (users can click and drag to pan and navigate the region). We’re also designing the region so images always align properly (no images clip when the user finishes panning).
I have a sample solution that looks like my design goal pictured at right that can be downloaded here or you can simply check it out online here. Please note that the UI in this solution has many buttons and features that are disabled because my primary focus was to provide code for the panning functionality.
Necessities
More detailed information can be found on the Silverlight.NET site
§ Microsoft Silverlight 1.1 Alpha September Refresh
§ Visual Studio 2008
§ Microsoft Silverlight Tools Alpha Refresh for Visual Studio 2008 Beta 2 (July 2007)
§ Expression Blend 2 September Preview
Details: Visual Studio (xaml)
Open the solution created in part 1 (a copy can be found here). Again, this is a wireframe version of only the panning region; not the full fidelity UI pictured above. The first thing we need to do is to make a few modifications to Page.xaml.
1. Open Page.xaml and find the canvas named ‘allImgs’. Right below the opening tag add the following clipping path xaml. This clipping path is sized the same as the parent canvas (remember, we planned to use the height and width of this canvas as defined in Blend as a guide when creating the clipping path in Visual Studio)
<Canvas.Clip>
<RectangleGeometry Rect="0,0,264,356"></RectangleGeometry>
</Canvas.Clip>
2. Locate the ‘allImgsRectangle’ and add an opacity attribute and set it to 0. Remember, we want to only show this rectangle when the mouse is in the panning region (we could have done this in Blend too ;)).
<Rectangle Width="280" Height="372" Stroke="#FF000000" Canvas.Left="12" Canvas.Top="12" x:Name="allImgsRectangle" Opacity="0"/>
3. Locate the ‘allImgsCanvas’ and then locate the <Canvas.RenderTransform> tag for the canvas (it should be right below the opening tag). Add an x:Name attribute to <TranslateTransform.. and name it ‘allImgsTransform’. This will allow us to control the transform values from code.
<Canvas.RenderTransform>
<TransformGroup>
<ScaleTransform ScaleX="1" ScaleY="1"/>
<SkewTransform AngleX="0" AngleY="0"/>
<RotateTransform Angle="0"/>
<TranslateTransform x:Name="allImgsTransform" X="0" Y="0"/>
</TransformGroup>
</Canvas.RenderTransform>
4. Locate the ‘allImgsPan’ storyboard (it should be at the very top of the page), and then locate the two <DoubleAnimationUsingKeyframe> tags that apply to the TranslateTransform X and Y values. Add x:Name attributes to both child <ChildDoubleKeyFrame> tags as shown below.
<Canvas.Resources>
<Storyboard x:Name="allImgsPan">
<DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="allImgsCanvas" Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[0].(ScaleTransform.ScaleX)">
<SplineDoubleKeyFrame KeyTime="00:00:00" Value="1"/>
<SplineDoubleKeyFrame KeyTime="00:00:00.5000000" Value="1.1"/>
<SplineDoubleKeyFrame KeyTime="00:00:01" Value="1"/>
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="allImgsCanvas" Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[0].(ScaleTransform.ScaleY)">
<SplineDoubleKeyFrame KeyTime="00:00:00" Value="1"/>
<SplineDoubleKeyFrame KeyTime="00:00:00.5000000" Value="1.1"/>
<SplineDoubleKeyFrame KeyTime="00:00:01" Value="1"/>
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="allImgsCanvas" Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[3].(TranslateTransform.X)">
<SplineDoubleKeyFrame x:Name="allImgsStartX" KeyTime="00:00:00.5000000" Value="0"/>
<SplineDoubleKeyFrame x:Name="allImgsEndX" KeyTime="00:00:01" Value="0"/>
</DoubleAnimationUsingKeyFrames>
<DoubleAnimationUsingKeyFrames BeginTime="00:00:00" Storyboard.TargetName="allImgsCanvas" Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[3].(TranslateTransform.Y)">
<SplineDoubleKeyFrame x:Name="allImgsStartY" KeyTime="00:00:00.5000000" Value="0"/>
<SplineDoubleKeyFrame x:Name="allImgsEndY" KeyTime="00:00:01" Value="0"/>
</DoubleAnimationUsingKeyFrames>
</Storyboard>
</Canvas.Resources>
At this point, we’re ready to finish all of our work in the VB code. You’ll find a copy of the project to this point here.
Details: Visual Studio (VB)
Open ‘Page.xaml.vb’ from the Solution Explorer to get started. The first thing we’re going to do is add mouse enter and leave events for the ‘allImgsCanvas’ canvas, so we can get the perimeter rectangle to hide and show as described earlier.
5. Create a MouseEnter event for the canvas. Set the opacity on the ‘allImgsRectangle’ to 100 and capture the Canvas sender object and set it’s cursor to a hand as shown below.
' All Imgs mouse enter
Private Sub allImgsCanvas_MouseEnter(ByVal sender As Object, ByVal e As System.Windows.Input.MouseEventArgs) Handles allImgsCanvas.MouseEnter
'Set opacity on allImgsRectangle so it's visible
allImgsRectangle.Opacity = 100
'Set the cursor to the hand
Dim _Canvas As Canvas = CType(sender, Canvas) 'Explicit conversion to Canvas
_Canvas.Cursor = Input.Cursors.Hand
End Sub
6. Create a MouseLeave event for the canvas. Set the opacity on the ‘allImgsRectangle’ back to 0 and, again, capture the Canvas sender object, but this time set it to an arrow as shown below.
' All Imgs mouse leave
Private Sub allImgsCanvas_MouseLeave(ByVal sender As Object, ByVal e As System.EventArgs) Handles allImgsCanvas.MouseLeave
'Set opacity on allImgsRectangle so it's invisible
allImgsRectangle.Opacity = 0
'Set the cursor to the arrow
Dim _Canvas As Canvas = CType(sender, Canvas) 'Explicit conversion to Canvas
_Canvas.Cursor = Input.Cursors.Arrow
End Sub
At this point, I recommend running your project to ensure that the perimeter rectangle does in fact appear on mouse enter and disappear on mouse leave. Next we’re going to implement the panning behavior.
7. Add a mouse down event for the ‘allImgsCanvas’ and capture the mouse position in a variable as well as create and set a variable (‘isMouseDown’) to true that will indicate that we have clicked down on the canvas (store these variables globally).
'Global variables
Private _lastPt As Point
Private _isMouseDown As Boolean = False
' All Imgs mouse down
Private Sub allImgsCanvas_MouseLeftButtonDown(ByVal sender As Object, ByVal e As
System.Windows.Input.MouseEventArgs) Handles allImgsCanvas.MouseLeftButtonDown
'Capture mouse down position
_lastPt = e.GetPosition(Me)
'Set isMouseDown to true
_isMouseDown = True
End Sub
8. Add a mouse up event for the ‘allImgsCanvas’ and set the ‘isMouseDown’ variable to false (we’re no longer mousing down in the canvas when we hit this event and need to keep track of this). Add this variable to the mouse leave event and set it to false there as well (for the same reason).
' All Imgs mouse leave
Private Sub allImgsCanvas_MouseLeave(ByVal sender As Object, ByVal e As System.EventArgs) Handles allImgsCanvas.MouseLeave
'Set opacity on allImgsRectangle so it's invisible
allImgsRectangle.Opacity = 0
'Set the cursor to the arrow
Dim _Canvas As Canvas = CType(sender, Canvas) 'Explicit conversion to Canvas
_Canvas.Cursor = Input.Cursors.Arrow
'Set isMouseDown to false
_isMouseDown = False
End Sub
' All Imgs mouse up
Private Sub allImgsCanvas_MouseLeftButtonUp(ByVal sender As Object, ByVal e As System.Windows.Input.MouseEventArgs) Handles allImgsCanvas.MouseLeftButtonUp
'Set isMouseDown to false
_isMouseDown = False
End Sub
9. Add a mouse move event for the ‘allImgsCanvas’. Capture the current mouse location, and if the mouse down event has happened set a variable to true to track that we’re doing the pan operation. Store this new variable globally also, and add it to the mouse up and leave events and set it to false in those locations (because when those events are hit we would no longer be panning).
'Global variables
Private _lastPt As Point
Private _isMouseDown As Boolean = False
Private _isMouseMove As Boolean = False
' All Imgs mouse leave
Private Sub allImgsCanvas_MouseLeave(ByVal sender As Object, ByVal e As System.EventArgs) Handles allImgsCanvas.MouseLeave
'Set opacity on allImgsRectangle so it's invisible
allImgsRectangle.Opacity = 0
'Set the cursor to the arrow
Dim _Canvas As Canvas = CType(sender, Canvas) 'Explicit conversion to Canvas
_Canvas.Cursor = Input.Cursors.Arrow
'Set isMouseDown to false
_isMouseDown = False
'Identify that we're NOT doing click and drag
_isMouseMove = False
End Sub
' All Imgs mouse up
Private Sub allImgsCanvas_MouseLeftButtonUp(ByVal sender As Object, ByVal e As System.Windows.Input.MouseEventArgs) Handles allImgsCanvas.MouseLeftButtonUp
'Set isMouseDown to false
_isMouseDown = False
'Identify that we're NOT doing click and drag
_isMouseMove = False
End Sub
' All Imgs mouse move
Private Sub allImgsCanvas_MouseMove(ByVal sender As Object, ByVal e As System.Windows.Input.MouseEventArgs) Handles allImgsCanvas.MouseMove
'Get the mouse location
Dim _curPt As Point = e.GetPosition(Me)
'Check to see that mouse up hasn't happened
If (_isMouseDown.Equals(True)) Then
'Identify that we're doing click and drag
_isMouseMove = True
End If
End Sub
10. Update the position of the canvas in the mouse move event by utilizing the ‘allImgsTransform’ object associated with the <TranslateTransform> tag in the ‘allImgsCanvas’. You’ll also need to update the globally stored ‘_lastPt’ variable to the last mouse position.
' All Imgs mouse move
Private Sub allImgsCanvas_MouseMove(ByVal sender As Object, ByVal e As System.Windows.Input.MouseEventArgs) Handles allImgsCanvas.MouseMove
'Get the mouse location
Dim _curPt As Point = e.GetPosition(Me)
'Check to see that mouse up hasn't happened
If (_isMouseDown.Equals(True)) Then
'Identify that we're doing click and drag
_isMouseMove = True
'Fix up the X value of the transform
allImgsTransform.X += (_curPt.X - _lastPt.X)
'Fix up the Y value of the transform
allImgsTransform.Y += (_curPt.Y - _lastPt.Y)
End If
'Update last mouse pt stored globally
_lastPt = _curPt
End Sub
At this point, run your project, and you’ll notice that you can pan the rectangles around. The only problem is that you can pan the rectangles out of the clipped region, and we don’t want this to happen, so we’ll fix this next.
11. Delete the following code from the ‘allImgsCanvas’ mouse move event.
'Fix up the X value of the transform
allImgsTransform.X += (_curPt.X - _lastPt.X)
'Fix up the Y value of the transform
allImgsTransform.Y += (_curPt.Y - _lastPt.Y)
12. Replace the code with the following code which will ensure that we cannot pan to far top/bottom and left/right.
'Ensure the x coordinates don't allow the canvas to disappear
If ((allImgsTransform.X + (_curPt.X - _lastPt.X)) > 0) Then
'Fix up the X value of the transform
allImgsTransform.X = 0
ElseIf ((allImgsTransform.X + (_curPt.X - _lastPt.X)) < -(_minX)) Then
'Fix up the X value of the transform
allImgsTransform.X = -(_minX)
Else
'Fix up the X value of the transform
allImgsTransform.X += (_curPt.X - _lastPt.X)
End If
'Ensure the Y coordinates don't allow the canvas to disappear
If ((allImgsTransform.Y + (_curPt.Y - _lastPt.Y)) > 0) Then
'Fix up the Y value of the transform
allImgsTransform.Y = 0
ElseIf ((allImgsTransform.Y + (_curPt.Y - _lastPt.Y)) < -(_minY)) Then
'Fix up the Y value of the transform
allImgsTransform.Y = -(_minY)
Else
'Fix up the Y value of the transform
allImgsTransform.Y += (_curPt.Y - _lastPt.Y)
End If
13. The ‘_minX’ and ‘_minY’ variables highlighted above are not available at this time, so we need to calculate them in ‘Page_Loaded’ and store them globally.
'Global variables
Private _lastPt As Point
Private _isMouseDown As Boolean = False
Private _isMouseMove As Boolean = False
Private _minX As Double
Private _minY As Double
Private Sub Page_Loaded(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Me.Loaded
'Required to initialize variables. Needs to be done from loaded event so FindName works properly.
Me.InitializeComponent()
' Set minimum change in x and y values allowed
_minX = (allImgsCanvas.Width - allImgs.Width)
_minY = (allImgsCanvas.Height - allImgs.Height) - 2 'one or more of my rectangles are aligned wrong so this is why I'm adding the -2 (and I couldn't find the problem quickly ;))
End Sub
If you run your project now, you’ll see that you can no longer pan the rectangles out of the viewing area. Now, let’s add the code to make it so the images always align nicely after they have been panned around. The work will be triggered in the mouse up and leave events.
14. In the ‘allImgsCanvas’ mouse leave and up events add the following code snippet before the part where you set ‘_isMouseDown’ and ‘_isMouseMove’ to false
'If the mouse has been down and we did some panning fix up alignment
If ((_isMouseDown.Equals(True)) And (_isMouseMove.Equals(True))) Then
'Fix panning alignment
fixPanningAlignment()
End If
15. Add the ‘fixPanningAlignment’ method and create a variable to store the column the canvas should snap to. To do this we need to know the size of the rectangles, and we can capture this data in the ‘Page_Loaded’ event by referencing one of the rectangles we named in Blend (Rectangle1 for example) and store it globally as shown below.
'Global variables
Private _lastPt As Point
Private _isMouseDown As Boolean = False
Private _isMouseMove As Boolean = False
Private _minX As Double
Private _minY As Double
Private _imgWidth As Double
Private _imgHeight As Double
Private Sub Page_Loaded(ByVal sender As System.Object, ByVal e As System.EventArgs) Handles Me.Loaded
'Required to initialize variables. Needs to be done from loaded event so FindName works properly.
Me.InitializeComponent()
' Set minimum change in x and y values allowed
_minX = (allImgsCanvas.Width - allImgs.Width)
_minY = (allImgsCanvas.Height - allImgs.Height) - 2 'a rectangle is aligned wrong is why I'm doing -2 (and I couldn't find it quickly :()
'Get the image width and height values and store globally
_imgWidth = Rectangle1.Width + 12 '+12 because each image has 6px padding or 12px space between images
_imgHeight = Rectangle1.Height + 12 '+12 because each image has 6px padding or 12px space between images
End Sub
' Fix the alignment of images after panning
Private Sub fixPanningAlignment()
'Find out how many times the current X position can be divided by _imgWidth
Dim _numWidths As Integer = allImgsTransform.X / _imgWidth
End Sub
16. Capture the direction of the mouse movement and use this to figure out which column we should be snapping to. To do this we not only need the last mouse position, but the one right before that, so we’re going to add a ‘_secondToLast’ global variable to store this information and we’ll capture this data in the mouse move event.
'Global variables
Private _lastPt As Point
Private _isMouseDown As Boolean = False
Private _isMouseMove As Boolean = False
Private _minX As Double
Private _minY As Double
Private _imgWidth As Double
Private _imgHeight As Double
Private _secondToLastPt As Point
' All Imgs mouse move
Private Sub allImgsCanvas_MouseMove(ByVal sender As Object, ByVal e As System.Windows.Input.MouseEventArgs) Handles allImgsCanvas.MouseMove
'Get the mouse location
Dim _curPt As Point = e.GetPosition(Me)
'Check to see that mouse up hasn't happened
If (_isMouseDown.Equals(True)) Then
'Identify that we're doing click and drag
_isMouseMove = True
'Ensure the x coordinates don't allow the canvas to disappear
If ((allImgsTransform.X + (_curPt.X - _lastPt.X)) > 0) Then
'Fix up the X value of the transform
allImgsTransform.X = 0
ElseIf ((allImgsTransform.X + (_curPt.X - _lastPt.X)) < -(_minX)) Then
'Fix up the X value of the transform
allImgsTransform.X = -(_minX)
Else
'Fix up the X value of the transform
allImgsTransform.X += (_curPt.X - _lastPt.X)
End If
'Ensure the Y coordinates don't allow the canvas to disappear
If ((allImgsTransform.Y + (_curPt.Y - _lastPt.Y)) > 0) Then
'Fix up the Y value of the transform
allImgsTransform.Y = 0
ElseIf ((allImgsTransform.Y + (_curPt.Y - _lastPt.Y)) < -(_minY)) Then
'Fix up the Y value of the transform
allImgsTransform.Y = -(_minY)
Else
'Fix up the Y value of the transform
allImgsTransform.Y += (_curPt.Y - _lastPt.Y)
End If
End If
'Update last mouse pt and second to last mouse pt stored globally
_secondToLastPt = _lastPt
_lastPt = _curPt
End Sub
' Fix the alignment of images after panning
Private Sub fixPanningAlignment()
'Find out how many times the current X position can be divided by _imgWidth
Dim _numWidths As Integer = allImgsTransform.X / _imgWidth
'Pickup direction of mouse movement in the x direction and adjust animation accordingly
If ((_lastPt.X < _secondToLastPt.X) And (_numWidths <> 0)) Then
'Decrement the number of widths
_numWidths -= 1
End If
End Sub
17. We need to make sure we don’t pan to far top/bottom or left/right similar to what we did in the mouse move event, so add the following code to the end of the ‘fixPanningAlignment’ method
'Don't scroll canvas too far to the right or left
Dim _checkWidth As Integer = _numWidths * _imgWidth
'Do check and reset values if need be
If (_checkWidth > 0) Then
_checkWidth = 0
ElseIf (_checkWidth < -(_minX)) Then
_checkWidth = -(_minX)
End If
18. Update the storyboard x:name attributes for the X coordinates by adding the following code to the end of the ‘fixPanningAlignment’ method
'Set storyboard values for the x coordinates
allImgsStartX.Value = allImgsTransform.X
allImgsEndX.Value = _checkWidth
19. In the ‘fixPanningAlignment’ method duplicate all of the code described above except update the Y values, and then start the storyboard. Your ‘fixPanningAlignment’ method should look as follows.
' Fix the alignment of images after panning
Private Sub fixPanningAlignment()
'Find out how many times the current X position can be divided by _imgWidth
Dim _numWidths As Integer = allImgsTransform.X / _imgWidth
'Pickup direction of mouse movement in the x direction and adjust animation accordingly
If ((_lastPt.X < _secondToLastPt.X) And (_numWidths <> 0)) Then
'Decrement the number of widths
_numWidths -= 1
End If
'Don't scroll canvas too far to the right or left
Dim _checkWidth As Integer = _numWidths * _imgWidth
'Do check and reset values if need be
If (_checkWidth > 0) Then
_checkWidth = 0
ElseIf (_checkWidth < -(_minX)) Then
_checkWidth = -(_minX)
End If
'Set storyboard values for the x coordinates
allImgsStartX.Value = allImgsTransform.X
allImgsEndX.Value = _checkWidth
'Find out how many times the current y position can be divided by _imgHeight
Dim _numHeights As Integer = allImgsTransform.Y / _imgHeight
'Pickup direction of flick in the y direction and adjust animation accordingly
If ((_lastPt.Y < _secondToLastPt.Y) And (_numHeights <> 0)) Then
'Decrement the number of heights
_numHeights -= 1
End If
'Fix scrolling canvas too far up or down
Dim _checkHeight As Integer = _numHeights * _imgHeight
'Do check and reset values if need be
If (_checkHeight > 0) Then
_checkHeight = 0
ElseIf (_checkHeight < -(_minY)) Then
_checkHeight = -(_minY)
End If
'Set storyboard values for the y coordinates
allImgsStartY.Value = allImgsTransform.Y
allImgsEndY.Value = _checkHeight
'Start(movie)
allImgsPan.Begin()
End Sub
Run your project now and the panning functionality should work as planned. You’ll find a finished wireframe project here.