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