Click here to Skip to main content
Click here to Skip to main content

A WPF Custom Control for Zooming and Panning

By , 21 Mar 2011
 

Sample Code

Introduction

This article examines the use and implementation of a reusable WPF custom control that is used to zoom and pan its content. The article and the sample code show how to use the control from XAML and from C# code.

The main class, ZoomAndPanControl, is derived from the WPF ContentControl class. This means that the primary purpose of the control is to display content. In XAML, content controls are wrapped around other UI elements. For example, the content might be an image, a map or a chart. In this article, I use a Canvas as the content. This Canvas contains some colored rectangles that can be dragged about by the user.

I'll present this article in two parts. The first part shows how to use ZoomAndPanControl and has walkthrough of the three sample projects. This should be enough if all you want to do is use the code or just try it out. The second part of the article goes into detail as to how the control is implemented. This part will be useful if you want to make your own modifications to ZoomAndPanControl or generally to help you understand how to develop a non-trivial custom control.

Screenshot

This screenshot shows the data-binding sample.

The large window with scrollbars is the viewport onto the content. The toolbar contains some buttons and a slider for changing the zoom level. It also shows the current zoom level as a percentage. The content, as already mentioned, is a Canvas with colored rectangles.

The small overview window in the bottom left corner shows an overview of the entire content. The transparent yellow rectangle shows the portion of the content that is currently visible in the viewport.

Assumed Knowledge

It is assumed that you already know C# and have a basic knowledge of using WPF and XAML.

Background

In my previous article, I alluded that I have been working on a flow-charting control. The working area in which the flow-chart is displayed and edited can be much larger than the window that contains it. Usually, this is the perfect place to use a ScrollViewer to wrap the content. ScrollViewer is a pretty easy class to use. It handles content larger than itself by providing a viewport onto the content. The viewport is optionally bounded by scrollbars that allow the user to view any portion of that content.

However I wanted the user to be able to zoom in and see more detail or zoom out to see an overview. Implementing the zooming by scaling the content is fairly easily done using WPF 2D transformations. Although making it play nicely with the ScrollViewer is another matter entirely!

Writing this kind of custom control is harder than you might think. My first implementation was a bit of a disaster (well not completely - it did inspire me to rewrite the code and then write this article). The code for zooming and panning was intertangled with the code for displaying and editing the flow-chart. It probably didn't help that I was also learning WPF at the time. There are one or two examples around on the internet that show how to do this kind of thing, however I found that they were either lacking in that they didn't do entirely what I wanted or that they came with baggage in the form of extra code that I just didn't need. Suffice to say that before I wrote the code for this article, what I had was a complicated mess that kept breaking and was generally difficult to modify.

ZoomAndPanControl: What it is, What it is not

My main aim with this article is to keep the complexity of the code to a minimum. To this end ZoomAndPanControl doesn't attempt to do anything much more than what I need of it. Notably I have not attempted to implement any kind of UI virtualisation.

In addition, there is no input handling logic in the reusable control. I think that this kind of code is application specific and likely to change. Implementing it generically would add complication and so I have delegated input handling to the application code. In the sample code, the input handling code can be found in the MainWindow class (which is derived from Window).

I have found that moving the zoom and pan logic to a custom control has allowed me to cleanly separate out this code from the rest of the application. As a result, both sets of code are simpler, cleaner and more understandable.

Part 1 - Sample Project Walkthroughs

I have included three sample projects to demonstrate the use of ZoomAndPanControl. Each of the samples have basically the same content: a small collection of colored rectangle that can be dragged around by the user.

  • SimpleZoomAndPanSample.zip demonstrates the simplest possible usage of ZoomAndPanControl. This project shows how to implement left-mouse-drag panning, simple mouse-wheel zooming and plus/minus keys for zooming in and out.

  • AdvancedZoomAndPanSample.zip adds more advanced features to the simple sample. This project demonstrates the use of animated zooming to zoom to a rectangle that the user has dragged out. It has Google-maps style mouse-wheel zooming and the backspace key allows the user to jump back to the previous zoom level. It also shows how to use other UI controls (a label, buttons and a slider) to control zooming functionality.

  • DataBindingZoomAndPanSample.zip is more advanced again. This project demonstrates the use of a simple data-model and data-binding in order to share the data (the colored rectangles) between the main window and an overview window. The overview window shows a view of the content in its entirety, Photoshop style, and has a transparent yellow rectangle that shows the extent of the content that is displayed in the viewport.

  • InfiniteWorkspaceZoomAndPanSample.zip is a new sample project that has been added to this article after it was originally written. It shows how the concept of an infinite workspace can be built using the ZoomAndPanControl. As this is a new addition, I don't mention it again in the article but I will describe it briefly right here. The content canvas is initially set to size 0,0 and is automatically expanded to contain the initial content. As the user drags the colored rectangles, the canvas is automatically expanded or contracted to contain the modified content. The overview window has been changed in this sample so that it adjusts its zoom level as the canvas is expanded/contracted so that the canvas always fills the viewport. In this sample, the scrollbars have been removed and left-mouse-drag-panning and the overview window are the only ways to navigate the content.

The Basics

ZoomAndPanControl is used from XAML in basically the same way as a regular ContentControl. It wraps up the content that it is to display. The content viewport shows a portion of that content. The content can be zoomed, that is to say scaled larger or smaller than the viewport. The user is able to move the viewport by panning with the mouse or by using the scrollbars.

Here is a quick look at the main classes and their relationships (thanks to StarUML). Also shown are the main ZoomAndPanControl dependency properties and examples of some of the methods. The solid lines represents inheritance. The dashed line represents a dependency.

The next diagram attempts to illustrate how the content viewport maps to the scaled content (please excuse my amateurish Photoshop skills) :

The previous diagram showed the relationships between the various coordinate systems that I will refer to in this article. Coordinates that are relative to the content, which for this article is a Canvas, are called 'content coordinates'. Coordinates that are relative to the viewport are called 'viewport coordinates'. It might be helpful for you to think of 'viewport coordinates' as 'screen coordinates'. This probably aids in understanding but it isn't really the case because WPF is resolution independent.

To go from content coordinates to viewport coordinates an XY point is transformed by the 'content offset' and then the 'content scale' as indicated by the following diagram:

Like any WPF control, ZoomAndPanControl has dependency properties that are used to set and retrieve the control's values at runtime.

The three most important properties are:

  • ContentScale - This specifies the zoom level, or more precisely the scale of the content being viewed. When set to the default value of 1.0, the content is being viewed at 100%. I refer to this property as 'content scale'.
  • ContentOffsetX and ContentOffsetY - These values constitute the XY offset of the content viewport. These values are specified in content coordinates. I refer to these two properties collectively as 'content offset'.

Simple Sample Walkthrough

To follow along with the walkthrough, you should load the simple sample in Visual Studio (I use VS 2008) and then build and run the application.

First, let's try out the input controls that are used to interact with ZoomAndPanControl. Simply pressing the plus and minus keys zooms in and out on the content. Holding down shift and left- or right-clicking, or using the mouse-wheel also zooms in and out on the content. Clicking with the left-mouse button in open space and dragging pans the content viewport. Left-dragging can also be used to move the colored rectangles.

Now let's look at the usage ZoomAndPanControl in MainWindow.xaml. The first thing that needs to be done is to reference the namespace and assembly that contains ZoomAndPanControl:

<Window x:Class="ZoomAndPanSample.MainWindow"
    ...
    xmlns:ZoomAndPan="clr-namespace:ZoomAndPan;assembly=ZoomAndPan"
    ...
    >

    ... main window content ...

</Window>

The next snippet shows the most basic definition of ZoomAndPanControl in XAML:

<ZoomAndPan:ZoomAndPanControl
    x:Name="zoomAndPanControl"
    ...
    >

    ... content to be zoomed and panned is defined here ...

</ZoomAndPan:ZoomAndPanControl>

The content to be displayed is embedded in the XAML within the ZoomAndPanControl. For this article, a Canvas is used as the content:

<ZoomAndPan:ZoomAndPanControl
    x:Name="zoomAndPanControl"
    ...
    >
    <Canvas
        x:Name="content"
        ...
        >

        ... contents of the canvas defined here ...

    </Canvas>
</ZoomAndPan:ZoomAndPanControl>

If you already know WPF, then you will know that assigning the name "content" to the Canvas in the previous snippet causes a MainWindow member variable to be generated that references the instance of the Canvas. Likewise a "zoomAndPanControl" member variable is also generated. Shortly, we will be using these generated member variables in C# code.

Adding a Width and Height to the Canvas sets the size of the content (in content coordinates):

<ZoomAndPan:ZoomAndPanControl
    x:Name="zoomAndPanControl"
    >
    <Canvas
        x:Name="content"
        Width="2000"
        Height="2000"
        ...
        >

        ... contents of the canvas defined here ...

    </Canvas>
</ZoomAndPan:ZoomAndPanControl>

Note that a value for ContentScale was not explicitly specified in the previous snippets. The default value for ContentScale is 1.0 which means that content coordinates and viewport coordinates are at the same scale. For example, with ContentScale at 1.0, the size of our content is 2000 by 2000 in both content and viewport coordinates. However if ContentScale were set to 0.5, then the size in viewport coordinates would be scaled to half the size (50%) or 1000 by 1000. Likewise if it were set to 2.0, then the size in viewport coordinates would be double the size (200%) or 4000 by 4000.

By wrapping the ZoomAndPanControl in a ScrollViewer, we get scrollbars for free:

<ScrollViewer
    ...
    CanContentScroll="True"
    VerticalScrollBarVisibility="Visible"
    HorizontalScrollBarVisibility="Visible"
    >
    <ZoomAndPan:ZoomAndPanControl
        x:Name="zoomAndPanControl"
        >

            ... content ...

    </ZoomAndPan:ZoomAndPanControl>
</ScrollViewer>

The ZoomAndPanControl class implements the IScrollInfo interface. This interface allows the control to have an intimate relationship with the ScrollViewer. Note that CanContentScroll is set to 'True'. This is required to instruct the ScrollViewer to communicate with the ZoomAndPanControl, via IScrollInfo, to determine the horizontal and vertical scrollbar offsets and the extent of its content. I'll talk more about IScrollInfo in part 2.

