Scott Hanselman

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

March 07, 2008 Comment on this post [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
Hosting By
Hosted in an Azure App Service
March 07, 2008 17:42
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 :)
March 07, 2008 20:37
"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....
March 07, 2008 22:05
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.
March 07, 2008 23:06
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.
March 08, 2008 0:23
I zoomed in too deep. Now I'm stuck in my computer....:(
Dan
March 08, 2008 14:03
Thanks Scott. I have updated our Expression Blog post to include a pointer to this.
March 08, 2008 19:01
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.
March 09, 2008 4:40
This is really great! Thanks for sharing!
March 09, 2008 4:53
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);
}
}
March 09, 2008 10:52
You seem to be adding two event handlers to MouseMove.
Is that on purpose?
March 09, 2008 12:25
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? ;)
March 10, 2008 11:43
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.
March 14, 2008 19:41
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.
March 17, 2008 9:26
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.
March 18, 2008 9:38
Can I make a WPF Windows application using Deep Zoom? Please comment.
March 19, 2008 12:10
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
March 21, 2008 22:17
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.
March 23, 2008 19:50
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.
March 24, 2008 20:44
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?
March 25, 2008 8:40
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.
March 26, 2008 1:24
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?
March 28, 2008 21:11
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...
April 01, 2008 13:35
The beta excuse. :) Seriously, how hard is it to insert a time.sleep(0.1) or somesuch?
April 01, 2008 23:40
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
May 04, 2008 8:27
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.