Free cookie consent management tool by TermsFeed Policy Generator

source: branches/HeuristicLab.Problems.GrammaticalOptimization/SharpVectorRuntime/ZoomPanControl.cs @ 13332

Last change on this file since 13332 was 12762, checked in by aballeit, 9 years ago

#2283 GUI updates, Tree-chart, MCTS Version 2 (prune leaves)

File size: 41.5 KB
Line 
1// <copyright>
2// This control is created by Ashley Davis and copyrighted under CPOL, and available as part of
3// a CodeProject article at
4//    http://www.codeproject.com/KB/WPF/zoomandpancontrol.aspx
5// <date>This code is based on the article dated: 29 Jun 2010</date>
6// </copyright>
7
8using System;
9using System.Collections.Generic;
10using System.Linq;
11using System.Text;
12using System.Windows.Controls;
13using System.Windows;
14using System.Windows.Media;
15using System.Windows.Media.Animation;
16using System.Diagnostics;
17using System.Windows.Threading;
18using System.Windows.Controls.Primitives;
19
20namespace SharpVectors.Runtime
21{
22    /// <summary>
23    /// A class that wraps up zooming and panning of it's content.
24    /// </summary>
25    public partial class ZoomPanControl : ContentControl, IScrollInfo
26    {
27        #region General Private Fields
28
29        /// <summary>
30        /// Reference to the underlying content, which is named PART_Content in the template.
31        /// </summary>
32        private FrameworkElement content;
33
34        /// <summary>
35        /// The transform that is applied to the content to scale it by 'ContentScale'.
36        /// </summary>
37        private ScaleTransform contentScaleTransform;
38
39        /// <summary>
40        /// The transform that is applied to the content to offset it by 'ContentOffsetX' and 'ContentOffsetY'.
41        /// </summary>
42        private TranslateTransform contentOffsetTransform;
43
44        /// <summary>
45        /// Enable the update of the content offset as the content scale changes.
46        /// This enabled for zooming about a point (Google-maps style zooming) and zooming to a rect.
47        /// </summary>
48        private bool enableContentOffsetUpdateFromScale;
49
50        /// <summary>
51        /// Used to disable synchronization between IScrollInfo interface and ContentOffsetX/ContentOffsetY.
52        /// </summary>
53        private bool disableScrollOffsetSync;
54
55        /// <summary>
56        /// Normally when content offsets changes the content focus is automatically updated.
57        /// This synchronization is disabled when 'disableContentFocusSync' is set to 'true'.
58        /// When we are zooming in or out we 'disableContentFocusSync' is set to 'true' because
59        /// we are zooming in or out relative to the content focus we don't want to update the focus.
60        /// </summary>
61        private bool disableContentFocusSync;
62
63        /// <summary>
64        /// The width of the viewport in content coordinates, clamped to the width of the content.
65        /// </summary>
66        private double constrainedContentViewportWidth;
67
68        /// <summary>
69        /// The height of the viewport in content coordinates, clamped to the height of the content.
70        /// </summary>
71        private double constrainedContentViewportHeight;
72
73        #endregion General Private Fields
74
75        #region IScrollInfo Private Fields
76
77        //
78        // These data members are for the implementation of the IScrollInfo interface.
79        // This interface works with the ScrollViewer such that when ZoomPanControl is
80        // wrapped (in XAML) with a ScrollViewer the IScrollInfo interface allows the ZoomPanControl to
81        // handle the the scrollbar offsets.
82        //
83        // The IScrollInfo properties and member functions are implemented in ZoomAndPanControl_IScrollInfo.cs.
84        //
85        // There is a good series of articles showing how to implement IScrollInfo starting here:
86        //     http://blogs.msdn.com/bencon/archive/2006/01/05/509991.aspx
87        //
88
89        /// <summary>
90        /// Set to 'true' when the vertical scrollbar is enabled.
91        /// </summary>
92        private bool canVerticallyScroll;
93
94        /// <summary>
95        /// Set to 'true' when the vertical scrollbar is enabled.
96        /// </summary>
97        private bool canHorizontallyScroll;
98
99        /// <summary>
100        /// Records the unscaled extent of the content.
101        /// This is calculated during the measure and arrange.
102        /// </summary>
103        private Size unScaledExtent;
104
105        /// <summary>
106        /// Records the size of the viewport (in viewport coordinates) onto the content.
107        /// This is calculated during the measure and arrange.
108        /// </summary>
109        private Size viewport;
110
111        /// <summary>
112        /// Reference to the ScrollViewer that is wrapped (in XAML) around the ZoomPanControl.
113        /// Or set to null if there is no ScrollViewer.
114        /// </summary>
115        private ScrollViewer scrollOwner;
116
117        #endregion IScrollInfo Private Fields
118
119        #region Dependency Property Definitions
120
121        //
122        // Definitions for dependency properties.
123        //
124
125        public static readonly DependencyProperty ContentScaleProperty =
126                DependencyProperty.Register("ContentScale", typeof(double), typeof(ZoomPanControl),
127                                            new FrameworkPropertyMetadata(1.0, ContentScale_PropertyChanged, ContentScale_Coerce));
128
129        public static readonly DependencyProperty MinContentScaleProperty =
130                DependencyProperty.Register("MinContentScale", typeof(double), typeof(ZoomPanControl),
131                                            new FrameworkPropertyMetadata(0.01, MinOrMaxContentScale_PropertyChanged));
132
133        public static readonly DependencyProperty MaxContentScaleProperty =
134                DependencyProperty.Register("MaxContentScale", typeof(double), typeof(ZoomPanControl),
135                                            new FrameworkPropertyMetadata(10.0, MinOrMaxContentScale_PropertyChanged));
136
137        public static readonly DependencyProperty ContentOffsetXProperty =
138                DependencyProperty.Register("ContentOffsetX", typeof(double), typeof(ZoomPanControl),
139                                            new FrameworkPropertyMetadata(0.0, ContentOffsetX_PropertyChanged, ContentOffsetX_Coerce));
140
141        public static readonly DependencyProperty ContentOffsetYProperty =
142                DependencyProperty.Register("ContentOffsetY", typeof(double), typeof(ZoomPanControl),
143                                            new FrameworkPropertyMetadata(0.0, ContentOffsetY_PropertyChanged, ContentOffsetY_Coerce));
144
145        public static readonly DependencyProperty AnimationDurationProperty =
146                DependencyProperty.Register("AnimationDuration", typeof(double), typeof(ZoomPanControl),
147                                            new FrameworkPropertyMetadata(0.4  ));
148
149        public static readonly DependencyProperty ContentZoomFocusXProperty =
150                DependencyProperty.Register("ContentZoomFocusX", typeof(double), typeof(ZoomPanControl),
151                                            new FrameworkPropertyMetadata(0.0));
152
153        public static readonly DependencyProperty ContentZoomFocusYProperty =
154                DependencyProperty.Register("ContentZoomFocusY", typeof(double), typeof(ZoomPanControl),
155                                            new FrameworkPropertyMetadata(0.0));
156
157        public static readonly DependencyProperty ViewportZoomFocusXProperty =
158                DependencyProperty.Register("ViewportZoomFocusX", typeof(double), typeof(ZoomPanControl),
159                                            new FrameworkPropertyMetadata(0.0));
160
161        public static readonly DependencyProperty ViewportZoomFocusYProperty =
162                DependencyProperty.Register("ViewportZoomFocusY", typeof(double), typeof(ZoomPanControl),
163                                            new FrameworkPropertyMetadata(0.0));
164
165        public static readonly DependencyProperty ContentViewportWidthProperty =
166                DependencyProperty.Register("ContentViewportWidth", typeof(double), typeof(ZoomPanControl),
167                                            new FrameworkPropertyMetadata(0.0));
168
169        public static readonly DependencyProperty ContentViewportHeightProperty =
170                DependencyProperty.Register("ContentViewportHeight", typeof(double), typeof(ZoomPanControl),
171                                            new FrameworkPropertyMetadata(0.0));
172
173        public static readonly DependencyProperty IsMouseWheelScrollingEnabledProperty =
174                DependencyProperty.Register("IsMouseWheelScrollingEnabled", typeof(bool), typeof(ZoomPanControl),
175                                            new FrameworkPropertyMetadata(false));
176
177        #endregion Dependency Property Definitions
178
179        #region Constructors and Destructor
180       
181        public ZoomPanControl()
182        {
183            unScaledExtent = new Size(0, 0);
184            viewport = new Size(0, 0);
185        }
186
187        /// <summary>
188        /// Static constructor to define metadata for the control (and link it to the style in Generic.xaml).
189        /// </summary>
190        static ZoomPanControl()
191        {
192            DefaultStyleKeyProperty.OverrideMetadata(typeof(ZoomPanControl),
193                new FrameworkPropertyMetadata(typeof(ZoomPanControl)));
194        }
195
196        #endregion
197
198        #region Public Events
199
200        /// <summary>
201        /// Event raised when the ContentOffsetX property has changed.
202        /// </summary>
203        public event EventHandler ContentOffsetXChanged;
204
205        /// <summary>
206        /// Event raised when the ContentOffsetY property has changed.
207        /// </summary>
208        public event EventHandler ContentOffsetYChanged;
209
210        /// <summary>
211        /// Event raised when the ContentScale property has changed.
212        /// </summary>
213        public event EventHandler ContentScaleChanged;
214
215        #endregion
216
217        #region Public Properties
218
219        /// <summary>
220        /// Get/set the X offset (in content coordinates) of the view on the content.
221        /// </summary>
222        public double ContentOffsetX
223        {
224            get
225            {
226                return (double)GetValue(ContentOffsetXProperty);
227            }
228            set
229            {
230                SetValue(ContentOffsetXProperty, value);
231            }
232        }
233
234        /// <summary>
235        /// Get/set the Y offset (in content coordinates) of the view on the content.
236        /// </summary>
237        public double ContentOffsetY
238        {
239            get
240            {
241                return (double)GetValue(ContentOffsetYProperty);
242            }
243            set
244            {
245                SetValue(ContentOffsetYProperty, value);
246            }
247        }
248
249        /// <summary>
250        /// Get/set the current scale (or zoom factor) of the content.
251        /// </summary>
252        public double ContentScale
253        {
254            get
255            {
256                return (double)GetValue(ContentScaleProperty);
257            }
258            set
259            {
260                SetValue(ContentScaleProperty, value);
261            }
262        }
263
264        /// <summary>
265        /// Get/set the minimum value for 'ContentScale'.
266        /// </summary>
267        public double MinContentScale
268        {
269            get
270            {
271                return (double)GetValue(MinContentScaleProperty);
272            }
273            set
274            {
275                SetValue(MinContentScaleProperty, value);
276            }
277        }
278
279        /// <summary>
280        /// Get/set the maximum value for 'ContentScale'.
281        /// </summary>
282        public double MaxContentScale
283        {
284            get
285            {
286                return (double)GetValue(MaxContentScaleProperty);
287            }
288            set
289            {
290                SetValue(MaxContentScaleProperty, value);
291            }
292        }
293
294        /// <summary>
295        /// The X coordinate of the content focus, this is the point that we are focusing on when zooming.
296        /// </summary>
297        public double ContentZoomFocusX
298        {
299            get
300            {
301                return (double)GetValue(ContentZoomFocusXProperty);
302            }
303            set
304            {
305                SetValue(ContentZoomFocusXProperty, value);
306            }
307        }
308
309        /// <summary>
310        /// The Y coordinate of the content focus, this is the point that we are focusing on when zooming.
311        /// </summary>
312        public double ContentZoomFocusY
313        {
314            get
315            {
316                return (double)GetValue(ContentZoomFocusYProperty);
317            }
318            set
319            {
320                SetValue(ContentZoomFocusYProperty, value);
321            }
322        }
323
324        /// <summary>
325        /// The X coordinate of the viewport focus, this is the point in the viewport (in viewport coordinates)
326        /// that the content focus point is locked to while zooming in.
327        /// </summary>
328        public double ViewportZoomFocusX
329        {
330            get
331            {
332                return (double)GetValue(ViewportZoomFocusXProperty);
333            }
334            set
335            {
336                SetValue(ViewportZoomFocusXProperty, value);
337            }
338        }
339
340        /// <summary>
341        /// The Y coordinate of the viewport focus, this is the point in the viewport (in viewport coordinates)
342        /// that the content focus point is locked to while zooming in.
343        /// </summary>
344        public double ViewportZoomFocusY
345        {
346            get
347            {
348                return (double)GetValue(ViewportZoomFocusYProperty);
349            }
350            set
351            {
352                SetValue(ViewportZoomFocusYProperty, value);
353            }
354        }
355
356        /// <summary>
357        /// The duration of the animations (in seconds) started by calling AnimatedZoomTo and the other animation methods.
358        /// </summary>
359        public double AnimationDuration
360        {
361            get
362            {
363                return (double)GetValue(AnimationDurationProperty);
364            }
365            set
366            {
367                SetValue(AnimationDurationProperty, value);               
368            }
369        }
370
371        /// <summary>
372        /// Get the viewport width, in content coordinates.
373        /// </summary>
374        public double ContentViewportWidth
375        {
376            get
377            {
378                return (double)GetValue(ContentViewportWidthProperty);
379            }
380            set
381            {
382                SetValue(ContentViewportWidthProperty, value);
383            }
384        }
385
386        /// <summary>
387        /// Get the viewport height, in content coordinates.
388        /// </summary>
389        public double ContentViewportHeight
390        {
391            get
392            {
393                return (double)GetValue(ContentViewportHeightProperty);
394            }
395            set
396            {
397                SetValue(ContentViewportHeightProperty, value);
398            }
399        }
400
401        /// <summary>
402        /// Set to 'true' to enable the mouse wheel to scroll the zoom and pan control.
403        /// This is set to 'false' by default.
404        /// </summary>
405        public bool IsMouseWheelScrollingEnabled
406        {
407            get
408            {
409                return (bool)GetValue(IsMouseWheelScrollingEnabledProperty);
410            }
411            set
412            {
413                SetValue(IsMouseWheelScrollingEnabledProperty, value);
414            }
415        }
416
417        #endregion
418
419        #region Public Methods
420
421        /// <summary>
422        /// Do an animated zoom to view a specific scale and rectangle (in content coordinates).
423        /// </summary>
424        public void AnimatedZoomTo(double newScale, Rect contentRect)
425        {
426            AnimatedZoomPointToViewportCenter(newScale, new Point(contentRect.X + (contentRect.Width / 2), contentRect.Y + (contentRect.Height / 2)),
427                delegate(object sender, EventArgs e)
428                {
429                    //
430                    // At the end of the animation, ensure that we are snapped to the specified content offset.
431                    // Due to zooming in on the content focus point and rounding errors, the content offset may
432                    // be slightly off what we want at the end of the animation and this bit of code corrects it.
433                    //
434                    this.ContentOffsetX = contentRect.X;
435                    this.ContentOffsetY = contentRect.Y;
436                });
437        }
438
439        /// <summary>
440        /// Do an animated zoom to the specified rectangle (in content coordinates).
441        /// </summary>
442        public void AnimatedZoomTo(Rect contentRect)
443        {
444            double scaleX = this.ContentViewportWidth / contentRect.Width;
445            double scaleY = this.ContentViewportHeight / contentRect.Height;
446            double newScale = this.ContentScale * Math.Min(scaleX, scaleY);
447
448            AnimatedZoomPointToViewportCenter(newScale, new Point(contentRect.X + (contentRect.Width / 2), contentRect.Y + (contentRect.Height / 2)), null);
449        }
450
451        /// <summary>
452        /// Instantly zoom to the specified rectangle (in content coordinates).
453        /// </summary>
454        public void ZoomTo(Rect contentRect)
455        {
456            double scaleX = this.ContentViewportWidth / contentRect.Width;
457            double scaleY = this.ContentViewportHeight / contentRect.Height;
458            double newScale = this.ContentScale * Math.Min(scaleX, scaleY);
459
460            ZoomPointToViewportCenter(newScale, new Point(contentRect.X + (contentRect.Width / 2), contentRect.Y + (contentRect.Height / 2)));
461        }
462
463        /// <summary>
464        /// Instantly center the view on the specified point (in content coordinates).
465        /// </summary>
466        public void SnapTo(Point contentPoint)
467        {
468            ZoomPanAnimationHelper.CancelAnimation(this, ContentOffsetXProperty);
469            ZoomPanAnimationHelper.CancelAnimation(this, ContentOffsetYProperty);
470
471            this.ContentOffsetX = contentPoint.X - (this.ContentViewportWidth / 2);
472            this.ContentOffsetY = contentPoint.Y - (this.ContentViewportHeight / 2);
473        }
474
475        /// <summary>
476        /// Use animation to center the view on the specified point (in content coordinates).
477        /// </summary>
478        public void AnimatedSnapTo(Point contentPoint)
479        {
480            double newX = contentPoint.X - (this.ContentViewportWidth / 2);
481            double newY = contentPoint.Y - (this.ContentViewportHeight / 2);
482
483            ZoomPanAnimationHelper.StartAnimation(this, ContentOffsetXProperty, newX, AnimationDuration);
484            ZoomPanAnimationHelper.StartAnimation(this, ContentOffsetYProperty, newY, AnimationDuration);
485        }
486
487        /// <summary>
488        /// Zoom in/out centered on the specified point (in content coordinates).
489        /// The focus point is kept locked to it's on screen position (ala google maps).
490        /// </summary>
491        public void AnimatedZoomAboutPoint(double newContentScale, Point contentZoomFocus)
492        {
493            newContentScale = Math.Min(Math.Max(newContentScale, MinContentScale), MaxContentScale);
494
495            ZoomPanAnimationHelper.CancelAnimation(this, ContentZoomFocusXProperty);
496            ZoomPanAnimationHelper.CancelAnimation(this, ContentZoomFocusYProperty);
497            ZoomPanAnimationHelper.CancelAnimation(this, ViewportZoomFocusXProperty);
498            ZoomPanAnimationHelper.CancelAnimation(this, ViewportZoomFocusYProperty);
499
500            ContentZoomFocusX = contentZoomFocus.X;
501            ContentZoomFocusY = contentZoomFocus.Y;
502            ViewportZoomFocusX = (ContentZoomFocusX - ContentOffsetX) * ContentScale;
503            ViewportZoomFocusY = (ContentZoomFocusY - ContentOffsetY) * ContentScale;
504
505            //
506            // When zooming about a point make updates to ContentScale also update content offset.
507            //
508            enableContentOffsetUpdateFromScale = true;
509
510            ZoomPanAnimationHelper.StartAnimation(this, ContentScaleProperty, newContentScale, AnimationDuration,
511                delegate(object sender, EventArgs e)
512                {
513                    enableContentOffsetUpdateFromScale = false;
514
515                    ResetViewportZoomFocus();
516                });
517        }
518
519        /// <summary>
520        /// Zoom in/out centered on the specified point (in content coordinates).
521        /// The focus point is kept locked to it's on screen position (ala google maps).
522        /// </summary>
523        public void ZoomAboutPoint(double newContentScale, Point contentZoomFocus)
524        {
525            newContentScale = Math.Min(Math.Max(newContentScale, MinContentScale), MaxContentScale);
526
527            double screenSpaceZoomOffsetX = (contentZoomFocus.X - ContentOffsetX) * ContentScale;
528            double screenSpaceZoomOffsetY = (contentZoomFocus.Y - ContentOffsetY) * ContentScale;
529            double contentSpaceZoomOffsetX = screenSpaceZoomOffsetX / newContentScale;
530            double contentSpaceZoomOffsetY = screenSpaceZoomOffsetY / newContentScale;
531            double newContentOffsetX = contentZoomFocus.X - contentSpaceZoomOffsetX;
532            double newContentOffsetY = contentZoomFocus.Y - contentSpaceZoomOffsetY;
533
534            ZoomPanAnimationHelper.CancelAnimation(this, ContentScaleProperty);
535            ZoomPanAnimationHelper.CancelAnimation(this, ContentOffsetXProperty);
536            ZoomPanAnimationHelper.CancelAnimation(this, ContentOffsetYProperty);
537
538            this.ContentScale = newContentScale;
539            this.ContentOffsetX = newContentOffsetX;
540            this.ContentOffsetY = newContentOffsetY;
541        }
542
543        /// <summary>
544        /// Zoom in/out centered on the viewport center.
545        /// </summary>
546        public void AnimatedZoomTo(double contentScale)
547        {
548            Point zoomCenter = new Point(ContentOffsetX + (ContentViewportWidth / 2), ContentOffsetY + (ContentViewportHeight / 2));
549            AnimatedZoomAboutPoint(contentScale, zoomCenter);
550        }
551
552        /// <summary>
553        /// Zoom in/out centered on the viewport center.
554        /// </summary>
555        public void ZoomTo(double contentScale)
556        {
557            Point zoomCenter = new Point(ContentOffsetX + (ContentViewportWidth / 2), ContentOffsetY + (ContentViewportHeight / 2));
558            ZoomAboutPoint(contentScale, zoomCenter);
559        }
560
561        /// <summary>
562        /// Do animation that scales the content so that it fits completely in the control.
563        /// </summary>
564        public void AnimatedScaleToFit()
565        {
566            if (content == null)
567            {
568                throw new ApplicationException("PART_Content was not found in the ZoomPanControl visual template!");
569            }
570
571            AnimatedZoomTo(new Rect(0, 0, content.ActualWidth, content.ActualHeight));
572        }
573
574        /// <summary>
575        /// Instantly scale the content so that it fits completely in the control.
576        /// </summary>
577        public void ScaleToFit()
578        {
579            if (content == null)
580            {
581                throw new ApplicationException("PART_Content was not found in the ZoomPanControl visual template!");
582            }
583
584            ZoomTo(new Rect(0, 0, content.ActualWidth, content.ActualHeight));
585        }
586
587        /// <summary>
588        /// Called when a template has been applied to the control.
589        /// </summary>
590        public override void OnApplyTemplate()
591        {
592            base.OnApplyTemplate();
593
594            content = this.Template.FindName("PART_Content", this) as FrameworkElement;
595            if (content != null)
596            {
597                //
598                // Setup the transform on the content so that we can scale it by 'ContentScale'.
599                //
600                this.contentScaleTransform = new ScaleTransform(this.ContentScale, this.ContentScale);
601
602                //
603                // Setup the transform on the content so that we can translate it by 'ContentOffsetX' and 'ContentOffsetY'.
604                //
605                this.contentOffsetTransform = new TranslateTransform();
606                UpdateTranslationX();
607                UpdateTranslationY();
608
609                //
610                // Setup a transform group to contain the translation and scale transforms, and then
611                // assign this to the content's 'RenderTransform'.
612                //
613                TransformGroup transformGroup = new TransformGroup();
614                transformGroup.Children.Add(this.contentOffsetTransform);
615                transformGroup.Children.Add(this.contentScaleTransform);
616                content.RenderTransform = transformGroup;
617            }
618        }
619
620        #endregion
621
622        #region Protected Methods
623
624        /// <summary>
625        /// Measure the control and it's children.
626        /// </summary>
627        protected override Size MeasureOverride(Size constraint)
628        {
629            Size infiniteSize = new Size(double.PositiveInfinity, double.PositiveInfinity);
630            Size childSize = base.MeasureOverride(infiniteSize);
631
632            if (childSize != unScaledExtent)
633            {
634                //
635                // Use the size of the child as the un-scaled extent content.
636                //
637                unScaledExtent = childSize;
638
639                if (scrollOwner != null)
640                {
641                    scrollOwner.InvalidateScrollInfo();
642                }
643            }
644
645            //
646            // Update the size of the viewport onto the content based on the passed in 'constraint'.
647            //
648            UpdateViewportSize(constraint);
649
650            double width = constraint.Width;
651            double height = constraint.Height;
652
653            if (double.IsInfinity(width))
654            {
655                //
656                // Make sure we don't return infinity!
657                //
658                width = childSize.Width;
659            }
660
661            if (double.IsInfinity(height))
662            {
663                //
664                // Make sure we don't return infinity!
665                //
666                height = childSize.Height;
667            }
668           
669            return new Size(width, height);
670        }
671
672        /// <summary>
673        /// Arrange the control and it's children.
674        /// </summary>
675        protected override Size ArrangeOverride(Size arrangeBounds)
676        {
677            Size size = base.ArrangeOverride(this.DesiredSize);
678
679            if (content.DesiredSize != unScaledExtent)
680            {
681                //
682                // Use the size of the child as the un-scaled extent content.
683                //
684                unScaledExtent = content.DesiredSize;
685
686                if (scrollOwner != null)
687                {
688                    scrollOwner.InvalidateScrollInfo();
689                }
690            }
691
692            //
693            // Update the size of the viewport onto the content based on the passed in 'arrangeBounds'.
694            //
695            UpdateViewportSize(arrangeBounds);
696
697            return size;
698        }
699
700        #endregion
701
702        #region Private Methods
703
704        /// <summary>
705        /// Zoom to the specified scale and move the specified focus point to the center of the viewport.
706        /// </summary>
707        private void AnimatedZoomPointToViewportCenter(double newContentScale, Point contentZoomFocus, EventHandler callback)
708        {
709            newContentScale = Math.Min(Math.Max(newContentScale, MinContentScale), MaxContentScale);
710
711            ZoomPanAnimationHelper.CancelAnimation(this, ContentZoomFocusXProperty);
712            ZoomPanAnimationHelper.CancelAnimation(this, ContentZoomFocusYProperty);
713            ZoomPanAnimationHelper.CancelAnimation(this, ViewportZoomFocusXProperty);
714            ZoomPanAnimationHelper.CancelAnimation(this, ViewportZoomFocusYProperty);
715
716            ContentZoomFocusX = contentZoomFocus.X;
717            ContentZoomFocusY = contentZoomFocus.Y;
718            ViewportZoomFocusX = (ContentZoomFocusX - ContentOffsetX) * ContentScale;
719            ViewportZoomFocusY = (ContentZoomFocusY - ContentOffsetY) * ContentScale;
720
721            //
722            // When zooming about a point make updates to ContentScale also update content offset.
723            //
724            enableContentOffsetUpdateFromScale = true;
725
726            ZoomPanAnimationHelper.StartAnimation(this, ContentScaleProperty, newContentScale, AnimationDuration,
727                delegate(object sender, EventArgs e)
728                {
729                    enableContentOffsetUpdateFromScale = false;
730
731                    if (callback != null)
732                    {
733                        callback(this, EventArgs.Empty);
734                    }
735                });
736
737            ZoomPanAnimationHelper.StartAnimation(this, ViewportZoomFocusXProperty, ViewportWidth / 2, AnimationDuration);
738            ZoomPanAnimationHelper.StartAnimation(this, ViewportZoomFocusYProperty, ViewportHeight / 2, AnimationDuration);
739        }
740
741        /// <summary>
742        /// Zoom to the specified scale and move the specified focus point to the center of the viewport.
743        /// </summary>
744        private void ZoomPointToViewportCenter(double newContentScale, Point contentZoomFocus)
745        {
746            newContentScale = Math.Min(Math.Max(newContentScale, MinContentScale), MaxContentScale);
747
748            ZoomPanAnimationHelper.CancelAnimation(this, ContentScaleProperty);
749            ZoomPanAnimationHelper.CancelAnimation(this, ContentOffsetXProperty);
750            ZoomPanAnimationHelper.CancelAnimation(this, ContentOffsetYProperty);
751
752            this.ContentScale = newContentScale;
753            this.ContentOffsetX = contentZoomFocus.X - (ContentViewportWidth / 2);
754            this.ContentOffsetY = contentZoomFocus.Y - (ContentViewportHeight / 2);
755        }
756
757        /// <summary>
758        /// Event raised when the 'ContentScale' property has changed value.
759        /// </summary>
760        private static void ContentScale_PropertyChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
761        {
762            ZoomPanControl c = (ZoomPanControl)o;
763
764            if (c.contentScaleTransform != null)
765            {
766                //
767                // Update the content scale transform whenever 'ContentScale' changes.
768                //
769                c.contentScaleTransform.ScaleX = c.ContentScale;
770                c.contentScaleTransform.ScaleY = c.ContentScale;
771            }
772
773            //
774            // Update the size of the viewport in content coordinates.
775            //
776            c.UpdateContentViewportSize();
777
778            if (c.enableContentOffsetUpdateFromScale)
779            {
780                try
781                {
782                    //
783                    // Disable content focus synchronization.  We are about to update content offset whilst zooming
784                    // to ensure that the viewport is focused on our desired content focus point.  Setting this
785                    // to 'true' stops the automatic update of the content focus when content offset changes.
786                    //
787                    c.disableContentFocusSync = true;
788
789                    //
790                    // Whilst zooming in or out keep the content offset up-to-date so that the viewport is always
791                    // focused on the content focus point (and also so that the content focus is locked to the
792                    // viewport focus point - this is how the Google maps style zooming works).
793                    //
794                    double viewportOffsetX = c.ViewportZoomFocusX - (c.ViewportWidth / 2);
795                    double viewportOffsetY = c.ViewportZoomFocusY - (c.ViewportHeight / 2);
796                    double contentOffsetX = viewportOffsetX / c.ContentScale;
797                    double contentOffsetY = viewportOffsetY / c.ContentScale;
798                    c.ContentOffsetX = (c.ContentZoomFocusX - (c.ContentViewportWidth / 2)) - contentOffsetX;
799                    c.ContentOffsetY = (c.ContentZoomFocusY - (c.ContentViewportHeight / 2)) - contentOffsetY;
800                }
801                finally
802                {
803                    c.disableContentFocusSync = false;
804                }
805            }
806
807            if (c.ContentScaleChanged != null)
808            {
809                c.ContentScaleChanged(c, EventArgs.Empty);
810            }
811
812            if (c.scrollOwner != null)
813            {
814                c.scrollOwner.InvalidateScrollInfo();
815            }
816        }
817
818        /// <summary>
819        /// Method called to clamp the 'ContentScale' value to its valid range.
820        /// </summary>
821        private static object ContentScale_Coerce(DependencyObject d, object baseValue)
822        {
823            ZoomPanControl c = (ZoomPanControl)d;
824            double value = (double) baseValue;
825            value = Math.Min(Math.Max(value, c.MinContentScale), c.MaxContentScale);
826            return value;
827        }
828
829        /// <summary>
830        /// Event raised 'MinContentScale' or 'MaxContentScale' has changed.
831        /// </summary>
832        private static void MinOrMaxContentScale_PropertyChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
833        {
834            ZoomPanControl c = (ZoomPanControl)o;
835            c.ContentScale = Math.Min(Math.Max(c.ContentScale, c.MinContentScale), c.MaxContentScale);
836        }
837
838        /// <summary>
839        /// Event raised when the 'ContentOffsetX' property has changed value.
840        /// </summary>
841        private static void ContentOffsetX_PropertyChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
842        {
843            ZoomPanControl c = (ZoomPanControl)o;
844
845            c.UpdateTranslationX();
846
847            if (!c.disableContentFocusSync)
848            {
849                //
850                // Normally want to automatically update content focus when content offset changes.
851                // Although this is disabled using 'disableContentFocusSync' when content offset changes due to in-progress zooming.
852                //
853                c.UpdateContentZoomFocusX();
854            }
855
856            if (c.ContentOffsetXChanged != null)
857            {
858                //
859                // Raise an event to let users of the control know that the content offset has changed.
860                //
861                c.ContentOffsetXChanged(c, EventArgs.Empty);
862            }
863
864            if (!c.disableScrollOffsetSync && c.scrollOwner != null)
865            {
866                //
867                // Notify the owning ScrollViewer that the scrollbar offsets should be updated.
868                //
869                c.scrollOwner.InvalidateScrollInfo();
870            }
871        }
872
873        /// <summary>
874        /// Method called to clamp the 'ContentOffsetX' value to its valid range.
875        /// </summary>
876        private static object ContentOffsetX_Coerce(DependencyObject d, object baseValue)
877        {
878            ZoomPanControl c = (ZoomPanControl)d;
879            double value = (double)baseValue;
880            double minOffsetX = 0.0;
881            double maxOffsetX = Math.Max(0.0, c.unScaledExtent.Width - c.constrainedContentViewportWidth);
882            value = Math.Min(Math.Max(value, minOffsetX), maxOffsetX);
883            return value;
884        }
885
886        /// <summary>
887        /// Event raised when the 'ContentOffsetY' property has changed value.
888        /// </summary>
889        private static void ContentOffsetY_PropertyChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
890        {
891            ZoomPanControl c = (ZoomPanControl)o;
892
893            c.UpdateTranslationY();
894
895            if (!c.disableContentFocusSync)
896            {
897                //
898                // Normally want to automatically update content focus when content offset changes.
899                // Although this is disabled using 'disableContentFocusSync' when content offset changes due to in-progress zooming.
900                //
901                c.UpdateContentZoomFocusY();
902            }
903
904            if (c.ContentOffsetYChanged != null)
905            {
906                //
907                // Raise an event to let users of the control know that the content offset has changed.
908                //
909                c.ContentOffsetYChanged(c, EventArgs.Empty);
910            }
911
912            if (!c.disableScrollOffsetSync && c.scrollOwner != null)
913            {
914                //
915                // Notify the owning ScrollViewer that the scrollbar offsets should be updated.
916                //
917                c.scrollOwner.InvalidateScrollInfo();
918            }
919
920        }
921
922        /// <summary>
923        /// Method called to clamp the 'ContentOffsetY' value to its valid range.
924        /// </summary>
925        private static object ContentOffsetY_Coerce(DependencyObject d, object baseValue)
926        {
927            ZoomPanControl c = (ZoomPanControl)d;
928            double value = (double)baseValue;
929            double minOffsetY = 0.0;
930            double maxOffsetY = Math.Max(0.0, c.unScaledExtent.Height - c.constrainedContentViewportHeight);
931            value = Math.Min(Math.Max(value, minOffsetY), maxOffsetY);
932            return value;
933        }
934
935        /// <summary>
936        /// Reset the viewport zoom focus to the center of the viewport.
937        /// </summary>
938        private void ResetViewportZoomFocus()
939        {
940            ViewportZoomFocusX = ViewportWidth / 2;
941            ViewportZoomFocusY = ViewportHeight / 2;
942        }
943
944        /// <summary>
945        /// Update the viewport size from the specified size.
946        /// </summary>
947        private void UpdateViewportSize(Size newSize)
948        {
949            if (viewport == newSize)
950            {
951                //
952                // The viewport is already the specified size.
953                //
954                return;
955            }
956
957            viewport = newSize;
958
959            //
960            // Update the viewport size in content coordiates.
961            //
962            UpdateContentViewportSize();
963
964            //
965            // Initialise the content zoom focus point.
966            //
967            UpdateContentZoomFocusX();
968            UpdateContentZoomFocusY();
969
970            //
971            // Reset the viewport zoom focus to the center of the viewport.
972            //
973            ResetViewportZoomFocus();
974
975            //
976            // Update content offset from itself when the size of the viewport changes.
977            // This ensures that the content offset remains properly clamped to its valid range.
978            //
979            this.ContentOffsetX = this.ContentOffsetX;
980            this.ContentOffsetY = this.ContentOffsetY;
981
982            if (scrollOwner != null)
983            {
984                //
985                // Tell that owning ScrollViewer that scrollbar data has changed.
986                //
987                scrollOwner.InvalidateScrollInfo();
988            }
989        }
990
991        /// <summary>
992        /// Update the size of the viewport in content coordinates after the viewport size or 'ContentScale' has changed.
993        /// </summary>
994        private void UpdateContentViewportSize()
995        {
996            ContentViewportWidth  = ViewportWidth / ContentScale;
997            ContentViewportHeight = ViewportHeight / ContentScale;
998
999            constrainedContentViewportWidth  = Math.Min(ContentViewportWidth, unScaledExtent.Width);
1000            constrainedContentViewportHeight = Math.Min(ContentViewportHeight, unScaledExtent.Height);
1001
1002            UpdateTranslationX();
1003            UpdateTranslationY();
1004        }
1005
1006        /// <summary>
1007        /// Update the X coordinate of the translation transformation.
1008        /// </summary>
1009        private void UpdateTranslationX()
1010        {
1011            if (this.contentOffsetTransform != null)
1012            {
1013                double scaledContentWidth = this.unScaledExtent.Width * this.ContentScale;
1014                if (scaledContentWidth < this.ViewportWidth)
1015                {
1016                    //
1017                    // When the content can fit entirely within the viewport, center it.
1018                    //
1019                    this.contentOffsetTransform.X = (this.ContentViewportWidth - this.unScaledExtent.Width) / 2;
1020                }
1021                else
1022                {
1023                    this.contentOffsetTransform.X = -this.ContentOffsetX;
1024                }
1025            }
1026        }
1027
1028        /// <summary>
1029        /// Update the Y coordinate of the translation transformation.
1030        /// </summary>
1031        private void UpdateTranslationY()
1032        {
1033            if (this.contentOffsetTransform != null)
1034            {
1035                double scaledContentHeight = this.unScaledExtent.Height * this.ContentScale;
1036                if (scaledContentHeight < this.ViewportHeight)
1037                {
1038                    //
1039                    // When the content can fit entirely within the viewport, center it.
1040                    //
1041                    this.contentOffsetTransform.Y = (this.ContentViewportHeight - this.unScaledExtent.Height) / 2;
1042                }
1043                else
1044                {
1045                    this.contentOffsetTransform.Y = -this.ContentOffsetY;
1046                }
1047            }
1048        }
1049
1050        /// <summary>
1051        /// Update the X coordinate of the zoom focus point in content coordinates.
1052        /// </summary>
1053        private void UpdateContentZoomFocusX()
1054        {
1055            ContentZoomFocusX = ContentOffsetX + (constrainedContentViewportWidth / 2);
1056        }
1057
1058        /// <summary>
1059        /// Update the Y coordinate of the zoom focus point in content coordinates.
1060        /// </summary>
1061        private void UpdateContentZoomFocusY()
1062        {
1063            ContentZoomFocusY = ContentOffsetY + (constrainedContentViewportHeight / 2);
1064        }
1065
1066        #endregion Private Methods
1067    }
1068}
Note: See TracBrowser for help on using the repository browser.