As mentioned previously, ZoomAndPanControl itself doesn't handle any user input. The implementation of user input is delegated to MainWindow. Event handlers are defined for the ZoomAndPanControl for all the common mouse operations:

<ZoomAndPan:ZoomAndPanControl
    x:Name="zoomAndPanControl"
    Background="LightGray"
    MouseDown="zoomAndPanControl_MouseDown"
    MouseUp="zoomAndPanControl_MouseUp"
    MouseMove="zoomAndPanControl_MouseMove"
    MouseWheel="zoomAndPanControl_MouseWheel"
    >
    <Canvas
        x:Name="content"
        Width="2000"
        Height="2000
        Background="White"
        >

        ... contents of the canvas defined here ...

    </Canvas>
</ZoomAndPan:ZoomAndPanControl>

Note that Background has been set for both the ZoomAndPanControl and the Canvas. Primarily, this is because Background must be set in order to receive mouse events on the ZoomAndPanControl. When Background is left unset, hit testing fails and the mouse events are not raised. Background is also set to highlight the difference between the content, which is white, and the background behind the content, which is light gray. You'll see what I mean if you zoom out from the content.

The mouse event handlers in MainWindow.xaml.cs perform zooming and panning by directly setting properties of ZoomAndPanControl. This next snippet illustrates how left-mouse-button dragging updates the content offset:

private void zoomAndPanControl_MouseMove(object sender, MouseEventArgs e) 
{ 
    if (mouseHandlingMode == MouseHandlingMode.Panning) 
    { 
        Point curContentMousePoint = e.GetPosition(content); 
        Vector dragOffset = curContentMousePoint - origContentMousePoint; 
        zoomAndPanControl.ContentOffsetX -= dragOffset.X; 
        zoomAndPanControl.ContentOffsetY -= dragOffset.Y; 
        e.Handled = true; 
    } // ... other mouse input handling ... 
} 

The distance the mouse has been dragged is calculated and assigned to dragOffset. This value is then used to calculate the new content offset. Note that dragOffset is calculated in content coordinates because we are working with points that are relative to the content. If you are paying attention, you might wonder how the code in the previous snippet can actually work? Surely if origContentMousePoint is initialised in zoomAndPanControl_MouseDown then as the mouse is dragged further and further from the original point dragOffset will get larger and larger making the panning get faster and faster. At first glance, this might appear to be the case, but you have to consider that the content itself is being moved (well, internally it is actually being translated by the WPF 2D transformation system). As part of the panning, the content moves along with the mouse cursor and therefore the point in the content that the mouse is hovering over is never far from the original point. This happens because as the cursor is moved far enough to warrant a call to zoomAndPanControl_MouseMove, the content is moved to bring the original point back under the current position of the mouse cursor.

Zooming is accomplished by updating the ContentScale property. In MainWindow.xaml.cs, the methods ZoomOut and ZoomIn are called in response to various input events (plus & minus keys, mouse-wheel and shift-left/right clicks). These methods simply increase or decrease ContentScale by a small amount. For instance, ZoomOut looks like this:

private void ZoomOut()
{
    zoomAndPanControl.ContentScale -= 0.1;
}

In these code samples, the mouse wheel is only used for zooming in and out. The event handler that does the work is zoomAndPanControl_MouseWheel. There is an alternative method of handling mouse wheel input - and that is to use it to pan the viewport in the same way as a standard ScrollViewer. To have mouse wheel input work this way, set the IsMouseWheelScrollingEnabled property of ZoomAndPanControl to true. Additionally, you should not handle the MouseWheel event for ZoomAndPanControl, that is to say you won't need to have the zoomAndPanControl_MouseWheel that exists in the simple sample.

The simple sample only has limited features. It restricts itself to manipulating content offset and content scale directly. As these are dependency properties the WPF animation system can be used to animate them. However, for convenience, ZoomAndPanControl provides a number of methods that perform animated zoom and pan operations. We will look at these methods over the next few sections.

Advanced Sample Walkthrough

For this section, you should load the advanced sample in Visual Studio and build and run the application.

The advanced sample has the same features that we looked at in the simple sample. In addition, it uses the convenient ZoomAndPanControl methods to perform animated zooming and panning.

To summarize, the new features are:

  • A label that shows the current zoom level as a percentage
  • A slider to select the current zoom level
  • A toolbar with buttons for various zoom operations
  • The backspace key jumps back to the previous zoom level
  • Double-clicking centers on the clicked location
  • Drag zooming; and
  • Google-maps style mouse wheel zooming

I'll first say a quick few words about the simpler features like the slider and the buttons before moving on to the more complex features: drag-zooming and the google-maps style zooming.

The label in the toolbar shows the current zoom level as a percentage. Looking in MainWindow.xaml, you will see that the ScaleToPercentage convertor is used to convert the scale value in ContentScale to the percentage value that is displayed in the label.

The Slider in the toolbar is used to change the zoom level and it also uses the ScaleToPercentage convertor. The Value of the slider is data-bound to ContentScale:

<Slider
    ...
    Value="{Binding ElementName=zoomAndPanControl, Path=ContentScale,
	Converter={StaticResource scaleToPercentConverter}}"
    />

I can skip the zoom-in/out buttons in the toolbar - they simply call the ZoomIn and ZoomOut methods already discussed in the simple sample walkthrough.

The Fill and 100% buttons show the first example of animated zooming. For example, Fill_Executed is the method that is called when the user clicks the Fill button. It calls AnimatedScaleToFit:

private void Fill_Executed(object sender, ExecutedRoutedEventArgs e)
{
    SavePrevZoomRect();

    zoomAndPanControl.AnimatedScaleToFit();
}

AnimatedScaleToFit starts an animation that zooms in or out so that the entire content fits completely within the viewport.

Note that Fill_Executed also calls SavePrevZoomRect. This method saves the current viewport rectangle and the content scale:

private void SavePrevZoomRect()
{
    prevZoomRect = new Rect(zoomAndPanControl.ContentOffsetX,
	zoomAndPanControl.ContentOffsetY, zoomAndPanControl.ContentViewportWidth,
	zoomAndPanControl.ContentViewportHeight);
    prevZoomScale = zoomAndPanControl.ContentScale;
    prevZoomRectSet = true;
}

When the user presses the backspace key, JumpBackToPrevZoom is called which jumps back to the previous zoom level by calling AnimatedZoomTo. The previously saved viewport rectangle and content scale are passed as arguments:

private void JumpBackToPrevZoom()
{
    zoomAndPanControl.AnimatedZoomTo(prevZoomScale, prevZoomRect);

    ClearPrevZoomRect();
}

Double-clicking in the content causes the clicked location to be centered in the viewport. This is another feature that makes use of the animation methods, in this case by calling AnimatedSnapTo.

Now that I have discussed the functionality behind some of the simpler features, I'll move on to the most interesting features, which are drag-zooming and google-maps style zooming.

The drag-zooming feature allows the user to hold down the shift key and left-drag out a rectangle. ZoomAndPanControl then zooms in so that the rectangle fills the entire viewport. The visual for the rectangle is a Border that is embedded within its own Canvas within the content. By default, this Border is hidden:

<Canvas
    x:Name="dragZoomCanvas"
    Visibility="Collapsed"
    >
    <Border
        x:Name="dragZoomBorder"
        BorderBrush="Black"
        BorderThickness="1"
        Background="Silver"
        CornerRadius="1"
        Opacity="0"
        />
</Canvas>

When the user starts dragging out the rectangle, the Border is made visible:

public void InitDragZoomRect(Point pt1, Point pt2)
{
    SetDragZoomRect(pt1, pt2);

    dragZoomCanvas.Visibility = Visibility.Visible;
    dragZoomBorder.Opacity = 0.5;
}

The call to SetDragZoomRect sets the position and the size of the Border based on the parameters pt1 and pt2. SetDragZoomRect is called repeatedly whilst the user continues to drag out the rectangle:

private void zoomAndPanControl_MouseMove(object sender, MouseEventArgs e)
{
    ... handle other mouse handling modes ...

    else if (mouseHandlingMode == MouseHandlingMode.DragZooming)
    {
        Point curContentMousePoint = e.GetPosition(content);
            SetDragZoomRect(origContentMouseDownPoint, curContentMousePoint);
        e.Handled = true;
        }
    }

If you look at the code for SetDragZoomRect, you will see that it has responsibility for reversing pt1 and pt2 if the user starts dragging out the rectangle left or up rather than right or down.

When the user has finished dragging out the rectangle, AnimatedZoomTo is called:

private void ApplyDragZoomRect()
{
    SavePrevZoomRect();

    double contentX = Canvas.GetLeft(dragZoomBorder);
    double contentY = Canvas.GetTop(dragZoomBorder);
    double contentWidth = dragZoomBorder.Width;
    double contentHeight = dragZoomBorder.Height;
    zoomAndPanControl.AnimatedZoomTo(new Rect
		(contentX, contentY, contentWidth, contentHeight));

    FadeOutDragZoomRect();
}

AnimatedZoomTo performs an animated zoom so that the dragged out rectangle fills the viewport. Also note the call to FadeOutDragZoomRect. This starts an animation that fades out the Border and returns it to its default hidden state.

The other advanced feature to mention is the google-maps style zooming. This is implemented by the method ZoomAboutPoint. This method zooms in or out while keeping the 'zoom focus' locked to the same point in the viewport:

private void ZoomOut(Point contentZoomCenter)
{
    zoomAndPanControl.ZoomAboutPoint
	(zoomAndPanControl.ContentScale - 0.1, contentZoomCenter);
}

This same method is called in response to shift left- or right-click and scrolling the mouse-wheel. In either case, the contentZoomCenter parameter that is passed is set to the position under the mouse cursor. Locking the zoom focus means that we can zoom in and out and the point that is under the mouse cursor remains under the mouse cursor as we zoom.

We are now finished looking at the advanced sample. I have covered a number of the animated zoom methods, for a full list see the section ZoomAndPanControl Methods. Now let's move onto the data binding sample.

