Search code examples
.netwpfimageuser-interfacezooming

WPF Scrollviewer not adjusting to content


I need to show an image in my WPF application, overlay it with transparent canvas and allow user to draw on it. User must be able to zoom image (in and out) and "navigate" in image while zoomed in using some kind of scrollbars docked to bottom and right of the window. To enable image zoom, I used ZoomBorder control, described in second answer. So this is my XAML code:

<ScrollViewer x:Name="imageScroll" HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto"  >
  <local:ZoomBorder x:Name="backgroundZoomBorder" ClipToBounds="True" HorizontalAlignment="Center" VerticalAlignment="Center"  Width="{Binding CurrentImageWidth}" Height="{Binding CurrentImageHeight}"  >
     <Grid x:Name="gridDrawing" MouseLeftButtonDown="background_MouseLeftButtonDown" MouseMove="background_MouseMove" MouseLeftButtonUp="background_MouseLeftButtonUp">                           
       <Image  x:Name="mainImage"  Source="{Binding Path=WriteableBmp}" />
       <Canvas x:Name="canvasDrawing"  Background="Transparent" />
     </Grid>
  </local:ZoomBorder>
</ScrollViewer>

This works, however, the scollviewer is not adjusting to content's size, it's still the same, if the image is zoomed in to maximum, or is zoomed out to minimum. It should disappear if the image is smaller than window area, or have bigger range when image is zoomed. Can I anyhow bind ScrollViever to Image's transformed size or something? Or is there any better control for this instead of ScrollViewer? Thank you


Solution

  • A ScrollViewer will never work with the ZoomBorder control because it is using a RenderTransform. A RenderTransform manipulates what you see after it has gone through the layout system. Because of this, its size never changes and thus the ScrollViewer will never activate.

    By modifying the ZoomBorder to use LayoutTransform, you can get the zooming (scroll wheel) functionality to work and the ScrollViewer will activate. However, getting panning to work with the mouse will take more significant modifications.

    Here is some example code that works:

    ZoomBorder using LayoutTransform

    using System.Linq;
    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Input;
    using System.Windows.Media;
    
    namespace SO
    {
        public class ZoomBorder : Border
        {
            private FrameworkElement child;
    
            private ScaleTransform GetScaleTransform(FrameworkElement element)
            {
                return (ScaleTransform)((TransformGroup)element.LayoutTransform)
                  .Children.First(tr => tr is ScaleTransform);
            }
    
            public override UIElement Child
            {
                get => base.Child;
                set
                {
                    if (value != null && value != Child)
                        Initialize(value);
                    base.Child = value;
                }
            }
    
            public void Initialize(UIElement element)
            {
                child = (FrameworkElement)element;
    
                if (child == null) return;
    
                var group = new TransformGroup();
                var st = new ScaleTransform();
    
                @group.Children.Add(st);
    
                child.LayoutTransform = @group;
                child.RenderTransformOrigin = new Point(0.0, 0.0);
    
                MouseWheel += child_MouseWheel;
                PreviewMouseRightButtonDown += child_PreviewMouseRightButtonDown;
            }
    
            public void Reset()
            {
                if (child == null) return;
    
                // reset zoom
                var st = GetScaleTransform(child);
                st.ScaleX = 1.0;
                st.ScaleY = 1.0;
            }
    
            #region Child Events
            private void child_MouseWheel(object sender, MouseWheelEventArgs e)
            {
                if (child != null)
                {
                    var st = GetScaleTransform(child);
    
                    var zoom = e.Delta > 0 ? .2 : -.2;
    
                    if (!(e.Delta > 0) && (st.ScaleX < .4 || st.ScaleY < .4)) return;
    
                    st.ScaleX += zoom;
                    st.ScaleY += zoom;
                }
            }
    
            void child_PreviewMouseRightButtonDown(object sender, MouseButtonEventArgs e)
            {
                Reset();
            }
            #endregion
        }
    }
    

    ScrollViewerPanBehavior A behavior that handles panning by dragging with the mouse.

    using System.Windows;
    using System.Windows.Controls;
    using System.Windows.Input;
    using System.Windows.Interactivity;
    
    namespace SO
    {
        public class ScrollViewerPanBehavior : Behavior<ScrollViewer>
        {
            private UIElement content;
            private Point scrollMousePoint;
            private double scrollHorizontalOffset;
            private double scrollVerticalOffset;
    
            public ScrollViewerPanBehavior()
            {
            }
    
            protected override void OnAttached()
            {
                base.OnAttached();
                AssociatedObject.Loaded += OnLoaded;
            }
    
            private void OnLoaded(object sender, RoutedEventArgs e)
            {
                content = (UIElement)AssociatedObject.Content;
                content.MouseLeftButtonDown += OnMouseLeftButtonDown;
                content.MouseMove += OnMouseMove;
                content.MouseLeftButtonUp += OnMouseLeftButtonUp;
            }
    
            private void OnMouseLeftButtonDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
            {
                content.CaptureMouse();
                AssociatedObject.Cursor = Cursors.Hand;
                scrollMousePoint = e.GetPosition(AssociatedObject);
                scrollHorizontalOffset = AssociatedObject.HorizontalOffset;
                scrollVerticalOffset = AssociatedObject.VerticalOffset;
            }
    
            private void OnMouseMove(object sender, System.Windows.Input.MouseEventArgs e)
            {
                if (content.IsMouseCaptured)
                {
                    var newVerticalOffset = scrollVerticalOffset + (scrollMousePoint.Y - e.GetPosition(AssociatedObject).Y);
                    var newHorizontalOffset = scrollHorizontalOffset + (scrollMousePoint.X - e.GetPosition(AssociatedObject).X);
    
                    AssociatedObject.ScrollToVerticalOffset(newVerticalOffset);
                    AssociatedObject.ScrollToHorizontalOffset(newHorizontalOffset);
                }
            }
    
            private void OnMouseLeftButtonUp(object sender, System.Windows.Input.MouseButtonEventArgs e)
            {
                content.ReleaseMouseCapture();
                AssociatedObject.Cursor = Cursors.Arrow;
            }
        }
    }
    

    Example usage:

    <ScrollViewer
        HorizontalScrollBarVisibility="Auto"
        VerticalScrollBarVisibility="Auto">
        <i:Interaction.Behaviors>
            <local:ScrollViewerPanBehavior />
        </i:Interaction.Behaviors>
        <local:ZoomBorder
            HorizontalAlignment="Center"
            VerticalAlignment="Center">
            <Grid>
                <Image Source="ThumbsUp.png" />
            </Grid>
        </local:ZoomBorder>
    </ScrollViewer>
    

    This was fun to work on!