Zooming To Mouse Point With ScrollView and ViewBox in Wpf

Lukehb picture Lukehb · Apr 29, 2012 · Viewed 7.8k times · Source

I have some paths drawn to the screen in wpf. The coordinates being used are quite small so they have been made to fill the screen with a view box. I am now trying to implement pan and zoom functionality. I would like to be able to zoom to wherever the mouse is in relation to the ui (i.e zoomed screen center is equal to mouse coordinates). The current outcome is that the center of the screen when zoomed is not reflective of the exact mouse position on the ui.

If you want to see what is happening... Here is my current solution file.

Or

Heres some code:

View Xaml

<Grid Name="MasterGrid" DataContext="{StaticResource mainWindowViewModel}">

    <ScrollViewer HorizontalScrollBarVisibility="Visible" VerticalScrollBarVisibility="Visible" Name="VisualisationScroller">
        <Viewbox Name="VisualisationBox" Stretch="Fill" Loaded="VisualisationBox_Loaded">

            <ItemsControl Name="CustomDrawingElement" ItemsSource="{Binding Trajectories}" Width="{Binding VisualisationWidth}" Height="{Binding VisualisationHeight}">
                <ItemsControl.ItemTemplate>
                    <DataTemplate DataType="data:VisualisedTrajectory">
                        <Path Data = "{Binding PathData}" Stroke="Red" StrokeThickness="0.001" Fill="Transparent" />
                    </DataTemplate>
                </ItemsControl.ItemTemplate>

                <ItemsControl.ItemsPanel>
                    <ItemsPanelTemplate>
                        <Canvas
                            Background="DarkGray"
                            IsItemsHost="True">
                        </Canvas>

                    </ItemsPanelTemplate>
                </ItemsControl.ItemsPanel>

                <ItemsControl.RenderTransform>

                    <TransformGroup>
                        <ScaleTransform ScaleX="1" ScaleY="1" />
                        <TranslateTransform />
                    </TransformGroup>

                </ItemsControl.RenderTransform>

            </ItemsControl>

        </Viewbox>
    </ScrollViewer>
</Grid>

View Model

public class MainWindowViewModel : BaseViewModel
{

    public MainWindowViewModel()
    {
        VisualiseRawTrajectories();

    }

    private ObservableCollection<VisualisedTrajectory> _trajectories = new ObservableCollection<VisualisedTrajectory>();
    public ObservableCollection<VisualisedTrajectory> Trajectories
    {
        get { return _trajectories; } 
    }

    #region VisualisationDimensions

    private double _visualisationWidth = 100;
    public double VisualisationWidth
    {
        get { return _visualisationWidth; }
        private set { _visualisationWidth = value; }
    }

    private double _visualisationHeight = 100;
    public double VisualisationHeight
    {
        get { return _visualisationHeight; }
        private set { _visualisationHeight = value; }
    }

    #endregion

    public void VisualiseRawTrajectories()
    {
        var rand = new Random();

        for (int i = 0; i < 5; i++)
        {
            var currentTrajectorySet = new List<Point>(); //each time through reinitialise
            for (int j = 0; j < 5; j++)
            {
                currentTrajectorySet.Add(new Point(rand.NextDouble() * 0.5, rand.NextDouble() * 0.5)); //add a new point with max 0.5
                if(j == 4)
                {
                    currentTrajectorySet.Add(new Point(0.5, 0.5)); //for good measure :)
                    _trajectories.Add(new VisualisedTrajectory(CreatePathData(currentTrajectorySet)));
                }
            }
        }

        VisualisationHeight = 0.5;
        VisualisationWidth = 0.5; //just for demonstration purposes
        OnPropertyChanged("VisualisationHeight");
        OnPropertyChanged("VisualisationWidth");

    }

    private Geometry CreatePathData(IList<Point> points)
    {
        var geometry = new StreamGeometry {FillRule = FillRule.EvenOdd};
        using (StreamGeometryContext ctx = geometry.Open())
        {
            ctx.BeginFigure(points[0], false, false); //use the first index
            ctx.PolyLineTo(points, true, true);
        }
        return (Geometry)geometry.GetAsFrozen(); 
    }

}

View Code Behind

