A Windows Phone 7 multi touch pan/zoom behaviour for Multi Scale Images

4 minute read

Some may have read my foray into using Windows Phone 7 to view maps, utilizing a Multi Scale Image (msi), MVVM Light and some extension properties. This application works quite well, but being mainly a study in applied architecture, the user experience leaves much to be desired. Studying Laurent Bugnion’s Multi Touch Behaviour got me on the right track. Although Laurent’s behaviour is very good, it basically works by translating, resizing (and optionally rotating) the control(s) inside the FrameworkElement is is attached to. For various reasons this is not an ideal solution for a map viewer.

So I set out to make my own behaviour, the first one I ever made by the way, and it turned out to remarkably easy – less than 90 lines of code, including whitespace and comments:

using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Interactivity;

namespace LocalJoost.Behaviours
{
  /// <summary>
  /// A behaviour for zooming and panning around on a MultiScaleImage
  /// using manipulation events
  /// </summary>
  public class PanZoomBehaviour : Behavior<MultiScaleImage>
  {
    /// <summary>
    /// Initialize the behavior
    /// </summary>
    protected override void OnAttached()
    {
      base.OnAttached();
      AssociatedObject.ManipulationStarted += AssociatedObject_ManipulationStarted;
      AssociatedObject.ManipulationDelta += AssociatedObject_ManipulationDelta;
    }
    /// <summary>
    /// Shortcut for the Multiscale image
    /// </summary>
    public MultiScaleImage Msi { get { return AssociatedObject; } }

    /// <summary>
    /// Screen point where the manipulation started
    /// </summary>
    private Point ManipulationOrigin { get; set; }

    /// <summary>
    /// Multiscale view point origin on the moment the manipulation started
    /// </summary>
    private Point MsiOrigin { get; set; }

    void AssociatedObject_ManipulationStarted(object sender,
ManipulationStartedEventArgs e) { // Save the current manipulation origin and MSI view point origin MsiOrigin = new Point(Msi.ViewportOrigin.X, Msi.ViewportOrigin.Y); ManipulationOrigin = e.ManipulationOrigin; } void AssociatedObject_ManipulationDelta(object sender, ManipulationDeltaEventArgs e) { if (e.DeltaManipulation.Scale.X == 0 && e.DeltaManipulation.Scale.Y == 0) { // No scaling took place (i.e. no multi touch) // 'Simply' calculate a new view point origin Msi.ViewportOrigin = new Point { X = MsiOrigin.X - (e.CumulativeManipulation.Translation.X / Msi.ActualWidth * Msi.ViewportWidth), Y = MsiOrigin.Y - (e.CumulativeManipulation.Translation.Y / Msi.ActualHeight * Msi.ViewportWidth), }; } else { // Multi touch - choose to interpretet this either as zoom or pinch var zoomscale = (e.DeltaManipulation.Scale.X + e.DeltaManipulation.Scale.Y) / 2; // Calculate a new 'logical point' - the MSI has its own 'coordinate system' var logicalPoint = Msi.ElementToLogicalPoint( new Point { X = ManipulationOrigin.X - e.CumulativeManipulation.Translation.X, Y = ManipulationOrigin.Y - e.CumulativeManipulation.Translation.Y } ); Msi.ZoomAboutLogicalPoint(zoomscale, logicalPoint.X, logicalPoint.Y); if (Msi.ViewportWidth > 1) Msi.ViewportWidth = 1; } } /// <summary> /// Occurs when detaching the behavior /// </summary> protected override void OnDetaching() { AssociatedObject.ManipulationStarted -= AssociatedObject_ManipulationStarted; AssociatedObject.ManipulationDelta -= AssociatedObject_ManipulationDelta; base.OnDetaching(); } } }

Method AssociatedObject_ManipulationStarted just records where the user started the manipulation, as well as what the MSI ViewportOrigin was on that moment. Method AssociatedObject_ManipulationDelta then simply checks if the delta event sports a scaling in x or y direction – if it does, it calculates the properties for a new ‘logical point’ to be fed into the ZoomAboutLogicalPoint method of the MultiScaleImage. If there is no scaling, the user just panned, and a new ViewportOrigin is being calculated in the MSI’s own coordinate system which runs from 0,0 to 1,1. And that’s all there is to it.

If you download my sample mapviewer application it’s actually quite simple to test drive this

  • Add a Windows Phone 7 class library LocalJoost.Behaviours to the projects
  • Reference this project from WP7viewer
  • Create the behaviour described above
  • Open MainPage.xaml in WP7Viewer
  • Find the MultiScaleImage called “msi” and remove all bindings except MapTileSource, so that only this remains:
<MultiScaleImage x:Name="msi" 
   MapMvvm:BindingHelpers.MapTileSource="{Binding CurrentTileSource.TileSource}">
</MultiScaleImage>
  • Add the behaviour to the MultiScaleImage using Blend or follow the manual procedure below:
  • Declare the namespace and the assembly in the phone:PhoneApplicationPage tag like this
xmlns:LJBehaviours="clr-namespace:LocalJoost.Behaviours;assembly=LocalJoost.Behaviours"
  • Add the behaviour to the MSI like this
<MultiScaleImage x:Name="msi" 
   MapMvvm:BindingHelpers.MapTileSource="{Binding CurrentTileSource.TileSource}">
  <i:Interaction.Behaviors>
      <LJBehaviours:PanZoomBehaviour/>
  </i:Interaction.Behaviors>
</MultiScaleImage>

And there you go. You can now zoom in and out using two or more fingers. That is, if you have a touch screen. If you don’t have a touch screen and you were not deemed important enough to be assigned a preview Windows Phone 7 device then you are in good company, for neither have I, and neither was I ;-). But fortunately there is Multi Touch Vista on CodePlex. It needs two mice (or more, but I don’t see how you can operate those), and it’s a bit cumbersome to set up, but at least I was able to test things properly. So, don’t let the lack of hardware deter you getting on the Windows Phone 7 bandwagon!

What I learned from this: behaviours will give an interesting twist to design decisions. When is it proper to solve things into behaviours, and when in a model by binding? For me, it’s clear that in this case the behaviour wins from the model – it’s far easier to make, understand and – above all – apply! For now, just drag and drop the behaviour on top of a MultiScaleImage and bang – zoom and pan. No complex binding expressions. 

Incidentally, although this behaviour was created with maps in mind, in can be applied to any MultiScaleImage of course, showing ‘ordinary’ image data.

For those who are not very fond of typing: the sample map application with the behaviour  already added and configured can be downloaded here. For those lucky b******s in possession of a real live device: you will find the XAP here. I would very much appreciate feedback on how the thing works in real life.