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
- 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
{
public partial class Page : UserControl
{
private MultiScaleImageWrapper mouseInteractionWrapper;
private bool isInitialMotionFinished;
public Page()
{
InitializeComponent();
mouseInteractionWrapper = new MultiScaleImageWrapper(image);
}
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;
}
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
{
public class MultiScaleImageWrapper
{
private const double ZOOM_FACTOR_CLICK = 2;
private const double ZOOM_FACTOR_WHEEL = 1.25;
private static MouseWheelListener mouseWheelListener = new MouseWheelListener();
private bool isMouseOver;
private bool isMouseDown;
private bool isMouseDrag;
private Point dragOrigin;
private Point dragPosition;
private Point cursorPosition;
public MultiScaleImage Image { get; private set; }
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;
}
private void Image_MouseEnter(object sender, MouseEventArgs e)
{
isMouseOver = true;
cursorPosition = e.GetPosition(Image);
}
private void Image_MouseLeave(object sender, MouseEventArgs e)
{
isMouseOver = false;
}
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;
}
}
private void Image_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
Image.CaptureMouse();
mouseWheelListener.IsEnabled = false;
isMouseDown = true;
isMouseDrag = false;
dragOrigin = Image.ViewportOrigin;
dragPosition = e.GetPosition(Image);
}
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;
}
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;
}
public void Zoom(double factor, Point point)
{
Point logicalPoint = Image.ElementToLogicalPoint(point);
Image.ZoomAboutLogicalPoint(factor, logicalPoint.X, logicalPoint.Y);
}
private class MouseWheelEventArgs : EventArgs
{
public double Delta { get; private set; }
public bool Handled { get; set; }
public MouseWheelEventArgs(double delta)
{
Delta = delta;
}
}
private class MouseWheelListener
{
public bool IsEnabled { get; set; }
public event EventHandler<MouseWheelEventArgs> MouseWheel;
public MouseWheelListener()
{
if (HtmlPage.IsEnabled)
{
HtmlPage.Document.AttachEvent("DOMMouseScroll",
Plugin_MouseWheelFirefox);
HtmlPage.Document.AttachEvent("onmousewheel",
Plugin_MouseWheelOther);
IsEnabled = true;
}
}
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();
}
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