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)">
<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 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"/>
</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
_Canvas.Cursor = Input.Cursors.Arrow
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
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).
'Set isMouseDown to false
_isMouseDown = False
' All Imgs mouse up
Private Sub allImgsCanvas_MouseLeftButtonUp(ByVal sender As Object, ByVal e As System.Windows.Input.MouseEventArgs) Handles allImgsCanvas.MouseLeftButtonUp
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).
Private _isMouseMove As Boolean = False
'Identify that we're NOT doing click and drag
_isMouseMove = False
' 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
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.
'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)
'Update last mouse pt stored globally
_lastPt = _curPt
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.
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
allImgsTransform.X = 0
ElseIf ((allImgsTransform.X + (_curPt.X - _lastPt.X)) < -(_minX)) Then
allImgsTransform.X = -(_minX)
Else
'Ensure the Y coordinates don't allow the canvas to disappear
If ((allImgsTransform.Y + (_curPt.Y - _lastPt.Y)) > 0) Then
allImgsTransform.Y = 0
ElseIf ((allImgsTransform.Y + (_curPt.Y - _lastPt.Y)) < -(_minY)) Then
allImgsTransform.Y = -(_minY)
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.
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 ;))
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()
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.
Private _imgWidth As Double
Private _imgHeight As Double
_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
' 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
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.
Private _secondToLastPt As Point
'Update last mouse pt and second to last mouse pt stored globally
_secondToLastPt = _lastPt
'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
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)
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.
'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
'Fix scrolling canvas too far up or down
Dim _checkHeight As Integer = _numHeights * _imgHeight
If (_checkHeight > 0) Then
_checkHeight = 0
ElseIf (_checkHeight < -(_minY)) Then
_checkHeight = -(_minY)
'Set storyboard values for the y coordinates
allImgsStartY.Value = allImgsTransform.Y
allImgsEndY.Value = _checkHeight
'Start(movie)
allImgsPan.Begin()
Run your project now and the panning functionality should work as planned. You’ll find a finished wireframe project here.