A feature-rich, production-ready Zoom and Pan control for WPF. Supports smooth animated zoom, mouse and keyboard panning, scroll-viewer integration, minimap/viewbox, undo/redo, and MVVM-friendly bindings.
- Features
- Screenshots
- Installation
- Quick Start
- XAML Example
- MVVM Usage
- Public API
- Architecture Overview
- How Zooming and Panning Work
- Customization
- Known Limitations
- Troubleshooting
- Contributing
- Development Setup
- Changelog
- License
- Smooth animated zoom with configurable duration (or instant, no-animation mode)
- Mouse-wheel zoom centered on the cursor position (Google Maps-style)
- Click-and-drag panning
- Drag-to-zoom: Shift + drag to draw a rectangle and zoom to it
- Shift + click to zoom in/out incrementally
- Fit / Fill the viewport with one command
- Zoom to a specific percentage or ratio from the minimum
- Undo / Redo zoom history with debounced auto-save
- ScrollViewer integration via
IScrollInfo— scrollbars work natively - ZoomAndPanScrollViewer wrapper for drop-in use with scrollbars
- ZoomAndPanViewBox minimap showing the current viewport position
- MVVM-friendly: exposes commands (
ZoomInCommand,ZoomOutCommand,FitCommand,FillCommand,UndoZoomCommand,RedoZoomCommand, etc.) and bindable properties (ViewportZoom,ContentOffsetX/Y,MousePosition, etc.) - Multi-targeting: .NET 6, 7, 8, and 10 (Windows)
- Configurable minimum zoom type: explicit value, fit-screen, or fill-screen
Place screenshots or an animated GIF here showing the control in action.
Suggested tools: ScreenToGif for recording, then add to a
docs/folder.
The package is published to NuGet with each new release: https://www.nuget.org/packages/ZoomAndPan.Wpf/
Install-Package ZoomAndPan.Wpf
dotnet add package ZoomAndPan.Wpf<PackageReference Include="ZoomAndPan.Wpf" Version="*" />Replace
*with the specific version you want to pin to. Check the NuGet page for the latest published version.
- Add the NuGet package (see above).
- Add the XML namespace to your XAML:
xmlns:zap="clr-namespace:ZoomAndPan.Wpf;assembly=ZoomAndPan.Wpf"- Drop in the control:
<zap:ZoomAndPanScrollViewer ZoomAndPanInitialPosition="FitScreen">
<Image Source="yourimage.png" />
</zap:ZoomAndPanScrollViewer>That's it. The control renders scrollbars, handles mouse-wheel zoom, and fits the image on load.
Below is a complete example showing common features and the control toolbar:
<Window xmlns:zap="clr-namespace:ZoomAndPan.Wpf;assembly=ZoomAndPan.Wpf"
xmlns:sys="clr-namespace:System;assembly=System.Runtime">
<!-- Keyboard shortcuts -->
<Window.InputBindings>
<KeyBinding Gesture="CTRL+OemPlus" Command="{Binding ElementName=viewer, Path=ZoomInCommand}" />
<KeyBinding Gesture="CTRL+OemMinus" Command="{Binding ElementName=viewer, Path=ZoomOutCommand}" />
<KeyBinding Gesture="CTRL+Z" Command="{Binding ElementName=viewer, Path=UndoZoomCommand}" />
<KeyBinding Gesture="CTRL+Y" Command="{Binding ElementName=viewer, Path=RedoZoomCommand}" />
</Window.InputBindings>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<!-- Main zoom/pan area -->
<zap:ZoomAndPanScrollViewer x:Name="viewer"
Grid.Row="0"
ZoomAndPanInitialPosition="FitScreen"
MinimumZoomType="FitScreen">
<Image Source="yourimage.png" />
</zap:ZoomAndPanScrollViewer>
<!-- Toolbar -->
<StackPanel Grid.Row="1" Orientation="Horizontal" HorizontalAlignment="Center" Margin="4">
<Button Content="Undo" Command="{Binding ElementName=viewer, Path=UndoZoomCommand}" Width="50" />
<Button Content="Redo" Command="{Binding ElementName=viewer, Path=RedoZoomCommand}" Width="50" />
<Button Content="Fit" Command="{Binding ElementName=viewer, Path=FitCommand}" Width="50" />
<Button Content="Fill" Command="{Binding ElementName=viewer, Path=FillCommand}" Width="50" />
<Button Content="100%" Command="{Binding ElementName=viewer, Path=ZoomPercentCommand}" Width="50" />
<Button Content="-" Command="{Binding ElementName=viewer, Path=ZoomOutCommand}" Width="30" />
<Slider Width="120"
Minimum="{Binding ElementName=viewer, Path=ZoomAndPanContent.MinimumZoomClamped}"
Maximum="10"
Value="{Binding ElementName=viewer, Path=ViewportZoom, Mode=TwoWay}" />
<Button Content="+" Command="{Binding ElementName=viewer, Path=ZoomInCommand}" Width="30" />
<TextBlock Text="{Binding ElementName=viewer, Path=ViewportZoom, StringFormat=p0}"
VerticalAlignment="Center" Width="55" TextAlignment="Right" />
</StackPanel>
</Grid>
</Window>| Input | Action |
|---|---|
| Left-drag | Pan the content |
| Mouse wheel | Zoom in/out centered on cursor |
| Shift + left-click | Zoom in by ~10% |
| Shift + right-click | Zoom out by ~9% |
| Shift + left-drag | Draw a zoom rectangle; releases to zoom to selection |
| Double-click | (ZoomAndPanViewBox) Snap/pan to that point |
Bind ViewportZoom two-way to your view model to programmatically control zoom:
<zap:ZoomAndPanScrollViewer
ViewportZoom="{Binding Zoom, Mode=TwoWay}"
ZoomAndPanInitialPosition="FitScreen">
<Canvas Width="{Binding CanvasWidth}" Height="{Binding CanvasHeight}">
<!-- your content -->
</Canvas>
</zap:ZoomAndPanScrollViewer>Expose commands directly from the XAML element name or relay them through your view model. All zoom/pan commands implement ICommand and update CanExecute automatically.
To programmatically navigate (e.g., from a button in your view model), access the inner ZoomAndPanContent property:
viewer.ZoomAndPanContent.AnimatedSnapTo(new Point(x, y));
viewer.ZoomAndPanContent.AnimatedZoomTo(new Rect(x, y, width, height));| Property | Type | Description |
|---|---|---|
ViewportZoom |
double |
Current zoom level (1.0 = 100%). Bindable two-way. |
ContentOffsetX |
double |
Horizontal pan offset in content coordinates. |
ContentOffsetY |
double |
Vertical pan offset in content coordinates. |
ContentViewportWidth |
double |
Width of the visible area in content coordinates. |
ContentViewportHeight |
double |
Height of the visible area in content coordinates. |
MinimumZoom |
double |
Minimum allowed zoom (used when MinimumZoomType = MinimumZoom). Default: 0.1. |
MaximumZoom |
double |
Maximum allowed zoom. Default: 10.0. |
MinimumZoomType |
MinimumZoomTypeEnum |
How the minimum is calculated: MinimumZoom, FitScreen, or FillScreen. |
AnimationDuration |
double |
Duration of animated transitions in seconds. Default: 0.4. |
UseAnimations |
bool |
Enable or disable smooth animation. Default: true. |
ZoomAndPanInitialPosition |
ZoomAndPanInitialPositionEnum |
Initial zoom/position on load: Default, FitScreen, FillScreen, OneHundredPercentCentered. |
IsMouseWheelScrollingEnabled |
bool |
When true, mouse wheel scrolls instead of zooms. Default: false. |
MousePosition |
Point? |
Current mouse position in content coordinates (null when outside). |
| Property | Type | Description |
|---|---|---|
FitZoomValue |
double |
Zoom level at which the content exactly fits the viewport. |
FillZoomValue |
double |
Zoom level at which the content fills the viewport (no empty space). |
MinimumZoomClamped |
double |
Effective minimum zoom after applying MinimumZoomType. |
| Event | Description |
|---|---|
ContentZoomChanged |
Fires when the zoom level changes. |
ContentOffsetXChanged |
Fires when the horizontal pan offset changes. |
ContentOffsetYChanged |
Fires when the vertical pan offset changes. |
| Command | Description |
|---|---|
ZoomInCommand |
Zoom in by ~10% centered on the current viewport center. |
ZoomOutCommand |
Zoom out by ~9% centered on the current viewport center. |
FitCommand |
Zoom to fit the content within the viewport. |
FillCommand |
Zoom to fill the viewport with content. |
ZoomPercentCommand |
Zoom to a percentage (pass as CommandParameter; 100 = 100%). |
ZoomRatioFromMinimumCommand |
Zoom to a multiple of the minimum zoom (pass as CommandParameter; 2 = 2× minimum). |
UndoZoomCommand |
Restore the previous zoom/pan state. |
RedoZoomCommand |
Re-apply an undone zoom/pan state. |
| Method | Description |
|---|---|
AnimatedZoomTo(double zoom) |
Animated zoom to a specific scale, centered on the content center. |
AnimatedZoomTo(Rect rect) |
Animated zoom to fit a content rectangle into the viewport. |
AnimatedZoomTo(double zoom, Rect rect) |
Animated zoom to a scale and rectangle. |
ZoomTo(double zoom) |
Instant zoom to a specific scale. |
ZoomTo(Rect rect) |
Instant zoom to fit a content rectangle. |
ZoomAboutPoint(double zoom, Point focus) |
Instant zoom keeping a content point fixed on screen. |
AnimatedZoomAboutPoint(double zoom, Point focus) |
Animated zoom keeping a content point fixed on screen. |
AnimatedZoomToCentered(double zoom) |
Animated zoom centered on the content center. |
SnapTo(Point contentPoint) |
Instantly pan so a content point is centered in the viewport. |
AnimatedSnapTo(Point contentPoint) |
Animated pan to center a content point in the viewport. |
SnapContentOffsetTo(Point offset) |
Instantly set the content offset. |
AnimatedScaleToFit() |
Animated zoom to fit the content. |
ScaleToFit() |
Instant zoom to fit the content. |
SaveZoom() |
Push the current state onto the undo stack. |
A ScrollViewer subclass that hosts a ZoomAndPanControl internally. Use this as your primary control in most scenarios. Exposes the same commands and key properties as ZoomAndPanControl, plus:
| Property | Description |
|---|---|
ZoomAndPanContent |
Access to the inner ZoomAndPanControl for advanced programmatic control. |
A minimap control that shows a thumbnail of the content with a draggable rectangle representing the current viewport.
Setup: Set the DataContext to the ZoomAndPanControl or ZoomAndPanScrollViewer instance, and set Visual to the content element being displayed.
<zap:ZoomAndPanViewBox DataContext="{Binding ElementName=viewer}"
Visual="{Binding ElementName=myContent}" />MinimumZoomTypeEnum
MinimumZoom— use theMinimumZoomproperty valueFitScreen— minimum is the zoom level that fits the contentFillScreen— minimum is the zoom level that fills the viewport
ZoomAndPanInitialPositionEnum
Default— no initial positioningFitScreen— fit content to viewport on loadFillScreen— fill viewport with content on loadOneHundredPercentCentered— 1:1 zoom centered on content
The library is split into three controls and several helpers:
ZoomAndPan.Wpf/
├── ZoomAndPanControl.cs # Core control (ContentControl + IScrollInfo + INPC)
│ ├── ZoomAndPanControl_EventHandlers.cs # Mouse events and commands
│ ├── ZoomAndPanControl_IScrollInfo.cs # IScrollInfo implementation (ScrollViewer integration)
│ ├── ZoomAndPanControl_Methods.cs # Public zoom/pan API
│ └── ZoomAndPanControl_UndoRedo.cs # Undo/Redo history
├── ZoomAndPanScrollViewer.cs # ScrollViewer wrapper (primary consumer control)
├── ZoomAndPanViewBox.cs # Minimap / viewport indicator
├── AnimationHelper.cs # DoubleAnimation helper
├── KeepAliveTimer.cs # Debouncing timer for undo saves
├── ViewportHelpers.cs # Math helpers (clamp, clip, fit/fill zoom)
├── VisualTreeHelpers.cs # Visual tree traversal
├── RelayCommand.cs / RelayCommandGeneric.cs # ICommand implementations
├── ZoomAdjustConverter.cs # Log-scale converter for zoom sliders
├── ZoomAndPanViewBoxClampConverter.cs # Minimap viewport rectangle sizing
└── *Enum.cs # MouseHandlingModeEnum, MinimumZoomTypeEnum, etc.
The control uses a partial class pattern across five files to keep the code organized. The main ZoomAndPanControl inherits from ContentControl, IScrollInfo, and INotifyPropertyChanged.
Internally, the content element's RenderTransform is set to a TransformGroup containing:
- A
TranslateTransform(driven byContentOffsetX/Y) - A
ScaleTransform(driven byInternalViewportZoom)
Template parts required:
PART_Content— theContentPresenterthat receives the transformsPART_DragZoomCanvas— overlay canvas for the drag-zoom rectanglePART_DragZoomBorder— the visible drag-zoom rectangle
Zoom is stored in InternalViewportZoom (a private dependency property used for animations) and exposed publicly as ViewportZoom. The coerce callback on InternalViewportZoom clamps it between MinimumZoomClamped and MaximumZoom.
When zooming about a point (e.g., mouse wheel), the control:
- Records the content-space focus point (
ContentZoomFocusX/Y) - Records the viewport-space anchor (
ViewportZoomFocusX/Y) - Sets
_enableContentOffsetUpdateFromScale = true - Animates
InternalViewportZoom— the property change handler then updatesContentOffsetX/Yto keep the focus point fixed on screen
This is the Google Maps-style zoom-about-point algorithm.
Panning updates ContentOffsetX/Y directly. A coerce callback on these properties clamps them so the content never scrolls beyond its boundaries. The TranslateTransform is updated immediately on each change.
When the content is smaller than the viewport, the control centers it automatically — no offset is applied and the translate transform is calculated to center the content.
ZoomAndPanControl implements IScrollInfo so it can be placed inside a standard ScrollViewer. The extent reports the content size multiplied by the current zoom. Scrollbar offsets are converted to/from content coordinates on every update.
Two Stack<UndoRedoStackItem> instances track zoom states. Each item stores ContentOffsetX/Y, ContentViewportWidth/Height, and ViewportZoom. State is pushed:
- Immediately on mouse-down (for discrete operations)
- After 750 ms of inactivity (for scroll-bar drags)
- After 1500 ms of inactivity (for zoom button holds)
Both ZoomAndPanControl and ZoomAndPanScrollViewer are lookless controls — they read their appearance entirely from a ControlTemplate. The sample project (ZoomAndPan.Wpf.Sample/MainWindow.xaml) contains a complete reference template.
Copy the template into your App.xaml or a ResourceDictionary to override the defaults.
The drag-zoom border uses {TemplateBinding Background} and {TemplateBinding BorderBrush}. Override these on the control instance or in your template:
<zap:ZoomAndPanScrollViewer Background="#20007ACC" BorderBrush="#007ACC" ... />For a slider that feels linear to the user, use the built-in ZoomAdjustConverter:
<Slider>
<Slider.Value>
<Binding Path="ViewportZoom" Mode="TwoWay"
Converter="{zap:ZoomAdjustConverter}" />
</Slider.Value>
</Slider><zap:ZoomAndPanScrollViewer UseAnimations="False" />- No touch/stylus support — the current implementation uses only mouse events. Touch pan and pinch-to-zoom are not implemented.
- AGPL-3.0 license — consumers of this library in network-facing applications must make their modified source available. Consider this before embedding the library in a commercial product.
- Content must be a
FrameworkElementforZoomAndPanInitialPositionandZoomAndPanViewBoxto work correctly (the control subscribes toSizeChanged). Content that is not aFrameworkElementis silently ignored. - Undo history is unlimited — there is no cap on the undo stack size. Very long sessions with frequent zoom changes will accumulate memory.
ZoomAndPanViewBox— requiresDataContextto be set to theZoomAndPanControlorZoomAndPanScrollViewerinstance; it does not auto-discover the control from the visual tree.
Content doesn't appear / appears clipped
- Ensure
PART_Contentis named correctly in yourControlTemplate. - Make sure the content element has a defined size (e.g., set
Width/Heighton aCanvasor use anImagewith an explicit source).
Zoom slider jumps or snaps
- Use the
ZoomAdjustConverterto apply a logarithmic scale so that slider movement feels linear across the full zoom range.
FitCommand / FillCommand are always disabled
- The commands disable themselves when the zoom is already within 1% of the fit/fill value. Verify that the content has a non-zero
ActualWidth/ActualHeightwhen the command is checked.
Mouse wheel scrolls instead of zooming
- Ensure
IsMouseWheelScrollingEnabledisfalse(the default). If you place the control in aScrollViewer, set this to control whether the wheel scrolls or zooms.
ZoomAndPanInitialPosition has no effect
- This is applied when the content's
SizeChangedfirst fires. If the content has a fixed size (e.g.,Width/Heightset in XAML), it fires during layout. If the content size changes later (e.g., anImageloaded async), the initial position is applied at that point. Ensure the content is aFrameworkElement.
Undo/Redo doesn't save the state I expect
- Call
SaveZoom()explicitly before any programmatic zoom operation to guarantee a state is captured.
Contributions are welcome! Please read CONTRIBUTING.md before submitting a pull request.
In short:
- Fork the repository
- Create a feature branch:
git checkout -b feature/my-feature - Make your changes and ensure the sample project runs correctly
- Submit a pull request
Prerequisites
- Visual Studio 2022+ or Rider with WPF support
- .NET 6 SDK or later (targeting net6/7/8/10-windows)
Clone and build
git clone https://github.com/thebibekbaniya/ZoomAndPan.git
cd ZoomAndPan
dotnet buildRun the sample
dotnet run --project ZoomAndPan.Wpf.SampleBuild the NuGet package
dotnet pack ZoomAndPan.Wpf/ZoomAndPan.Wpf.csproj -c ReleaseSee CHANGELOG.md for version history and release notes.
This project is licensed under the GNU Affero General Public License v3.0 (AGPL-3.0).
See the LICENSE file for the full text.
The RelayCommand.cs and RelayCommandGeneric.cs files are derived from work by Laurent Bugnion (GalaSoft) and Josh Smith, used under the MIT license.
|
Bibek Baniya Author & Maintainer |