Data Binding Sample Walkthrough

The purpose of this project is to show how a data-model and data-binding can be used to share content between the main window and an overview window. The main window is the same as it was in the advanced sample. It shows a view of the content that we can zoom and pan. The overview window is new in this project and shows all of the content in its entirety. It displays a transparent yellow rectangle that shows the position and size of the main window's viewport onto the content.

The simple and advanced projects both use a simple Canvas as the container for our content. The content, the colored rectangles, was embedded statically within the XAML. Now that we are using a data-model to share content between views, we need to replace the Canvas with a control that supports data-binding. I chose to use a ListBox partially because of its good data-binding support, but also because I wanted to demonstrate how its selection logic could be reused for content that can also be zoomed and panned. For example, if you left-click one of the colored rectangles, it will be selected and a blue border is displayed. A Canvas is still used however, but it is now embedded with in the ListBox. The ListBox is bound to the data-source and it fills the Canvas with UI elements that are generated from data-templates.

Load the data-binding sample in Visual Studio. The data-model can be found in DataModel.cs. Purely for convenience, DataModel is a singleton class. It has a Rectangles property which is a list of RectangleData objects. This property is the data-source that will populate the list boxes in both the main window and the overview window.

Let's look at the code for the overview window. Open OverviewWindow.xaml and you will see it contains a ZoomAndPanControl. Note that the SizeChanged event is handled:

<ZoomAndPan:ZoomAndPanControl
    x:Name="overview"
    SizeChanged="overview_SizeChanged"
    >

    ... overview content ...

</ZoomAndPan:ZoomAndPanControl>

The implementation of overview_SizeChanged in OverviewWindow.xaml.cs calls ScaleToFit on the ZoomAndPanControl. Whenever the user resizes the overview window the content is rescaled so that it fits in its entirety:

private void overview_SizeChanged(object sender, SizeChangedEventArgs e)
{
    overview.ScaleToFit();
}

Next have a look at MainWindow.xaml and how a ListBox is used as the content. The ListBox's ItemsSource property is data-bound to the Rectangles property of the data-model:

<ListBox
    x:Name="content"
    ...
    ItemsSource="{Binding Source={x:Static local:DataModel.Instance},
	Path=Rectangles}"
    ...
    />

The ListBox is restyled to provide a new visual template:

<ListBox
    x:Name="content"
        ...
    ItemsSource="{Binding Source={x:Static local:DataModel.Instance},
	Path=Rectangles}"
    Style="{StaticResource noScrollViewerListBoxStyle}"
    ...
    />

The replacement visual template that is specified by noScrollViewerListBoxStyle is one that has no embedded ScrollViewer:

<Style x:Key="noScrollViewerListBoxStyle" TargetType="ListBox">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="ListBox">
                <Canvas
                    Background="{TemplateBinding Background}"
                    IsItemsHost="True"
                    />
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

The ScrollViewer in the default visual template is redundant because we already have a ScrollViewer wrapped around the ZoomAndPanControl in MainWindow.xaml. Note that the replacement visual template is where the Canvas is defined as the ListBox's panel, this is why IsItemsHost is set to True.

A style is also specified for each ListBoxItem by setting ItemContainerStyle.

<ListBox
    x:Name="content"
        ...
    ItemsSource="{Binding Source={x:Static local:DataModel.Instance},
	Path=Rectangles}"
    Style="{StaticResource noScrollViewerListBoxStyle}"
    ItemContainerStyle="{StaticResource listBoxItemStyle}"
    />

listBoxItemStyle contains data-bindings that are used to position each list box item within the Canvas:

<Style
    x:Key="listBoxItemStyle"
    TargetType="ListBoxItem"
    >
    <Setter
        Property="Canvas.Left"
        Value="{Binding X}"
        />
    <Setter
        Property="Canvas.Top"
        Value="{Binding Y}"
        />

    ...

</Style> 

The style also defines a Border that is used to show when the item is selected. Normally, the Border is transparent and thus invisible. However a trigger changes the Border to blue when IsSelected is set to true:

<Style
    x:Key="listBoxItemStyle"
    TargetType="ListBoxItem"
    >

    ...

    <Setter
        Property="IsSelected"
        Value="{Binding IsSelected}"
        />

    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="ListBoxItem">
                <Border
                    Name="Border"
                    BorderThickness="1"
                    Padding="2"
                    >
                    <ContentPresenter />
                </Border>
                <ControlTemplate.Triggers>
                    <!--
                    When the ListBoxItem is selected draw a
				simple blue border around it.
                    -->
                    <Trigger Property="IsSelected" Value="true">
                        <Setter
                            TargetName="Border"
                            Property="BorderBrush"
                            Value="Blue"
                            />
                    </Trigger>
              </ControlTemplate.Triggers>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

Now we will examine the implementation of the transparent yellow rectangle in the overview window that shows the position and extent of the content viewport. Looking at OverviewWindow.xaml again, we can see that it is a Thumb that has its visual template set to a transparent yellow Border:

<Canvas>
    <Thumb
        x:Name="overviewZoomRectThumb"
        ...
        Opacity="0.5"
        >
        <Thumb.Template>
            <ControlTemplate TargetType="{x:Type Thumb}">
                <Border
                    ...
                    Background="Yellow"
                    />
            </ControlTemplate>
        </Thumb.Template>
    </Thumb>
</Canvas>                           

A Thumb is used because it conveniently provides the DragDelta event. DragDelta allows us respond to the user dragging the Thumb:

<Canvas>
    <Thumb
        x:Name="overviewZoomRectThumb"
            ...
        DragDelta="overviewZoomRectThumb_DragDelta"
        Opacity="0.5"
        >
        <Thumb.Template>
            <ControlTemplate TargetType="{x:Type Thumb}">
                <Border
                        ...
                    Background="Yellow"
                    />
            </ControlTemplate>
        </Thumb.Template>
    </Thumb>
</Canvas>

The event handler pans the viewport when the user drags the yellow rectangle. It achieves this by simply updating the Canvas position of the Thumb:

private void overviewZoomRectThumb_DragDelta
		(object sender, DragDeltaEventArgs e)
{
    double newContentOffsetX = Math.Min(Math.Max(0.0, Canvas.GetLeft
	(overviewZoomRectThumb) + e.HorizontalChange), DataModel.Instance.ContentWidth -
	DataModel.Instance.ContentViewportWidth);
    Canvas.SetLeft(overviewZoomRectThumb, newContentOffsetX);

    double newContentOffsetY = Math.Min(Math.Max(0.0, Canvas.GetTop
	(overviewZoomRectThumb) + e.VerticalChange),
	DataModel.Instance.ContentHeight - DataModel.Instance.ContentViewportHeight);
    Canvas.SetTop(overviewZoomRectThumb, newContentOffsetY);
}

You should notice, in the previous snippet, that the content offset is kept clamped within its valid range. For the X offset that is from 0.0 (ContentWidth - ContentViewportWidth) (both members of DataModel) and a similar formula is used for the Y offset.

How does updating the Canvas position of the Thumb pan the viewport? Because the position and size of the Thumb are bound to the data-model:

<Canvas>
    <Thumb
        x:Name="overviewZoomRectThumb"
        Canvas.Left="{Binding Source={x:Static local:DataModel.Instance}, 
			Path=ContentOffsetX, Mode=TwoWay}"
            Canvas.Top="{Binding Source={x:Static local:DataModel.Instance}, 
			Path=ContentOffsetY, Mode=TwoWay}"
            Width="{Binding Source={x:Static local:DataModel.Instance}, 
			Path=ContentViewportWidth}"
            Height="{Binding Source={x:Static local:DataModel.Instance}, 
			Path=ContentViewportHeight}"
        DragDelta="overviewZoomRectThumb_DragDelta"
        Opacity="0.5"
        >
        <Thumb.Template>
            <ControlTemplate TargetType="{x:Type Thumb}">
                <Border
                    ...
                    Background="Yellow"
                    />
            </ControlTemplate>
        </Thumb.Template>
    </Thumb>
</Canvas>

Switching back to MainWindow.xaml, we can see that the position and extent of the content viewport here are also bound to the data-model:

<ZoomAndPan:ZoomAndPanControl
    x:Name="zoomAndPanControl"
    ContentScale="{Binding Source={x:Static local:DataModel.Instance}, 
		Path=ContentScale, Mode=TwoWay}"
    ContentOffsetX="{Binding Source={x:Static local:DataModel.Instance}, 
		Path=ContentOffsetX, Mode=TwoWay}"
    ContentOffsetY="{Binding Source={x:Static local:DataModel.Instance}, 
		Path=ContentOffsetY, Mode=TwoWay}"
    ContentViewportWidth="{Binding Source={x:Static local:DataModel.Instance}, 
		Path=ContentViewportWidth, Mode=OneWayToSource}"
    ContentViewportHeight="{Binding Source={x:Static local:DataModel.Instance}, 
		Path=ContentViewportHeight, Mode=OneWayToSource}"
    ...
    >

    ... content defined here ...

</ZoomAndPan:ZoomAndPanControl>

A few paragraphs back, I mentioned the ContentWidth property of DataModel. There is also a matching ContentHeight property. These properties define the size of the content. In MainWindow.xaml, we can see that the Width and Height properties are bound to the data-model properties:

<ZoomAndPan:ZoomAndPanControl
    ...
    >

    <Grid
        Width="{Binding Source={x:Static local:DataModel.Instance}, Path=ContentWidth}"
        Height="{Binding Source={x:Static local:DataModel.Instance}, Path=ContentHeight}"
        >

        ... content defined here ...

    </Grid>
</ZoomAndPan:ZoomAndPanControl>

This concludes the walkthrough of the sample projects. Data binding, if you are not already versed, is arguably a hard topic to come to terms with and hopefully you have made it this far! The main thing I wanted to show is that data-binding is a good way to keep the main window viewport and the overview window synchronized.

The next two sections are a summary of ZoomAndPanControl properties and methods. After that, we move onto Part 2 which discusses the implementation of ZoomAndPanControl.

ZoomAndPanControl Properties

