0

I am working on an ItemsControl with Canvas as ItemPanel. Trying to implement a zoom behavior according to the answer to an other question.

The minimum code needed for reproduction should be this.

MainWindow.xaml

<Window x:Class="WpfApp7.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:local="clr-namespace:WpfApp7"
    mc:Ignorable="d"
    Title="MainWindow" Height="450" Width="800">
<Window.Resources>
    <Style TargetType="{x:Type local:DesignerSheetView}" BasedOn="{StaticResource {x:Type ItemsControl}}">
        <Style.Resources>
        </Style.Resources>
        <Setter Property="ItemsControl.ItemsPanel">
            <Setter.Value>
                <ItemsPanelTemplate>
                    <Canvas  local:ZoomBehavior.IsZoomable="True" local:ZoomBehavior.ZoomFactor="0.1" local:ZoomBehavior.ModifierKey="Ctrl">
                        <Canvas.Background>
                            <VisualBrush TileMode="Tile" Viewport="-1,-1,20,20" ViewportUnits="Absolute" Viewbox="-1,-1,20,20" ViewboxUnits="Absolute">
                                <VisualBrush.Visual>
                                    <Grid Width="20" Height="20">
                                        <Ellipse Height="2" Width="2" Stroke="Black" StrokeThickness="1" HorizontalAlignment="Left" VerticalAlignment="Top" Margin="-1,-1" />
                                    </Grid>
                                </VisualBrush.Visual>
                            </VisualBrush>
                        </Canvas.Background>
                    </Canvas>
                </ItemsPanelTemplate>
            </Setter.Value>
        </Setter>
        <Setter Property="ItemsControl.ItemContainerStyle">
            <Setter.Value>
                <Style TargetType="ContentPresenter">
                    <Setter Property="Canvas.Top" Value="{Binding Path=YPos}" />
                    <Setter Property="Canvas.Left" Value="{Binding Path=XPos}" />
                    <Setter Property="VerticalAlignment" Value="Stretch" />
                    <Setter Property="HorizontalAlignment" Value="Stretch" />
                </Style>
            </Setter.Value>
        </Setter>
        <Setter Property="Focusable" Value="True" />
        <Setter Property="IsEnabled" Value="True" />

    </Style>
</Window.Resources>

<Grid>
    <ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">
        <local:DesignerSheetView Background="Beige">

        </local:DesignerSheetView>
    </ScrollViewer>
</Grid>

The DesignerSheetView codebehind:

    public class DesignerSheetView : ItemsControl
{
    static DesignerSheetView()
    {
        DefaultStyleKeyProperty.OverrideMetadata(typeof(DesignerSheetView), new FrameworkPropertyMetadata(typeof(DesignerSheetView)));
    }
}

And the modified ZoomBehavior

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;

namespace WpfApp7
{
public static class ZoomBehavior
{
    //example from https://stackoverflow.com/questions/46424149/wpf-zoom-canvas-center-on-mouse-position

    #region ZoomFactor
    public static double GetZoomFactor(DependencyObject obj)
    {
        return (double)obj.GetValue(ZoomFactorProperty);
    }
    public static void SetZoomFactor(DependencyObject obj, double value)
    {
        obj.SetValue(ZoomFactorProperty, value);
    }
    // Using a DependencyProperty as the backing store for ZoomFactor.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty ZoomFactorProperty =
        DependencyProperty.RegisterAttached("ZoomFactor", typeof(double), typeof(ZoomBehavior), new PropertyMetadata(1.05));
    #endregion

    #region ModifierKey       
    public static ModifierKeys? GetModifierKey(DependencyObject obj)
    {
        return (ModifierKeys?)obj.GetValue(ModifierKeyProperty);
    }
    public static void SetModifierKey(DependencyObject obj, ModifierKeys? value)
    {
        obj.SetValue(ModifierKeyProperty, value);
    }
    // Using a DependencyProperty as the backing store for ModifierKey.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty ModifierKeyProperty =
        DependencyProperty.RegisterAttached("ModifierKey", typeof(ModifierKeys?), typeof(ZoomBehavior), new PropertyMetadata(null));
    #endregion
    public static TransformMode ModeOfTransform { get; set; } = TransformMode.Layout;
    private static Transform _transform;
    private static Canvas _view;

    #region IsZoomable        
    public static bool GetIsZoomable(DependencyObject obj)
    {
        return (bool)obj.GetValue(IsZoomableProperty);
    }
    public static void SetIsZoomable(DependencyObject obj, bool value)
    {
        obj.SetValue(IsZoomableProperty, value);
    }
    // Using a DependencyProperty as the backing store for IsZoomable.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty IsZoomableProperty =
        DependencyProperty.RegisterAttached(
            "IsZoomable",
            typeof(bool),
            typeof(ZoomBehavior),
            new UIPropertyMetadata(false, OnIsZoomableChanged));
    #endregion

    private static void OnIsZoomableChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        _view = d as Canvas;
        if (null == _view)
        {
            System.Diagnostics.Debug.Assert(false, "Wrong dependency object type");
            return;
        }
        if ((e.NewValue is bool) == false)
        {
            System.Diagnostics.Debug.Assert(false, "Wrong value type assigned to dependency object");
            return;
        }

