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:
Screenshot after zooming out, incorrectly (the navy blue and pink squares are far off screen):
Screenshot after zooming out, what it should look like:
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 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
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With