Scott Hanselman

The Weekly Source Code 18 - Deep Zoom (Seadragon) Silverlight 2 MultiScaleImage Mouse Wheel Zooming and Panning Edition

March 7, '08 Comments [25] Posted in Mix | Silverlight | Source Code
Sponsored By

Silverlight Project Test Page - Windows Internet Explorer (4)Dear Reader, I present to you eighteenth in a infinite number of posts of "The Weekly Source Code." Here's some source I was reading - and writing - this week at Mix.

I have been checking out Deep Zoom in Silverlight 2, but I thought it was a bummer that there wasn't (yet) a "Hello DeepZoom World!" example that includes panning, zooming (mouse wheel) support out of the box. The Vertigo example behaves exactly as I'd want my stuff to behave. You can see my working Deep Zoom example here or by clicking the picture at right.

Adding Mouse Wheel support to Silverlight can happen a couple of ways. Mouse Wheel events are sourced by the browser, not by Silverlight itself (which is the way you'd want it as Silverlight lives inside the browser, it shouldn't replace its behaviors, IMHO).

So, you could use the Javascript code from Adomas along with the work that Jeff Prosise did to reach into Silverlight and call methods. The events would be handled in JavaScript and the Zoom method would be called via JavaScript over the bridge into Silverlight managed code.

However, you can also reach out from inside managed code and set managed handlers for DOM (JavaScript events) like Pete Blois does with his Mouse Wheel Helper class. I use this class directly by downloading it from Pete's blog and adding it to my project. This is significant because it doesn't require ANY external JavaScript files. All the events are handled by managed code.

if (HtmlPage.IsEnabled) {
  HtmlPage.Window.AttachEvent("DOMMouseScroll", this.HandleMouseWheel);
  HtmlPage.Window.AttachEvent("onmousewheel", this.HandleMouseWheel);
  HtmlPage.Document.AttachEvent("onmousewheel", this.HandleMouseWheel);
}

I took this along with snippets from Yasser Makram and John posting in Yasser's blog's comments got me mostly what I needed.

I've seen some basic examples using either mouse clicking or key-downs to get the zooming effect, but I wanted to support mouse wheel events as well, just like the stuff shown off at Mix.

This more complete example gives you:

  • Drag to pan
  • Click to zoom in, Shift Click to zoom out
  • Mouse wheel to zoom in and out
  • No JavaScript dependency at all - everything is in managed code.

First, start with the output of the DeepZoom composer (I just used the Windows Wallpapers I had on this machine to compose a DeepZoom image in the editor) and copy the resulting exported folder structure somewhere (I put it under bin/debug for ease, but you can put it wherever as long as the source attribute lines up in your XAML:

<UserControl
	xmlns="http://schemas.microsoft.com/client/2007"
	xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
	x:Class="SilverlightApplication1.Page"
	Width="800" Height="600" >
  <Grid
    x:Name="LayoutRoot"
    Background="Gray">
    <MultiScaleImage 
      x:Name="msi"
      ViewportWidth="1.0"
      Source="http://www.yourdomain.com/foo/items.bin" />
  </Grid>
</UserControl>

RANDOM NOTE: Here's a cool switch you can set on MultiScaleImage. It's UseSprings="false" and it'll turn off the zooming animation. Why would you want to do this?  Well, that very zoom-in/zoom-out animation gives DeepZoom an opportunity to perform its "visual slight of hand" and transition between images. When the animation happens, you are less likely to notice the transition between tiles (a good thing). Of course, I want to get my head around how it all works so I liked seeing the transitions.

Keep in mind it's four in the morning, so this code is a little wonky and not at all thought-out and I've only been at it for an hour. I'm REALLY interested in what you, Dear Reader, can do with it and make it better so we all have a canonical example to start from. This is NOT that example, I'm actually kind of reticent to post it here because it's so hacked together, but that's half the fun, right? It works OK, I think.

One thing to point out, note that the name of the control is "msi," set in the XAML above via x:name="msi" so you'll see me referencing properties like msi.thisandthat in the managed XAML code-behind below. Also, the "using akadia" below is Pete's MouseHandler code's namespace referenced from my page.

My code hooks up a bunch of events from the constructor using anonymous delegates, and those work together to call a single Zoom() helper method.

using System; 
using System.Windows; 
using System.Windows.Controls; 
using System.Windows.Documents; 
using System.Windows.Ink; 
using System.Windows.Input; 
using System.Windows.Media; 
using System.Windows.Media.Animation; 
using System.Windows.Shapes; 
using System.Windows.Threading; 
using akadia; 

namespace SilverlightApplication1 
{ 
    public partial class Page : UserControl 
    { 
        Point lastMousePos = new Point(); 
        double _zoom = 1; 
        bool mouseButtonPressed = false; 
        bool mouseIsDragging = false; 
        Point dragOffset; 
        Point currentPosition; 

        public double ZoomFactor 
        { 
            get { return _zoom; } 
            set { _zoom = value; } 
        } 

        public Page() 
        { 
            this.InitializeComponent(); 

            this.MouseMove += delegate(object sender, MouseEventArgs e) 
            { 
                if (mouseButtonPressed) 
                { 
                    mouseIsDragging = true; 
                } 
                this.lastMousePos = e.GetPosition(this.msi);   
            }; 

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

            this.msi.MouseLeave += delegate(object sender, MouseEventArgs e) 
            { 
                mouseIsDragging = false; 
            }; 

            this.MouseLeftButtonUp += delegate(object sender, MouseButtonEventArgs e) 
            { 
                mouseButtonPressed = false; 
                if (mouseIsDragging == false) 
                { 
                    bool shiftDown = (Keyboard.Modifiers & ModifierKeys.Shift) == ModifierKeys.Shift; 

                    ZoomFactor = 2.0; 
                    if(shiftDown) ZoomFactor = 0.5; //back out when shift is down 
                    Zoom(ZoomFactor, this.lastMousePos); 
                } 
                mouseIsDragging = false; 
            }; 

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

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

                Zoom(ZoomFactor, this.lastMousePos); 
            }; 
        } 

        public void Zoom(double zoom, Point pointToZoom) 
        { 
            Point logicalPoint = this.msi.ElementToLogicalPoint(pointToZoom); 
            this.msi.ZoomAboutLogicalPoint(zoom, logicalPoint.X, logicalPoint.Y); 
        } 
    } 
}