This section is a summary of ZoomAndPanControl dependency properties.

Name Description
ContentScale The zoom level, or more precisely the scale of the content being viewed.
MinContentScale, MaxContentScale The valid range of values for ContentScale.
ContentOffsetX, ContentOffsetY The XY offset of the content viewport (in content coordinates).
AnimationDuration The duration of the zoom and pan animations (in seconds) started by calling AnimatedZoomTo and the other animation methods.
ContentZoomFocusX, ContentZoomFocusY The offset in the content (in content coordinates) that currently has the zoom focus. This is automatically updated whenever the viewport is panned and when AnimatedZoomTo or the other animation methods are called.
ViewportZoomFocusX, ViewportZoomFocusY The offset in the viewport (in viewport coordinates) that currently has the zoom focus. This is usually set to the center of the viewport, but is automatically updated when AnimatedZoomTo or the other animation methods are called.
ContentViewportWidth, ContentViewportHeight The width and height of the viewport, but specified in content coordinates. These are updated automatically when ever the viewport is resized.
IsMouseWheelScrollingEnabled Set to true to enable the control to pan the viewport in response to mouse wheel input. This is set to false by default.

The following properties of IScrollInfo are implemented (although they aren't dependency properties):

Name Description
HorizontalOffset, VerticalOffset The XY offset of the viewport (in scaled content coordinates).
ViewportWidth, ViewportHeight The width and height of the viewport (in viewport coordinates).
ExtentWidth, ExtentHeight The width and height of the content (in scaled content coordinates).

ZoomAndPanControl Methods

ZoomAndPanControl contains a number of methods that perform animated and non-animated zooming and panning. Some of these methods have already been discussed and some others have not. This section has a summary of all such methods.

Note: The duration of the animation can be set by setting the value of the AnimationDuration property.

The Rectangle and Point parameters to all methods are specified in content coordinates.

Name Description
AnimatedSnapTo(Point contentPoint)
SnapTo(Point contentPoint) 
Snaps the position of the viewport so that it is centered on a particular point (without changing the content scale).
AnimatedZoomTo(double contentScale)
ZoomTo(double contentScale) 
Zooms to the specified content scale.
AnimatedZoomTo(Rect contentRect)
ZoomTo(Rect contentRect) 
Zooms in or out so that the specified rectangle fits the viewport.
AnimatedZoomTo(double newScale, Rect contentRect) 
This is a special version of AnimatedZoomTo that specifies the content rectangle to zoom to and in addition specifies what the final content scale for when the zoom animation has completed. This is used to jump back to the previous zoom level where we already know the exact content scale to return to. Specifying the content scale exactly removes the possibility of rounding errors creeping in as the content offset is updated during the zoom animation.
AnimatedZoomAboutPoint
(double newContentScale, Point contentZoomFocus)
ZoomAboutPoint
(double newContentScale, Point contentZoomFocus) 
Zooms to the specified content scale. The content zoom focus point is kept locked to its current position in the viewport. This method is used to implement Google-maps style zooming.
AnimatedScaleToFit()
ScaleToFit() 
Scales the content so that it fits entirely in the viewport.

Part 2 - ZoomAndPanControl Internals

This part of the article looks at how the ZoomAndPanControl is implemented.

The main class ZoomAndPanControl is defined in ZoomAndPanControl.cs and derives from ContentControl:

public class ZoomAndPanControl : ContentControl, IScrollInfo
{
    // ...
}

As you can see, ZoomAndPanControl also implements IScrollInfo, but I won't mention that again until the end of part 2.

As a custom control, it can be restyled to customize or replace the default UI. The default visual template for ZoomAndPanControl is defined by the WPF Style that is found in ZoomAndPan\Themes\Generic.xaml. The XAML definition is simple and contains only one named part which is called PART_Content:

    <Style
        TargetType="{x:Type local:ZoomAndPanControl}"
        >
        <Setter Property="Template">
            <Setter.Value>
                <ControlTemplate TargetType="{x:Type local:ZoomAndPanControl}">
                    <Border
                        ...
                        >
                        <ContentPresenter x:Name="PART_Content"/>
                    </Border>
                </ControlTemplate>
            </Setter.Value>
        </Setter>
    </Style>

PART_Content is defined as a ContentPresenter. It is this UI element that displays (or presents) the content. Its RenderTransform is used to scale and translate the content.

As with all custom controls, ZoomAndPanControl is associated with its Style via a call to OverrideMetadata in the static class constructor:

public class ZoomAndPanControl : ContentControl, IScrollInfo
{
    ...

    static ZoomAndPanControl()
    {
        DefaultStyleKeyProperty.OverrideMetadata(typeof(ZoomAndPanControl), 
		new FrameworkPropertyMetadata(typeof(ZoomAndPanControl)));
    }

    ...
}

When developing a custom control, remember to add the following to Assembly.cs:

[assembly: ThemeInfo(
    ResourceDictionaryLocation.None,
    ResourceDictionaryLocation.SourceAssembly
)]

This is required for Generic.xaml to be located when the visual template is applied to the control. I often forget to add this when creating a new custom control!

The OnApplyTemplate method in ZoomAndPanControl is called when the visual template has been applied to the control. Here PART_Content is retrieved and cached for later use:

public override void OnApplyTemplate()
{
    base.OnApplyTemplate();

    content = this.Template.FindName("PART_Content", this) as FrameworkElement;

    ...
}

Note that PART_Content is only referenced as a FrameworkElement. We don't actually need to use any ContentPresenter properties or methods so the base class is used to reference the content.

WPF 2D transformations are used to scale and translate content. A ScaleTransform and TranslateTransform are instanced and added to a TransformGroup. This is then assigned to RenderTransform:

public override void OnApplyTemplate()
{
    base.OnApplyTemplate();

    content = this.Template.FindName("PART_Content", this) as FrameworkElement;
    if (content != null)
    {
        this.contentScaleTransform = 
		new ScaleTransform(this.ContentScale, this.ContentScale);
        this.contentOffsetTransform = new TranslateTransform();
        UpdateTranslationX();
        UpdateTranslationY();

        TransformGroup transformGroup = new TransformGroup();
        transformGroup.Children.Add(this.contentOffsetTransform);
        transformGroup.Children.Add(this.contentScaleTransform);
        content.RenderTransform = transformGroup;
    }
}

These transforms are kept synchronized with the current values of ContentScale, ContentOffsetX and ContentOffsetY. Whenever the values of these properties are changed, the 'property changed' event handlers execute code that updates the cached transforms. For example, ContentOffsetX_PropertyChanged calls UpdateTranslationX which updates the X coordinate of contentOffsetTransform based on the current value of ContentOffsetX:

private static void ContentOffsetX_PropertyChanged
		(DependencyObject o, DependencyPropertyChangedEventArgs e)
{
    ZoomAndPanControl c = (ZoomAndPanControl)o;

    c.UpdateTranslationX();

    ...
}

UpdateTranslationX actually recalculates contentOffsetTransform.X in one of two ways. When the content fits completely within the viewport, in this case in the horizontal axis, then the X translation is calculated so that the content is centered within the viewport. Otherwise, when the content doesn't fit within the viewport, the X translation is calculated simply from ContentOffsetX:

private void UpdateTranslationX()
{
    if (this.contentOffsetTransform != null)
    {
        double scaledContentWidth = this.unScaledExtent.Width * this.ContentScale;
        if (scaledContentWidth < this.ViewportWidth)
        {
            //
            // 1st case: When the content can fit entirely within the viewport, 
            // center it.
            //
            this.contentOffsetTransform.X = -this.ContentOffsetX + 
		((this.ContentViewportWidth - this.unScaledExtent.Width) / 2);
        }
        else
        {
            //
            // 2nd case: When the content doesn't fit within the viewport.
            //
            this.contentOffsetTransform.X = -this.ContentOffsetX;
        }
    }
}

The code for ContentOffsetY_PropertyChanged is similar. ContentScale_PropertyChanged, on the other hand, does a lot more work.

First it updates contentScaleTransform from ContentScale:

private static void ContentScale_PropertyChanged
	(DependencyObject o, DependencyPropertyChangedEventArgs e)
{
    ZoomAndPanControl c = (ZoomAndPanControl)o;

    if (c.contentScaleTransform != null)
    {
        c.contentScaleTransform.ScaleX = c.ContentScale;
        c.contentScaleTransform.ScaleY = c.ContentScale;
    }

    ...
}

Then it calls UpdateContentViewportSize:

private static void ContentScale_PropertyChanged
	(DependencyObject o, DependencyPropertyChangedEventArgs e)
{
    ZoomAndPanControl c = (ZoomAndPanControl)o;

    if (c.contentScaleTransform != null)
    {
        c.contentScaleTransform.ScaleX = c.ContentScale;
        c.contentScaleTransform.ScaleY = c.ContentScale;
    }

    c.UpdateContentViewportSize();

    ...
}

UpdateContentViewportSize first calculates the size of the viewport in content coordinates:

private void UpdateContentViewportSize()
{
    ContentViewportWidth = ViewportWidth / ContentScale;
    ContentViewportHeight = ViewportHeight / ContentScale;

    ...
}

UpdateContentViewportSize then calculates and caches some values that represent the 'constrained content viewport size'. These are set to the size of the viewport in content coordinates, but they actually max out at the size of the content:

private void UpdateContentViewportSize()
{
    ContentViewportWidth = ViewportWidth / ContentScale;
    ContentViewportHeight = ViewportHeight / ContentScale;

    constrainedContentViewportWidth = 
		Math.Min(ContentViewportWidth, unScaledExtent.Width);
    constrainedContentViewportHeight = 
		Math.Min(ContentViewportHeight, unScaledExtent.Height);

    ...
}

The 'coerce' callbacks for ContentOffsetX and ContentOffsetY use the cached 'constrained content viewport size' to keep the values of these properties with in the valid range of viewable area in the content. For example, ContentOffsetX_Coerce looks like this:

    private static object ContentOffsetX_Coerce(DependencyObject d, object baseValue)
    {
        ZoomAndPanControl c = (ZoomAndPanControl)d;
        double value = (double)baseValue;
        double minOffsetX = 0.0;
        double maxOffsetX = Math.Max(0.0, c.unScaledExtent.Width - 
			c.constrainedContentViewportWidth);
        value = Math.Min(Math.Max(value, minOffsetX), maxOffsetX);
        return value;
    }

The last two lines of code in UpdateContentViewportSize update contentOffsetTransform. When the content fits within the viewport, this causes the content to be centered within the viewport as the viewport changes size.

private void UpdateContentViewportSize()
{
    ContentViewportWidth = ViewportWidth / ContentScale;
    ContentViewportHeight = ViewportHeight / ContentScale;

    constrainedContentViewportWidth = 
		Math.Min(ContentViewportWidth, unScaledExtent.Width);
    constrainedContentViewportHeight = 
		Math.Min(ContentViewportHeight, unScaledExtent.Height);
		
    UpdateTranslationX();    
        UpdateTranslationY();
    }

Now, finally back in ContentScale_PropertyChanged the content offset is conditionally recalculated. enableContentOffsetUpdateFromScale is only set to true when a zoom animation is in progress such as zooming about a point (Google-maps style zooming) or zooming to a particular rectangle that the user has dragged out. The calculation involves the viewport zoom focus and the content zoom focus. When enabled this code keeps the two focus points locked together:

private static void ContentScale_PropertyChanged
		(DependencyObject o, DependencyPropertyChangedEventArgs e)
{
    ZoomAndPanControl c = (ZoomAndPanControl)o;

    if (c.contentScaleTransform != null)
    {
        c.contentScaleTransform.ScaleX = c.ContentScale;
        c.contentScaleTransform.ScaleY = c.ContentScale;
    }

    c.UpdateContentViewportSize();

    if (c.enableContentOffsetUpdateFromScale)
    {
        ...

        double viewportOffsetX = c.ViewportZoomFocusX - (c.ViewportWidth / 2);
        double viewportOffsetY = c.ViewportZoomFocusY - (c.ViewportHeight / 2);
        double contentOffsetX = viewportOffsetX / c.ContentScale;
        double contentOffsetY = viewportOffsetY / c.ContentScale;
        c.ContentOffsetX = (c.ContentZoomFocusX - 
		(c.ContentViewportWidth / 2)) - contentOffsetX;
        c.ContentOffsetY = (c.ContentZoomFocusY - 
		(c.ContentViewportHeight / 2)) - contentOffsetY;

        ...
    }

    ...
}

The drag-zooming feature in the advanced and data-binding samples uses the AnimatedZoomTo method. We will now look at this method to investigate how animated zooming is implemented.

First let's look at how AnimatedZoomTo is called in MainWindow.xaml.cs:

private void ApplyDragZoomRect()
{
    ...
    
    double contentX = ...
    double contentY = ...
    double contentWidth = ...
    double contentHeight = ...
    zoomAndPanControl.AnimatedZoomTo(new Rect
		(contentX, contentY, contentWidth, contentHeight));

        ...
}

AnimatedZoomTo is passed a rectangle that specifies the area of the content to zoom to. An animation is run that results in the content scale and content offset changing so that the rectangle fits within the viewport.

Internally AnimatedZoomTo calculates a new content scale that is derived from the passed rectangle. This is followed by a call to the internal helper method AnimatedZoomPointToViewportCenter. This method is passed the new content scale and the center of the rectangle, which is the location in the content that the zoom will focus on:

public void AnimatedZoomTo(Rect contentRect)
{
    double scaleX = this.ContentViewportWidth / contentRect.Width;
    double scaleY = this.ContentViewportHeight / contentRect.Height;
    double newScale = this.ContentScale * Math.Min(scaleX, scaleY);

    AnimatedZoomPointToViewportCenter(newScale, 
	new Point(contentRect.X + (contentRect.Width / 2), contentRect.Y + 
	(contentRect.Height / 2)), null);
}

AnimatedZoomPointToViewportCenter starts by canceling any current animations. This is achieved by calling CancelAnimation for each dependency property that might already have animation in progress:

private void AnimatedZoomPointToViewportCenter
	(double newContentScale, Point contentZoomFocus, EventHandler callback)
{
    ...

    AnimationHelper.CancelAnimation(this, ContentZoomFocusXProperty);
    AnimationHelper.CancelAnimation(this, ContentZoomFocusYProperty);
    AnimationHelper.CancelAnimation(this, ViewportZoomFocusXProperty);
    AnimationHelper.CancelAnimation(this, ViewportZoomFocusYProperty);

    ...
}

CancelAnimation is method from my AnimationHelper class. This class contains a few simple wrappers that make the WPF animation system a bit easier to use.

Next the zoom focus points are determined. The point that specifies the content zoom focus is passed in as an argument. The viewport zoom focus, however, is calculated by transforming the content zoom focus into viewport coordinates:

private void AnimatedZoomPointToViewportCenter
	(double newContentScale, Point contentZoomFocus, EventHandler callback)
{
    ...

    AnimationHelper.CancelAnimation(this, ContentZoomFocusXProperty);
    AnimationHelper.CancelAnimation(this, ContentZoomFocusYProperty);
    AnimationHelper.CancelAnimation(this, ViewportZoomFocusXProperty);
    AnimationHelper.CancelAnimation(this, ViewportZoomFocusYProperty);

    ContentZoomFocusX = contentZoomFocus.X;
    ContentZoomFocusY = contentZoomFocus.Y;
    ViewportZoomFocusX = (ContentZoomFocusX - ContentOffsetX) * ContentScale;
    ViewportZoomFocusY = (ContentZoomFocusY - ContentOffsetY) * ContentScale;

    ...
}

Lastly the dependency property animations that perform the zoom are started. This is achieved by calling StartAnimation. enableContentOffsetUpdateFromScale is set to true whilst the animation is in progress so that the code to lock the focus points in ContentScale_PropertyChanged is enabled. The anonymous function that is passed to StartAnimation is called when the animation has completed and it resets enableContentOffsetUpdateFromScale to false:

private void AnimatedZoomPointToViewportCenter
	(double newContentScale, Point contentZoomFocus, EventHandler callback)
{
    ...

    AnimationHelper.CancelAnimation(this, ContentZoomFocusXProperty);
    AnimationHelper.CancelAnimation(this, ContentZoomFocusYProperty);
    AnimationHelper.CancelAnimation(this, ViewportZoomFocusXProperty);
    AnimationHelper.CancelAnimation(this, ViewportZoomFocusYProperty);

    ContentZoomFocusX = contentZoomFocus.X;
    ContentZoomFocusY = contentZoomFocus.Y;
    ViewportZoomFocusX = (ContentZoomFocusX - ContentOffsetX) * ContentScale;
    ViewportZoomFocusY = (ContentZoomFocusY - ContentOffsetY) * ContentScale;

    enableContentOffsetUpdateFromScale = true;

    AnimationHelper.StartAnimation(this, ContentScaleProperty, 
			newContentScale, AnimationDuration,
            delegate(object sender, EventArgs e)
            {
                enableContentOffsetUpdateFromScale = false;

                if (callback != null)
                {
                    callback(this, EventArgs.Empty);
                }
            });

    AnimationHelper.StartAnimation(this, ViewportZoomFocusXProperty, 
			ViewportWidth / 2, AnimationDuration);
    AnimationHelper.StartAnimation(this, ViewportZoomFocusYProperty, 
			ViewportHeight / 2, AnimationDuration);
}

This may seem like a round-about and unintuitive way to implement animated zooming, I will try to explain my thinking. From reading the code, you can see that ContentScale is animated from its present value to the new value. It is probably not obvious why the viewport zoom focus is being animated. ViewportZoomFocusX and ViewportZoomFocusY track the current point in the viewport that is the focus of zooming. Usually this is set to the center of the viewport and this means that when we hit the plus or minus buttons we zoom in or out focused on the center of the viewport. However when using Google-maps style zooming, the viewport zoom focus is set to the location of the mouse cursor. As already discussed, the code in ContentScale_PropertyChanged keeps the viewport zoom focus locked to the content zoom focus while zooming is in progress, and this is how the Google-maps style zooming works.

As it turns out, the zoom focus locking also makes for a good implementation of drag-zooming. The viewport zoom focus is animated from its present value to the center of the viewport. Because viewport zoom focus and content zoom focus are locked together, this animation has the effect of shifting the content zoom focus. As ContentScale_PropertyChanged calculates the content offset from the content zoom focus, then this has the effect of panning the content viewport while the content scale is changing. I experimented with multiple ways of implementing the animation to zoom to the rectangle, but this implementation fits in nicely with the google-maps style zooming and results in smoother animated zooming.

The very last thing is to mention IScrollInfo. This interface allows a control that is embedded within a ScrollViewer to communicate with that ScrollViewer. It is how the ScrollViewer determines the position and range of the scrollbars. My implementation of the IScrollInfo methods can be found in ZoomAndPanControl_IScrollInfo.cs. This file contains a partial implementation of ZoomAndPanControl that contains only those methods and properties required by IScrollInfo. To understand how IScrollInfo works, I'll refer you to the following articles that are already out there: WPF Tutorial - Implementing IScrollInfo and IScrollInfo in Avalon part I, part II, part III and part IV.

Conclusion

This example has explained a reusable WPF custom control that does zooming and panning of generic content. In doing so, we have touched on a number of non-trivial areas in WPF such as animation, 2D transformation, custom controls and the implementation of the IScrollInfo interface. It took a lot of time to develop the ideas and code presented in this article and I hope it will be of use to others.

As I mentioned in the beginning, I haven't tried to implement any kind of UI virtualisation. Maybe this will be the topic of a future article. I welcome feedback and improvements to the code. Thanks for reading the article.

Updates

  • 08/06/2010 
    • Based on feedback from Paul Selormey, I modified the code to constrain the content offset to within the viewable area of the content. The article has been updated accordingly.
  • 09/06/2010 
    • Based on more feedback from Paul Selormey, I have added the property IsMouseWheelScrollingEnabled to ZoomAndPanControl. Setting this property to true enables the control to pan the viewport in response to mouse wheel input. This works in much the same way as mouse wheel input for a standard ScrollViewer. I also added a list of ZoomAndPanControl properties to accompany the existing list of methods.
  • 18/06/2010
    • Setting the value of ContentScale used to result in ContentOffsetX and ContentOffsetY being automatically updated. It no longer works this way. I discovered a bad effect that can happen when you bind all three dependency properties to a data-source and then want to update all three of them at once by changing the data-source. The binding to ContentScale updates it, which in turn wrongly updates ContentOffsetX and ContentOffsetY in your data-source! This doesn't happen anymore and the automatic update of ContentOffsetX and ContentOffsetY is now limited to only where it is needed and that is while the animations for Google-maps style zooming and drag zooming are in progress.
  • 22/06/2010 
    • When the viewport is resized (due to the window resizing), ContentOffsetX and ContentOffsetY are now constrained to the valid range. This is a fix to an issue reported by Patrick Walz. When the scrollbars are at the bottom or right extents and you resize the associated edge of the window (eg bottom or right side of the window) the content offset is now clamped at the edge of the content, rather than allowing the area beyond the content to be displayed.
  • 29/06/2010
    • Fixed an issue with centering the content when the viewport is larger than the content. This was working ok when the content was scaled down, but when the content was unscaled and the window was maximized so that the viewport was larger than the content the centering wasn't working correctly. I had to add explicit alignment to the ContentPresenter declared in Generic.xaml and modified the calculation that determines the centered content offset.
  • 19/11/2010
    • Added function SnapContentOffsetTo. This makes the ZoomAndPanControl snap ContentOffsetX and Y to the specified point.
    • Fixed the MakeVisible function (that implements IScrollInfo). This function now works as you would expect.
    • Fixed an issue reported by tmsife and Member 7483521. Setting the size of the content from code behind now works as expected.
  • 09/12/2010
    • A new sample project has been added that demonstrates the use of ZoomAndPanControl to create the concept of an infinite workspace. A small section has been added near the start of part 1 that describes this new sample project.
  • 21/03/2011
    • Fixed an issue in the Advanced Sample that was reported by skybluecodeflier. The size of the ZoomAndPanControl content wasn't being set and so panning and scrolling wasn't working.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

About the Author

Ashley Davis
Software Developer (Senior)
Australia Australia
Member
No Biography provided

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
You must Sign In to use this message board.
Search this forum  
    Spacing  Noise  Layout  Per page   
QuestionDataTemlate polygon point bindingmemberwangka201326 Apr '13 - 0:24 
Thank for such a wonderful control. but I must add a Polygon of every Rectangle. How can I using Polygon Point binding. DataTemplate wie folloeing:
<Canvas>
            <Rectangle
                Width="{Binding Width}"
                Height="{Binding Height}"
                ......
                />
              
                <Polygon Points="20,30 30,30 30,20" (binding?)            
                Fill="{Binding ColorP, Converter={StaticResource colorToBrushConverter}}"   
                Stroke="Black"
                Canvas.ZIndex="1"
                StrokeThickness="1" Height="{Binding Height}" Width="{Binding Width}"/>
            </Canvas>
     

AnswerRe: DataTemlate polygon point bindingmemberAshley Davis26 Apr '13 - 2:30 
Thanks for your feedback. I'm sorry I don't know how to bind your polygon points. It sounds like it would be possible but I've never needed to know how. Good luck with your research.
 
Ash
QuestionJagged border artifact on zoomingmemberare_all_nicks_taken_or_what20 Mar '13 - 6:22 
Run your sample. If you quickly zoom in and zoom out of the image using the mouse wheel, you can see that the border becomes "jagged". The best description of this would be "vertical sync paint issue", but I'm using a LED screen so that doesn't seem right Smile | :)
 
