A client of mine wanted to center a scrollable region (ScrollViewer) in Silverlight whenever it zooms in or out. Currently, whenever you do a zoom, it will zoom in or out based on the upper left hand corner. There isn’t a lot of information regarding this, and many of which doesn’t fit this situation.
Some solutions use ScaleTransform within a RenderTransform which will zoom, however if within a ScrollViewer, it will not show the scroll simply because the view area is constraint. Another solution was to use a Viewbox and apply the zoom there, but I never did get it working.
The one solution that works was to zoom the canvas itself (or in this case I wrapped it around a LayoutTransformer from the Silverlight Toolkit) and then move the ScrollViewer‘s scrollbar itself to the position I wanted. That’s the gist of the solution, but it’ll require some explanation. Let’s get started.
[sourcecode language="xml"]
<Grid x:Name="LayoutRoot">
<ScrollViewer x:Name="Scroller" HorizontalScrollBarVisibility="Visible" VerticalScrollBarVisibility="Visible" VerticalContentAlignment="Center" HorizontalContentAlignment="Center">
<layoutToolkit:LayoutTransformer x:Name="zoomTransformer">
<layoutToolkit:LayoutTransformer.Content>
<!– Some content here –>
</layoutToolkit:LayoutTransformer.Content>
<layoutToolkit:LayoutTransformer.LayoutTransform>
<TransformGroup>
<ScaleTransform x:Name="scaleTransform" ScaleX="1" ScaleY="1"/>
</TransformGroup>
</layoutToolkit:LayoutTransformer.LayoutTransform>
</layoutToolkit:LayoutTransformer>
</ScrollViewer>
</Grid>
[/sourcecode]
The sourcecode above is just a very simple ScrollViewer with a LayoutTransformer in it. I want to get Line 5 to center zoom whenever I zoom in or out. Right now it zooms and stays on the Top Left corner.
First up, I need to get the values of the scrollbar offsets whenever it changes. However, Silverlight 4 doesn’t have ScrollChanged event. You’ll need to simulate this by following this sample code: ScrollViewer Scroll Change Event in Silverlight. In a quick change of the code, you’ll get the following:
[sourcecode language="xml" highlight="34,35"]
<UserControl.Resources>
<!– This is from the sample provided at http://dotplusnet.blogspot.com/2010/05/scrollviewer-scroll-change-event-in.html. It gives us access to an event when the scroll viewer is changed –>
<Style x:Key="svScrollerStyle2" TargetType="ScrollViewer">
<Setter Property="HorizontalContentAlignment" Value="Left"/>
<Setter Property="VerticalContentAlignment" Value="Top"/>
<Setter Property="VerticalScrollBarVisibility" Value="Visible"/>
<Setter Property="Padding" Value="4"/>
<Setter Property="BorderThickness" Value="1"/>
<Setter Property="BorderBrush">
<Setter.Value>
<LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
<GradientStop Color="#FFA3AEB9" Offset="0"/>
<GradientStop Color="#FF8399A9" Offset="0.375"/>
<GradientStop Color="#FF718597" Offset="0.375"/>
<GradientStop Color="#FF617584" Offset="1"/>
</LinearGradientBrush>
</Setter.Value>
</Setter>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ScrollViewer">
<Border BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="2">
<Grid Background="{TemplateBinding Background}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*"/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<ScrollContentPresenter x:Name="ScrollContentPresenter" Cursor="{TemplateBinding Cursor}" Margin="{TemplateBinding Padding}" ContentTemplate="{TemplateBinding ContentTemplate}"/>
<Rectangle Fill="#FFE9EEF4" Grid.Column="1" Grid.Row="1"/>
<ScrollBar x:Name="VerticalScrollBar" Margin="0,-1,-1,-1" Width="18" Visibility="{TemplateBinding ComputedVerticalScrollBarVisibility}" IsTabStop="False" Grid.Column="1" Grid.Row="0" Maximum="{TemplateBinding ScrollableHeight}" Minimum="0" Value="{TemplateBinding VerticalOffset}" Orientation="Vertical" ViewportSize="{TemplateBinding ViewportHeight}" Loaded="VerticalScrollBar_Loaded" />
<ScrollBar x:Name="HorizontalScrollBar" Height="18" Margin="-1,0,-1,-1" Visibility="{TemplateBinding ComputedHorizontalScrollBarVisibility}" IsTabStop="False" Grid.Column="0" Grid.Row="1" Maximum="{TemplateBinding ScrollableWidth}" Minimum="0" Value="{TemplateBinding HorizontalOffset}" Orientation="Horizontal" ViewportSize="{TemplateBinding ViewportWidth}" Loaded="HorizontalScrollBar_Loaded"/>
</Grid>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</UserControl.Resources>
[/sourcecode]
The most important parts are Line 34 and 35, where you set the “Loaded” event of each scrollbar. Within both “Loaded” events, you simulate a ScrollBar ScrollChanged like this:
[sourcecode language="csharp"]
private ScrollBar _horizontalScrollBar;
private ScrollBar _verticalScrollBar;
private double _relScrollX;
private double _relScrollY;
private void HorizontalScrollBar_Loaded(object sender, RoutedEventArgs e)
{
// Register a handler any time a user scrolls horizontally
_horizontalScrollBar = sender as ScrollBar;
_horizontalScrollBar.Scroll += horizontalScrollBar_Scroll;
}
private void VerticalScrollBar_Loaded(object sender, RoutedEventArgs e)
{
// Register a handler any time a user scrolls vertically
_verticalScrollBar = sender as ScrollBar;
_verticalScrollBar.Scroll += verticalScrollBar_Scroll;
}
void horizontalScrollBar_Scroll(object sender, ScrollEventArgs e)
{
SaveScrollerValues();
}
void verticalScrollBar_Scroll(object sender, ScrollEventArgs e)
{
SaveScrollerValues();
}
/// <summary>
/// Save the values of the scroller any time they are changed
/// </summary>
private void SaveScrollerValues()
{
// This concept is modified from the code at http://social.msdn.microsoft.com/Forums/en/wpf/thread/5202aae5-b2cc-4fc3-aa43-4541fcc856fb
if (Scroller.ExtentWidth > 0)
{
_relScrollX = (Scroller.HorizontalOffset + 0.5 * Scroller.ViewportWidth) / Scroller.ExtentWidth;
}
if (Scroller.ExtentHeight > 0)
{
_relScrollY = (Scroller.VerticalOffset + 0.5 * Scroller.ViewportHeight) / Scroller.ExtentHeight;
}
}
[/sourcecode]
The code above is pretty much self-explanatory. I save the relative scroll values and use them when I’m actually scrolling. In order to get the relative scroll values, I need to get current Offset (i.e. current position of the scroll) and add the mid-section of the ViewportWidth then divide that by the ExtentWidth to get the relative scroll position. There’s some math going on, but I’ll add a bit more explanation if I ever get around drawing it out.
Of course, now we need to attach the new style created above with our ScrollViewer.
[sourcecode language="xml" highlight="1"]
<ScrollViewer x:Name="Scroller" HorizontalScrollBarVisibility="Visible" VerticalScrollBarVisibility="Visible" VerticalContentAlignment="Center" HorizontalContentAlignment="Center" Style="{StaticResource svScrollerStyle2}">
<layoutToolkit:LayoutTransformer x:Name="zoomTransformer">
<layoutToolkit:LayoutTransformer.Content>
<!– Some content here –>
</layoutToolkit:LayoutTransformer.Content>
<layoutToolkit:LayoutTransformer.LayoutTransform>
<TransformGroup>
<ScaleTransform x:Name="scaleTransform" ScaleX="1" ScaleY="1"/>
</TransformGroup>
</layoutToolkit:LayoutTransformer.LayoutTransform>
</layoutToolkit:LayoutTransformer>
</ScrollViewer>
[/sourcecode]
Now that we’ve got everything set up, let’s do the zooming! Just attach whatever events you’re attaching it to and start zooming. For me, I attached it to a KeyUp event on the Page itself.
[sourcecode language="csharp" highlight="17,18"]
private void Page_KeyUp(object sender, KeyEventArgs e)
{
if (e.Key == Key.X)
{
// Shrink the viewbox.
scaleTransform.ScaleX -= 0.1;
scaleTransform.ScaleY -= 0.1;
}
if (e.Key == Key.Z)
{
// Expand the viewbox.
scaleTransform.ScaleX += 0.1;
scaleTransform.ScaleY += 0.1;
}
zoomTransformer.ApplyLayoutTransform();
MyContent.UpdateLayout();
Scroller.ScrollToHorizontalOffset(Math.Max(_relScrollX * Scroller.ExtentWidth – 0.5 * Scroller.ViewportWidth, 0) == 0 ? 0.5 * Scroller.ScrollableWidth : _relScrollX * Scroller.ExtentWidth – 0.5 * Scroller.ViewportWidth);
Scroller.ScrollToVerticalOffset(Math.Max(_relScrollY * Scroller.ExtentHeight – 0.5 * Scroller.ViewportHeight, 0) == 0 ? 0.5 * Scroller.ScrollableHeight : _relScrollY * Scroller.ExtentHeight – 0.5 * Scroller.ViewportHeight);
}
[/sourcecode]
Just a quick explanation what’s going on. Line 3 to Line 14 does the scaling. You can change the scaling factor however you want depending on how much you want to scale. After scaling, you need to apply the layout transform in order for the zooming to be affected.
Line 16 is just to refresh my content within the LayoutTransformer.Content. You have to do this to get the reflected values on the ScrollViewer on Line 17 and 18.
Here’s where the magic begins. Line 17 and 18 essentially figures out where your scroll offsets are and sets them. There’s more math magic going on here. However, I had a problem where _relScrollX and _relScrollY wasn’t set at all if I didn’t move the scrollbar. So what I did was to use half of ScrollableWidth and ScrollableHeight (0.5 * Scroller.ScrollableWidth) whenever the values were 0 so it wouldn’t continue to zoom from the Top Left. This fixed the centering issues I had.
That’s it! Now you have a zoomed content with your ScrollViewer still able to scroll to your other “hidden” segments of your content.
P.S. The solution was gotten from the Silverlight Forums LayoutTransformer, ScaleTransform and RenderTransformOrigin which I simplified and explained what’s going on here.
