Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

DeepZoom

0.00/5 (No votes)
24 Feb 2009 2  
An article showing how to do DeepZoom in Silverlight 2.0.

Introduction

Since I wrote my first article on Silverlight (Silverlight 1.1 Fun and Games) which was based on Silverlight 1.1 Alpha, things have a changed a bit, and now a Silverlight 2.0 Beta is available.

There have been some real improvements since the 1.1 Alpha; for example, within the 1.1 Alpha, there were no input/layout controls to speak of. OK, there was Canvas and that was about it. So I, for one, am quite pleased with the new controls that the Silverlight 2.0 Beta has available.

For example, if I use Expression Blend Beta 2.5 to create a new Silverlight 2.0 Beta project, I get to use all of the following controls (believe it or not, there never used to be Buttons in 1.1, you had to write those yourself... ouch).

Now, this is all cool, but I am still not that interested in writing another Silverlight article just to show these new controls. What does interest me (and has interested me) enough to write this article is the new MultiScaleImage which doesn't exist in the full WPF toolset.

A Bit of History

The new MultiScaleImage is a strange thing. From what I can tell from my Googling, this owes its existence to an old product called Sea Dragon, which Microsoft bought and rebranded DeepZoom.

What Does DeepZoom Do For Me?

As the name suggests, the MultiScaleImage more than likely allows multiple scale images to be viewed in a single image container. This is the case. The DeepZoom (a.k.a. Sea Dragon) allows a composition to be made where the image (or more typically, a collage of images) has various different stages of information stored, which allows the MultiScaleImage to work correctly. In order to create the required information, there is a DeepZoom composer which aids in the design of DeepZoom compositions. If you want to see an online version, have a look at Vertigo's hardrock.com site.

Prerequisites

Before we proceed, I'll just list what I think you will need to construct Silverlight 2.0 apps that use DeepZoom:

Oh, and Visual Studio 2008 is always required.

Creating a DeepZoom Enabled Silverlight App

I am going to outline the steps that I followed in order to get DeepZoom working (I hope this helps, because I had to jump through a few hoops).

Step 1: Creating a DeepZoom Composition Project

The first step is to create a composer project, so once you have downloaded and installed the Deep Zoom Composer, simply create a new project and import and compose some images.

The only thing that I had to do here was give the Export a name and press the Export button.