        if (true == (bool)e.NewValue)
        {
            _view.MouseWheel += Canvas_MouseWheel;
            if (ModeOfTransform == TransformMode.Render)
            {
                _transform = _view.RenderTransform = new MatrixTransform();
            }
            else
            {
                _transform = _view.LayoutTransform = new MatrixTransform();
            }
        }
        else
        {
            _view.MouseWheel -= Canvas_MouseWheel;
        }
    }





    public static double GetZoomScale(DependencyObject obj)
    {
        return (double)obj.GetValue(ZoomScaleProperty);
    }

    public static void SetZoomScale(DependencyObject obj, double value)
    {
        obj.SetValue(ZoomScaleProperty, value);
    }

    // Using a DependencyProperty as the backing store for ZoomScale.  This enables animation, styling, binding, etc...
    public static readonly DependencyProperty ZoomScaleProperty =
        DependencyProperty.RegisterAttached("ZoomScale", typeof(double), typeof(ZoomBehavior), new PropertyMetadata(1.0));



    private static void Canvas_MouseWheel(object sender, MouseWheelEventArgs e)
    {
        ModifierKeys? modifierkey = GetModifierKey(sender as DependencyObject);

        if (!modifierkey.HasValue)
        {
            return;
        }

        if((Keyboard.Modifiers & (modifierkey.Value)) == ModifierKeys.None)
        {
            return;
        }

        if (!(_transform is MatrixTransform transform))
        {
            return;
        }

        var pos1 = e.GetPosition(_view);

        double zoomfactor = GetZoomFactor(sender as DependencyObject);
        double scale = GetZoomScale(sender as DependencyObject);
        //scale = (e.Delta < 0) ? (scale * zoomfactor) : (scale / zoomfactor);
        scale = (e.Delta < 0) ? (scale + zoomfactor) : (scale - zoomfactor);
        scale = (scale < 0.1) ? 0.1 : scale;
        SetZoomScale(sender as DependencyObject, scale);

        var mat = transform.Matrix;            
        mat.ScaleAt(scale, scale, pos1.X, pos1.Y);
        //transform.Matrix = mat;
        if (TransformMode.Layout == ModeOfTransform)
        {
            _view.LayoutTransform = new MatrixTransform(mat);
        }
        else
        {
            _view.RenderTransform = new MatrixTransform(mat);
        }

        e.Handled = true;
    }

    public enum TransformMode
    {
        Layout,
        Render,
    }
}

}

I think the ZoomBehavior should be ok, I did not change it that much. The problem is somewhere in the xaml. I observe multiple things, for which I seek a solution here:

  • If I use the RenderTransform mode, the zoom happens at the mouse position, as intended. The problem is, that the background does not fill the container/window. enter image description here
  • If I use the LayoutTransform mode, the background fills the window, but the zoom does not happen on the mouse position. The transform origin is at (0,0). enter image description here
  • The ScrollBar-s are not activated, no matter which transform mode I choose.

Most of the questions on SO start with the asker trying to solve the zoom problem with layout transformation. Almost all answers use RenderTransform instead of LayoutTrasform (e.g. this, this and this). None of the answers provide explanation, why a RenderTransform better suits the task than a LayoutTransform. Is this because with LayoutTransform one needs to change the position of the Canvas too?

What should I change in order to make the RenderTransform work (background filling whole container and ScrollBars appearing)?

G. B.
  • 528
  • 2
  • 15
  • Take a look at [Transforming a FrameworkElement](https://learn.microsoft.com/en-us/dotnet/framework/wpf/graphics-multimedia/transforms-overview#transforming-a-frameworkelement). – Clemens Nov 26 '19 at 19:11

1 Answers1

0

Basically you have to apply a LayoutTransform to your Canvas (or a parent Grid, say), translate it by the inverse of your zoom point (taking the current zoom factor into account) and then transform it back again to it's original position (taking the new zoom into account). So this:

// zoom level. typically changes by +/- 1 (or some other constant)
// at a time and updates this.Zoom which is the actual zoom
// multiplication factor.
private double _ZoomLevel = 1;
private double ZoomLevel
{
    get { return this._ZoomLevel; }
    set
    {
        var zoomPointX = this.ViewportWidth / 2;
        var zoomPointY = this.ViewportHeight / 2;
        if (this.MouseOnCanvas)
        {
            zoomPointX = this.LastMousePos.X * this.Zoom - this.HorizontalOffset;
            zoomPointY = this.LastMousePos.Y * this.Zoom - this.VerticalOffset;
        }
        var imageX = (this.HorizontalOffset + zoomPointX) / this.Zoom;
        var imageY = (this.VerticalOffset + zoomPointY) / this.Zoom;
        this._ZoomLevel = value;
        this.Zoom = 0.25 * Math.Pow(Math.Sqrt(2), value);
        this.HorizontalOffset = imageX * this.Zoom - zoomPointX;
        this.VerticalOffset = imageY * this.Zoom - zoomPointY;
    }
}

A few things to note about this code:

  • It assumes that the Canvas is inside a ScrollViewer, so that the user can scroll around when zoomed in. For this you need a behavior that binds to the HorizontalOffset and VerticalOffset properties.
  • You also need to know the width and height of the scrollviewer client area, so you'll need to modify that behavior to also provide properties for those.
  • You need to track the current mouse coordinate relative to the Canvas, which means intercepting MouseEnter/MouseMove/MouseLeave and maintain the MouseOnCanvas property.
Mark Feldman
  • 15,731
  • 3
  • 31
  • 58