Three One thing I am having trouble with, but I haven't run this under a debugger yet as I haven't installed the single Silverlight 2 Beta 1 Tools Installer (Troubleshooting Silverlight 2 Beta 1 Tools Installer). I just built it in Expression Blend 2.5 and Notepad2. I was enjoying myself so much I didn't want to stop and install anything. ;)

  • One, if you scroll WHILE the "spring zoom" animation is happening, something goes wrong and you'll continue zooming in, no matter what direction you're scrolling.
  • Second, more subtlety, if you scroll in, stop, then start scrolling out, it'll scroll in a step, then start scrolling out, so there's clearly a state issue I'm goofing up. Stated another way, if a managed event comes in WHILE the animation is happening, the direction it's currently zooming just keeps going. (Maybe a subtle bug, or my code needs to be debounced because it seems like the mouse wheel events are coming in too fast.) In my example, you have to let the animation "settle down" in order to zoom the other way. Of course, if you just F5 to Refresh you can get back to home and reset the zoom.
  • I figured it out, I was using a relative calculated ZoomFactor when I should have used an absolute factor.
  • Third, I want to prevent the image from panning outside the viewable area (meaning, I don't want folks to get lost) and I'd like to stop the zooming in and out at some reasonable max/min.
  • Don't forget to setup .xap files as the mime type application/x-silverlight-app on on your hoster's IIS instance.

Very cool and fairly easy considering all that's going on. I'll look into other ways this can be exploited in the morning.

About Scott

Scott Hanselman is a former professor, former Chief Architect in finance, now speaker, consultant, father, diabetic, and Microsoft employee. He is a failed stand-up comic, a cornrower, and a book author.

facebook twitter subscribe
About   Newsletter
Sponsored By
Hosting By
Dedicated Windows Server Hosting by ORCS Web
Friday, March 07, 2008 1:42:07 PM UTC
Thanks Scott, Very useful .. I was playing with this last night and got into a bit of a mess .. this should help sort it out :)
[mRg]
Friday, March 07, 2008 4:37:37 PM UTC
"I'll look into other ways this can be exploited in the morning."

It *WAS* morning when I saw you post this. Last night, as I sat up with my new baby to give my wife some rest, it was DEFINITELY confirmed for me. Scott Hanselman does not sleep. Maybe he's like some kind of developer vampire, he gets what he needs to live through the act of coding....
Friday, March 07, 2008 6:05:42 PM UTC
Peter's idea to utilise the HtmlBridge is so simple and so brilliant - why didn't I think of that! Thanks for finding it and sharing.
Friday, March 07, 2008 7:06:51 PM UTC
Here's the working DeepZoom example running on my blog here. It's bandwidth heavy, so I may have to remove it one day. You can also click on the image above.
Friday, March 07, 2008 8:23:57 PM UTC
I zoomed in too deep. Now I'm stuck in my computer....:(
Dan
Saturday, March 08, 2008 10:03:08 AM UTC
Thanks Scott. I have updated our Expression Blog post to include a pointer to this.
Saturday, March 08, 2008 3:01:32 PM UTC
Hey Scott.. how did you get the Background of the MultiScaleImage control to be transparent.. or inherited from the parent container? Mine has a white background and it's not inheriting from Grid.
Sunday, March 09, 2008 12:40:24 AM UTC
This is really great! Thanks for sharing!
Abdon
Sunday, March 09, 2008 12:53:14 AM UTC
I was playing around today and figured out how to stop zooming at a specific depth. I've updated your Zoom method below to show how I did it.