Can you please explain why you see this effect around the image? Is it a paint issue of the zoom and pan control?
AnswerRe: Jagged border artifact on zoomingmemberAshley Davis25 Mar '13 - 16:00 
Sorry I can't explain the issue. If you solve the problem please let us know, I'm sure others using the control will appreciate it.
 
Cheers
Ash
QuestionCan I add a rectangle to the canvas by click a button?memberyouwu10 Mar '13 - 11:47 
Hi
I am doing a project using zooming and panning and I think your project is very useful. But I need to add a new object to the canvas. How can I do it. Thanks a lot!!
AnswerRe: Can I add a rectangle to the canvas by click a button?memberAshley Davis10 Mar '13 - 13:08 
You can't add objects directly to the zoom and pan control. You can only wrap another control. So you need to use one of the exist WPF controls (eg list box) or you need to design your own container control.
 
Cheers
Ash
QuestionAdd Canvas Gridmemberangi_sfj2 Mar '13 - 5:11 
hi!
 
i've added a web to the canvas conposed by lines.
 
for (double x = 0; x < content.Width; x += 10)
{
   Line l = new Line();
   l.StrokeThickness = 0.1;
   l.Stroke = Brushes.Gray;
   l.X1 = x;
   l.Y1 = 0;
   l.X2 = x;
   l.Y2 = content.Height;
   content.Children.Add(l);
}
 
