using System; using System.ComponentModel; using System.Runtime.CompilerServices; using System.Windows; using System.Windows.Controls; using System.Windows.Controls.Primitives; using System.Windows.Media; namespace Bro.Common.ImageCanvas { /// /// A class that wraps up zooming and panning of it's content. /// public partial class ZoomAndPanControl : ContentControl, IScrollInfo, INotifyPropertyChanged { #region Fields /// /// Reference to the underlying content, which is named PART_Content in the template. /// private FrameworkElement _content = null; /// /// The transform that is applied to the content to scale it by 'ViewportZoom'. /// private ScaleTransform _contentZoomTransform = null; /// /// The transform that is applied to the content to offset it by 'ContentOffsetX' and 'ContentOffsetY'. /// private TranslateTransform _contentOffsetTransform = null; /// /// The height of the viewport in content coordinates, clamped to the height of the content. /// private double _constrainedContentViewportHeight = 0.0; /// /// The width of the viewport in content coordinates, clamped to the width of the content. /// private double _constrainedContentViewportWidth = 0.0; /// /// Normally when content offsets changes the content focus is automatically updated. /// This syncronization 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 = false; /// /// 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 = false; /// /// Used to disable syncronization between IScrollInfo interface and ContentOffsetX/ContentOffsetY. /// private bool _disableScrollOffsetSync = false; #endregion #region constructor and overrides /// /// Static constructor to define metadata for the control (and link it to the style in Generic.xaml). /// static ZoomAndPanControl() { DefaultStyleKeyProperty.OverrideMetadata(typeof(ZoomAndPanControl), new FrameworkPropertyMetadata(typeof(ZoomAndPanControl))); } /// /// Need to update zoom values if size changes, and update ViewportZoom if too low /// protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo) { base.OnRenderSizeChanged(sizeInfo); if (sizeInfo.NewSize.Width <= 1 || sizeInfo.NewSize.Height <= 1) return; switch (_currentZoomTypeEnum) { case CurrentZoomTypeEnum.Fit: InternalViewportZoom = ViewportHelpers.FitZoom(sizeInfo.NewSize.Width, sizeInfo.NewSize.Height, _content?.ActualWidth, _content?.ActualHeight); break; case CurrentZoomTypeEnum.Fill: InternalViewportZoom = ViewportHelpers.FillZoom(sizeInfo.NewSize.Width, sizeInfo.NewSize.Height, _content?.ActualWidth, _content?.ActualHeight); break; } if (InternalViewportZoom < MinimumZoomClamped) InternalViewportZoom = MinimumZoomClamped; // // INotifyPropertyChanged property update // OnPropertyChanged(nameof(MinimumZoomClamped)); OnPropertyChanged(nameof(FillZoomValue)); OnPropertyChanged(nameof(FitZoomValue)); } /// /// Called when a template has been applied to the control. /// public override void OnApplyTemplate() { base.OnApplyTemplate(); _content = Template.FindName("PART_Content", this) as FrameworkElement; if (_content != null) { // // Setup the transform on the content so that we can scale it by 'ViewportZoom'. // _contentZoomTransform = new ScaleTransform(InternalViewportZoom, InternalViewportZoom); // // Setup the transform on the content so that we can translate it by 'ContentOffsetX' and 'ContentOffsetY'. // _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'. // var transformGroup = new TransformGroup(); transformGroup.Children.Add(_contentOffsetTransform); transformGroup.Children.Add(_contentZoomTransform); _content.RenderTransform = transformGroup; ZoomAndPanControl_EventHandlers_OnApplyTemplate(); } } /// /// Measure the control and it's children. /// protected override Size MeasureOverride(Size constraint) { var infiniteSize = new Size(double.PositiveInfinity, double.PositiveInfinity); var childSize = base.MeasureOverride(infiniteSize); if (childSize != _unScaledExtent) { // // Use the size of the child as the un-scaled extent content. // _unScaledExtent = childSize; ScrollOwner?.InvalidateScrollInfo(); } // // Update the size of the viewport onto the content based on the passed in 'constraint'. // UpdateViewportSize(constraint); var width = constraint.Width; var height = constraint.Height; if (double.IsInfinity(width)) width = childSize.Width; if (double.IsInfinity(height)) height = childSize.Height; UpdateTranslationX(); UpdateTranslationY(); return new Size(width, height); } /// /// Arrange the control and it's children. /// protected override Size ArrangeOverride(Size arrangeBounds) { var size = base.ArrangeOverride(DesiredSize); if (_content.DesiredSize != _unScaledExtent) { // // Use the size of the child as the un-scaled extent content. // _unScaledExtent = _content.DesiredSize; ScrollOwner?.InvalidateScrollInfo(); } // // Update the size of the viewport onto the content based on the passed in 'arrangeBounds'. // UpdateViewportSize(arrangeBounds); return size; } #endregion #region IScrollInfo Data Members // // These data members are for the implementation of the IScrollInfo interface. // This interface works with the ScrollViewer such that when ZoomAndPanControl is // wrapped (in XAML) with a ScrollViewer the IScrollInfo interface allows the ZoomAndPanControl 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 // /// /// Records the unscaled extent of the content. /// This is calculated during the measure and arrange. /// private Size _unScaledExtent = new Size(0, 0); /// /// Records the size of the viewport (in viewport coordinates) onto the content. /// This is calculated during the measure and arrange. /// private Size _viewport = new Size(0, 0); #endregion IScrollInfo Data Members #region Dependency Property Definitions // // Definitions for dependency properties. // /// /// This allows the same property name be used for direct and indirect access to this control. /// public ZoomAndPanControl ZoomAndPanContent => this; /// /// 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); } } public static readonly DependencyProperty AnimationDurationProperty = DependencyProperty.Register("AnimationDuration", typeof(double), typeof(ZoomAndPanControl), new FrameworkPropertyMetadata(0.4)); /// /// The duration of the animations (in seconds) started by calling AnimatedZoomTo and the other animation methods. /// public ZoomAndPanInitialPositionEnum ZoomAndPanInitialPosition { get { return (ZoomAndPanInitialPositionEnum)GetValue(ZoomAndPanInitialPositionProperty); } set { SetValue(ZoomAndPanInitialPositionProperty, value); } } public static readonly DependencyProperty ZoomAndPanInitialPositionProperty = DependencyProperty.Register("ZoomAndPanInitialPosition", typeof(ZoomAndPanInitialPositionEnum), typeof(ZoomAndPanControl), new FrameworkPropertyMetadata(ZoomAndPanInitialPositionEnum.Default, ZoomAndPanInitialPositionChanged)); private static void ZoomAndPanInitialPositionChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { var zoomAndPanControl = (ZoomAndPanControl)d; // zoomAndPanControl.SetZoomAndPanInitialPosition(); } /// /// 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); } } public static readonly DependencyProperty ContentOffsetXProperty = DependencyProperty.Register("ContentOffsetX", typeof(double), typeof(ZoomAndPanControl), new FrameworkPropertyMetadata(0.0, ContentOffsetX_PropertyChanged, ContentOffsetX_Coerce)); /// /// 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); } } public static readonly DependencyProperty ContentOffsetYProperty = DependencyProperty.Register("ContentOffsetY", typeof(double), typeof(ZoomAndPanControl), new FrameworkPropertyMetadata(0.0, ContentOffsetY_PropertyChanged, ContentOffsetY_Coerce)); /// /// Get the viewport height, in content coordinates. /// public double ContentViewportHeight { get { return (double)GetValue(ContentViewportHeightProperty); } set { SetValue(ContentViewportHeightProperty, value); } } public static readonly DependencyProperty ContentViewportHeightProperty = DependencyProperty.Register("ContentViewportHeight", typeof(double), typeof(ZoomAndPanControl), new FrameworkPropertyMetadata(0.0)); /// /// Get the viewport width, in content coordinates. /// public double ContentViewportWidth { get { return (double)GetValue(ContentViewportWidthProperty); } set { SetValue(ContentViewportWidthProperty, value); } } public static readonly DependencyProperty ContentViewportWidthProperty = DependencyProperty.Register("ContentViewportWidth", typeof(double), typeof(ZoomAndPanControl), new FrameworkPropertyMetadata(0.0)); /// /// 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); } } public static readonly DependencyProperty ContentZoomFocusXProperty = DependencyProperty.Register("ContentZoomFocusX", typeof(double), typeof(ZoomAndPanControl), new FrameworkPropertyMetadata(0.0)); /// /// 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); } } public static readonly DependencyProperty ContentZoomFocusYProperty = DependencyProperty.Register("ContentZoomFocusY", typeof(double), typeof(ZoomAndPanControl), new FrameworkPropertyMetadata(0.0)); /// /// 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); } } public static readonly DependencyProperty IsMouseWheelScrollingEnabledProperty = DependencyProperty.Register("IsMouseWheelScrollingEnabled", typeof(bool), typeof(ZoomAndPanControl), new FrameworkPropertyMetadata(false)); /// /// Get/set the maximum value for 'ViewportZoom'. /// public double MaximumZoom { get { return (double)GetValue(MaximumZoomProperty); } set { SetValue(MaximumZoomProperty, value); } } public static readonly DependencyProperty MaximumZoomProperty = DependencyProperty.Register("MaximumZoom", typeof(double), typeof(ZoomAndPanControl), new FrameworkPropertyMetadata(10.0, MinimumOrMaximumZoom_PropertyChanged)); /// /// Get/set the maximum value for 'ViewportZoom'. /// public MinimumZoomTypeEnum MinimumZoomType { get { return (MinimumZoomTypeEnum)GetValue(MinimumZoomTypeProperty); } set { SetValue(MinimumZoomTypeProperty, value); } } public static readonly DependencyProperty MinimumZoomTypeProperty = DependencyProperty.Register("MinimumZoomType", typeof(MinimumZoomTypeEnum), typeof(ZoomAndPanControl), new FrameworkPropertyMetadata(MinimumZoomTypeEnum.MinimumZoom)); /// /// Get/set the MinimumZoom value for 'ViewportZoom'. /// public double MinimumZoom { get { return (double)GetValue(MinimumZoomProperty); } set { SetValue(MinimumZoomProperty, value); } } public static readonly DependencyProperty MinimumZoomProperty = DependencyProperty.Register("MinimumZoom", typeof(double), typeof(ZoomAndPanControl), new FrameworkPropertyMetadata(0.1, MinimumOrMaximumZoom_PropertyChanged)); /// /// Get/set the MinimumZoom value for 'ViewportZoom'. /// public Point? MousePosition { get { return (Point?)GetValue(MousePositionProperty); } set { SetValue(MousePositionProperty, value); } } public static readonly DependencyProperty MousePositionProperty = DependencyProperty.Register("MousePosition", typeof(Point?), typeof(ZoomAndPanControl), new FrameworkPropertyMetadata(null, MinimumOrMaximumZoom_PropertyChanged)); /// /// This is used for binding a slider to control the zoom. Cannot use the InternalUseAnimations because of all the /// assumptions in when the this property is changed. THIS IS NOT USED FOR THE ANIMATIONS /// public bool UseAnimations { get { return (bool)GetValue(UseAnimationsProperty); } set { SetValue(UseAnimationsProperty, value); } } public static readonly DependencyProperty UseAnimationsProperty = DependencyProperty.Register("UseAnimations", typeof(bool), typeof(ZoomAndPanControl), new FrameworkPropertyMetadata(true)); /// /// This is used for binding a slider to control the zoom. Cannot use the InternalViewportZoom because of all the /// assumptions in when the this property is changed. THIS IS NOT USED FOR THE ANIMATIONS /// public double ViewportZoom { get { return (double)GetValue(ViewportZoomProperty); } set { SetValue(ViewportZoomProperty, value); } } public static readonly DependencyProperty ViewportZoomProperty = DependencyProperty.Register("ViewportZoom", typeof(double), typeof(ZoomAndPanControl), new FrameworkPropertyMetadata(1.0, ViewportZoom_PropertyChanged)); /// /// 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); } } public static readonly DependencyProperty ViewportZoomFocusXProperty = DependencyProperty.Register("ViewportZoomFocusX", typeof(double), typeof(ZoomAndPanControl), new FrameworkPropertyMetadata(0.0)); /// /// 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); } } public static readonly DependencyProperty ViewportZoomFocusYProperty = DependencyProperty.Register("ViewportZoomFocusY", typeof(double), typeof(ZoomAndPanControl), new FrameworkPropertyMetadata(0.0)); #endregion Dependency Property Definitions #region 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 ViewportZoom property has changed. /// public event EventHandler ContentZoomChanged; #endregion #region Event Handlers /// /// This is required for the animations, but has issues if set by something like a slider. /// private double InternalViewportZoom { get { return (double)GetValue(InternalViewportZoomProperty); } set { SetValue(InternalViewportZoomProperty, value); } } private static readonly DependencyProperty InternalViewportZoomProperty = DependencyProperty.Register("InternalViewportZoom", typeof(double), typeof(ZoomAndPanControl), new FrameworkPropertyMetadata(1.0, InternalViewportZoom_PropertyChanged, InternalViewportZoom_Coerce)); /// /// Event raised when the 'ViewportZoom' property has changed value. /// private static void InternalViewportZoom_PropertyChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e) { var c = (ZoomAndPanControl)dependencyObject; if (c._contentZoomTransform != null) { // // Update the content scale transform whenever 'ViewportZoom' changes. // c._contentZoomTransform.ScaleX = c.InternalViewportZoom; c._contentZoomTransform.ScaleY = c.InternalViewportZoom; } // // Update the size of the viewport in content coordinates. // c.UpdateContentViewportSize(); if (c._enableContentOffsetUpdateFromScale) { try { // // Disable content focus syncronization. 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). // var viewportOffsetX = c.ViewportZoomFocusX - (c.ViewportWidth / 2); var viewportOffsetY = c.ViewportZoomFocusY - (c.ViewportHeight / 2); var contentOffsetX = viewportOffsetX / c.InternalViewportZoom; var contentOffsetY = viewportOffsetY / c.InternalViewportZoom; c.ContentOffsetX = (c.ContentZoomFocusX - (c.ContentViewportWidth / 2)) - contentOffsetX; c.ContentOffsetY = (c.ContentZoomFocusY - (c.ContentViewportHeight / 2)) - contentOffsetY; } finally { c._disableContentFocusSync = false; } } c.ContentZoomChanged?.Invoke(c, EventArgs.Empty); c.ViewportZoom = c.InternalViewportZoom; c.OnPropertyChanged(new DependencyPropertyChangedEventArgs(ViewportZoomProperty, c.ViewportZoom, c.InternalViewportZoom)); c.ScrollOwner?.InvalidateScrollInfo(); c.SetCurrentZoomTypeEnum(); c.RaiseCanExecuteChanged(); } /// /// Method called to clamp the 'ViewportZoom' value to its valid range. /// private static object InternalViewportZoom_Coerce(DependencyObject dependencyObject, object baseValue) { var c = (ZoomAndPanControl)dependencyObject; var value = Math.Max((double)baseValue, c.MinimumZoomClamped); switch (c.MinimumZoomType) { case MinimumZoomTypeEnum.FitScreen: value = Math.Min(Math.Max(value, c.FitZoomValue), c.MaximumZoom); break; case MinimumZoomTypeEnum.FillScreen: value = Math.Min(Math.Max(value, c.FillZoomValue), c.MaximumZoom); break; case MinimumZoomTypeEnum.MinimumZoom: value = Math.Min(Math.Max(value, c.MinimumZoom), c.MaximumZoom); break; default: throw new ArgumentOutOfRangeException(); } return value; } #endregion #region DependencyProperty Event Handlers /// /// Event raised 'MinimumZoom' or 'MaximumZoom' has changed. /// private static void MinimumOrMaximumZoom_PropertyChanged(DependencyObject o, DependencyPropertyChangedEventArgs e) { var c = (ZoomAndPanControl)o; c.InternalViewportZoom = Math.Min(Math.Max(c.InternalViewportZoom, c.MinimumZoomClamped), c.MaximumZoom); } /// /// Event raised when the 'ContentOffsetX' property has changed value. /// private static void ContentOffsetX_PropertyChanged(DependencyObject o, DependencyPropertyChangedEventArgs e) { var c = (ZoomAndPanControl)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(); // // Raise an event to let users of the control know that the content offset has changed. // c.ContentOffsetXChanged?.Invoke(c, EventArgs.Empty); if (!c._disableScrollOffsetSync) // // Notify the owning ScrollViewer that the scrollbar offsets should be updated. // c.ScrollOwner?.InvalidateScrollInfo(); } static readonly double _allowedDist = 0; /// /// Method called to clamp the 'ContentOffsetX' value to its valid range. /// private static object ContentOffsetX_Coerce(DependencyObject d, object baseValue) { var c = (ZoomAndPanControl)d; var value = (double)baseValue; var minOffsetX = 0 - _allowedDist; var maxOffsetX = Math.Max(0.0, c._unScaledExtent.Width - c._constrainedContentViewportWidth) + _allowedDist; value = Math.Min(Math.Max(value, minOffsetX), maxOffsetX); return value; //return (double)baseValue; } /// /// Event raised when the 'ContentOffsetY' property has changed value. /// private static void ContentOffsetY_PropertyChanged(DependencyObject o, DependencyPropertyChangedEventArgs e) { var c = (ZoomAndPanControl)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._disableScrollOffsetSync) // // Notify the owning ScrollViewer that the scrollbar offsets should be updated. // c.ScrollOwner?.InvalidateScrollInfo(); // // Raise an event to let users of the control know that the content offset has changed. // c.ContentOffsetYChanged?.Invoke(c, EventArgs.Empty); } /// /// Method called to clamp the 'ContentOffsetY' value to its valid range. /// private static object ContentOffsetY_Coerce(DependencyObject d, object baseValue) { var c = (ZoomAndPanControl)d; var value = (double)baseValue; var minOffsetY = 0 - _allowedDist; var maxOffsetY = Math.Max(0.0, c._unScaledExtent.Height - c._constrainedContentViewportHeight) + _allowedDist; value = Math.Min(Math.Max(value, minOffsetY), maxOffsetY); return value; //return (double)baseValue; } private static void ViewportZoom_PropertyChanged(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs e) { var c = (ZoomAndPanControl)dependencyObject; var newZoom = (double)e.NewValue; if (c.InternalViewportZoom != newZoom) { var centerPoint = new Point(c.ContentOffsetX + (c._constrainedContentViewportWidth / 2), c.ContentOffsetY + (c._constrainedContentViewportHeight / 2)); c.ZoomAboutPoint(newZoom, centerPoint); } } #endregion /// /// 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) 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. // ContentOffsetX = ContentOffsetX; ContentOffsetY = ContentOffsetY; // // 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 'ViewportZoom' has changed. /// private void UpdateContentViewportSize() { ContentViewportWidth = ViewportWidth / InternalViewportZoom; ContentViewportHeight = ViewportHeight / InternalViewportZoom; _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 (_contentOffsetTransform != null) { var scaledContentWidth = _unScaledExtent.Width * InternalViewportZoom; if (scaledContentWidth < ViewportWidth) // // When the content can fit entirely within the viewport, center it. // _contentOffsetTransform.X = (ContentViewportWidth - _unScaledExtent.Width) / 2; else _contentOffsetTransform.X = -ContentOffsetX; } } /// /// Update the Y coordinate of the translation transformation. /// private void UpdateTranslationY() { if (_contentOffsetTransform != null) { var scaledContentHeight = _unScaledExtent.Height * InternalViewportZoom; if (scaledContentHeight < ViewportHeight) // // When the content can fit entirely within the viewport, center it. // _contentOffsetTransform.Y = (ContentViewportHeight - _unScaledExtent.Height) / 2; else _contentOffsetTransform.Y = -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); } public double FitZoomValue => ViewportHelpers.FitZoom(ActualWidth, ActualHeight, _content?.ActualWidth, _content?.ActualHeight); public double FillZoomValue => ViewportHelpers.FillZoom(ActualWidth, ActualHeight, _content?.ActualWidth, _content?.ActualHeight); public double MinimumZoomClamped => ((MinimumZoomType == MinimumZoomTypeEnum.FillScreen) ? FillZoomValue : (MinimumZoomType == MinimumZoomTypeEnum.FitScreen) ? FitZoomValue : MinimumZoom).ToRealNumber(); public event PropertyChangedEventHandler PropertyChanged; protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null) { PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); } private enum CurrentZoomTypeEnum { Fill, Fit, Other } private CurrentZoomTypeEnum _currentZoomTypeEnum; private void SetCurrentZoomTypeEnum() { if (ViewportZoom.IsWithinOnePercent(FitZoomValue)) _currentZoomTypeEnum = CurrentZoomTypeEnum.Fit; else if (ViewportZoom.IsWithinOnePercent(FillZoomValue)) _currentZoomTypeEnum = CurrentZoomTypeEnum.Fill; else _currentZoomTypeEnum = CurrentZoomTypeEnum.Other; } } }