Search code examples
c#wpfcanvasscaletransform

ScaleTransform within a canvas: Centering around mouse pointer causing issues


I have a canvas (OuterCanvas) within a canvas (InnerCanvas) and am handling zooming in an out as follows:

private ScaleTransform ScaleTransform = new ScaleTransform();

private void OuterCanvas_MouseWheel(Object sender, MouseWheelEventArgs e)
{
    if (e.Delta >= 1)
    {
        ScaleTransform.ScaleX += 0.03;
        ScaleTransform.ScaleY += 0.03;
    }
    else
    {
        ScaleTransform.ScaleX -= 0.03;
        ScaleTransform.ScaleY -= 0.03;
    }
    ScaleTransform.CenterX = ActualWidth / 2;
    ScaleTransform.CenterY = ActualHeight / 2;
    InnerCanvas.RenderTransform = TransformGroup;
}

This zooms in and out quite okay at the centre of the canvas. I'd like to zoom in and out around the mouse pointer, so change the CenterX and CenterY lines as follows:

ScaleTransform.CenterX = e.GetPosition(this).X;
ScaleTransform.CenterY = e.GetPosition(this).Y;

Again this works satisfactory, however if I attempt to move the mouse during two zoom events (mouse wheel events), the whole canvas jumps rather drastically as the scale factor increases or decreases.

Demonstration of problem

This is the same issue as when the CenterX and CenterY properties are set to the ActualWidth/2 and ActualHeight/2 and the window size is changed.

What should the correct handling be of the CenterX and CenterY properties be to avoid this issue?


Solution

  • Here is a more or less complete solution, including the possibility to pan the inner Canvas:

    <Canvas x:Name="outerCanvas"
            MouseWheel="OnMouseWheel"
            MouseMove="OnMouseMove"
            MouseLeftButtonDown="OnMouseLeftButtonDown"
            MouseLeftButtonUp="OnMouseLeftButtonUp">
        <Canvas x:Name="innerCanvas"
                Width="400" Height="400" Background="AliceBlue">
            <Canvas.RenderTransform>
                <MatrixTransform />
            </Canvas.RenderTransform>
        </Canvas>
    </Canvas>
    

    with these event handlers:

    private double zoomScaleFactor = 1.1;
    private Point? mousePos;
    
    private void OnMouseWheel(object sender, MouseWheelEventArgs e)
    {
        var pos = e.GetPosition(outerCanvas);
        var scale = e.Delta > 0 ? zoomScaleFactor : 1 / zoomScaleFactor;
        var transform = (MatrixTransform)innerCanvas.RenderTransform;
        var matrix = transform.Matrix;
        matrix.ScaleAt(scale, scale, pos.X, pos.Y);
        transform.Matrix = matrix;
    }
    
    private void OnMouseMove(object sender, MouseEventArgs e)
    {
        if (mousePos.HasValue)
        {
            var pos = e.GetPosition(outerCanvas);
            var delta = pos - mousePos.Value;
            var transform = (MatrixTransform)innerCanvas.RenderTransform;
            var matrix = transform.Matrix;
            matrix.Translate(delta.X, delta.Y);
            transform.Matrix = matrix;
            mousePos = pos;
        }
    }
    
    private void OnMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
    {
        if (outerCanvas.CaptureMouse())
        {
            mousePos = e.GetPosition(outerCanvas);
        }
    }
    
    private void OnMouseLeftButtonUp(object sender, MouseButtonEventArgs e)
    {
        outerCanvas.ReleaseMouseCapture();
        mousePos = null;
    }