// // This control is created by Ashley Davis and copyrighted under CPOL, and available as part of // a CodeProject article at // http://www.codeproject.com/KB/WPF/zoomandpancontrol.aspx // This code is based on the article dated: 29 Jun 2010 // using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Windows.Controls; using System.Windows; using System.Windows.Media; using System.Windows.Media.Animation; using System.Diagnostics; using System.Windows.Threading; using System.Windows.Controls.Primitives; namespace SharpVectors.Runtime { /// /// A class that wraps up zooming and panning of it's content. /// public partial class ZoomPanControl : ContentControl, IScrollInfo { #region General Private Fields /// /// Reference to the underlying content, which is named PART_Content in the template. /// private FrameworkElement content; /// /// The transform that is applied to the content to scale it by 'ContentScale'. /// private ScaleTransform contentScaleTransform; /// /// The transform that is applied to the content to offset it by 'ContentOffsetX' and 'ContentOffsetY'. /// private TranslateTransform contentOffsetTransform; /// /// Enable the update of the content offset as the content scale changes. /// This enabled for zooming about a point (Google-maps style zooming) and zooming to a rect. /// private bool enableContentOffsetUpdateFromScale; /// /// Used to disable synchronization between IScrollInfo interface and ContentOffsetX/ContentOffsetY. /// private bool disableScrollOffsetSync; /// /// Normally when content offsets changes the content focus is automatically updated. /// This synchronization is disabled when 'disableContentFocusSync' is set to 'true'. /// When we are zooming in or out we 'disableContentFocusSync' is set to 'true' because /// we are zooming in or out relative to the content focus we don't want to update the focus. /// private bool disableContentFocusSync; /// /// The width of the viewport in content coordinates, clamped to the width of the content. /// private double constrainedContentViewportWidth; /// /// The height of the viewport in content coordinates, clamped to the height of the content. /// private double constrainedContentViewportHeight; #endregion General Private Fields #region IScrollInfo Private Fields // // These data members are for the implementation of the IScrollInfo interface. // This interface works with the ScrollViewer such that when ZoomPanControl is // wrapped (in XAML) with a ScrollViewer the IScrollInfo interface allows the ZoomPanControl to // handle the the scrollbar offsets. // // The IScrollInfo properties and member functions are implemented in ZoomAndPanControl_IScrollInfo.cs. // // There is a good series of articles showing how to implement IScrollInfo starting here: // http://blogs.msdn.com/bencon/archive/2006/01/05/509991.aspx // /// /// Set to 'true' when the vertical scrollbar is enabled. /// private bool canVerticallyScroll; /// /// Set to 'true' when the vertical scrollbar is enabled. /// private bool canHorizontallyScroll; /// /// Records the unscaled extent of the content. /// This is calculated during the measure and arrange. /// private Size unScaledExtent; /// /// Records the size of the viewport (in viewport coordinates) onto the content. /// This is calculated during the measure and arrange. /// private Size viewport; /// /// Reference to the ScrollViewer that is wrapped (in XAML) around the ZoomPanControl. /// Or set to null if there is no ScrollViewer. /// private ScrollViewer scrollOwner; #endregion IScrollInfo Private Fields #region Dependency Property Definitions // // Definitions for dependency properties. // public static readonly DependencyProperty ContentScaleProperty = DependencyProperty.Register("ContentScale", typeof(double), typeof(ZoomPanControl), new FrameworkPropertyMetadata(1.0, ContentScale_PropertyChanged, ContentScale_Coerce)); public static readonly DependencyProperty MinContentScaleProperty = DependencyProperty.Register("MinContentScale", typeof(double), typeof(ZoomPanControl), new FrameworkPropertyMetadata(0.01, MinOrMaxContentScale_PropertyChanged)); public static readonly DependencyProperty MaxContentScaleProperty = DependencyProperty.Register("MaxContentScale", typeof(double), typeof(ZoomPanControl), new FrameworkPropertyMetadata(10.0, MinOrMaxContentScale_PropertyChanged)); public static readonly DependencyProperty ContentOffsetXProperty = DependencyProperty.Register("ContentOffsetX", typeof(double), typeof(ZoomPanControl), new FrameworkPropertyMetadata(0.0, ContentOffsetX_PropertyChanged, ContentOffsetX_Coerce)); public static readonly DependencyProperty ContentOffsetYProperty = DependencyProperty.Register("ContentOffsetY", typeof(double), typeof(ZoomPanControl), new FrameworkPropertyMetadata(0.0, ContentOffsetY_PropertyChanged, ContentOffsetY_Coerce)); public static readonly DependencyProperty AnimationDurationProperty = DependencyProperty.Register("AnimationDuration", typeof(double), typeof(ZoomPanControl), new FrameworkPropertyMetadata(0.4 )); public static readonly DependencyProperty ContentZoomFocusXProperty = DependencyProperty.Register("ContentZoomFocusX", typeof(double), typeof(ZoomPanControl), new FrameworkPropertyMetadata(0.0)); public static readonly DependencyProperty ContentZoomFocusYProperty = DependencyProperty.Register("ContentZoomFocusY", typeof(double), typeof(ZoomPanControl), new FrameworkPropertyMetadata(0.0)); public static readonly DependencyProperty ViewportZoomFocusXProperty = DependencyProperty.Register("ViewportZoomFocusX", typeof(double), typeof(ZoomPanControl), new FrameworkPropertyMetadata(0.0)); public static readonly DependencyProperty ViewportZoomFocusYProperty = DependencyProperty.Register("ViewportZoomFocusY", typeof(double), typeof(ZoomPanControl), new FrameworkPropertyMetadata(0.0)); public static readonly DependencyProperty ContentViewportWidthProperty = DependencyProperty.Register("ContentViewportWidth", typeof(double), typeof(ZoomPanControl), new FrameworkPropertyMetadata(0.0)); public static readonly DependencyProperty ContentViewportHeightProperty = DependencyProperty.Register("ContentViewportHeight", typeof(double), typeof(ZoomPanControl), new FrameworkPropertyMetadata(0.0)); public static readonly DependencyProperty IsMouseWheelScrollingEnabledProperty = DependencyProperty.Register("IsMouseWheelScrollingEnabled", typeof(bool), typeof(ZoomPanControl), new FrameworkPropertyMetadata(false)); #endregion Dependency Property Definitions #region Constructors and Destructor public ZoomPanControl() { unScaledExtent = new Size(0, 0); viewport = new Size(0, 0); } /// /// Static constructor to define metadata for the control (and link it to the style in Generic.xaml). /// static ZoomPanControl() { DefaultStyleKeyProperty.OverrideMetadata(typeof(ZoomPanControl), new FrameworkPropertyMetadata(typeof(ZoomPanControl))); } #endregion #region Public Events /// /// Event raised when the ContentOffsetX property has changed. /// public event EventHandler ContentOffsetXChanged; /// /// Event raised when the ContentOffsetY property has changed. /// public event EventHandler ContentOffsetYChanged; /// /// Event raised when the ContentScale property has changed. /// public event EventHandler ContentScaleChanged; #endregion #region Public Properties /// /// Get/set the X offset (in content coordinates) of the view on the content. /// public double ContentOffsetX { get { return (double)GetValue(ContentOffsetXProperty); } set { SetValue(ContentOffsetXProperty, value); } } /// /// Get/set the Y offset (in content coordinates) of the view on the content. /// public double ContentOffsetY { get { return (double)GetValue(ContentOffsetYProperty); } set { SetValue(ContentOffsetYProperty, value); } } /// /// Get/set the current scale (or zoom factor) of the content. /// public double ContentScale { get { return (double)GetValue(ContentScaleProperty); } set { SetValue(ContentScaleProperty, value); } } /// /// Get/set the minimum value for 'ContentScale'. /// public double MinContentScale { get { return (double)GetValue(MinContentScaleProperty); } set { SetValue(MinContentScaleProperty, value); } } /// /// Get/set the maximum value for 'ContentScale'. /// public double MaxContentScale { get { return (double)GetValue(MaxContentScaleProperty); } set { SetValue(MaxContentScaleProperty, value); } } /// /// The X coordinate of the content focus, this is the point that we are focusing on when zooming. /// public double ContentZoomFocusX { get { return (double)GetValue(ContentZoomFocusXProperty); } set { SetValue(ContentZoomFocusXProperty, value); } } /// /// The Y coordinate of the content focus, this is the point that we are focusing on when zooming. /// public double ContentZoomFocusY { get { return (double)GetValue(ContentZoomFocusYProperty); } set { SetValue(ContentZoomFocusYProperty, value); } } /// /// The X coordinate of the viewport focus, this is the point in the viewport (in viewport coordinates) /// that the content focus point is locked to while zooming in. /// public double ViewportZoomFocusX { get { return (double)GetValue(ViewportZoomFocusXProperty); } set { SetValue(ViewportZoomFocusXProperty, value); } } /// /// The Y coordinate of the viewport focus, this is the point in the viewport (in viewport coordinates) /// that the content focus point is locked to while zooming in. /// public double ViewportZoomFocusY { get { return (double)GetValue(ViewportZoomFocusYProperty); } set { SetValue(ViewportZoomFocusYProperty, value); } } /// /// The duration of the animations (in seconds) started by calling AnimatedZoomTo and the other animation methods. /// public double AnimationDuration { get { return (double)GetValue(AnimationDurationProperty); } set { SetValue(AnimationDurationProperty, value); } } /// /// Get the viewport width, in content coordinates. /// public double ContentViewportWidth { get { return (double)GetValue(ContentViewportWidthProperty); } set { SetValue(ContentViewportWidthProperty, value); } } /// /// Get the viewport height, in content coordinates. /// public double ContentViewportHeight { get { return (double)GetValue(ContentViewportHeightProperty); } set { SetValue(ContentViewportHeightProperty, value); } } /// /// Set to 'true' to enable the mouse wheel to scroll the zoom and pan control. /// This is set to 'false' by default. /// public bool IsMouseWheelScrollingEnabled { get { return (bool)GetValue(IsMouseWheelScrollingEnabledProperty); } set { SetValue(IsMouseWheelScrollingEnabledProperty, value); } } #endregion #region Public Methods /// /// Do an animated zoom to view a specific scale and rectangle (in content coordinates). /// public void AnimatedZoomTo(double newScale, Rect contentRect) { AnimatedZoomPointToViewportCenter(newScale, new Point(contentRect.X + (contentRect.Width / 2), contentRect.Y + (contentRect.Height / 2)), delegate(object sender, EventArgs e) { // // At the end of the animation, ensure that we are snapped to the specified content offset. // Due to zooming in on the content focus point and rounding errors, the content offset may // be slightly off what we want at the end of the animation and this bit of code corrects it. // this.ContentOffsetX = contentRect.X; this.ContentOffsetY = contentRect.Y; }); } /// /// Do an animated zoom to the specified rectangle (in content coordinates). /// 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); } /// /// Instantly zoom to the specified rectangle (in content coordinates). /// public void ZoomTo(Rect contentRect) { double scaleX = this.ContentViewportWidth / contentRect.Width; double scaleY = this.ContentViewportHeight / contentRect.Height; double newScale = this.ContentScale * Math.Min(scaleX, scaleY); ZoomPointToViewportCenter(newScale, new Point(contentRect.X + (contentRect.Width / 2), contentRect.Y + (contentRect.Height / 2))); } /// /// Instantly center the view on the specified point (in content coordinates). /// public void SnapTo(Point contentPoint) { ZoomPanAnimationHelper.CancelAnimation(this, ContentOffsetXProperty); ZoomPanAnimationHelper.CancelAnimation(this, ContentOffsetYProperty); this.ContentOffsetX = contentPoint.X - (this.ContentViewportWidth / 2); this.ContentOffsetY = contentPoint.Y - (this.ContentViewportHeight / 2); } /// /// Use animation to center the view on the specified point (in content coordinates). /// public void AnimatedSnapTo(Point contentPoint) { double newX = contentPoint.X - (this.ContentViewportWidth / 2); double newY = contentPoint.Y - (this.ContentViewportHeight / 2); ZoomPanAnimationHelper.StartAnimation(this, ContentOffsetXProperty, newX, AnimationDuration); ZoomPanAnimationHelper.StartAnimation(this, ContentOffsetYProperty, newY, AnimationDuration); } /// /// Zoom in/out centered on the specified point (in content coordinates). /// The focus point is kept locked to it's on screen position (ala google maps). /// public void AnimatedZoomAboutPoint(double newContentScale, Point contentZoomFocus) { newContentScale = Math.Min(Math.Max(newContentScale, MinContentScale), MaxContentScale); ZoomPanAnimationHelper.CancelAnimation(this, ContentZoomFocusXProperty); ZoomPanAnimationHelper.CancelAnimation(this, ContentZoomFocusYProperty); ZoomPanAnimationHelper.CancelAnimation(this, ViewportZoomFocusXProperty); ZoomPanAnimationHelper.CancelAnimation(this, ViewportZoomFocusYProperty); ContentZoomFocusX = contentZoomFocus.X; ContentZoomFocusY = contentZoomFocus.Y; ViewportZoomFocusX = (ContentZoomFocusX - ContentOffsetX) * ContentScale; ViewportZoomFocusY = (ContentZoomFocusY - ContentOffsetY) * ContentScale; // // When zooming about a point make updates to ContentScale also update content offset. // enableContentOffsetUpdateFromScale = true; ZoomPanAnimationHelper.StartAnimation(this, ContentScaleProperty, newContentScale, AnimationDuration, delegate(object sender, EventArgs e) { enableContentOffsetUpdateFromScale = false; ResetViewportZoomFocus(); }); } /// /// Zoom in/out centered on the specified point (in content coordinates). /// The focus point is kept locked to it's on screen position (ala google maps). /// public void ZoomAboutPoint(double newContentScale, Point contentZoomFocus) { newContentScale = Math.Min(Math.Max(newContentScale, MinContentScale), MaxContentScale); double screenSpaceZoomOffsetX = (contentZoomFocus.X - ContentOffsetX) * ContentScale; double screenSpaceZoomOffsetY = (contentZoomFocus.Y - ContentOffsetY) * ContentScale; double contentSpaceZoomOffsetX = screenSpaceZoomOffsetX / newContentScale; double contentSpaceZoomOffsetY = screenSpaceZoomOffsetY / newContentScale; double newContentOffsetX = contentZoomFocus.X - contentSpaceZoomOffsetX; double newContentOffsetY = contentZoomFocus.Y - contentSpaceZoomOffsetY; ZoomPanAnimationHelper.CancelAnimation(this, ContentScaleProperty); ZoomPanAnimationHelper.CancelAnimation(this, ContentOffsetXProperty); ZoomPanAnimationHelper.CancelAnimation(this, ContentOffsetYProperty); this.ContentScale = newContentScale; this.ContentOffsetX = newContentOffsetX; this.ContentOffsetY = newContentOffsetY; } /// /// Zoom in/out centered on the viewport center. /// public void AnimatedZoomTo(double contentScale) { Point zoomCenter = new Point(ContentOffsetX + (ContentViewportWidth / 2), ContentOffsetY + (ContentViewportHeight / 2)); AnimatedZoomAboutPoint(contentScale, zoomCenter); } /// /// Zoom in/out centered on the viewport center. /// public void ZoomTo(double contentScale) { Point zoomCenter = new Point(ContentOffsetX + (ContentViewportWidth / 2), ContentOffsetY + (ContentViewportHeight / 2)); ZoomAboutPoint(contentScale, zoomCenter); } /// /// Do animation that scales the content so that it fits completely in the control. /// public void AnimatedScaleToFit() { if (content == null) { throw new ApplicationException("PART_Content was not found in the ZoomPanControl visual template!"); } AnimatedZoomTo(new Rect(0, 0, content.ActualWidth, content.ActualHeight)); } /// /// Instantly scale the content so that it fits completely in the control. /// public void ScaleToFit() { if (content == null) { throw new ApplicationException("PART_Content was not found in the ZoomPanControl visual template!"); } ZoomTo(new Rect(0, 0, content.ActualWidth, content.ActualHeight)); } /// /// Called when a template has been applied to the control. /// public override void OnApplyTemplate() { base.OnApplyTemplate(); content = this.Template.FindName("PART_Content", this) as FrameworkElement; if (content != null) { // // Setup the transform on the content so that we can scale it by 'ContentScale'. // this.contentScaleTransform = new ScaleTransform(this.ContentScale, this.ContentScale); // // Setup the transform on the content so that we can translate it by 'ContentOffsetX' and 'ContentOffsetY'. // this.contentOffsetTransform = new TranslateTransform(); UpdateTranslationX(); UpdateTranslationY(); // // Setup a transform group to contain the translation and scale transforms, and then // assign this to the content's 'RenderTransform'. // TransformGroup transformGroup = new TransformGroup(); transformGroup.Children.Add(this.contentOffsetTransform); transformGroup.Children.Add(this.contentScaleTransform); content.RenderTransform = transformGroup; } } #endregion #region Protected Methods /// /// Measure the control and it's children. /// protected override Size MeasureOverride(Size constraint) { Size infiniteSize = new Size(double.PositiveInfinity, double.PositiveInfinity); Size childSize = base.MeasureOverride(infiniteSize); if (childSize != unScaledExtent) { // // Use the size of the child as the un-scaled extent content. // unScaledExtent = childSize; if (scrollOwner != null) { scrollOwner.InvalidateScrollInfo(); } } // // Update the size of the viewport onto the content based on the passed in 'constraint'. // UpdateViewportSize(constraint); double width = constraint.Width; double height = constraint.Height; if (double.IsInfinity(width)) { // // Make sure we don't return infinity! // width = childSize.Width; } if (double.IsInfinity(height)) { // // Make sure we don't return infinity! // height = childSize.Height; } return new Size(width, height); } /// /// Arrange the control and it's children. /// protected override Size ArrangeOverride(Size arrangeBounds) { Size size = base.ArrangeOverride(this.DesiredSize); if (content.DesiredSize != unScaledExtent) { // // Use the size of the child as the un-scaled extent content. // unScaledExtent = content.DesiredSize; if (scrollOwner != null) { scrollOwner.InvalidateScrollInfo(); } } // // Update the size of the viewport onto the content based on the passed in 'arrangeBounds'. // UpdateViewportSize(arrangeBounds); return size; } #endregion #region Private Methods /// /// Zoom to the specified scale and move the specified focus point to the center of the viewport. /// private void AnimatedZoomPointToViewportCenter(double newContentScale, Point contentZoomFocus, EventHandler callback) { newContentScale = Math.Min(Math.Max(newContentScale, MinContentScale), MaxContentScale); ZoomPanAnimationHelper.CancelAnimation(this, ContentZoomFocusXProperty); ZoomPanAnimationHelper.CancelAnimation(this, ContentZoomFocusYProperty); ZoomPanAnimationHelper.CancelAnimation(this, ViewportZoomFocusXProperty); ZoomPanAnimationHelper.CancelAnimation(this, ViewportZoomFocusYProperty); ContentZoomFocusX = contentZoomFocus.X; ContentZoomFocusY = contentZoomFocus.Y; ViewportZoomFocusX = (ContentZoomFocusX - ContentOffsetX) * ContentScale; ViewportZoomFocusY = (ContentZoomFocusY - ContentOffsetY) * ContentScale; // // When zooming about a point make updates to ContentScale also update content offset. // enableContentOffsetUpdateFromScale = true; ZoomPanAnimationHelper.StartAnimation(this, ContentScaleProperty, newContentScale, AnimationDuration, delegate(object sender, EventArgs e) { enableContentOffsetUpdateFromScale = false; if (callback != null) { callback(this, EventArgs.Empty); } }); ZoomPanAnimationHelper.StartAnimation(this, ViewportZoomFocusXProperty, ViewportWidth / 2, AnimationDuration); ZoomPanAnimationHelper.StartAnimation(this, ViewportZoomFocusYProperty, ViewportHeight / 2, AnimationDuration); } /// /// Zoom to the specified scale and move the specified focus point to the center of the viewport. /// private void ZoomPointToViewportCenter(double newContentScale, Point contentZoomFocus) { newContentScale = Math.Min(Math.Max(newContentScale, MinContentScale), MaxContentScale); ZoomPanAnimationHelper.CancelAnimation(this, ContentScaleProperty); ZoomPanAnimationHelper.CancelAnimation(this, ContentOffsetXProperty); ZoomPanAnimationHelper.CancelAnimation(this, ContentOffsetYProperty); this.ContentScale = newContentScale; this.ContentOffsetX = contentZoomFocus.X - (ContentViewportWidth / 2); this.ContentOffsetY = contentZoomFocus.Y - (ContentViewportHeight / 2); } /// /// Event raised when the 'ContentScale' property has changed value. /// private static void ContentScale_PropertyChanged(DependencyObject o, DependencyPropertyChangedEventArgs e) { ZoomPanControl c = (ZoomPanControl)o; if (c.contentScaleTransform != null) { // // Update the content scale transform whenever 'ContentScale' changes. // c.contentScaleTransform.ScaleX = c.ContentScale; c.contentScaleTransform.ScaleY = c.ContentScale; } // // Update the size of the viewport in content coordinates. // c.UpdateContentViewportSize(); if (c.enableContentOffsetUpdateFromScale) { try { // // Disable content focus synchronization. We are about to update content offset whilst zooming // to ensure that the viewport is focused on our desired content focus point. Setting this // to 'true' stops the automatic update of the content focus when content offset changes. // c.disableContentFocusSync = true; // // Whilst zooming in or out keep the content offset up-to-date so that the viewport is always // focused on the content focus point (and also so that the content focus is locked to the // viewport focus point - this is how the Google maps style zooming works). // 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; } finally { c.disableContentFocusSync = false; } } if (c.ContentScaleChanged != null) { c.ContentScaleChanged(c, EventArgs.Empty); } if (c.scrollOwner != null) { c.scrollOwner.InvalidateScrollInfo(); } } /// /// Method called to clamp the 'ContentScale' value to its valid range. /// private static object ContentScale_Coerce(DependencyObject d, object baseValue) { ZoomPanControl c = (ZoomPanControl)d; double value = (double) baseValue; value = Math.Min(Math.Max(value, c.MinContentScale), c.MaxContentScale); return value; } /// /// Event raised 'MinContentScale' or 'MaxContentScale' has changed. /// private static void MinOrMaxContentScale_PropertyChanged(DependencyObject o, DependencyPropertyChangedEventArgs e) { ZoomPanControl c = (ZoomPanControl)o; c.ContentScale = Math.Min(Math.Max(c.ContentScale, c.MinContentScale), c.MaxContentScale); } /// /// Event raised when the 'ContentOffsetX' property has changed value. /// private static void ContentOffsetX_PropertyChanged(DependencyObject o, DependencyPropertyChangedEventArgs e) { ZoomPanControl c = (ZoomPanControl)o; c.UpdateTranslationX(); if (!c.disableContentFocusSync) { // // Normally want to automatically update content focus when content offset changes. // Although this is disabled using 'disableContentFocusSync' when content offset changes due to in-progress zooming. // c.UpdateContentZoomFocusX(); } if (c.ContentOffsetXChanged != null) { // // Raise an event to let users of the control know that the content offset has changed. // c.ContentOffsetXChanged(c, EventArgs.Empty); } if (!c.disableScrollOffsetSync && c.scrollOwner != null) { // // Notify the owning ScrollViewer that the scrollbar offsets should be updated. // c.scrollOwner.InvalidateScrollInfo(); } } /// /// Method called to clamp the 'ContentOffsetX' value to its valid range. /// private static object ContentOffsetX_Coerce(DependencyObject d, object baseValue) { ZoomPanControl c = (ZoomPanControl)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; } /// /// Event raised when the 'ContentOffsetY' property has changed value. /// private static void ContentOffsetY_PropertyChanged(DependencyObject o, DependencyPropertyChangedEventArgs e) { ZoomPanControl c = (ZoomPanControl)o; c.UpdateTranslationY(); if (!c.disableContentFocusSync) { // // Normally want to automatically update content focus when content offset changes. // Although this is disabled using 'disableContentFocusSync' when content offset changes due to in-progress zooming. // c.UpdateContentZoomFocusY(); } if (c.ContentOffsetYChanged != null) { // // Raise an event to let users of the control know that the content offset has changed. // c.ContentOffsetYChanged(c, EventArgs.Empty); } if (!c.disableScrollOffsetSync && c.scrollOwner != null) { // // Notify the owning ScrollViewer that the scrollbar offsets should be updated. // c.scrollOwner.InvalidateScrollInfo(); } } /// /// Method called to clamp the 'ContentOffsetY' value to its valid range. /// private static object ContentOffsetY_Coerce(DependencyObject d, object baseValue) { ZoomPanControl c = (ZoomPanControl)d; double value = (double)baseValue; double minOffsetY = 0.0; double maxOffsetY = Math.Max(0.0, c.unScaledExtent.Height - c.constrainedContentViewportHeight); value = Math.Min(Math.Max(value, minOffsetY), maxOffsetY); return value; } /// /// Reset the viewport zoom focus to the center of the viewport. /// private void ResetViewportZoomFocus() { ViewportZoomFocusX = ViewportWidth / 2; ViewportZoomFocusY = ViewportHeight / 2; } /// /// Update the viewport size from the specified size. /// private void UpdateViewportSize(Size newSize) { if (viewport == newSize) { // // The viewport is already the specified size. // return; } viewport = newSize; // // Update the viewport size in content coordiates. // UpdateContentViewportSize(); // // Initialise the content zoom focus point. // UpdateContentZoomFocusX(); UpdateContentZoomFocusY(); // // Reset the viewport zoom focus to the center of the viewport. // ResetViewportZoomFocus(); // // Update content offset from itself when the size of the viewport changes. // This ensures that the content offset remains properly clamped to its valid range. // this.ContentOffsetX = this.ContentOffsetX; this.ContentOffsetY = this.ContentOffsetY; if (scrollOwner != null) { // // Tell that owning ScrollViewer that scrollbar data has changed. // scrollOwner.InvalidateScrollInfo(); } } /// /// Update the size of the viewport in content coordinates after the viewport size or 'ContentScale' has changed. /// private void UpdateContentViewportSize() { ContentViewportWidth = ViewportWidth / ContentScale; ContentViewportHeight = ViewportHeight / ContentScale; constrainedContentViewportWidth = Math.Min(ContentViewportWidth, unScaledExtent.Width); constrainedContentViewportHeight = Math.Min(ContentViewportHeight, unScaledExtent.Height); UpdateTranslationX(); UpdateTranslationY(); } /// /// Update the X coordinate of the translation transformation. /// private void UpdateTranslationX() { if (this.contentOffsetTransform != null) { double scaledContentWidth = this.unScaledExtent.Width * this.ContentScale; if (scaledContentWidth < this.ViewportWidth) { // // When the content can fit entirely within the viewport, center it. // this.contentOffsetTransform.X = (this.ContentViewportWidth - this.unScaledExtent.Width) / 2; } else { this.contentOffsetTransform.X = -this.ContentOffsetX; } } } /// /// Update the Y coordinate of the translation transformation. /// private void UpdateTranslationY() { if (this.contentOffsetTransform != null) { double scaledContentHeight = this.unScaledExtent.Height * this.ContentScale; if (scaledContentHeight < this.ViewportHeight) { // // When the content can fit entirely within the viewport, center it. // this.contentOffsetTransform.Y = (this.ContentViewportHeight - this.unScaledExtent.Height) / 2; } else { this.contentOffsetTransform.Y = -this.ContentOffsetY; } } } /// /// Update the X coordinate of the zoom focus point in content coordinates. /// private void UpdateContentZoomFocusX() { ContentZoomFocusX = ContentOffsetX + (constrainedContentViewportWidth / 2); } /// /// Update the Y coordinate of the zoom focus point in content coordinates. /// private void UpdateContentZoomFocusY() { ContentZoomFocusY = ContentOffsetY + (constrainedContentViewportHeight / 2); } #endregion Private Methods } }