public MainWindow()
    {
        InitializeComponent();
        VisualisationScroller.PreviewMouseWheel += OnPreviewMouseWheel;
    }

    private Point originalDimensions;
    private void VisualisationBox_Loaded(object sender, RoutedEventArgs e)
    {
        Viewbox viewBox = sender as Viewbox;
        viewBox.Width = viewBox.ActualWidth;
        viewBox.Height = viewBox.ActualHeight;
        originalDimensions = new Point(viewBox.ActualWidth, viewBox.ActualHeight);

    }


    #region Zoom

    private int _numberDrawnItems = 0;
    private void OnPreviewMouseWheel(object sender, MouseWheelEventArgs e)
    {
        var zoomScale = new Point(CustomDrawingElement.RenderTransform.Value.M11,
                                  CustomDrawingElement.RenderTransform.Value.M22); //gets the scale x and scale y


        if (CustomDrawingElement != null && _numberDrawnItems != CustomDrawingElement.Items.Count) //if there was something draw to screen
        {
            _numberDrawnItems = CustomDrawingElement.Items.Count;
            CustomDrawingElement.RenderTransformOrigin = new Point(0.5, 0.5); //if not set zoom from center
        }
        if (e.Delta > 0)
        {
            if (CustomDrawingElement != null)
            {

                VisualisationBox.Width = originalDimensions.X * (zoomScale.X + 1);
                VisualisationBox.Height = originalDimensions.Y * (zoomScale.Y + 1);


                var mousePosition = e.GetPosition(MasterGrid);
                CustomDrawingElement.RenderTransformOrigin = new Point(mousePosition.X / MasterGrid.ActualWidth, mousePosition.Y / MasterGrid.ActualHeight);
                CustomDrawingElement.RenderTransform = new MatrixTransform(zoomScale.X + 1, 0, 0, zoomScale.Y + 1, 0, 0);


            }
        }
        if (e.Delta < 0)
        {
            if (zoomScale.X > 1 && zoomScale.Y > 1) //stops you from zooming out too much
            {
                if (CustomDrawingElement != null)
                {

                    VisualisationBox.Width = VisualisationBox.Width - originalDimensions.X;
                    VisualisationBox.Height = VisualisationBox.Height - originalDimensions.Y;

                    CustomDrawingElement.RenderTransform = new MatrixTransform(zoomScale.X - 1, 0, 0, zoomScale.Y - 1, 0, 0);


                }
            }
        }

        e.Handled = true;
    }

    #endregion //Zooming code


}

Answer

Lukehb picture Lukehb · May 2, 2012

Solved it changed the back code for zooming the View to:

if (e.Delta > 0)
        {
            if (CustomDrawingElement != null)
            {
                //zoom

                _zoomScale++; //increase it now that we have zoomed
                VisualisationBox.Width = _originalDimensions.X * (_zoomScale);
                VisualisationBox.Height = _originalDimensions.Y * (_zoomScale);

                ScrollerDimensions.Content = VisualisationScroller.ActualWidth + "x" + VisualisationScroller.ActualHeight;
                BoxDimensions.Content = VisualisationBox.ActualWidth + "x" + VisualisationBox.ActualHeight;

                var mousePosition = e.GetPosition(MasterGrid);
                mousePosition = MasterGrid.TransformToVisual(VisualisationBox).Transform(mousePosition);

                ScrolledPoint.Content = mousePosition.X + "," + mousePosition.Y;
                VisualisationScroller.ScrollToHorizontalOffset(mousePosition.X);
                VisualisationScroller.ScrollToVerticalOffset(mousePosition.Y);

            }
        }
        if (e.Delta < 0)
        {
            if (_zoomScale > 1) //stops you from zooming out too much
            {
                if (CustomDrawingElement != null)
                {
                    var mousePosition = e.GetPosition(MasterGrid);

                    _zoomScale -= 1; //decrease the zoom
                    VisualisationBox.Width = VisualisationBox.Width - _originalDimensions.X;
                    VisualisationBox.Height = VisualisationBox.Height - _originalDimensions.Y;
                    mousePosition = MasterGrid.TransformToVisual(VisualisationBox).Transform(mousePosition);
                    mousePosition = new Point(mousePosition.X - _originalDimensions.X, mousePosition.Y - _originalDimensions.Y);

                    VisualisationScroller.ScrollToHorizontalOffset(mousePosition.X);
                    VisualisationScroller.ScrollToVerticalOffset(mousePosition.Y);
                }
            }
        }

        e.Handled = true;

If anyone is interested HERE is the finished solution file with panning implemented too.