jayars
jayars

Reputation: 1357

WPF Zoom + Scrollbar?

I'm trying to zoom some contents within scrollviewer.

The zoom behavior I'm looking for is that of a RenderTransform+ScaleTransform. But this does not work with the ScrollViewer.

Using LayoutTransform+ScaleTransform, the scrollviewer does get affected (ContentTemplate1 only), but does not behave like a zoom.

Assuming ContentTemplate1/ContentTemplate2 cannot be changed (ie, 3rd party controls), how can I get zoom to work with a scrollviewer?

<Grid>
    <Grid.Resources>
        <!-- Content type 1 -->
        <DataTemplate x:Key="ContentTemplate1">
            <Grid>
                <Grid.ColumnDefinitions>
                    <ColumnDefinition Width="150"/>
                    <ColumnDefinition />
                </Grid.ColumnDefinitions>
                <TextBlock Background="DodgerBlue" Text="Left"/>
                <TextBlock Grid.Column="1" Background="DarkGray" Text="Right"/>
            </Grid>
        </DataTemplate>

        <!-- Content type 2 -->
        <DataTemplate x:Key="ContentTemplate2">
            <Viewbox>
                <TextBlock Background="DodgerBlue" Text="Scale to fit" Width="100" Height="70" Foreground="White" TextAlignment="Center"/>
            </Viewbox>
        </DataTemplate>
    </Grid.Resources>
    <Grid.RowDefinitions>
        <RowDefinition />
        <RowDefinition Height="Auto" />
    </Grid.RowDefinitions>

    <TabControl>
        <!-- Content 1 -->
        <TabControl.Resources>
            <ScaleTransform x:Key="ScaleTransform"
                            ScaleX="{Binding ElementName=ZoomSlider,Path=Value}"
                            ScaleY="{Binding ElementName=ZoomSlider,Path=Value}" />
        </TabControl.Resources>
        <TabItem Header="Content 1">
            <ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">
                <ContentControl ContentTemplate="{StaticResource ContentTemplate1}" Margin="10" RenderTransformOrigin=".5,.5">
                    <!-- Affects scrollviewer, but does not behave like a zoom -->
                    <!--<FrameworkElement.LayoutTransform>
                        <StaticResource ResourceKey="ScaleTransform" />
                    </FrameworkElement.LayoutTransform>-->

                    <!-- Expected zoom behavior, but doesn't affect scrollviewer -->
                    <FrameworkElement.RenderTransform>
                        <StaticResource ResourceKey="ScaleTransform" />
                    </FrameworkElement.RenderTransform>
                </ContentControl>
            </ScrollViewer>
        </TabItem>
        <!-- Content 2 -->
        <TabItem Header="Content 2">
            <ContentControl ContentTemplate="{StaticResource ContentTemplate2}" Margin="10" RenderTransformOrigin=".5,.5">
                <!-- Affects scrollviewer, but does not behave like a zoom -->
                <!--<FrameworkElement.LayoutTransform>
                        <StaticResource ResourceKey="ScaleTransform" />
                    </FrameworkElement.LayoutTransform>-->

                <!-- Expected zoom behavior, but doesn't affect scrollviewer -->
                <FrameworkElement.RenderTransform>
                    <StaticResource ResourceKey="ScaleTransform" />
                </FrameworkElement.RenderTransform>
            </ContentControl>

        </TabItem>
    </TabControl>

    <StackPanel Grid.Row="1" Orientation="Horizontal">
        <!-- Zoom -->
        <Slider x:Name="ZoomSlider"
                Width="100"
                Maximum="5"
                Minimum="0.1"
                Orientation="Horizontal"
                Value="1" />

        <!-- Autofit -->
        <CheckBox Content="Autofit?" x:Name="AutoFitCheckBox" />
    </StackPanel>
</Grid>

Upvotes: 2

Views: 5071

Answers (3)

Sphinxxx
Sphinxxx

Reputation: 13057

To make the zoomed elements get the exact RenderTransform look, we may as well stick with RenderTransform, and instead tell the ScrollViewer how to behave by implementing our own scrolling logic. This approach is based on this excellent tutorial:

