Silverlight Ux Musings: Providing Panning Functionality for a Canvas of Objects - Part 2 [Corrina Barber]

Published 05 December 07 01:08 AM

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