Creating A Scrollable Control Surface In WPF
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 the 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. Its 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 its his area) so we have 2 versions, the XAML is the same for both
Lets 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: // Save starting point, used later when determining
6: //how much to scroll.
7: scrollStartPoint = e.GetPosition(this);
8: scrollStartOffset.X = ScrollViewer.HorizontalOffset;
9: scrollStartOffset.Y = ScrollViewer.VerticalOffset;
10:
11: // Update the cursor if can scroll or not.
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: // Get the new scroll position.
30: Point point = e.GetPosition(this);
31:
32: // Determine the new amount to scroll.
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: // Scroll to the new position.
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: /// <summary>
20: /// Demonstrates how to make a scrollable (via the mouse) area that
21: /// would be useful for storing a large object, such as diagram or
22: /// something like that
23: /// </summary>
24: public partial class Window1 : Window
25: {
26: #region Data
27: // Used when manually scrolling.
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: //this could be any large object, imagine a diagram...
56: //though for this example im just using loads
57: //of Rectangles
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: // Save starting point, used later when determining how much to scroll.
130: scrollStartPoint = e.GetPosition(this);
131: scrollStartOffset.X = ScrollViewer.HorizontalOffset;
132: scrollStartOffset.Y = ScrollViewer.VerticalOffset;
133:
134: // Update the cursor if can scroll or not.
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: // Determine the new amount to scroll.
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: // Scroll to the new position.
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)
And here is a link to the demo app (Frictionless) scrollablearea.zip
And here is a link to the demo app (Friction) scrollablearea_friction.zip


























