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.
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;
} }
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>
-->
<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
.
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
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. |
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)
{
this.contentOffsetTransform.X = -this.ContentOffsetX +
((this.ContentViewportWidth - this.unScaledExtent.Width) / 2);
}
else
{
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.