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.
- 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).
- 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)?