Sample Code

Introduction

This article examines the use and implementation of a reusable WPFcustom 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.

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 ZoomAndPanControldependency 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:

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):

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:

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:

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:

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:

privatevoid 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 ZoomAndPanControlmethods 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:

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:

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:

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:

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:

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:

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.

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:

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:

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:

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:

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.

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):

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.

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:

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:

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:

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:

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:

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.

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:

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:

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:

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:

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 ZoomAndPanControlproperties 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 MIT License

Comments and Discussions

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