Pinch and Zoom - OnImageOpened() is not executing when image source is a data binding RRS feed

  • Question

  • I was trying to adapt this code for pinch and zoom an image, but it only works when I set the image source to an image in the Assets folder.  When I set the image to a binding source the pinch and zoom doesn't work and on debugging the OnImageOpened method doesn't get called.

    Does anyone know why and what I do to fix it?

    Code source = Image Recipes

    EDIT: I discovered that by setting img.Source to the App.ViewModel.Image I was able to get it to work but I don't understand why since I already set the DataContext to the viewmodel in the constructor.


       <!--ContentPanel - place additional content here-->
        <Grid x:Name="ContentPanel" Grid.Row="1" Margin="12,0,12,0">
            <ViewportControl x:Name="viewport"  
                ManipulationStarted="OnManipulationStarted" ManipulationDelta="OnManipulationDelta"  
                             ManipulationCompleted="OnManipulationCompleted" ViewportChanged="viewport_ViewportChanged">
                <Canvas x:Name="canvas">
                    <Image x:Name="img" Source="{Binding MyMapImage, Mode=OneWay}" 
                            RenderTransformOrigin="0,0" CacheMode="BitmapCache"
                            <ScaleTransform x:Name="xform"/>


    public partial class PinchAndZoom : PhoneApplicationPage { const double MaxScale = 10; double _scale = 0.5; double _minScale; double _coercedScale; double _originalScale; Size _viewportSize; bool _pinching; Point _screenMidpoint; Point _relativeMidpoint; BitmapImage _bitmap; public PinchAndZoom() { InitializeComponent(); DataContext = App.ViewModel;

    img.Source = App.ViewModel.MyMapImage; } /// <summary> /// Either the user has manipulated the image or the size of the viewport has changed. We only /// care about the size. /// </summary> void viewport_ViewportChanged(object sender, System.Windows.Controls.Primitives.ViewportChangedEventArgs e) { Size newSize = new Size(viewport.Viewport.Width, viewport.Viewport.Height); if (newSize != _viewportSize) { _viewportSize = newSize; CoerceScale(true); ResizeImage(false); } } /// <summary> /// Handler for the ManipulationStarted event. Set initial state in case /// it becomes a pinch later. /// </summary> void OnManipulationStarted(object sender, ManipulationStartedEventArgs e) { _pinching = false; _originalScale = _scale; } /// <summary> /// Handler for the ManipulationDelta event. It may or may not be a pinch. If it is not a /// pinch, the ViewportControl will take care of it. /// </summary> /// <param name="sender"></param> /// <param name="e"></param> void OnManipulationDelta(object sender, ManipulationDeltaEventArgs e) { if (e.PinchManipulation != null) { e.Handled = true; if (!_pinching) { _pinching = true; Point center = e.PinchManipulation.Original.Center; _relativeMidpoint = new Point(center.X / TestImage.ActualWidth, center.Y / TestImage.ActualHeight); var xform = TestImage.TransformToVisual(viewport); _screenMidpoint = xform.Transform(center); } _scale = _originalScale * e.PinchManipulation.CumulativeScale; CoerceScale(false); ResizeImage(false); } else if (_pinching) { _pinching = false; _originalScale = _scale = _coercedScale; } } /// <summary> /// The manipulation has completed (no touch points anymore) so reset state. /// </summary> void OnManipulationCompleted(object sender, ManipulationCompletedEventArgs e) { _pinching = false; _scale = _coercedScale; } /// <summary> /// When a new image is opened, set its initial scale. /// </summary> void OnImageOpened(object sender, RoutedEventArgs e) { _bitmap = (BitmapImage)TestImage.Source; // Set scale to the minimum, and then save it. _scale = 0; CoerceScale(true); _scale = _coercedScale; ResizeImage(true); } /// <summary> /// Adjust the size of the image according to the coerced scale factor. Optionally /// center the image, otherwise, try to keep the original midpoint of the pinch /// in the same spot on the screen regardless of the scale. /// </summary> /// <param name="center"></param> void ResizeImage(bool center) { if (_coercedScale != 0 && _bitmap != null) { double newWidth = canvas.Width = Math.Round(_bitmap.PixelWidth * _coercedScale); double newHeight = canvas.Height = Math.Round(_bitmap.PixelHeight * _coercedScale); xform.ScaleX = xform.ScaleY = _coercedScale; viewport.Bounds = new Rect(0, 0, newWidth, newHeight); if (center) { viewport.SetViewportOrigin( new Point( Math.Round((newWidth - viewport.ActualWidth) / 2), Math.Round((newHeight - viewport.ActualHeight) / 2) )); } else { Point newImgMid = new Point(newWidth * _relativeMidpoint.X, newHeight * _relativeMidpoint.Y); Point origin = new Point(newImgMid.X - _screenMidpoint.X, newImgMid.Y - _screenMidpoint.Y); viewport.SetViewportOrigin(origin); } } } /// <summary> /// Coerce the scale into being within the proper range. Optionally compute the constraints /// on the scale so that it will always fill the entire screen and will never get too big /// to be contained in a hardware surface. /// </summary> /// <param name="recompute">Will recompute the min max scale if true.</param> void CoerceScale(bool recompute) { if (recompute && _bitmap != null && viewport != null) { // Calculate the minimum scale to fit the viewport double minX = viewport.ActualWidth / _bitmap.PixelWidth; double minY = viewport.ActualHeight / _bitmap.PixelHeight; _minScale = Math.Min(minX, minY); } _coercedScale = Math.Min(MaxScale, Math.Max(_scale, _minScale)); } // Sample code for building a localized ApplicationBar private void BuildLocalizedApplicationBar() { // Set the page's ApplicationBar to a new instance of ApplicationBar. ApplicationBar = new ApplicationBar(); // Create a new button and set the text value to the localized string from AppResources. ApplicationBarIconButton appBarButton = new ApplicationBarIconButton(new Uri("/Assets/appbar_info.png", UriKind.Relative)); appBarButton.Click += appBarButton_Click; appBarButton.Text = AppResources.AppBarButtonInfoText; ApplicationBar.Buttons.Add(appBarButton); } void appBarButton_Click(object sender, EventArgs e) { MessageBox.Show(AppResources.PinchZoomHelpText, AppResources.InfoCaption,MessageBoxButton.OK); } }

    • Edited by Sal_S Thursday, March 20, 2014 4:13 AM
    Wednesday, March 19, 2014 2:14 AM

All replies

  • EDIT: I discovered that by setting img.Source to the App.ViewModel.Image I was able to get it to work but I don't understand why since I already set the DataContext to the viewmodel in the constructor.

    Please show exactly what you did, because there is no object named img in the code you posted. Are Image and MyMapImage both properties in the view model? Please show the code for the view model class and the declaration for App.ViewModel.

    Wednesday, March 19, 2014 4:25 AM
  • I updated the code in the original post to reflect what it looks like now. Changes are bolded.

    The view model property is called MyMapImage

    The image object (xaml) name is img

    The image is being loaded on the MainPage of the application from an http source, into the viewmodel property MyMapImage.When the user taps the image they get navigated to the page (shown above in original post)  where they can pinch and zoom for more detail. 

    • Edited by Sal_S Thursday, March 20, 2014 4:20 AM
    Thursday, March 20, 2014 4:18 AM
  • using System;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Input;
    using System.Windows.Media;
    using System.Windows.Media.Animation;
    namespace ZoomWpfTest
        public class PanAndZoomViewer : ContentControl
            private Point ScreenStartPoint = new Point(0, 0);
            private FrameworkElement source;
            private Point startOffset;
            private TransformGroup transformGroup;
            private TranslateTransform translateTransform;
            private ScaleTransform zoomTransform;
            public PanAndZoomViewer()
                DefaultZoomFactor = 1.4;
                MaximumZoom = double.MaxValue;
                MinimumZoom = double.MinValue;
            public double DefaultZoomFactor { get; set; }
            public double MaximumZoom { get; set; }
            public double MinimumZoom { get; set; }
            public override void OnApplyTemplate()
            private void Setup()
                source = VisualTreeHelper.GetChild(this, 0) as FrameworkElement;
                translateTransform = new TranslateTransform();
                zoomTransform = new ScaleTransform();
                transformGroup = new TransformGroup();
                source.RenderTransform = transformGroup;
                Focusable = true;
                KeyDown += source_KeyDown;
                MouseMove += control_MouseMove;
                MouseDown += source_MouseDown;
                MouseUp += source_MouseUp;
                MouseWheel += source_MouseWheel;
            private void source_KeyDown(object sender, KeyEventArgs e)
                // hit escape to reset everything
                if (e.Key == Key.Escape) Reset();
            private void source_MouseWheel(object sender, MouseWheelEventArgs e)
                // zoom into the content.  Calculate the zoom factor based on the direction of the mouse wheel.
                double zoomFactor = DefaultZoomFactor;
                if (e.Delta <= 0) zoomFactor = 1.0 / DefaultZoomFactor;
                // DoZoom requires both the logical and physical location of the mouse pointer
                Point physicalPoint = e.GetPosition(this);
                DoZoom(zoomFactor, transformGroup.Inverse.Transform(physicalPoint), physicalPoint);
            private void source_MouseUp(object sender, MouseButtonEventArgs e)
                if (IsMouseCaptured)
                    // we're done.  reset the cursor and release the mouse pointer
                    Cursor = Cursors.Arrow;
            private void source_MouseDown(object sender, MouseButtonEventArgs e)
                // Save starting point, used later when determining how much to scroll.
                ScreenStartPoint = e.GetPosition(this);
                startOffset = new Point(translateTransform.X, translateTransform.Y);
                Cursor = Cursors.ScrollAll;
    private void control_MouseMove(object sender, MouseEventArgs e)
        if (!IsMouseCaptured)
            return; // don't care.
        // if the mouse is captured then move the content by changing the translate transform.  
        // use the Pan Animation to animate to the new location based on the delta between the 
        // starting point of the mouse and the current point.
        Point physicalPoint = e.GetPosition(this);
        // where you'd like to move the top left corner of the view to
        double toX = physicalPoint.X - ScreenStartPoint.X + startOffset.X;
        double toY = physicalPoint.Y - ScreenStartPoint.Y + startOffset.Y;
        Console.WriteLine("You're attempting to move to " + toX + "," + toY);
        double scaleValue = zoomTransform.ScaleX;
        var content = (FrameworkElement)Content;
        // minimum values we can shift the origin to - 
        // maximum values is always 0 (we don't want the left side of the content
        // ever being beyond the left part of the view
        double minToX = content.Width - (content.Width * scaleValue);
        double minToY = content.Height - (content.Height * scaleValue);
        // correct any invalid amounts:
        if (toX > 0)
            toX = 0;
        else if (toX < minToX)
            toX = minToX;
        if (toY > 0)
            toY = 0;
        else if (toY < minToY)
            toY = minToY;
        translateTransform.BeginAnimation(TranslateTransform.XProperty, CreatePanAnimation(toX),
        translateTransform.BeginAnimation(TranslateTransform.YProperty, CreatePanAnimation(toY),
            /// <summary>Helper to create the panning animation for x,y coordinates.</summary>
            /// <param name="toValue">New value of the coordinate.</param>
            /// <returns>Double animation</returns>
            private static DoubleAnimation CreatePanAnimation(double toValue)
                var da = new DoubleAnimation(toValue, new Duration(TimeSpan.FromMilliseconds(300)))
                                 AccelerationRatio = 0.1,
                                 DecelerationRatio = 0.9,
                                 FillBehavior = FillBehavior.HoldEnd
                return da;
            /// <summary>Helper to create the zoom double animation for scaling.</summary>
            /// <param name="toValue">Value to animate to.</param>
            /// <returns>Double animation.</returns>
            private static DoubleAnimation CreateZoomAnimation(double toValue)
                var da = new DoubleAnimation(toValue, new Duration(TimeSpan.FromMilliseconds(500)))
                                 AccelerationRatio = 0.1,
                                 DecelerationRatio = 0.9,
                                 FillBehavior = FillBehavior.HoldEnd
                return da;
            /// <summary>Zoom into or out of the content.</summary>
            /// <param name="deltaZoom">Factor to mutliply the zoom level by. </param>
            /// <param name="mousePosition">Logical mouse position relative to the original content.</param>
            /// <param name="physicalPosition">Actual mouse position on the screen (relative to the parent window)</param>
            public void DoZoom(double deltaZoom, Point mousePosition, Point physicalPosition)
                // Keep Zoom within bounds declared by Minimum/MaximumZoom
                double currentZoom = zoomTransform.ScaleX;
                currentZoom *= deltaZoom;
                if (currentZoom < MinimumZoom)
                    currentZoom = MinimumZoom;
                else if (currentZoom > MaximumZoom)
                    currentZoom = MaximumZoom;
                                                  CreateZoomAnimation(-1 *
                                                                      (mousePosition.X * currentZoom - physicalPosition.X)));
                                                  CreateZoomAnimation(-1 *
                                                                      (mousePosition.Y * currentZoom - physicalPosition.Y)));
                zoomTransform.BeginAnimation(ScaleTransform.ScaleXProperty, CreateZoomAnimation(currentZoom));
                zoomTransform.BeginAnimation(ScaleTransform.ScaleYProperty, CreateZoomAnimation(currentZoom));
            /// <summary>Reset to default zoom level and centered content.</summary>
            public void Reset()
                translateTransform.BeginAnimation(TranslateTransform.XProperty, CreateZoomAnimation(0));
                translateTransform.BeginAnimation(TranslateTransform.YProperty, CreateZoomAnimation(0));
                zoomTransform.BeginAnimation(ScaleTransform.ScaleXProperty, CreateZoomAnimation(1));
                zoomTransform.BeginAnimation(ScaleTransform.ScaleYProperty, CreateZoomAnimation(1));

    	xmlns:ZoomWpfTest="clr-namespace:ZoomWpfTest" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" mc:Ignorable="d"
    	Title="Zoom Original Image"
    	Width="640" Height="480" ResizeMode="CanMinimize" Background="#FF7E7E7E" Icon="Images/Sampleicon.png">
    	<ZoomWpfTest:PanAndZoomViewer x:Name="zoomViewer" MinimumZoom="1" MaximumZoom="4" Margin="0,0,1,1">
    		<Canvas x:Name="ImageLayout" Background="White" Width="633" Height="456" HorizontalAlignment="Left" VerticalAlignment="Bottom">
    			<Image x:Name="imageRetrived" HorizontalAlignment="Left" Height="456" VerticalAlignment="Bottom" Width="631.839" Canvas.Left="1.161" Stretch="Fill" />

    Happy Coding :-)

    <Kabilan>Learning C#</Kabilan>

    Thursday, March 20, 2014 8:38 AM
  • Is MyMapImage of type BitmapImage? Show how it is defined and assigned a value.
    Thursday, March 20, 2014 4:28 PM
  • Is MyMapImage of type BitmapImage? Show how it is defined and assigned a value.

    Yes BitmapImage

    Code from ViewModel

            private BitmapImage _myMapImage;
            public BitmapImage MyMapImage
                get { return this._myMapImage; }
                    this._myMapImage = value;
                    OnPropertyChanged(() => MyMapImage);

    On MainPage I call this function to set the image from isolated storage

    /// <summary>
    /// Sample code for loading image from IsolatedStorage
    /// </summary> 
    private void LoadAndDisplayImageFromIsolatedStorage()
    	// The image will be read from isolated storage into the following byte array
    	byte[] data;
    	// Read the entire image in one go into a byte array
    		using (IsolatedStorageFile isf = IsolatedStorageFile.GetUserStoreForApplication())
    			// Open the file - error handling omitted for brevity
    			// Note: If the image does not exist in isolated storage the following exception will be generated:
    			// System.IO.IsolatedStorage.IsolatedStorageException was unhandled 
    			// Message=Operation not permitted on IsolatedStorageFileStream 
    			using (IsolatedStorageFileStream isfs = isf.OpenFile(strImageName, FileMode.Open, FileAccess.Read))
    				// Allocate an array large enough for the entire file
    				data = new byte[isfs.Length];
    				// Read the entire file and then close it
    				isfs.Read(data, 0, data.Length);
    		// Create memory stream and bitmap
    		MemoryStream ms = new MemoryStream(data);
    		BitmapImage bi = new BitmapImage();
    		// Set bitmap source to memory stream
    		// Create an image UI element – Note: this could be declared in the XAML instead
    		//Image image = new Image();
    		// Set size of image to bitmap size for this demonstration
    		//image.Height = bi.PixelHeight;
    		//image.Width = bi.PixelWidth;
    		// Assign the bitmap image to the image’s source
    		App.ViewModel.MyMapImage = bi;
    		// Add the image to the grid in order to display the bit map
    	catch (Exception e)
    		// handle the exception

    • Edited by Sal_S Thursday, March 20, 2014 6:47 PM
    Thursday, March 20, 2014 6:44 PM
  • Your databinding looks generally correct, but it's hard to tell what's wrong without seeing the entire application.

    Here's a simple example of databinding an image:

    Friday, March 21, 2014 5:31 AM