A while ago I wrote about how to create a scrollable design surface in WPF, and how you could also add friction into the mix.
My original post was called “Creating A Scrollable Control Surface In WPF†which can be found at the following url:
I have been asked for my original code a lot, and another of my friends, and founder of the WPF Disciples, Marlon Grech took my code and has further improved it for WPF users, by making it an attached behaviour so all you have to do is hook up one property on your ScrollViewer and bingo its a Friction enabled surface. Neato I say.
1: using System;
2: using System.Collections.Generic;
3: using System.Linq;
4: using System.Text;
5: using System.Windows;
6: using System.Windows.Controls;
7: using System.Windows.Input;
8: using System.Windows.Threading;
9:
10: namespace ScrollableArea
11: {
12: public class KineticBehaviour
13: {
14: #region Friction
15:
16: /// <summary>
17: /// Friction Attached Dependency Property
18: /// </summary>
19: public static readonly DependencyProperty FrictionProperty =
20: DependencyProperty.RegisterAttached("Friction", typeof(double), typeof(KineticBehaviour),
21: new FrameworkPropertyMetadata((double)0.95));
22:
23: /// <summary>
24: /// Gets the Friction property. This dependency property
25: /// indicates ....
26: /// </summary>
27: public static double GetFriction(DependencyObject d)
28: {
29: return (double)d.GetValue(FrictionProperty);
30: }
31:
32: /// <summary>
33: /// Sets the Friction property. This dependency property
34: /// indicates ....
35: /// </summary>
36: public static void SetFriction(DependencyObject d, double value)
37: {
38: d.SetValue(FrictionProperty, value);
39: }
40:
41: #endregion
42:
43: #region ScrollStartPoint
44:
45: /// <summary>
46: /// ScrollStartPoint Attached Dependency Property
47: /// </summary>
48: private static readonly DependencyProperty ScrollStartPointProperty =
49: DependencyProperty.RegisterAttached("ScrollStartPoint", typeof(Point), typeof(KineticBehaviour),
50: new FrameworkPropertyMetadata((Point)new Point()));
51:
52: /// <summary>
53: /// Gets the ScrollStartPoint property. This dependency property
54: /// indicates ....
55: /// </summary>
56: private static Point GetScrollStartPoint(DependencyObject d)
57: {
58: return (Point)d.GetValue(ScrollStartPointProperty);
59: }
60:
61: /// <summary>
62: /// Sets the ScrollStartPoint property. This dependency property
63: /// indicates ....
64: /// </summary>
65: private static void SetScrollStartPoint(DependencyObject d, Point value)
66: {
67: d.SetValue(ScrollStartPointProperty, value);
68: }
69:
70: #endregion
71:
72: #region ScrollStartOffset
73:
74: /// <summary>
75: /// ScrollStartOffset Attached Dependency Property
76: /// </summary>
77: private static readonly DependencyProperty ScrollStartOffsetProperty =
78: DependencyProperty.RegisterAttached("ScrollStartOffset", typeof(Point), typeof(KineticBehaviour),
79: new FrameworkPropertyMetadata((Point)new Point()));
80:
81: /// <summary>
82: /// Gets the ScrollStartOffset property. This dependency property
83: /// indicates ....
84: /// </summary>
85: private static Point GetScrollStartOffset(DependencyObject d)
86: {
87: return (Point)d.GetValue(ScrollStartOffsetProperty);
88: }
89:
90: /// <summary>
91: /// Sets the ScrollStartOffset property. This dependency property
92: /// indicates ....
93: /// </summary>
94: private static void SetScrollStartOffset(DependencyObject d, Point value)
95: {
96: d.SetValue(ScrollStartOffsetProperty, value);
97: }
98:
99: #endregion
100:
101: #region InertiaProcessor
102:
103: /// <summary>
104: /// InertiaProcessor Attached Dependency Property
105: /// </summary>
106: private static readonly DependencyProperty InertiaProcessorProperty =
107: DependencyProperty.RegisterAttached("InertiaProcessor", typeof(InertiaHandler), typeof(KineticBehaviour),
108: new FrameworkPropertyMetadata((InertiaHandler)null));
109:
110: /// <summary>
111: /// Gets the InertiaProcessor property. This dependency property
112: /// indicates ....
113: /// </summary>
114: private static InertiaHandler GetInertiaProcessor(DependencyObject d)
115: {
116: return (InertiaHandler)d.GetValue(InertiaProcessorProperty);
117: }
118:
119: /// <summary>
120: /// Sets the InertiaProcessor property. This dependency property
121: /// indicates ....
122: /// </summary>
123: private static void SetInertiaProcessor(DependencyObject d, InertiaHandler value)
124: {
125: d.SetValue(InertiaProcessorProperty, value);
126: }
127:
128: #endregion
129:
130: #region HandleKineticScrolling
131:
132: /// <summary>
133: /// HandleKineticScrolling Attached Dependency Property
134: /// </summary>
135: public static readonly DependencyProperty HandleKineticScrollingProperty =
136: DependencyProperty.RegisterAttached("HandleKineticScrolling", typeof(bool),
137: typeof(KineticBehaviour),
138: new FrameworkPropertyMetadata((bool)false,
139: new PropertyChangedCallback(OnHandleKineticScrollingChanged)));
140:
141: /// <summary>
142: /// Gets the HandleKineticScrolling property. This dependency property
143: /// indicates ....
144: /// </summary>
145: public static bool GetHandleKineticScrolling(DependencyObject d)
146: {
147: return (bool)d.GetValue(HandleKineticScrollingProperty);
148: }
149:
150: /// <summary>
151: /// Sets the HandleKineticScrolling property. This dependency property
152: /// indicates ....
153: /// </summary>
154: public static void SetHandleKineticScrolling(DependencyObject d, bool value)
155: {
156: d.SetValue(HandleKineticScrollingProperty, value);
157: }
158:
159: /// <summary>
160: /// Handles changes to the HandleKineticScrolling property.
161: /// </summary>
162: private static void OnHandleKineticScrollingChanged(DependencyObject d,
163: DependencyPropertyChangedEventArgs e)
164: {
165: ScrollViewer scoller = d as ScrollViewer;
166: if ((bool)e.NewValue)
167: {
168: scoller.MouseDown += OnMouseDown;
169: scoller.MouseMove += OnMouseMove;
170: scoller.MouseUp += OnMouseUp;
171: SetInertiaProcessor(scoller, new InertiaHandler(scoller));
172: }
173: else
174: {
175: scoller.MouseDown -= OnMouseDown;
176: scoller.MouseMove -= OnMouseMove;
177: scoller.MouseUp -= OnMouseUp;
178: var inertia = GetInertiaProcessor(scoller);
179: if (inertia != null)
180: inertia.Dispose();
181: }
182:
183: }
184:
185: #endregion
186:
187: #region Mouse Events
188: private static void OnMouseDown(object sender, MouseButtonEventArgs e)
189: {
190: var scrollViewer = (ScrollViewer)sender;
191: if (scrollViewer.IsMouseOver)
192: {
193: // Save starting point, used later when determining how much to scroll.
194: SetScrollStartPoint(scrollViewer, e.GetPosition(scrollViewer));
195: SetScrollStartOffset(scrollViewer, new
196: Point(scrollViewer.HorizontalOffset, scrollViewer.VerticalOffset));
197: scrollViewer.CaptureMouse();
198: }
199: }
200:
201:
202: private static void OnMouseMove(object sender, MouseEventArgs e)
203: {
204: var scrollViewer = (ScrollViewer)sender;
205: if (scrollViewer.IsMouseCaptured)
206: {
207: Point currentPoint = e.GetPosition(scrollViewer);
208:
209: var scrollStartPoint = GetScrollStartPoint(scrollViewer);
210: // Determine the new amount to scroll.
211: Point delta = new Point(scrollStartPoint.X - currentPoint.X,
212: scrollStartPoint.Y - currentPoint.Y);
213:
214: var scrollStartOffset = GetScrollStartOffset(scrollViewer);
215: Point scrollTarget = new Point(scrollStartOffset.X + delta.X,
216: scrollStartOffset.Y + delta.Y);
217:
218: var inertiaProcessor = GetInertiaProcessor(scrollViewer);
219: if (inertiaProcessor != null)
220: inertiaProcessor.ScrollTarget = scrollTarget;
221:
222: // Scroll to the new position.
223: scrollViewer.ScrollToHorizontalOffset(scrollTarget.X);
224: scrollViewer.ScrollToVerticalOffset(scrollTarget.Y);
225: }
226: }
227:
228: private static void OnMouseUp(object sender, MouseButtonEventArgs e)
229: {
230: var scrollViewer = (ScrollViewer)sender;
231: if (scrollViewer.IsMouseCaptured)
232: {
233: scrollViewer.ReleaseMouseCapture();
234: }
235: }
236: #endregion
237:
238: #region Inertia Stuff
239:
240: /// <summary>
241: /// Handles the inertia
242: /// </summary>
243: class InertiaHandler : IDisposable
244: {
245: private Point previousPoint;
246: private Vector velocity;
247: ScrollViewer scroller;
248: DispatcherTimer animationTimer;
249:
250: private Point scrollTarget;
251: public Point ScrollTarget {
252: get { return scrollTarget; }
253: set { scrollTarget = value; } }
254:
255: public InertiaHandler(ScrollViewer scroller)
256: {
257: this.scroller = scroller;
258: animationTimer = new DispatcherTimer();
259: animationTimer.Interval = new TimeSpan(0, 0, 0, 0, 20);
260: animationTimer.Tick += new EventHandler(HandleWorldTimerTick);
261: animationTimer.Start();
262: }
263:
264: private void HandleWorldTimerTick(object sender, EventArgs e)
265: {
266: if (scroller.IsMouseCaptured)
267: {
268: Point currentPoint = Mouse.GetPosition(scroller);
269: velocity = previousPoint - currentPoint;
270: previousPoint = currentPoint;
271: }
272: else
273: {
274: if (velocity.Length > 1)
275: {
276: scroller.ScrollToHorizontalOffset(ScrollTarget.X);
277: scroller.ScrollToVerticalOffset(ScrollTarget.Y);
278: scrollTarget.X += velocity.X;
279: scrollTarget.Y += velocity.Y;
280: velocity *= KineticBehaviour.GetFriction(scroller);
281: }
282: }
283: }
284:
285: #region IDisposable Members
286:
287: public void Dispose()
288: {
289: animationTimer.Stop();
290: }
291:
292: #endregion
293: }
294:
295: #endregion
296: }
297: }