for (double y = 0; y < content.Height; y += 10)
{
   Line l = new Line();
   l.StrokeThickness = 0.1;
   l.Stroke = Brushes.Gray;
   l.X1 = 0;
   l.Y1 = y;
   l.X2 = content.Width;
   l.Y2 = y;
   content.Children.Add(l);
}
 
when i zoom in, everything ok,
but when i zoom out, the result isn't a regular web..
 
any suggestion?
AnswerRe: Add Canvas Gridmemberangi_sfj2 Mar '13 - 5:56 
ok, solved!
 
i've also changed the zooming factor to
 
zoomAndPanControl.ContentScale * 0.8
and
zoomAndPanControl.ContentScale * 1.2
 
so the painting cannont work properly.
setting the zoom rounded to one decimal the problem doesn't shows up any more! Smile | :)
AnswerRe: Add Canvas GridmemberAshley Davis17 Mar '13 - 0:31 
No idea sorry. My only suggestion is that you shouldn't add graphical sub-objects directly to the canvas you should use data-binding to add them.
 
Cheers
Ash
QuestionScrollViewer IssuememberSaurabhSavaliya29 Nov '12 - 19:41 
Hi Ashley,
 
I m very thankful for such a wonderful zooming control. But I have an issue with it that When I add any annotation such as freehand, circle, square,etc..., ScrollViewer position changed and move to bottom right corner of Canvas every time.
 
I will trying more n more but i didn't get any solution yet. Can you help me? If i get any solution definitely, i will post here
 
Best Regards,
Saurabh Savaliya
AnswerRe: ScrollViewer IssuememberAshley Davis3 Dec '12 - 10:27 
Hi, thanks for your feedback.
 
I'm not really sure what you are trying to do. If you could include example code and describe how it fits in then I might get a better idea and be able to give you some tips.
 
Cheers
Ash
SuggestionHow to improve canvas infinite scale performance (dragging elements to left and top border of canvas)memberJogi18 Nov '12 - 13:10 
Hey Ashley,
 
this is just an amazing control you have created - thanks for the great article!
As I tested the controls infinite expand canvas sample I noticed that the expansion to the bottom and right direction was very direct and smooth. In case of expanding to the top and left direct this didn't work the way I expected it to work like in the other directions. Also the canvas didn't resize back to fit the real size of its content.
 
I made some minor changes to the ExpandContent() method of MainWindow.xaml.cs of the InfiniteZoomAndPanSample-Project to fix the expanding behaviour to these other directions (see attached code).
 
Maybe this is helpfull for anybody else using the control - I personally prefer this slightly modified behaviour.
 
Kind regards,
Joachim
 
 private void ExpandContent()
        {
            double xOffset = 0;
            double yOffset = 0;
            Rect contentRect = new Rect(0, 0, 0, 0);
 
            foreach (RectangleData rectangleData in DataModel.Instance.Rectangles)
            {
                if (rectangleData.X < xOffset)
                {
                    xOffset = rectangleData.X;
                }
 
                if (rectangleData.Y < yOffset)
                {
                    yOffset = rectangleData.Y;
                }
 
                contentRect.Union(new Rect(rectangleData.X, rectangleData.Y, rectangleData.Width, rectangleData.Height));
            }
 
// ADDED MODIFICATION TO CHECK LEFT AND TOP BOUNDARY OF CONTENT AND SCALE TO FIT
//-------------------------------------------------------------------------------

            double minXOffset = double.MaxValue;
            double minYOffset = double.MaxValue;
 
            // Handle Top and Left shrink
            foreach (RectangleData rectangleData in DataModel.Instance.Rectangles)
            {
                if (rectangleData.X < minXOffset)
                    minXOffset = rectangleData.X;
                if (rectangleData.Y < minYOffset)
                    minYOffset = rectangleData.Y;
            }
 
            //
            // Translate all rectangles so they are in positive space.
            //
            xOffset = Math.Abs(xOffset) - minXOffset;
            yOffset = Math.Abs(yOffset) - minYOffset;
 
//-------------------------------------------------------------------------------
// END OF CHANGES

            foreach (RectangleData rectangleData in DataModel.Instance.Rectangles.Where(rect=>!rect.IsSelected))
            {
                rectangleData.X += xOffset;
                rectangleData.Y += yOffset;
            }
 
            DataModel.Instance.ContentWidth = contentRect.Width;
            DataModel.Instance.ContentHeight = contentRect.Height;
        }

GeneralRe: How to improve canvas infinite scale performance (dragging elements to left and top border of canvas)memberAshley Davis18 Nov '12 - 20:39 
Thanks for your feedback and thanks for contributing your modified code.
 
Ash
QuestionFixed size contentmemberMember 94570557 Nov '12 - 0:14 
I am wonder how to do zoom with without changing the size of content and only update the scaled position?
 
