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

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

  • Comments 1

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.

Leave a Comment
  • Please add 1 and 4 and type the answer here:
  • Post