Thursday, March 6, 2008

Silverlight 2.0 Deep Zoom using MultiScaleImage Control

This is my first Silverlight 2.0 tutorial. I have seen MIX keynote yesterday and was really impressed with Hard Rock's Memorabilia sample http://memorabilia.hardrock.com/. I tried to reproduce the sample but could not find any documentation about Deep Zoom. After some search, I found a tool released by Microsoft called "Deep Zoom Composer". You can download the tool from here . And you can get the user's guide from here. The composer is very simple, you will import your images, arrange your photos, then export. These steps went smoothly, but I didn't know what I am supposed to do with the exported output, but after some hacking I could display the image, zoom in and out, and move the canvas. So I decided to share how I accomplished these tasks.


Displaying Deep Zoom Content

This is the simplest task, all you need to do is inserting a MultiScaleImage control and set its Source property. But there are a couple of tricks here, first you need to copy the folder that "Deep Zoom Composer" generated to your clientbin folder. The second is that the Source property should refer to your items.bin file if you exported your content with "Create Collection" option selected, or info.bin file otherwise.

Zooming
You can zoom either by using ViewPortWidth property, or better using ZoomAboutLogicalPoint method, the method takes zooming factor, and logical x, y co-ordinates to zoom around. The following code sample shows how to use this method
if (!isCtrlDown)
                    this.DeepZoom.ZoomAboutLogicalPoint(1.5, this.DeepZoom.ElementToLogicalPoint(e.GetPosition(this.DeepZoom)).X, this.DeepZoom.ElementToLogicalPoint(e.GetPosition(this.DeepZoom)).Y);
                else
                    this.DeepZoom.ZoomAboutLogicalPoint(0.75, this.DeepZoom.ElementToLogicalPoint(e.GetPosition(this.DeepZoom)).X, this.DeepZoom.ElementToLogicalPoint(e.GetPosition(this.DeepZoom)).Y);

The isCtrlDown field is set in the KeyDown, KeyUp events of the root canvas. I have tried setting the events on the MultiScaleImage control, but it did not fire, not sure why, here is the code
        private void DeepZoom_KeyDown(object sender, KeyEventArgs e)
        {
            if (e.Key == Key.Ctrl)
                isCtrlDown = true;
        }

        private void DeepZoom_KeyUp(object sender, KeyEventArgs e)
        {
            if (e.Key == Key.Ctrl)
                isCtrlDown = false;
        }   

Moving Content
This is the most tricky part because of the logic needed to handle dragging. But when it comes to the MuliScaleImage control, it is relatively easy and require only one line of code to modify the ViewportOrigin property, here is some sample code
            if (isDragging)
            {
                Point newOrigin = new Point();
                newOrigin.X = this.DeepZoom.ViewportOrigin.X - ((e.GetPosition(this.DeepZoom).X - lastMousePosition.X)/this.DeepZoom.ActualWidth);
                newOrigin.Y = this.DeepZoom.ViewportOrigin.Y - ((e.GetPosition(this.DeepZoom).Y - lastMousePosition.Y) / this.DeepZoom.ActualHeight);
                this.DeepZoom.ViewportOrigin = newOrigin;
            }
You can download the complete sample project with source code from here

4 comments:

John said...

Very close but don't you notice as you zoomin the pan becomes to much, zoomout not enough? I found you need to multiply by the "ViewportWidth" property to adjust for this. I also stored the current position and offset on mousedown and then used this, also cleared the isdraging flag on the leave event so you don't miss a mouse up. -don't know how it will render in comment but here is a snippet
John.

private void deepZoomObject_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
dragInProgress = true;
dragOffset = e.GetPosition(this);
currentPosition = deepZoomObject.ViewportOrigin;
}

private void deepZoomObject_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
dragInProgress = false;
}

private void deepZoomObject_MouseMove(object sender, MouseEventArgs e)
{
if (dragInProgress)
{
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;
}
}

private void UserControl_MouseLeave(object sender, MouseEventArgs e)
{
dragInProgress = false;
}

Yasser Makram said...

You are absolutely right, I was going to use an easing out equation to control the speed of movement, and adjust for the ViewportWidth. I did not test the sample extensively, but I wanted to share the code as fast as I can because there was no info out there about this exciting new feature. Thanks for your comments and enhancements to the code!

John said...

No problems, Its great that everyone is playing and posting code around. No waiting for MSFT to actually document it ;)
I've produced a full sample from all the bits floating around -
http://www.soulsolutions.com.au/Blog/tabid/73/EntryID/394/Default.aspx
It includes the mouse wheel, keyboard controls and catching a few bugs. Download the code and put in your images.
See the sample of high res images (54mb) from Thailand as an example (zoom in on a small monkey):
thailand.soulclients.com
John.

Deep Zoom said...

Don't ask me why, but I still find flash so much easier :) heh.

I hope MS will catchup with time