I called the export "composedoutput", so I got (oh and I didn't click Create Collection):

OutputSdi

  • composedoutput.sdi
  • composedoutput folder
    • Lots of subfolders
    • info.bin
    • info.xml
  • SparseImageSceneGraph.xml

Step 2: Creating a Silverlight 2.0 App in Visual Studio 2008

I then started VS2008 and created a new Silverlight project:

And chose to let VS2008 create a new web site for the Silverlight app:

I then cleaned up the web project a bit to remove unwanted files. The ones below with lines through them I didn't need:

I then built the Silverlight app, which created a ClientBin directory in the web project.

Also of note is the single .xap file. This is actually a compiled Silverlight file (you can rename this to .Zip and examine its contents), and it should contain the referenced DLLs, and the actual app DLL, and a manifest file.

So far, it's all cool. At this point, you should be able to run the web app and see the Silverlight app embedded in the web page just fine.

OK, now comes some code.

Step 3: Adding DeepZoom to the Silverlight App

We have a nice Silverlight app, but this article is about DeepZoom. We need to make some changes to our Silverlight app to DeepZoom'ify it. Let's look at the XAML required to do this:

<UserControl x:Class="DeepZoomApp.Page"
    xmlns="http://schemas.microsoft.com/client/2007" 
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" 
    Width="800" Height="420">
    <Grid x:Name="LayoutRoot" Background="White">
        <MultiScaleImage x:Name="image" UseSprings="False" 
                         ViewportWidth="1.0"
                         Loaded="image_Loaded" 
                         MotionFinished="image_InitialMotionFinished" />
    </Grid>
</UserControl>

And here is the full code-behind for this Silverlight page:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Shapes;

namespace DeepZoomApp
{
    /// <summary>
    /// A very simple silverlight control demonstrating how to wire up mouse 
    /// interactions to a MultiScaleImage instance.
    /// </summary>
    public partial class Page : UserControl
    {
        /// <summary>
        /// Stores an instance of the wrapper class that handles mouse interactions 
        /// (panning, zooming, etc)
        /// </summary>
        private MultiScaleImageWrapper mouseInteractionWrapper;

        /// <summary>
        /// Indicates if the first motion has completed yet 
        /// (the initial zoom behavior is disabled)
        /// </summary>
        private bool isInitialMotionFinished;

        public Page()
        {
            InitializeComponent();

            // initialize the wrapper class to enable mouse interactions
            mouseInteractionWrapper = new MultiScaleImageWrapper(image);
        }

        /// <summary>
        /// Wires up an absolute Uri to the MultiScaleImage collection path 
        /// (for some reason, relative values are not currently supported). 
        /// This should make this example work, no matter what port is auto-assigned
        /// by Cassini.
        /// </summary>
        /// <param name="sender">The MultiScaleImage instance.</param>
        /// <param name="e">Unused RoutedEvent arguments.</param>
        private void image_Loaded(object sender, RoutedEventArgs e)
        {
            Uri collectionUri;

            if (Uri.TryCreate(App.Current.Host.Source, 
                "OutputSdi/composedoutput/info.bin", out collectionUri))
                image.Source = collectionUri;
        }

        /// <summary>
        /// Handles the "MotionFinished" event fired by the MultiScaleImage, 
        /// but only re-enables the "UseSprings" property after the first 
        /// motion completes (a little trick to properly bypass the initial "zoom in
        /// from nowhere" animation when first loading)
        /// </summary>
        /// <param name="sender">The MultiScaleImage instance.</param>
        /// <param name="e">Unused RoutedEvent arguments.</param>
        void image_InitialMotionFinished(object sender, RoutedEventArgs e)
        {
            if (!isInitialMotionFinished)
            {
                isInitialMotionFinished = true;
                image.UseSprings = true;
            }
        }
    }
}

The more eagle eyed amongst you may notice that there is a class used here called "MultiScaleImageWrapper". I can take no credit what so ever for this class. That's all down to the most excellent folk at Vertigo, who were kind enough to create a CodePlex page with this class. This class allows the users to use the MouseScrollWheel in a browser to interact with the DeepZoom MultiScaleImage. This is done by hooking into the hosting element events. Let's have a look at the code, shall we?

using System;
using System.Windows;
using System.Windows.Browser;
using System.Windows.Controls;
using System.Windows.Input;

namespace DeepZoomApp
{
    /// <summary>
    /// Provides mouse panning and zooming support for a MultiScaleImage.
    /// </summary>
    public class MultiScaleImageWrapper
    {
        /// <summary>
        /// Specifies the zoom factor when the MultiScaleImage receives a mouse click event.
        /// </summary>
        private const double ZOOM_FACTOR_CLICK = 2;

        /// <summary>
        /// Specifies the zoom factor when the MultiScaleImage receives a mouse wheel event.
        /// </summary>
        private const double ZOOM_FACTOR_WHEEL = 1.25;

        /// <summary>
        /// Listens to mouse wheel events raised by the HTML document.
        /// </summary>
        private static MouseWheelListener mouseWheelListener = new MouseWheelListener();

        /// <summary>
        /// Indicates whether or not the mouse is over the MultiScaleImage.
        /// </summary>
        private bool isMouseOver;

        /// <summary>
        /// Indicates whether or not the left mouse button
        /// is pressed for the MultiScaleImage.
        /// </summary>
        private bool isMouseDown;

        /// <summary>
        /// Indicates whether or not the mouse is being dragged over the MultiScaleImage.
        /// </summary>
        private bool isMouseDrag;

        /// <summary>
        /// Specifies the initial mouse drag origin for the MultiScaleImage.
        /// </summary>
        private Point dragOrigin;

        /// <summary>
        /// Specifies the initial mouse drag position for the MultiScaleImage.
        /// </summary>
        private Point dragPosition;

        /// <summary>
        /// Specifies the current mouse cursor position for the MultiScaleImage.
        /// </summary>
        private Point cursorPosition;

        /// <summary>
        /// Gets the wrapped MultiScaleImage instance. 
        /// </summary>
        public MultiScaleImage Image { get; private set; }

        /// <summary>
        /// Initializes a new instance of the MultiScaleImageWrapper class
        /// to provide mouse panning and zooming support for the specified MultiScaleImage.
        /// </summary>
        /// <param name="image">The MultiScaleImage instance to wrap.</param>
        public MultiScaleImageWrapper(MultiScaleImage image)
        {
            Image = image;
            Image.MouseEnter += Image_MouseEnter;
            Image.MouseLeave += Image_MouseLeave;
            Image.MouseMove += Image_MouseMove;
            Image.MouseLeftButtonDown += Image_MouseLeftButtonDown;
            Image.MouseLeftButtonUp += Image_MouseLeftButtonUp;
            mouseWheelListener.MouseWheel += Image_MouseWheel;
        }

        /// <summary>
        /// Handles the MouseEnter event for the MultiScaleImage.
        /// Sets the isMouseOver flag to true and saves the currentPosition of the cursor.
        /// </summary>
        /// <param name="sender">The wrapped MultiScaleImage instance.</param>
        /// <param name="e">The mouse event arguments.</param>
        private void Image_MouseEnter(object sender, MouseEventArgs e)
        {
            isMouseOver = true;
            cursorPosition = e.GetPosition(Image);
        }

        /// <summary>
        /// Handles the MouseLeave event for the MultiScaleImage.
        /// Sets the isMouseOver flag to false.
        /// </summary>
        /// <param name="sender">The wrapped MultiScaleImage instance.</param>
        /// <param name="e">The mouse event arguments.</param>
        private void Image_MouseLeave(object sender, MouseEventArgs e)
        {
            isMouseOver = false;
        }

        /// <summary>
        /// Handles the MouseMove event for the MultiScaleImage.
        /// Saves the currentPosition of the cursor and pans when the mouse is dragged.
        /// </summary>
        /// <param name="sender">The wrapped MultiScaleImage instance.</param>
        /// <param name="e">The mouse event arguments.</param>
        private void Image_MouseMove(object sender, MouseEventArgs e)
        {
            cursorPosition = e.GetPosition(Image);

            if (isMouseDown)
            {
                isMouseDrag = true;

                Point origin = new Point();
                origin.X = dragOrigin.X - (cursorPosition.X - dragPosition.X) / 
                                           Image.ActualWidth * Image.ViewportWidth;
                origin.Y = dragOrigin.Y - (cursorPosition.Y - dragPosition.Y) / 
                                           Image.ActualWidth * Image.ViewportWidth;

                Image.ViewportOrigin = origin;
            }
        }

        /// <summary>
        /// Handles the MouseLeftButtonDown event for the MultiScaleImage.
        /// Saves the initial dragOrigin and dragPosition in case the user begins to pan.
        /// </summary>
        /// <param name="sender">The wrapped MultiScaleImage instance.</param>
        /// <param name="e">The mouse button event arguments.</param>
        private void Image_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
        {
            Image.CaptureMouse();
            mouseWheelListener.IsEnabled = false;

            isMouseDown = true;
            isMouseDrag = false;

            dragOrigin = Image.ViewportOrigin;
            dragPosition = e.GetPosition(Image);
        }

        /// <summary>
        /// Handles the MouseLeftButtonUp event for the MultiScaleImage.
        /// Zooms in if the user is not completing a pan and resets the mouse.
        /// </summary>
        /// <param name="sender">The wrapped MultiScaleImage instance.</param>
        /// <param name="e">The mouse button event arguments.</param>
        private void Image_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
        {
            if (!isMouseDrag && isMouseDown)
            {
                bool isShiftDown = 
                    (Keyboard.Modifiers & ModifierKeys.Shift) == ModifierKeys.Shift;
                double factor = isShiftDown ? 1 / ZOOM_FACTOR_CLICK : ZOOM_FACTOR_CLICK;
                Zoom(factor, cursorPosition);
            }

            isMouseDown = false;
            isMouseDrag = false;

            Image.ReleaseMouseCapture();
            mouseWheelListener.IsEnabled = true;
        }

        /// <summary>
        /// Handles the MouseWheel event for the MultiScaleImage.
        /// Zooms in or out depending on the mouse wheel direction.
        /// </summary>
        /// <param name="sender">The MouseWheelWrapper instance.</param>
        /// <param name="e">The mouse wheel event arguments.</param>
        private void Image_MouseWheel(object sender, MouseWheelEventArgs e)
        {
            if (!isMouseOver || e.Delta == 0)
                return;

            double factor = e.Delta > 0 ? ZOOM_FACTOR_WHEEL : 1 / ZOOM_FACTOR_WHEEL;
            Zoom(factor, cursorPosition);
            e.Handled = true;
        }

        /// <summary>
        /// Zooms in or out of the MultiScaleImage.
        /// </summary>
        /// <param name="factor">The zoom factor.</param>
        /// <param name="point">The zoom center point.</param>
        public void Zoom(double factor, Point point)
        {
            Point logicalPoint = Image.ElementToLogicalPoint(point);
            Image.ZoomAboutLogicalPoint(factor, logicalPoint.X, logicalPoint.Y);
        }

        /// <summary>
        /// Provides data for a mouse wheel event.
        /// </summary>
        private class MouseWheelEventArgs : EventArgs
        {
            /// <summary>
            /// Gets a value that specifies the delta and direction
            /// of the mouse wheel event. This value is normalized across browsers.
            /// </summary>
            public double Delta { get; private set; }

            /// <summary>
            /// Gets or sets a value that indicates whether
            /// or not the mouse wheel event has been handled.
            /// </summary>
            public bool Handled { get; set; }

            /// <summary>
            /// Initializes a new instance of the MouseWheelEventArgs
            /// class with the specified delta.
            /// </summary>
            /// <param name="delta">The delta
            /// calculated for the mouse wheel event.</param>
            public MouseWheelEventArgs(double delta)
            {
                Delta = delta;
            }
        }

        /// <summary>
        /// Provides cross-browser support for the mouse wheel.
        /// </summary>
        private class MouseWheelListener
        {
            /// <summary>
            /// Indicates whether or not the mouse wheel is enabled.
            /// </summary>
            public bool IsEnabled { get; set; }

            /// <summary>
            /// Occurs when a mouse wheel event is fired.
            /// </summary>
            public event EventHandler<MouseWheelEventArgs> MouseWheel;

            /// <summary>
            /// Initializes a new instance of the MouseWheelListener class
            /// that listens to mouse wheel events fired by the HTML document. 
            /// </summary>
            public MouseWheelListener()
            {
                if (HtmlPage.IsEnabled)
                {
                    HtmlPage.Document.AttachEvent("DOMMouseScroll", 
                                                  Plugin_MouseWheelFirefox);
                    HtmlPage.Document.AttachEvent("onmousewheel", 
                                                  Plugin_MouseWheelOther);
                    IsEnabled = true;
                }
            }

            /// <summary>
            /// Handles mouse wheel events for Firefox.
            /// </summary>
            /// <param name="sender">The HTML element for the plug-in.</param>
            /// <param name="e">The HTML event arguments.</param>
            private void Plugin_MouseWheelFirefox(object sender, HtmlEventArgs e)
            {
                if (!IsEnabled)
                {
                    e.PreventDefault();
                    return;
                }

                double delta = (double)e.EventObject.GetProperty("detail") / -3;
                MouseWheelEventArgs args = new MouseWheelEventArgs(delta);
                MouseWheel(this, args);

                if (args.Handled)
                    e.PreventDefault();
            }

            /// <summary>
            /// Handles mouse wheel events for browsers other than Firefox.
            /// </summary>
            /// <param name="sender">The HTML element for the plug-in.</param>
            /// <param name="e">The HTML event arguments.</param>
            private void Plugin_MouseWheelOther(object sender, HtmlEventArgs e)
            {
                if (!IsEnabled)
                {
                    e.EventObject.SetProperty("returnValue", false);
                    return;
                }

                double delta = (double)e.EventObject.GetProperty("wheelDelta") / 120;
                MouseWheelEventArgs args = new MouseWheelEventArgs(delta);
                MouseWheel(this, args);

                if (args.Handled)
                    e.EventObject.SetProperty("returnValue", false);
            }
        }
    }
}

Step 4: Copying the DeepZoom Composer Output to the Web Site

I then copied the folder that contained the following files:

  • composedoutput.sdi
  • composedoutput folder
    • Lots of subfolders
    • info.bin
    • info.xml
  • SparseImageSceneGraph.xml

So for me, that was the whole OutputSdi folder, so my ClientBin folder now looked like this:

Step 5: Changing the Silverlight MultiScaleImage URI

The very last step was to change the place the MultiScaleImage looked for its source. This is done by this line within the MultiScaleImage loaded event:

if (Uri.TryCreate(App.Current.Host.Source, 
    "OutputSdi/composedoutput/info.bin", out collectionUri))
    image.Source = collectionUri;

And that's it. Job done.

History

  • v1.0: 13/05/08.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here