https://web.archive.org/web/20140809230047/http://tech.pro/tutorial/907/wpf-tutorial-implementing-iscrollinfo

We create our own custom "ZoomableContentControl" which implements IScrollInfo and tell the ScrollViewer to get its scrolling logic from there (ScrollViewer.CanContentScroll = True). The magic happens in ArrangeOverride() where we play with ExtentWidth/ExtentHeight and RenderTransformOrigin.

public class ZoomableContentControl : ContentControl, IScrollInfo
{
    public ZoomableContentControl()
    {
        this.RenderTransformOrigin = new Point(0.5, 0.5);
    }

    private ScaleTransform _scale = null;
    private ScaleTransform Scale
    {
        get
        {
            if (_scale == null)
            {
                _scale = this.RenderTransform as ScaleTransform;

                //RenderTransforms don't update the layout, so we need to trigger that ourselves:
                _scale.Changed += (s, e) => { InvalidateArrange(); };
            }
            return _scale;
        }
    }
    protected override Size ArrangeOverride(Size arrangeBounds)
    {
        Statics.MessageIfDebug("Arranging");
        var layout = base.ArrangeOverride(arrangeBounds);

        var scale = this.Scale;
        if (scale != null)
        {
            //Because RenderTransforms don't update the layout,
            //we need to pretend we're bigger than we are to make room for our zoomed content:
            _extent = new Size(layout.Width * scale.ScaleX, layout.Height * scale.ScaleY);
            _viewport = layout;

            //Coerce offsets..
            var maxOffset = new Vector(ExtentWidth - ViewportWidth, ExtentHeight - ViewportHeight);
            _offset.X = Math.Max(0, Math.Min(_offset.X, maxOffset.X));
            _offset.Y = Math.Max(0, Math.Min(_offset.Y, maxOffset.Y));

            //..and move the zoomed content within the ScrollViewer:
            var renderOffsetX = (maxOffset.X > 0) ? (_offset.X / maxOffset.X) : 0.5;
            var renderOffsetY = (maxOffset.Y > 0) ? (_offset.Y / maxOffset.Y) : 0.5;
            this.RenderTransformOrigin = new Point(renderOffsetX, renderOffsetY);

            if (ScrollOwner != null)
            {
                ScrollOwner.InvalidateScrollInfo();
            }
        }

        return layout;
    }


    #region IScrollInfo

    //This is the boilerplate IScrollInfo implementation, 
    //which can be found in *the first half* of this tutorial:
    //https://web.archive.org/web/20140809230047/http://tech.pro/tutorial/907/wpf-tutorial-implementing-iscrollinfo
    //(down to and including SetHorizontalOffset()/SetVerticalOffset()).
    //Note the bug reported by "Martin" in the comments.

    ...

Usage:

<TabItem Header="Content 1">
    <ScrollViewer CanContentScroll="True"
                  HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">
            <v:ZoomableContentControl ContentTemplate="{StaticResource ContentTemplate1}" Margin="10" >
            <FrameworkElement.RenderTransform>
                <StaticResource ResourceKey="ScaleTransform" />
            </FrameworkElement.RenderTransform>
        </v:ZoomableContentControl>
    </ScrollViewer>
</TabItem>
<TabItem Header="Content 2">
    <ScrollViewer CanContentScroll="True"
                  HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">
        <v:ZoomableContentControl ContentTemplate="{StaticResource ContentTemplate2}" Margin="10" >
            <FrameworkElement.RenderTransform>
                <StaticResource ResourceKey="ScaleTransform" />
            </FrameworkElement.RenderTransform>
        </v:ZoomableContentControl>
    </ScrollViewer>
</TabItem>

Upvotes: 0

Andrej Benedik
Andrej Benedik

Reputation: 119

My first advice is to check what kind of zooming functionality you can with using a commercial third party zoom control that already have support for ScrollViewer and also has many additional zooming and panning features.

Now to the solution of your problem:

You can make your code work with using LayoutTransform, but you will need to set the size of the ScrollViewer's content to a fixed value.

Currently you have a Grid inside a ScrollViewer. The Grid does not have its size defined, so it takes all the space it can get. So if you now scale the Grid, for example by factor 2, this means that the content of the Grid is scaled by factor 2, but the Grid will still take all the space it can get. If you would specify the width of the Grid to be 500, then scaling it by 2, would make the Grid's width 1000. But if you say: Grid, you can take all space that the parent gives you and then scale the Grid, it will be still the same. This means that scaling the auto-sized content of ScrollViewer will not show scrollbars.

In your sample this is true until the content of the Grid (the first column with width = 150 + width of the "Right" text in the second column) exceed the available size - at that point the DesiredSize of the Grid would be bigger than the size that ScrollViewer can provide and ScrollViewer will show scrollbars.

For example:

1) let's say that when you start your application, scale is set to 1 and ScrollViewer provides 500 points horizontally for the Grid. The Grid shows first column with 150 points width and shows "Right" text without any scale. The second column is set to fill the remaining space - so: 500 - 150 = 350 points is used by second column.

