Silverlight Deep Zoom Sample Code Part 2

johnWeeGo.jpgThe Expression team have posted about how to manipulate collections within the Silverlight 2 multiscaleimage control. So what a great time to revisit my little sample code and make some improvements. Let make sure we have mouse, mouse wheel, keyboard, bounds checks and resorting.

See my updated sample here. Use the “R” key to resort the collection.

DeepZoomThai2.jpg

First up you should read my first post that explains creating the image assets and setting up your project. In this post we will cover the following:

  1. Move event handlers into anonymous methods
  2. Use Pete Blois’ mouse scroll wheel code that requires no JavaScript
  3. Add zoom bounds code for minimum and maximum zoom
  4. Resorting the images into a randomly ordered grid

As the expression blog mentions the first step is to ensure you export your assets as a collection:

ExportAsCollection.png

Anonymous Methods for Events

The concept here is to move all the event handlers into the constructor and implement them up as very simple code. I understand the reason why you would want to remove the handlers from the XAML. If you were working in tandem with a designer who was creating the XAML for the UI this does keep the code very separated. Our new XAML is cut down to this:


<UserControl x:Class="SoulSolutions.DeepZoom.Page"
    xmlns="http://schemas.microsoft.com/client/2007" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" >
    <Grid x:Name="LayoutRoot" Background="Black">
        <MultiScaleImage x:Name="deepZoomObject" Source="thai2007/items.bin" />
    </Grid>
</UserControl>

While we hook up the events directly to the event in the constructor as follows:


public Page()
{
    InitializeComponent();
    deepZoomObject.MouseMove += delegate(object sender, MouseEventArgs e)
    {
        if (mouseButtonPressed)
        {
            mouseIsDragging = true;
        }
        lastMousePos = e.GetPosition(deepZoomObject);
    };

    deepZoomObject.MouseLeftButtonDown 
        += delegate(object sender, MouseButtonEventArgs e)
    {
        mouseButtonPressed = true;
        mouseIsDragging = false;
        dragOffset = e.GetPosition(this);
        currentPosition = deepZoomObject.ViewportOrigin;
    };

    deepZoomObject.MouseLeave += delegate
    {
        mouseIsDragging = false;
    };

    deepZoomObject.MouseLeftButtonUp += delegate
    {
        mouseButtonPressed = false;
        if (mouseIsDragging == false)
        {
            if ((Keyboard.Modifiers & ModifierKeys.Shift) == ModifierKeys.Shift)
            {
                Zoom(0.5, lastMousePos);
            }
            else
            {
                Zoom(2, lastMousePos);
            }
            
        }else
        {
            mouseIsDragging = false;
        }
    };

    deepZoomObject.MouseMove += delegate(object sender, MouseEventArgs e)
    {
        if (mouseIsDragging)
        {
            Point newOrigin = new Point();
            newOrigin.X = currentPosition.X - 
                (((e.GetPosition(deepZoomObject).X - dragOffset.X) 
                / deepZoomObject.ActualWidth) * deepZoomObject.ViewportWidth);
            newOrigin.Y = currentPosition.Y - 
                (((e.GetPosition(deepZoomObject).Y - dragOffset.Y) 
                / deepZoomObject.ActualHeight) * deepZoomObject.ViewportWidth);
            deepZoomObject.ViewportOrigin = newOrigin;
        }
    };

    new MouseWheelHelper(deepZoomObject).Moved 
        += delegate(object sender, MouseWheelEventArgs e)
    {
        e.Handled = true;
        if (e.Delta > 0)
        {
            Zoom(1.2, lastMousePos);
        }
        else
        {
            Zoom(.80, lastMousePos);
        }
    };

    KeyDown += delegate(object sender, KeyEventArgs e)
    {
        Point p = new Point((deepZoomObject.Width/2),
                            ((deepZoomObject.Width/deepZoomObject.AspectRatio)/2));

       switch (e.Key)
       {
           case Key.I:
           case Key.C:
           case Key.Add:
               Zoom(1.1, p);
               break;
           case Key.O:
           case Key.Space:
           case Key.Subtract:
               Zoom(0.9, p);
               break;
           case Key.Left:
           case Key.A:
               deepZoomObject.ViewportOrigin = new Point(deepZoomObject.ViewportOrigin.X 
                - (0.1 * deepZoomObject.ViewportWidth), deepZoomObject.ViewportOrigin.Y);
               break;
           case Key.Right:
           case Key.D:
               deepZoomObject.ViewportOrigin = new Point(deepZoomObject.ViewportOrigin.X  
                + (0.1 * deepZoomObject.ViewportWidth), deepZoomObject.ViewportOrigin.Y);
               break;
           case Key.Up:
           case Key.W:
               deepZoomObject.ViewportOrigin = new Point(deepZoomObject.ViewportOrigin.X,
                deepZoomObject.ViewportOrigin.Y - (0.1 * deepZoomObject.ViewportWidth));
               break;
           case Key.Down:
           case Key.S:
               deepZoomObject.ViewportOrigin = new Point(deepZoomObject.ViewportOrigin.X,
                deepZoomObject.ViewportOrigin.Y + (0.1 * deepZoomObject.ViewportWidth));
               break;
           case Key.R:
               ArrangeIntoGrid();
               break;
           default:
               break;
       }
    };
}

