Have you ever had a requirement that called for the user to be able to scroll around a large object, such as a diagram. Well I have, and I have just started working on a hobby project where I need just such a feature. We probably all know that WPF has a ScrollViewer
control which allows users to scroll using the scrollbars, which is fine, but it just looks ugly. What I want is for the user to not really ever realise that there is a scroll area, I want them to just use the mouse to pan around the large area.
To this end, I set about looking around, and I have pieced together a little demo project to illustrate this. It's not very elaborate, but it does the job well.
In the end, you still use the native WPF ScrollViewer
but you hide its ScrollBars
, and just respond to mouse events. I have now responded to people requests to add some friction (well my old team leader did it, as it's his area) so we have 2 versions, the XAML is the same for both.
Let's see some code, shall we?
1: <Window x:Class="ScrollableArea.Window1"
2: xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
3: xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
4: Title="Window1" Height="300" Width="300">
5: <Window.Resources>
6:
7: <!– scroll viewer Style –>
8: <Style x:Key="ScrollViewerStyle"
9: TargetType="{x:Type ScrollViewer}">
10: <Setter Property="HorizontalScrollBarVisibility"
11: Value="Hidden" />
12: <Setter Property="VerticalScrollBarVisibility"
13: Value="Hidden" />
14: </Style>
15:
16: </Window.Resources>
17:
18: <ScrollViewer x:Name="ScrollViewer"
19: Style="{StaticResource ScrollViewerStyle}">
20: <ItemsControl x:Name="itemsControl"
21: VerticalAlignment="Center"/>
22: </ScrollViewer>
23:
24: </Window>
It can be seen that there is a single ScrollViewer
which contains an ItemsControl
, but the ItemsControl
could be replaced with a Diagram
control or something else, you choose. The only important part here is that the ScrollViewer
has its HorizontalScrollBarVisibility
/VerticalScrollBarVisibility
set to be Hidden
, so that they are not visible to the user.
Frictionless Version
Next, we need to respond to the Mouse
events. This is done as follows:
1: protected override void OnPreviewMouseDown(MouseButtonEventArgs e)
2: {
3: if (ScrollViewer.IsMouseOver)
4: {
5:
6:
7: scrollStartPoint = e.GetPosition(this);
8: scrollStartOffset.X = ScrollViewer.HorizontalOffset;
9: scrollStartOffset.Y = ScrollViewer.VerticalOffset;
10:
11:
12: this.Cursor = (ScrollViewer.ExtentWidth >
13: ScrollViewer.ViewportWidth) ||
14: (ScrollViewer.ExtentHeight >
15: ScrollViewer.ViewportHeight) ?
16: Cursors.ScrollAll : Cursors.Arrow;
17:
18: this.CaptureMouse();
19: }
20:
21: base.OnPreviewMouseDown(e);
22: }
23:
24:
25: protected override void OnPreviewMouseMove(MouseEventArgs e)
26: {
27: if (this.IsMouseCaptured)
28: {
29:
30: Point point = e.GetPosition(this);
31:
32:
33: Point delta = new Point(
34: (point.X > this.scrollStartPoint.X) ?
35: -(point.X - this.scrollStartPoint.X) :
36: (this.scrollStartPoint.X - point.X),
37:
38: (point.Y > this.scrollStartPoint.Y) ?
39: -(point.Y - this.scrollStartPoint.Y) :
40: (this.scrollStartPoint.Y - point.Y));
41:
42:
43: ScrollViewer.ScrollToHorizontalOffset(
44: this.scrollStartOffset.X + delta.X);
45: ScrollViewer.ScrollToVerticalOffset(
46: this.scrollStartOffset.Y + delta.Y);
47: }
48:
49: base.OnPreviewMouseMove(e);
50: }
51:
52:
53:
54: protected override void OnPreviewMouseUp(
55: MouseButtonEventArgs e)
56: {
57: if (this.IsMouseCaptured)
58: {
59: this.Cursor = Cursors.Arrow;
60: this.ReleaseMouseCapture();
61: }
62:
63: base.OnPreviewMouseUp(e);
64: }
Friction Version
Use the Friction
property to set a value between 0
and 1
, 0
being no friction, 1
is full friction meaning the panel won’t "auto-scroll
".
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.Data;
8: using System.Windows.Documents;
9: using System.Windows.Input;
10: using System.Windows.Media;
11: using System.Windows.Media.Imaging;
12: using System.Windows.Navigation;
13: using System.Windows.Shapes;
14: using System.Windows.Threading;
15: using System.Diagnostics;
16:
17: namespace ScrollableArea
18: {
19: 20: 21: 22: 23: 24: public partial class Window1 : Window
25: {
26: #region Data
27:
28: private Point scrollTarget;
29: private Point scrollStartPoint;
30: private Point scrollStartOffset;
31: private Point previousPoint;
32: private Vector velocity;
33: private double friction;
34: private DispatcherTimer animationTimer = new DispatcherTimer();
35: #endregion
36:
37: #region Ctor
38:
39: public Window1()
40: {
41: InitializeComponent();
42: this.LoadStuff();
43:
44: friction = 0.95;
45:
46: animationTimer.Interval = new TimeSpan(0, 0, 0, 0, 20);
47: animationTimer.Tick += new EventHandler(HandleWorldTimerTick);
48: animationTimer.Start();
49: }
50: #endregion
51:
52: #region Load DUMMY Items
53: void LoadStuff()
54: {
55:
56:
57:
58: itemsControl.Items.Add(CreateStackPanel(Brushes.Salmon));
59: itemsControl.Items.Add(CreateStackPanel(Brushes.Goldenrod));
60: itemsControl.Items.Add(CreateStackPanel(Brushes.Green));
61: itemsControl.Items.Add(CreateStackPanel(Brushes.Yellow));
62: itemsControl.Items.Add(CreateStackPanel(Brushes.Purple));
63: itemsControl.Items.Add(CreateStackPanel(Brushes.SeaShell));
64: itemsControl.Items.Add(CreateStackPanel(Brushes.SlateBlue));
65: itemsControl.Items.Add(CreateStackPanel(Brushes.Tomato));
66: itemsControl.Items.Add(CreateStackPanel(Brushes.Violet));
67: itemsControl.Items.Add(CreateStackPanel(Brushes.Plum));
68: itemsControl.Items.Add(CreateStackPanel(Brushes.PapayaWhip));
69: itemsControl.Items.Add(CreateStackPanel(Brushes.Pink));
70: itemsControl.Items.Add(CreateStackPanel(Brushes.Snow));
71: itemsControl.Items.Add(CreateStackPanel(Brushes.YellowGreen));
72: itemsControl.Items.Add(CreateStackPanel(Brushes.Tan));
73:
74: }
75:
76: private StackPanel CreateStackPanel(SolidColorBrush color)
77: {
78:
79: StackPanel sp = new StackPanel();
80: sp.Orientation = Orientation.Horizontal;
81:
82: for (int i = 0; i < 50; i++)
83: {
84: Rectangle rect = new Rectangle();
85: rect.Width = 100;
86: rect.Height = 100;
87: rect.Margin = new Thickness(5);
88: rect.Fill = i % 2 == 0 ? Brushes.Black : color;
89: sp.Children.Add(rect);
90: }
91: return sp;
92: }
93: #endregion
94:
95: #region Friction Stuff
96: private void HandleWorldTimerTick(object sender, EventArgs e)
97: {
98: if (IsMouseCaptured)
99: {
100: Point currentPoint = Mouse.GetPosition(this);
101: velocity = previousPoint - currentPoint;
102: previousPoint = currentPoint;
103: }
104: else
105: {
106: if (velocity.Length > 1)
107: {
108: ScrollViewer.ScrollToHorizontalOffset(scrollTarget.X);
109: ScrollViewer.ScrollToVerticalOffset(scrollTarget.Y);
110: scrollTarget.X += velocity.X;
111: scrollTarget.Y += velocity.Y;
112: velocity *= friction;
113: }
114: }
115: }
116:
117: public double Friction
118: {
119: get { return 1.0 - friction; }
120: set { friction = Math.Min(Math.Max(1.0 - value, 0), 1.0); }
121: }
122: #endregion
123:
124: #region Mouse Events
125: protected override void OnPreviewMouseDown(MouseButtonEventArgs e)
126: {
127: if (ScrollViewer.IsMouseOver)
128: {
129:
130: scrollStartPoint = e.GetPosition(this);
131: scrollStartOffset.X = ScrollViewer.HorizontalOffset;
132: scrollStartOffset.Y = ScrollViewer.VerticalOffset;
133:
134:
135: this.Cursor = (ScrollViewer.ExtentWidth > ScrollViewer.ViewportWidth) ||
136: (ScrollViewer.ExtentHeight > ScrollViewer.ViewportHeight) ?
137: Cursors.ScrollAll : Cursors.Arrow;
138:
139: this.CaptureMouse();
140: }
141:
142: base.OnPreviewMouseDown(e);
143: }
144:
145:
146: protected override void OnPreviewMouseMove(MouseEventArgs e)
147: {
148: if (this.IsMouseCaptured)
149: {
150: Point currentPoint = e.GetPosition(this);
151:
152:
153: Point delta = new Point(scrollStartPoint.X -
154: currentPoint.X, scrollStartPoint.Y - currentPoint.Y);
155:
156: scrollTarget.X = scrollStartOffset.X + delta.X;
157: scrollTarget.Y = scrollStartOffset.Y + delta.Y;
158:
159:
160: ScrollViewer.ScrollToHorizontalOffset(scrollTarget.X);
161: ScrollViewer.ScrollToVerticalOffset(scrollTarget.Y);
162: }
163:
164: base.OnPreviewMouseMove(e);
165: }
166:
167: protected override void OnPreviewMouseUp(MouseButtonEventArgs e)
168: {
169: if (this.IsMouseCaptured)
170: {
171: this.Cursor = Cursors.Arrow;
172: this.ReleaseMouseCapture();
173: }
174:
175: base.OnPreviewMouseUp(e);
176: }
177: #endregion
178:
179:
180:
181: }
182: }
And that’s it, we now have a nice scrollable design surface. Here is a screen shot of the demo app, where the user can happily scroll around using the mouse (mouse button must be down).
Here is a link to the demo app (Frictionless
).
Here is a link to the demo app (Friction
).