2) Now user sets the scale to 2. The Grid scales the first column to 300 points. This means that the second column can now take only 200 points. The Grid also scales the "Right" text, but the content (300 of first column + width of text) still does not exceed 500 point that are provided by the ScrollViewer.

3) User now sets scale to 3. Now the total width of the content of the grid exceed 500 points and this means that ScrollViewer will show scroll bars.

So having autosized controls, ScrollViewer and scaling does not work well.

But if you would fix the size of the Grid to 500, than you would get much more predictable results when scaling and using ScrollViewer. For example if you would scale by 10%, than the Grid's size would be 550 and would already exceed the size of the ScrollViewer - so ScrollViewer would show scrollbars. This would also give you the expected behaviour when you would increase the size of the window - the size of the Grid would remain the same and at some point the scrollbars would disappear (when the window would be big enough to show the whole content of the scaled Grid).

To conclude: my advice to you is to set the fixed size to the content of ScrollViewer controls. If you have a fixed size window, that you can set the Width and Height based on that size. Otherwise you can set it dynamically when the control is first loaded:

You can change the XAML for the content control into:

<ContentControl Name="ContentControl1" 
                ContentTemplate="{StaticResource ContentTemplate1}" 
                Margin="10" 
                Loaded="ContentControl1_OnLoaded" >

And also add the ContentControl1_OnLoaded handled that would just set the size to the initial size:

private void ContentControl1_OnLoaded(object sender, RoutedEventArgs e)
{
    ContentControl1.Width = ContentControl1.ActualWidth;
    ContentControl1.Height = ContentControl1.ActualHeight;
}

Zooming and panning may seem like a very simple task to do. But my experience shows (I am the author of ZoomPanel control) that this task can quickly become very complicated.

Upvotes: 0

Sphinxxx
Sphinxxx

Reputation: 13057

If I understand correctly:

  • You want to zoom with the ZoomSlider slider?
  • You want scrollbars to appear if the content is too large to fit within its tab?

If so, it's LayoutTransform you want. That transformation is done before all elements are measured and laid out, and the ScrollViewer will be able to tell whether scrollbars are needed.

On my machine, the "Content 1" tab works as expected if you just switch to LayoutTransform (note that you have to zoom a lot before "Right" disappears off-screen, toggling the scrollbar):

Content 1, un-zoomed Content 1, zoomed

"Content 2" requires a little more work. First of all, there's no ScrollViewer in that tab, so that needs to be added. Secondly, ContentTemplate2 uses a ViewBox, which stretches by default, so zooming won't have an effect until you zoom in really close. To disable the ViewBox' built-in "zooming", you can center the ContentControl container (using HorizontalAlignment/VerticalAlignment), which forces it to take up as little space as possible:

<TabItem Header="Content 2">
    <ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">
        <ContentControl ContentTemplate="{StaticResource ContentTemplate2}" ...
                        HorizontalAlignment="Center" VerticalAlignment="Center" >
            <FrameworkElement.LayoutTransform>
                ...

Content 2, un-zoomed Content 2, zoomed

Upvotes: 2

Related Questions