For the Mouse scroll wheel event I have dropped the JavaScript helper in favour of Pete Blois’ version.

Zoom Bounds

To tidy things up a little I’ve added a little check from Chip Aubry to the zoom method:


private void Zoom(double zoom, Point pointToZoom)
{
    if ((zoom >= 1.0 && deepZoomObject.ViewportWidth > 0.05) || 
        (zoom < 1.0 && deepZoomObject.ViewportWidth < 2))
    {
        Point logicalPoint = deepZoomObject.ElementToLogicalPoint(pointToZoom);
        deepZoomObject.ZoomAboutLogicalPoint(zoom, logicalPoint.X, logicalPoint.Y);
    }
}

Resorting the images

I’ve hooked up the “R” key to resort the collection into a simple grid. The code is from the expression team’s blog and randomising the image order before laying out the images as a simple grid. The method animates the change of position for each image:


private void ArrangeIntoGrid()
{
    List<MultiScaleSubImage> randomList = RandomizedListOfImages();
    int numberOfImages = randomList.Count;
    int totalImagesAdded = 0;
    int totalColumns = (int)Math.Sqrt(numberOfImages) + 1;
    int totalRows = numberOfImages / (totalColumns - 1);

    for (int col = 0; col < totalColumns; col++)
    {
        for (int row = 0; row < totalRows; row++)
        {
            if (numberOfImages != totalImagesAdded)
            {
                MultiScaleSubImage currentImage = randomList[totalImagesAdded];

                Point currentPos = currentImage.ViewportOrigin;
                currentImage.ViewportWidth = totalColumns;
                Point futurePosition = new Point(-1.2*col, -1.6*row);

                // Set up the animation to layout in grid
                Storyboard moveStoryboard = new Storyboard();

                // Create Animation
                PointAnimationUsingKeyFrames moveAnimation 
                    = new PointAnimationUsingKeyFrames();

                // Create Keyframe
                SplinePointKeyFrame startKeyframe = new SplinePointKeyFrame();
                startKeyframe.Value = currentPos;
                startKeyframe.KeyTime = KeyTime.FromTimeSpan(TimeSpan.Zero);

                startKeyframe = new SplinePointKeyFrame();
                startKeyframe.Value = futurePosition;
                startKeyframe.KeyTime = KeyTime.FromTimeSpan(TimeSpan.FromSeconds(1));

                KeySpline ks = new KeySpline();
                ks.ControlPoint1 = new Point(0, 1);
                ks.ControlPoint2 = new Point(1, 1);
                startKeyframe.KeySpline = ks;
                moveAnimation.KeyFrames.Add(startKeyframe);

                Storyboard.SetTarget(moveAnimation, currentImage);
                Storyboard.SetTargetProperty(moveAnimation, "ViewportOrigin");

                moveStoryboard.Children.Add(moveAnimation);
                deepZoomObject.Resources.Add(moveStoryboard);

                // Play Storyboard
                moveStoryboard.Begin();

                totalImagesAdded++;
            }
            else
            {
                break;
            }
        }
    }
}

Download the source code here. (Excludes the export from Deep Zoom Composer, add your own!)

http://thailand.soulclients.com/SoulSolutions.DeepZoomPart2.zip (11.6KB)

Future Thoughts

This concept is really getting there, we can now not only show collections of images and navigate them, we can now manipulate the individual sub-images. The next step is to actually identify the sub images with metadata so we can do things like sort, filter and show information.