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:
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. ;)
Very cool and fairly easy considering all that's going on. I'll look into other ways this can be exploited in the morning.
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); } }
this.lastMousePos = e.GetPosition(this.msi);
dragOffset = e.GetPosition(this);