Rudi Grobler said
am April 10 2008 @ 10:18 am
Hi Sacha,
nice article… something I needed for a project!
Rudi
PS. This is the stuff attached properties are made for
sacha barber said
am April 10 2008 @ 10:33 am
Thanks Rudi. Yeah I think this is handy for sure
Marlon Grech said
am April 10 2008 @ 6:28 pm
Great stuff!!! It would be cool if you add just a little bit of physics…. Yet still this is simple brilliant dude !!!
Thanks for sharing !
sacha said
am April 11 2008 @ 6:57 am
Thanks Marlon. Friction is now there, have a play.
Josh Smith said
am April 11 2008 @ 1:21 pm
Very cool work. This would make an excellent starting point for a lot of projects.
sacha barber said
am April 11 2008 @ 1:29 pm
Thanks Josh…you should see where these 2 came from, its way cool. Some of my best yet, its all just working to plan. Excellent. Cant wait to share it.
John Henkel said
am April 30 2008 @ 12:54 pm
Oh man. You just cut at least 3 days off my project debugging time. Very nice work!
sacha said
am April 30 2008 @ 1:05 pm
Thanks John. I wrapped this into a scrollable ScrollViewer in my codeproject article http://www.codeproject.com/KB/WPF/SortingLists.aspx in case you are interested.
I like the friction touch its cool.
Thanks for liking it.
Dave said
am May 29 2008 @ 7:50 pm
Very cool. The animation on the friction scroll viewer was a little less performant than I wanted, so I reimplemented it as a spline animation and allowed for specifying the animation’s duration instead of the friction. Now the animation is smoother and it leverages WPF animation goodness. I still use the timer to calculate velocity though – that still seemed the best way.
sacha said
am May 30 2008 @ 7:58 am
Dave
Sounds great
Care to send me the code and Ill ammend this blog and credit you where I put a link to your code.
fcchan said
am March 26 2009 @ 8:18 am
i wish i am as good programmer as you are. cool code. thanks man
sacha said
am March 26 2009 @ 8:30 am
Cheers man
fcchan said
am March 26 2009 @ 9:35 am
oh no overrided the mouse down. my do drags now have to interwine with your FrictionScrollViewer.cs
sacha said
am March 26 2009 @ 10:09 am
OK so fo not override them, you could put all this in form code if you prefer.
fcchan said
am March 26 2009 @ 10:42 am
My drag drop are from another genius’ who enable drag and drop operations on UIElement passed in.
The art now is to amalgam 2 code:
friction scroll viewer and drag drop helper.
I modified drag drop helper, when a textblock is dragged and if her parent is a border, both drags. Cute shapes with labels that gets dragged to another panel! Cute!!
The scrollviewer somehow interefered with MouseEventArgs original source. Having trouble understanding why….
Have u ever played Dungeons and Dragons 3rd edition? I am like the Rogue, who have a chance of casting a spell in an Archmage scroll but never truly understand the content
fcchan said
am March 26 2009 @ 11:56 am
This Rogue programmer found a workaround in FrictionScrollViewer.cs –>protected override void OnMouseDown
if (e.OriginalSource is StackPanel)
{
CaptureMouse();
}
Sorry Sacha, I turn your code into something so not elegant! But if it’s any consolation, I scroll on StackPanel surface now it scrolls with friction. If I push the mouse on those little ’shapes’, it drags.
Plus drag drop helper class no need to change!
Here my XAML code is like this. Just wanna show you and readers here how my little shapes looks like. But with CornerRadius and stuff the possibilities for cute is endless.
fcchan said
am March 26 2009 @ 11:57 am
fcchan said
am March 26 2009 @ 12:02 pm
sorry had to change to little symbols (can’t type here) to _ or here it won’t accept.
_diagram:FrictionScrollViewer x:Name=”frictionScroller1″ Height=”250″ CanContentScroll=”False” Friction=”0.85″ VerticalScrollBarVisibility=”Hidden”_
_StackPanel x:Name=”canvas1″ Background=”AliceBlue” Opacity=”1″ Grid.Row=”0″ Grid.RowSpan=”1″ Grid.ColumnSpan=”1″ CanHorizontallyScroll=”True” CanVerticallyScroll=”False”_
_Border HorizontalAlignment=”Center” Width=”100″ BorderBrush=”#008″ BorderThickness=”2″ CornerRadius=”20″ Padding=”30″ Margin=”1″_
_Border.Background_
_RadialGradientBrush RadiusX=”1″ RadiusY=”1″ GradientOrigin=”0.7,0.3″_
_GradientStop Color=”White” Offset=”0″ /_
_GradientStop Color=”Black” Offset=”1″ /_
_/RadialGradientBrush_
_/Border.Background_
_TextBlock FontSize=”25″ FontFamily=”Pericles”__/TextBlock_
_/Border_
_/StackPanel_
_/diagram:FrictionScrollViewer_
fcchan said
am March 26 2009 @ 12:18 pm
Forgot to say I got the FrictionScrollViewer.cs here:
A Spider type control tree thingy for WPF
By Sacha Barber, Fredrik Bornander
http://www.codeproject.com/KB/WPF/SpiderControl.aspx
sacha said
am March 26 2009 @ 12:29 pm
fcchan
Its all cool, if you modify the code and it works for you its all cool.
fcchan said
am March 26 2009 @ 12:59 pm
hmmmm… u look a bit like Jack Sheppard of Lost
sachabarber.net » Scrollable Friction Canvas For Silverlight said
am March 28 2009 @ 2:50 pm
[...] a post about creating a friction enabled scrolling canvas in WPF (the old post can be found at http://sachabarber.net/?p=225), which I thought was way cool. It turns out that I was not the only one that thought this, and one [...]
André Knuth said
am April 17 2009 @ 2:32 pm
very nice idea, going to port this to silverlight…
thanks!
sacha said
am April 17 2009 @ 4:20 pm
Andre
Here is a Silverlight version (Sl3 though)
http://sachabarber.net/?p=481
André Knuth said
am April 17 2009 @ 7:06 pm
Thanks, mate. That will do!
Mark Pearl said
am May 27 2009 @ 10:19 am
Thanks so much. This really helped me with ym WPF project.
sacha said
am May 27 2009 @ 10:31 am
You are welcome
sandra742 said
am September 9 2009 @ 3:17 pm
Hi! I was surfing and found your blog post… nice! I love your blog.
Cheers! Sandra. R.
sacha said
am September 9 2009 @ 4:20 pm
Thanks Sandra
sachabarber.net » Friction Scrolling Now An WPF Attached Behaviour Too said
am December 24 2009 @ 7:01 am
[...] http://sachabarber.net/?p=225 [...]