Please help and thanks in advance.
AnswerRe: Fixed size contentmemberAshley Davis7 Nov '12 - 9:46 
Sorry, I'm not too sure what you are asking. The whole point of zoom is that it changes the size (well the scale) of the content. I'm not sure what you mean by scaled position either.
GeneralRe: Fixed size contentmemberStupid Guy 201211 Nov '12 - 14:53 
I try to say the situation like a map with pin. when the map is enlarged, the pin is stick to its map relative coordinate without changing the size of the pin. Could you show me how to do it?
GeneralRe: Fixed size contentmemberAshley Davis7 Dec '12 - 21:15 
The sample code already does something like this, when you use the mouse wheel to zoom in and out it pins the location under the mouse cursor. It is what I call google map style zooming in the article.
QuestionThank youmemberJean R. Bisson24 Oct '12 - 8:20 
Your Zoom and Pan control was exactly what I was looking for to get started with a recent project. I was able to redefine the functions of the mouse down/up and drag to suit the needs of my display. Very easy to understand and very well documented. Hats off to you mate Smile | :)
 
Jean
GeneralMy vote of 5memberLucky Vdb12 Oct '12 - 6:53 
This is it !
QuestionHow implement RotateTransform ?memberLorenzo716 Sep '12 - 3:28 
I'm trying to implement the rotation...
 
        private RotateTransform contentRotateTransform = null;
 
        public static readonly DependencyProperty ContentRotationProperty =
        DependencyProperty.Register("ContentRotation", typeof(double), typeof(ZoomAndPanControl),
                                    new FrameworkPropertyMetadata(0.0, ContentRotation_PropertyChanged));
        
        private static void ContentRotation_PropertyChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
        {
            ZoomAndPanControl c = (ZoomAndPanControl)o;
 
            if (c.contentRotateTransform != null)
            {
                Point zoomCenter = new Point(c.ContentOffsetX + (c.ContentViewportWidth / 2), c.ContentOffsetY + (c.ContentViewportHeight / 2));
                c.contentRotateTransform.CenterX = zoomCenter.X;
                c.contentRotateTransform.CenterY = zoomCenter.Y;
                c.contentRotateTransform.Angle = c.ContentRotation;
 
                c.SnapContentOffsetTo(new Point(0, 0));
            }
        }
 
I'm unable translate coordinates... Thanks Confused | :confused:
AnswerRe: How implement RotateTransform ?memberAshley Davis11 Sep '12 - 1:38 
Hi, no idea sorry. I didn't try rotation, it should be possible though.
 
Ash
GeneralMy vote of 5memberChandan Kumar Rath1 Aug '12 - 7:55 
nicely explained.
GeneralCombined with Silverlight port into a single codebasememberGeorge I. Birbilis18 Jun '12 - 7:40 
I've managed to combine this code with a cut-down Silverlight port of it that was out there:
 
A Silverlight custom control for zooming and panning with a small window to see he image with thumb[^]
 
I did it in a way that the Silverlight version is source-code compatible with the WPF version (via a WPF compatibility layer that even implements value coercion), so that both the WPF and the Silverlight projects share the same source code files. The resulting libraries for WPF and Silverlight work with the existing samples (from this article and from the aformentioned Silverlight version article) without any changes to them
 
You can check out the latest version (plan to add the samples there too for convenience) under the "Client" subfolder of ClipFlair sourcecode base at http://clipflair.codeplex.com
Computer & Informatics Engineer
Microsoft MVP J# 2004-2010
Borland Spirit of Delphi 2001
QuickTime, QTVR, Delphi VCL,
ActiveX, COM, .NET, Robotics
http://zoomicon.com
http://zoomicon.wordpress.com

QuestionQuestion about the data modelmemberPaul 992916 Mar '12 - 6:48 
First off I think your article is great!
I have an application I would like to use this in but I’m not sure how to accomplish the data model. What I need to do is display a very large graphic with approximately 100 user controls overlaid on the graphic at specific locations. Is there a way to convert the existing XAML for this to a data model so it can be shared between the main window and the overview? Any guidance would be appreciated. I just haven’t done this before and am not sure about how this is done without lots of hours of hand coding each control. Thanks in advance.
Paul Wheeler

AnswerRe: Question about the data modelmemberAshley Davis18 Mar '12 - 18:18 
Hi Paul,
 
Thanks for your feedback.
 
You can easily share a data-model between the main window and the overview. I can't really explain how data-models, views and in general how MVMM works as that would be an entire article in itself!
 
There are plenty of great articles for learning WPF and MVVM that you should look into first. Do a search for MVVM right here on code project, or maybe search for something like 'best MVVM tutorial' on stackoverflow. Be prepared to put some effort into learning this! Once you are over the learning curve though it gets easier.
 
I have another article, the NetworkView one, that does share the data-model between the main window and the overview, however this article is quite advanced and you should learn MVVM first anyway, but once you do that article might be a good example for you.
 
Cheers
Ash
QuestionZoomAndPanControl repaintmemberMember 865271828 Feb '12 - 18:54 
Hello!
 
At first I want to thank you for sharing this project. I was looking for something like this to test my ideas. However I am pretty new to WPF, so I don't understand how repaint works in this custom control. I checked a few DependencyProperties, but none of them has AffectsRender set. Does the DependencyProperty value change trigger the repaint, or some other event? Thanks for your help in advance.
 
Br.
 
Mark
AnswerRe: ZoomAndPanControl repaintmemberAshley Davis28 Feb '12 - 19:04 
Repaint is generally automatic in WPF. Changing a dependency property can trigger a repaint, but it depends on how the dependency property is configured.
 
Internally the control can call InvalidateVisual if it determines that a repaint is necessary, but you probably shouldn't call this function externally on a control unless you are really desperate for a workaround.
 
Do a search for WPF and repaint or invalidate and you will find loads of info.
 
Cheers
Ash
GeneralRe: ZoomAndPanControl repaintmemberMember 865271828 Feb '12 - 21:45 
Hello Ashley!
 
I did a quick search in the morning, and found some articles about how repaint works. I started to check the source code of the attached projects, but ZoomAndPanControl seems to define dependency properties without the configuration you mentioned. (I also printed out the values of FrameworkPropertyMetadata.AffectsArrange, FrameworkPropertyMetadata. AffectsMeasure and FrameworkPropertyMetadata.AffectsRender in a few cases, but those values were false every time.) I don't remember that InvalidateVisual is called inside the class. So this is why it's not clear for me how the repaint could work, because it definitely updates the interface during zoom/pan operations. Smile | :) Sorry for the question, I am a Java dev interested in learning WPF/C#. Smile | :)
 
Br.
 
Mark
GeneralRe: ZoomAndPanControl repaintmemberAshley Davis29 Feb '12 - 11:02 
Well you haven't really given me much context about your problem so I'm not really sure how I can help you.
 
Like I said WPF repaint is almost completely automatic and the only way I could think it wouldn't work for you is if you had created your control, which you have have embedded in the ZoomAndPanControl, and it is your own control that is failing to repaint as need.
 
It is of course possible that ZoomAndPanControl has a repaint problem, although I think that is unlikely after over 6,000 download with no reported repaint problems.
 
Ash
GeneralRe: ZoomAndPanControl repaintmemberMember 865271829 Feb '12 - 22:23 
Hello Ashley!
 
The samples work for me as well, but I would like to understand the code better, because I want to make some modifications. Maybe I wasn't clear, so let me explain my intentions and the background of my question.
 
Right now the control does not support UI virtualisation, as you mentioned in the article. I would like to add this feature. What I am planning to do is adding new elements to canvas or replace the canvas with a new one on-the-fly during pan and zoom operations. These are just very crude ideas, but I think it is important to know what triggers the repaint.
 
I understand that repaint is automatic; but can't find the configuration for it in the ZoomandPanControl class. I thought that certain DependencyProperties might have AffectsRender set to true, or InvalidateVisual is called, but what I checked does not confirm this idea. MSDN documentation suggests that changing the values of a DependencyProperty does not trigger repaint. (It seems that you have to create DependencyProperty with the right FrameworkPropertyMetadata configuration to enable this feature.)This is why I am confused. For example this tutorial[^]defines AffectsRender in FrameworkPropertyMetadata.
 
Maybe I am on the wrong track. Frown | :( Could you point me to the right direction? Thanks for your help again. Smile | :)
 
Br.
 
Mark
QuestionAny reason this would not work in silverlight?memberDouglasJWoods11 Dec '11 - 16:24 
I am thinking of using this in a silverlight app. Do you know of any reason why it would not work?
Douglas

AnswerRe: Any reason this would not work in silverlight?memberAshley Davis11 Dec '11 - 16:47 
There could be lots of reasons Wink | ;)
 
I have never tried it in silverlight. Let me know how it works out.
 
Cheers
Ash
QuestionZoom not centering when fullscreenmemberpierceblaylock7 Nov '11 - 12:38 
I found another odd behaviour as well. When you run the sample in its default window mode and click between the Fill and 100% buttons you can see that it zooms in and out from the center of the content. However, if you maximise the window to fullscreen and then click between the Fill and 100% buttons, it seems to zoom in and out from the center of the right hand edge. It looks like it is centering vertically, but not horizontally for some reason.
AnswerRe: Zoom not centering when fullscreenmemberpierceblaylock7 Nov '11 - 15:54 
I have noticed that it seems to have something to do with shape of the main window. For example, if the window is wider than it is higher, then the viewport will zoom in and out of the right edge. If the window is higher than it is wider, then the viewport will zoom in and out of the bottom edge.
 
I think there might need to be an adjustment made to the AnimatedZoomTo method. Specifically the following line.
Point zoomCenter = new Point(ContentOffsetX + (ContentViewportWidth / 2), ContentOffsetY + (ContentViewportHeight / 2));

GeneralRe: Zoom not centering when fullscreenmemberAshley Davis21 Nov '11 - 15:03 
Hi, I just got around to looking at this, sorry for the delay.
 
I wasn't able to repro the problem you describe. I was testing using the data binding sample.
 