public void Zoom(double zoom, Point pointToZoom)
{
// check to see if we are trying zoom in and make sure we're not past our desired width / depth
// -or-
// allow us to back out if the zoom factor is less than 1.0
if ((zoom >= 1.0 && deepZoom.ViewportWidth > 0.05) || zoom < 1.0)
{
Point logicalPoint = this.deepZoom.ElementToLogicalPoint(pointToZoom);
this.deepZoom.ZoomAboutLogicalPoint(zoom, logicalPoint.X, logicalPoint.Y);
}
}
Chip Aubry
Sunday, March 09, 2008 6:52:44 AM UTC
You seem to be adding two event handlers to MouseMove.
Is that on purpose?
Nadav P.
Sunday, March 09, 2008 8:25:18 AM UTC
You're now my hero. I was literally playing poker when you wrote this. Scottha writes code till 4. Philha plays poker till 4. Who's going places at MS? ;)
Monday, March 10, 2008 7:43:55 AM UTC
Its really awesome to see the community figuring this one out while we wait for some offical samples!
Here is my go at it:
http://www.soulsolutions.com.au/Blog/tabid/73/EntryID/394/Default.aspx
Next on the list is the filtering and dynamic resorting - anyone?
John.
Friday, March 14, 2008 3:41:29 PM UTC
Here is another proof of concept DeepZoom demo, using images from the static google maps api.

http://static.johnspurlock.com/silverlight/bostonzoom/demo

It's really pretty simple to put these things together with the new mix08 toolset.
Monday, March 17, 2008 5:26:00 AM UTC
Scott, Thanks for the article. The part I can't figure out is how to relate the mouse pointer in the msi control to the individual source image it's located in. If you post an answer please email me the link. Thanks again.
Bill White
Tuesday, March 18, 2008 5:38:30 AM UTC
Can I make a WPF Windows application using Deep Zoom? Please comment.
dxzfs
Wednesday, March 19, 2008 8:10:00 AM UTC
Hi Scott,

Tnx, nice article! One question: Can a DeepZoom app be hosted in IIS6 or even just by clicking F5 or must it be hosted in ISS7? I have tried various examples to run them on my XP machine with no luck? I have VS2008 + IIS6? Any sugestions?

Regards,

Rudi Grobler
http://dotnet.org.za/rudi
Friday, March 21, 2008 6:17:36 PM UTC
Hi, on my site http://www.xamltemplates.net doesn't promote something like this to show the users how easy is to stylize the controls, without buyn other extras.
Sunday, March 23, 2008 3:50:00 PM UTC
The expression team blog has added some sample code to show repositioning of sub images.
http://blogs.msdn.com/expression/archive/2008/03/22/deep-zoom-collections-example.aspx
I've updated my little sample with keyboard events also:
http://www.soulsolutions.com.au/Blog/tabid/73/EntryID/410/Default.aspx
John.
Monday, March 24, 2008 4:44:43 PM UTC
I disagree about whether the browser should handle mousewheel events. Why not put an option in Silverlight to say - Hey I'd like Silverlight to handle Mousewheel events instead of trying to do this with paperclips and chewing gum?
Dave
Tuesday, March 25, 2008 4:40:18 AM UTC
Rudi - Yes, as long as the MIME/TYPE is set correctly for Silverlight!

Dave - That philosophy isn't a Silverlight-ism, but rather a "rule" for browser plugins to make sure their behavior is consistant.
Tuesday, March 25, 2008 9:24:43 PM UTC
OH MY WORD,

It is hammering that CPU and RAM so hard it is incredible.

Can anyone seriously expect to run this bloat anywhere? For a remotely serious app?
hals
Friday, March 28, 2008 5:11:17 PM UTC
Remotely serious app? Sure...how about medical imaging? Stellar imaging? Mars surface studies? Deep water exploration? Satellite imagery analysis? Just because it's hammering your system now (at version 0.9 I might add) doesn't mean it's a poor technology. It's still baking...
Tuesday, April 01, 2008 9:35:02 AM UTC
The beta excuse. :) Seriously, how hard is it to insert a time.sleep(0.1) or somesuch?
Cees
Tuesday, April 01, 2008 7:40:13 PM UTC
If anyone is interested, I have an example of a Deep Zoom collection with filter functionality (using Linq) here - http://projectsilverlight.blogspot.com/2008/04/dissecting-hard-rock-memorabilia-and.html

The whole thread on my adventures with Deep Zoom can be found here -
http://projectsilverlight.blogspot.com

Note that this is my personal blog and not associated with this blog in any way. I have some code snippets on my site and thought that it might help some people and hence posted the links here...

Wilfred
Sunday, May 04, 2008 4:27:57 AM UTC
Excellent code. I do want to mention a bug I came across. The mouse positioning only works if the multiscaleimage and the root element are positioned together.

in MouseMove event you have:
this.lastMousePos = e.GetPosition(this.msi);


in MouseLeftButtonDown event you have:
dragOffset = e.GetPosition(this);


You need to call GetPosition on the same element to make it work regardless of the relationship of the MSI and the root elements positions. A minor nit but it bit me on my project. I changed them both to "GetPosition(this.msi)" and it works just fine.
Comments are closed.

Disclaimer: The opinions expressed herein are my own personal opinions and do not represent my employer's view in any way.