Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Maintain scrollviewer's relative scrollbar offset when resizing child

Tags:

c#

wpf

I have a scrollviewer with a grid as the child. I am changing the grid's width and height properties to show different "zoom" levels. The grid contains 2 rows with many columns of images, all the same size.

However, I want the relative position of the scrollbar to stay the same. Whatever is on the center of the screen should still be on the center of the screen after changing the grid's size.

Default "zoomed in" view:

private void SizeGrid()
{
    grid1.Width = (scrollViewer1.ViewportWidth / 2) * grid1.ColumnDefinitions.Count;
    grid1.Height = (scrollViewer1.ViewportHeight / 2) * grid1.RowDefinitions.Count;        
}

"Zoomed out" view:

private void scrollViewer1_KeyDown(object sender, KeyEventArgs e)
{
    if (e.KeyboardDevice.IsKeyDown(Key.Insert))
    {
        grid1.Width = (scrollViewer1.ViewportWidth / 2) * grid1.ColumnDefinitions.Count / 5;
        grid1.Height = (scrollViewer1.ViewportHeight / 2) * grid1.RowDefinitions.Count / 3;
    }
}

What I tried doing...

If I know what column is focused (I don't want to need to know this):

double shiftAmount = (scrollViewer1.ScrollableWidth / (grid1.ColumnDefinitions.Count - columnsOnScreen));
scrollViewer1.ScrollToHorizontalOffset(column * shiftAmount);

If I don't know exactly what column they are looking at, but I just want to keep the relative position...

double previousScrollRatio = scrollViewer1.HorizontalOffset / scrollViewer1.ScrollableWidth;
//resize grid...
scrollViewer1.ScrollToHorizontalOffset(previousScrollRatio * scrollViewer1.ScrollableWidth);

Neither approach works. If I zoom out with the scrollbar centered, then the scrollbar will go to the far right. Any idea?

A minimal code example can be found here plus the scroll_KeyDown method from above.


Screenshot of the default zoom:

enter image description here

Screenshot after zooming out, incorrectly (the navy blue and pink squares are far off screen):

enter image description here

Screenshot after zooming out, what it should look like:

enter image description here

like image 977
Austin Henley Avatar asked Jun 26 '14 19:06

Austin Henley


1 Answers

Here is a solution to keep the content in center while zooming in or out

    //variables to store the offset values
    double relX;
    double relY;
    void scrollViewer1_ScrollChanged(object sender, ScrollChangedEventArgs e)
    {
        ScrollViewer scroll = sender as ScrollViewer;
        //see if the content size is changed
        if (e.ExtentWidthChange != 0 || e.ExtentHeightChange != 0)
        {
            //calculate and set accordingly
            scroll.ScrollToHorizontalOffset(CalculateOffset(e.ExtentWidth, e.ViewportWidth, scroll.ScrollableWidth, relX));
            scroll.ScrollToVerticalOffset(CalculateOffset(e.ExtentHeight, e.ViewportHeight, scroll.ScrollableHeight, relY));
        }
        else
        {
            //store the relative values if normal scroll
            relX = (e.HorizontalOffset + 0.5 * e.ViewportWidth) / e.ExtentWidth;
            relY = (e.VerticalOffset + 0.5 * e.ViewportHeight) / e.ExtentHeight;
        }
    }

    private static double CalculateOffset(double extent, double viewPort, double scrollWidth, double relBefore)
    {
        //calculate the new offset
        double offset = relBefore * extent - 0.5 * viewPort;
        //see if it is negative because of initial values
        if (offset < 0)
        {
            //center the content
            //this can be set to 0 if center by default is not needed
            offset = 0.5 * scrollWidth;
        }
        return offset;
    }

idea behind is to store the last scroll position and use it to calculate the new offset whenever the content size is changed which will make the change in extent.

just attach the event ScrollChanged of ScrollViewer to this event handler in constructor etc. and leave the rest to it.

eg

    scrollViewer1.ScrollChanged += scrollViewer1_ScrollChanged;

above solution will ensure to keep the grid in center, even for first load

sample of centered content

zoomed in

zoomed in

zoomed out

zoomed out

Extra

I also tried to create an attachable behavior for the same so you do not need to wire the events, just setting up the property will enable or disable the behavior

namespace CSharpWPF
{
    public class AdvancedZooming : DependencyObject
    {
        public static bool GetKeepInCenter(DependencyObject obj)
        {
            return (bool)obj.GetValue(KeepInCenterProperty);
        }

        public static void SetKeepInCenter(DependencyObject obj, bool value)
        {
            obj.SetValue(KeepInCenterProperty, value);
        }

        // Using a DependencyProperty as the backing store for KeepInCenter.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty KeepInCenterProperty =
            DependencyProperty.RegisterAttached("KeepInCenter", typeof(bool), typeof(AdvancedZooming), new PropertyMetadata(false, OnKeepInCenterChanged));

        // Using a DependencyProperty as the backing store for Behavior.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty BehaviorProperty =
            DependencyProperty.RegisterAttached("Behavior", typeof(AdvancedZooming), typeof(AdvancedZooming), new PropertyMetadata(null));

        private static void OnKeepInCenterChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            ScrollViewer scroll = d as ScrollViewer;

            if ((bool)e.NewValue)
            {
                //attach the behavior
                AdvancedZooming behavior = new AdvancedZooming();
                scroll.ScrollChanged += behavior.scroll_ScrollChanged;
                scroll.SetValue(BehaviorProperty, behavior);
            }
            else
            {
                //dettach the behavior
                AdvancedZooming behavior = scroll.GetValue(BehaviorProperty) as AdvancedZooming;
                if (behavior != null)
                    scroll.ScrollChanged -= behavior.scroll_ScrollChanged;
                scroll.SetValue(BehaviorProperty, null);
            }
        }

        //variables to store the offset values
        double relX;
        double relY;
        void scroll_ScrollChanged(object sender, ScrollChangedEventArgs e)
        {
            ScrollViewer scroll = sender as ScrollViewer;
            //see if the content size is changed
            if (e.ExtentWidthChange != 0 || e.ExtentHeightChange != 0)
            {
                //calculate and set accordingly
                scroll.ScrollToHorizontalOffset(CalculateOffset(e.ExtentWidth, e.ViewportWidth, scroll.ScrollableWidth, relX));
                scroll.ScrollToVerticalOffset(CalculateOffset(e.ExtentHeight, e.ViewportHeight, scroll.ScrollableHeight, relY));
            }
            else
            {
                //store the relative values if normal scroll
                relX = (e.HorizontalOffset + 0.5 * e.ViewportWidth) / e.ExtentWidth;
                relY = (e.VerticalOffset + 0.5 * e.ViewportHeight) / e.ExtentHeight;
            }
        }

        private static double CalculateOffset(double extent, double viewPort, double scrollWidth, double relBefore)
        {
            //calculate the new offset
            double offset = relBefore * extent - 0.5 * viewPort;
            //see if it is negative because of initial values
            if (offset < 0)
            {
                //center the content
                //this can be set to 0 if center by default is not needed
                offset = 0.5 * scrollWidth;
            }
            return offset;
        }
    }
}

enabling the behavior

via xaml

<ScrollViewer l:AdvancedZooming.KeepInCenter="True">

or

<Style TargetType="ScrollViewer" x:Key="zoomCenter">
    <Setter Property="l:AdvancedZooming.KeepInCenter"
            Value="True" />
</Style>

or via code like

scrollViewer1.SetValue(AdvancedZooming.KeepInCenterProperty, true);

or

AdvancedZooming.SetKeepInCenter(scrollViewer1, true);

set the property l:AdvancedZooming.KeepInCenter="True" inline, via styles or programmatically in order to enable the behavior on any scrollviewer

l: refers to the namespace to AdvancedZooming class xmlns:l="clr-namespace:CSharpWPF" in this example

like image 194
pushpraj Avatar answered Sep 18 '22 16:09

pushpraj