Cheers
Ash
GeneralMy vote of 5memberAn.Tom2 Nov '11 - 12:51 
I was starting an own Zoom and Pan coding, but then found this article. I am deeply impressed about the elegance of this solution.
QuestionDragging Yellow Rectangle Off Overview Windowmemberpierceblaylock31 Oct '11 - 0:21 
Great article, however I noticed a small problem that I haven't been able to figure out in the Data Binding sample. If you hold down the left mouse button in the Main window and drag the view port, you can actually cause the yellow rectangle to drag off the Overview window completely. You then need to click in either the Main window or the Overview window to force the yellow rectangle to reset back. Is there a way to fix this?
AnswerRe: Dragging Yellow Rectangle Off Overview WindowmemberAshley Davis1 Nov '11 - 1:22 
Hi, thanks for your feedback.
 
I'm not sure that I am clear on what the problem is. Is it that you want to always constrain the viewport to the area of the content? The yellow rectangle always follows the viewport so if you drag the viewport beyond the boundaries of the content I would expect the yellow rectangle to be outside the overview window. Early on I did try constraining the viewport so it couldn't move beyond the boundaries of the content. Unfortunately it was very difficult to get the constraints working nicely with the zooming aspects of the control.
 
Anyway if I have misunderstood the problem or if you can think of a viable fix to your issue please let me know.
 
Cheers
Ash
GeneralRe: Dragging Yellow Rectangle Off Overview Windowmemberpierceblaylock6 Nov '11 - 1:49 
Hi Ashley,
 
I probably didn't explain it very well. I'll try to give some steps to reproduce.
 
Hold down the left mouse button in the main window and start dragging the viewport around. The yellow rectangle in the overview window moves around as expected. Now (with the left mouse button still down) continue dragging the mouse in one direction until the yellow rectangle in the overview window hits the edge of the overview window. Then continue to drag the mouse in the same direction (left mouse button still down this whole time). Watch what happens to the yellow rectangle. It starts moving off the edge of the overview window and will eventually disappear completly. If you now release the left mouse button and then just click once anywhere in the main window, the yellow rectangle will snap back into the overview window.
 
Hopefully that makes sense and allows you to reproduce the problem.
GeneralRe: Dragging Yellow Rectangle Off Overview WindowmemberAshley Davis6 Nov '11 - 16:20 
I think I see what you are getting at. Basically the behaviour of the viewport in the ZoomAndPanControl is inconsistent with the behaviour of the yellow triangle.
 
This problem has something to do with the constraints that are applied to ZoomAndPanControl's viewport in the ContentOffsetX_Coerce and ContentOffsetY_Coerce.
 
It kind of looks like the constraints are applied to the viewport while dragging is in progress (thus the viewport remains constrained to the content) but that the constrained offset values are not propagated back to the view-model until after dragging has completed (hence the snapping of the overview's rectangle after dragging).
 
Off the top of my head I can think of two ways to deal with this. The first and easiest is to remove the constraints that are applied to ZoomAndPanControl's viewport. You can do this by modifying the code in the ZoomAndPanControl.cs methods ContentOffsetX_Coerce and ContentOffsetY_Coerce.
 
However this won't help you if what you really want is to keep the constraints and apply them both to the viewport and the overview.
 
To achieve that you need to find a way of applying the constraints to the overview window's yellow rectangle as well as the ZoomAndPanControl's viewport. The best way to do this is to move the constraints into the view-model so that they can be applied to both parts of the UI.
 
Unfortunately I can't think of a particularly elegant way of doing this because the contraints are dependent on ZoomAndPanControl's internal data (the internal variables constrainedContentViewportWidth and constrainedContentViewportHeight). You could make those variables dependency properties and then data-bind them to the view-model, that way the values would be available for use in the view-model to clamp the viewport offset (as is done in ContentOffsetX_Coerce and ContentOffsetY_Coerce).
 
Hope this helps, if I think of anything better I'll let you know. Also if you think of a good solution I'd love to hear about it.
 
Cheers
Ash
GeneralRe: Dragging Yellow Rectangle Off Overview Windowmemberpierceblaylock7 Nov '11 - 12:35 
OK, thanks for the reply. I'll let you know if I find a good solution. In the meantime I have found another small problem, but I'll start a new thread for it.
QuestionY-Scale for a graphmemberNoel Macara7 Oct '11 - 16:32 
Dear Mr. Davis,
I am still fairly new to WPF and I am finding the learning curve very steep, as I am an old bear of very little brain.
 
I am writing a financial charting package which produces various types of price-action graph. I was anxious to find a zooming function for the graphs, which is how I came across your control here in the Code Project.
 
Your beautiful code has taught me much about laying out my own code, for which I am already grateful to you.
 
I have had no difficulty using and adapting your code to my personal requirements, even managing to integrate it with some (no doubt very crude, but effective) UI virtualization. But I have been stuck for a week now on a very silly problem, and I am hoping that you can point me to or suggest a solution.
 
I have a canvas which presents the graphical plot. Next to this, I placed another canvas which presents the Y-scale for the plot. Everything works beautifully: when I pan up and down and zoom in and out the scale is in perfect sync with the plot. But I run into a problem when I pan right because the scale moves left and out of sight along with the left-hand side of the plot. Fair enough, and completely predictable (except that I did not ... predict it, that is). Of course, I need the Y-scale to remain unchanged and visible during a purely horizontal pan.
 
So I next tried moving the scale canvas outside the ZoomAndPan control stacking it horizontally to the left of the control, and intending to place another scale on the RHS. I never got as far as that second scale, being still stuck on the LHS after more than a week! I am feeling very stupid!
 
Here is the problem. The text blocks comprising the scale are correctly formatted, and they respond to both the panning and zooming correctly. But try as I might I cannot get them positioned vertically in sync with the plot.
 
I would be very grateful if you, or any of your readers, could direct me to a solution for this problem.
 
Thank you,
 
Noel
Noel Macara

AnswerRe: Y-Scale for a graphmemberAshley Davis7 Oct '11 - 20:53 
Hi Noel,
 
Thanks for your feedback.
 
It is a little hard to visualise exactly what you are trying to achieve. Maybe what you want is to have a main ZoomAndPanControl in the center and then two additional ZoomAndPanControls on the left and right hand sides. The one in the center would be your main content and you could synchronize its scale/zoom with the other two. You would probably also want to synchronize the viewport offset in the Y axis. Whatever, it sounds like you will have some figuring out to do. Good luck!
 
Ash
GeneralRe: Y-Scale for a graphmemberNoel Macara7 Oct '11 - 23:47 
Ash,
 
Thank you for your prompt response.
 
I thought that I had replied to that response via the discussion board - but my reply is not appearing there, so I suspect that I might have inadvertently used the Email option. If I did, then you will know that I am close to an answer and will post it here when I have one. If I did not, and my reply is lost in cyber-space, then this is a necessary post to let you know that I have read your reply with thanks.
 
Cheers,
 
Noel
Noel Macara

QuestionDrag-zooming does not work fine when VerticalScrollBarVisibility & HorizontalScrollBarVisibility set to AutomemberGRF756 Sep '11 - 4:37 
That's it. Change VerticalScrollBarVisibility & HorizontalScrollBarVisibility for ScrollViewer in the project to Auto. The zoomed position (probably the offset) is wrong. If I find how to solve it, I'll let you know. If you find the solution before, post it please. Again, great control!
AnswerRe: Drag-zooming does not work fine when VerticalScrollBarVisibility & HorizontalScrollBarVisibility set to AutomemberAshley Davis29 Sep '11 - 16:30 
Hi, thanks for your feedback.
 
In the DataBindingZoomAndPanSample solution I tried setting both VerticalScrollBarVisibility and HorizontalScrollBarVisibility to auto and found no problems with drag zooming.
 
If you can be more specific about your setup I'll have another look at it.
 
Cheers
Ash
GeneralRe: Drag-zooming does not work fine when VerticalScrollBarVisibility & HorizontalScrollBarVisibility set to AutomemberThomas Polaert5 Mar '12 - 22:22 
It only occurs when the content can fit entirely whitin the viewport.
 
Steps to reproduce:
 
using DataBindingZoomAndPanSample solution,
1. set VerticalScrollBarVisibility & HorizontalScrollBarVisibility to Auto
2. zoom out until the content fit entirely whin the viewport
3. drag-zoom -> scale is ok, but offset is not
 
cheers
Tom
GeneralRe: Drag-zooming does not work fine when VerticalScrollBarVisibility & HorizontalScrollBarVisibility set to AutomemberAshley Davis23 Mar '12 - 20:25 
Hey, thanks for the update.
 
I just tested this according to the steps you listed. The offset actually works exactly the way that I intended it to, if this isn't what you need then you will have to modify the code. If you do that and you make it an option I'll be happy to include your modifications in the article's code.
 
I did notice a problem with the overview window however, the 'yellow viewport' area is moved around even though the main window doesn't allow this, so thanks for bring that to my attention.
 
Cheers
Ash
GeneralRe: Drag-zoomingmemberThomas Polaert26 Mar '12 - 23:33 
Hi,
 
That's strange. I double-checked with a fresh download.
Please take a look at the screen capture below:
http://vimeo.com/39255703[^]
 
cheers
tom
GeneralRe: Drag-zoomingmemberAshley Davis7 May '12 - 17:57 
Hi, thanks for going to the trouble of making a video to illustrate the problem. What software did you use to create the video?
 
It is easy to see the point you are making after watching the video.
 
I did some debugging and I suspect the problem occurs because the zoom-in animation depends on the viewport dimensions. By the time the animation has finished, eg by the time the zoom-in has finished, the viewport dimensions have changed because the scrollbars have been displayed. So things are kind of out whack. One way to fix this problem would be to update the animation based on the changing viewport dimensions as the animation is playing, although I'm not sure of exactly how I could do this without taking over the animation from WPF (which I may have to do in the end).
 
If you are happy to use the non-animating zooming you can change ApplyDragZoomRect to call the non-animated zoom function zoomAndPanControl.ZoomTo(...) then this will fix the problem for you.
 
Anyway, right at the moment I don't have a proper solution that will fix the animated version. I'll think about it for bit and get back to you if I find a fix.
 
Cheers
Ash

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Permalink | Advertise | Privacy | Mobile
Web03 | 2.6.130516.1 | Last Updated 21 Mar 2011
Article Copyright 2010 by Ashley Davis
Everything else Copyright © CodeProject, 1999-2013
Terms of Use
Layout: fixed | fluid