imagexamlzoomingwinui-3winui

How to zoom and center on double click in Image in WinUI 3?


I have scrollViewer in WinUI 3 that contain Image and when I press double click on mouse I want to zoom in/out image and center the image to position of my cursor. I do this code for zoom in/out on double click scrollViewer:

private void Scroll_DoubleTapped(object sender, DoubleTappedRoutedEventArgs e)
{
    var zoom = scroll.ZoomFactor == 1 ? 3 : 1;
    scroll.ChangeView(null, null, zoom, true);
    ZoomSlider.Value = zoom;
}

now I try to center my image to position of cursor and I try like this:

private void Scroll_DoubleTapped(object sender, DoubleTappedRoutedEventArgs e)
{
    var zoom = scroll.ZoomFactor == 1 ? 3 : 1;
    var pointerPosition = e.GetPosition(scroll);
    scroll.ChangeView(pointerPosition.X, pointerPosition.Y, zoom, true);
    ZoomSlider.Value = zoom;
}

but its not center to position of my cursor. what can I do?

this my code in .xaml file:

    
    <Slider x:Name="ZoomSlider" Width="200" Maximum="5" StepFrequency="0.1" ValueChanged="ZoomSlider_ValueChanged" Margin="0,5,0,0" />
   
    <ScrollViewer x:Name="scroll" ZoomMode="Enabled" Grid.Row="1"
                  MinZoomFactor="1" MaxZoomFactor="5.0"
                  IsTabStop="True" 
                  IsVerticalScrollChainingEnabled="True"
                  IsHorizontalScrollChainingEnabled="True"
                  HorizontalAlignment="Stretch" 
                  VerticalAlignment="Stretch"
                  HorizontalScrollMode="Enabled" 
                  HorizontalScrollBarVisibility="Hidden"
                  VerticalScrollMode="Enabled" 
                  VerticalScrollBarVisibility="Hidden"
                  ViewChanged="ScrollViewer_ViewChanged"> 
    <Image x:Name="MyImage"
           Source="{x:Bind ViewModel.Image, Mode=OneWay}" 
           PointerPressed="Image_PointerPressed"
           PointerMoved="Image_PointerMoved"
           PointerReleased="Image_PointerReleased"
           DoubleTapped="Image_DoubleTapped"
           Stretch="Uniform" HorizontalAlignment="Stretch" 
           VerticalAlignment="Stretch"/>
 </ScrollViewer>

and this is my code behind from xaml.cs file:

    internal abstract class ImageBasePage : BasePage<ImageViewModel> { }

    internal sealed partial class ImagePage : ImageBasePage
    {
        bool _isSyncing = false, _isDragging = false, _isManualZoomChange = false;
        double _initialHorizontalOffset, _initialVerticalOffset;

        public ImagePage()
        {
            this.InitializeComponent();
        }

        private void Scroll_SizeChanged(object sender, SizeChangedEventArgs e)
        {
            MyImage.MaxHeight = scroll.ActualHeight;
            MyImage.MaxWidth = scroll.ActualWidth;
        }

        private void ZoomSlider_ValueChanged(object sender, RangeBaseValueChangedEventArgs e)
        {
            if (!_isSyncing && !_isManualZoomChange)
            {
                _isSyncing = true;
                scroll.ChangeView(null, null, (float)e.NewValue);
                _isSyncing = false;
            }
        }

        private void ScrollViewer_ViewChanged(object sender, ScrollViewerViewChangedEventArgs e)
        {
            if (!_isSyncing && !_isManualZoomChange)
            {
                _isSyncing = true;
                ZoomSlider.Value = scroll.ZoomFactor;
                _isSyncing = false;
            }
        }

        private void Image_DoubleTapped(object sender, DoubleTappedRoutedEventArgs e)
        {
            _isManualZoomChange = true;
            if (sender is not Image image)
            {
                return;
            }

            var pointerPoint = e.GetPosition(image);

            float zoomFactor = scroll.ZoomFactor;
            float nextZoomFactor = zoomFactor == 1 ? 3 : 1;

            double nextHorizontalOffset = Math.Max(pointerPoint.X * nextZoomFactor - (scroll.ViewportWidth / 2), 0);
            double nextVerticalOffset = Math.Max(pointerPoint.Y * nextZoomFactor - (scroll.ViewportHeight / 2), 0);
            _ = scroll.ChangeView(nextHorizontalOffset, nextVerticalOffset,nextZoomFactor,true);
            ZoomSlider.Value = nextZoomFactor;
            _isManualZoomChange = false;
        }
  
        private void Image_PointerMoved(object sender, PointerRoutedEventArgs e)
        {
            if (_isDragging)
            {
                var currentPoint = e.GetCurrentPoint(scroll).Position;

                var deltaX = currentPoint.X - _initialPoint.X;
                var deltaY = currentPoint.Y - _initialPoint.Y;

                scroll.ChangeView(_initialHorizontalOffset - deltaX, _initialVerticalOffset - deltaY, null);
            }
        }

        private void Image_PointerReleased(object sender, PointerRoutedEventArgs e)
        {
            _isDragging = false;
            (sender as UIElement).ReleasePointerCapture(e.Pointer);
        }
    }

Solution

  • You need to take ViewportWidth and ViewportHeight into consideration. The following should work:

    <ScrollViewer
        x:Name="ScrollViewer"
        HorizontalScrollBarVisibility="Auto"
        MinZoomFactor="1"
        MaxZoomFactor="10"
        PointerMoved="ScrollViewer_PointerMoved"
        VerticalScrollBarVisibility="Auto"
        ZoomMode="Enabled">
        <Image
            DoubleTapped="Image_DoubleTapped"
            Source="/Assets/test.png" />
    </ScrollViewer>
    
    private void Image_DoubleTapped(object sender, DoubleTappedRoutedEventArgs e)
    {
        if (sender is not Image image)
        {
            return;
        }
    
        var pointerPoint = e.GetPosition(image);
    
        float zoomFactor = this.ScrollViewer.ZoomFactor;
        float nextZoomFactor = Math.Min(zoomFactor * 2.0f, this.ScrollViewer.MaxZoomFactor);
    
        double nextHorizontalOffset = Math.Max(pointerPoint.X * nextZoomFactor - (this.ScrollViewer.ViewportWidth / 2), 0);
        double nextVerticalOffset = Math.Max(pointerPoint.Y * nextZoomFactor - (this.ScrollViewer.ViewportHeight / 2), 0);
        _ = this.ScrollViewer.ChangeView(
            nextHorizontalOffset,
            nextVerticalOffset,
            nextZoomFactor);
    }