Click here to Skip to main content
15,867,704 members
Articles / Desktop Programming / WPF

Creating a Scrollable Control Surface In WPF

Rate me:
Please Sign up or sign in to vote.
4.44/5 (10 votes)
17 Jun 2009CPOL2 min read 81K   3.5K   18   26
How to create 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 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:          // 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 I'm 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).

37349/image-thumb.png

Here is a link to the demo app (Frictionless).

Here is a link to the demo app (Friction).

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
United Kingdom United Kingdom
I currently hold the following qualifications (amongst others, I also studied Music Technology and Electronics, for my sins)

- MSc (Passed with distinctions), in Information Technology for E-Commerce
- BSc Hons (1st class) in Computer Science & Artificial Intelligence

Both of these at Sussex University UK.

Award(s)

I am lucky enough to have won a few awards for Zany Crazy code articles over the years

  • Microsoft C# MVP 2016
  • Codeproject MVP 2016
  • Microsoft C# MVP 2015
  • Codeproject MVP 2015
  • Microsoft C# MVP 2014
  • Codeproject MVP 2014
  • Microsoft C# MVP 2013
  • Codeproject MVP 2013
  • Microsoft C# MVP 2012
  • Codeproject MVP 2012
  • Microsoft C# MVP 2011
  • Codeproject MVP 2011
  • Microsoft C# MVP 2010
  • Codeproject MVP 2010
  • Microsoft C# MVP 2009
  • Codeproject MVP 2009
  • Microsoft C# MVP 2008
  • Codeproject MVP 2008
  • And numerous codeproject awards which you can see over at my blog

Comments and Discussions

 
SuggestionScroll Wheel Velocity Snippet Pin
tom kefauver7-Nov-21 9:07
tom kefauver7-Nov-21 9:07 
QuestionScrollable Control Surface including ScrollBar Funtions Pin
majoma10-Aug-21 21:59
majoma10-Aug-21 21:59 
QuestionSimplification to delta calculation Pin
DaveInCaz18-Mar-20 8:45
DaveInCaz18-Mar-20 8:45 
QuestionHow to make difference between click on clickable control (button/listboxitem) and click-hold to move scrollviewer content? Pin
Ganther825-Nov-19 21:35
Ganther825-Nov-19 21:35 
QuestionBetter performance for friction Pin
User 1071150922-Sep-19 8:39
User 1071150922-Sep-19 8:39 
AnswerRe: Better performance for friction Pin
DaveInCaz18-Mar-20 8:57
DaveInCaz18-Mar-20 8:57 
Questionhow to keep the image within bounds when panning Pin
SivaKtamilan28-May-15 3:29
SivaKtamilan28-May-15 3:29 
QuestionMouse Event Bug Fix Pin
Member 1025022724-Dec-14 5:06
Member 1025022724-Dec-14 5:06 
Questionmany many thanks Pin
Kapistiotis30-Aug-12 21:57
Kapistiotis30-Aug-12 21:57 
Generalconvert to a Canvas Pin
tcpatiii30-May-11 11:49
tcpatiii30-May-11 11:49 
GeneralRe: convert to a Canvas Pin
Sacha Barber30-May-11 19:54
Sacha Barber30-May-11 19:54 
GeneralRe: convert to a Canvas Pin
tcpatiii3-Jun-11 4:45
tcpatiii3-Jun-11 4:45 
GeneralRe: convert to a Canvas Pin
tcpatiii3-Jun-11 4:46
tcpatiii3-Jun-11 4:46 
GeneralRe: convert to a Canvas Pin
Sacha Barber3-Jun-11 5:17
Sacha Barber3-Jun-11 5:17 
GeneralRe: convert to a Canvas Pin
tcpatiii3-Jun-11 6:38
tcpatiii3-Jun-11 6:38 
GeneralRe: convert to a Canvas Pin
Sacha Barber5-Jun-11 19:54
Sacha Barber5-Jun-11 19:54 
GeneralHow can we scroll content in loop - like coverflow Pin
AmerZafar29-May-11 4:14
AmerZafar29-May-11 4:14 
GeneralRe: How can we scroll content in loop - like coverflow Pin
Sacha Barber29-May-11 22:47
Sacha Barber29-May-11 22:47 
To be honest that sort of behaviour is something that you would have to code yourself. All I provide here is a scrollable area.

If you want a coverflow thing for WPF, this is it : http://fluidkit.codeplex.com/[^]
Sacha Barber
  • Microsoft Visual C# MVP 2008-2011
  • Codeproject MVP 2008-2011
Open Source Projects
Cinch SL/WPF MVVM

Your best friend is you.
I'm my best friend too. We share the same views, and hardly ever argue

My Blog : sachabarber.net

Generalhow would you apply this to a wpf listbox? Pin
faggus214-May-11 6:11
faggus214-May-11 6:11 
GeneralRe: how would you apply this to a wpf listbox? Pin
Sacha Barber14-May-11 21:26
Sacha Barber14-May-11 21:26 
Questionafter using the above code for creating a scrollable control surface, how do I enable click? Pin
funny_hacks25-Apr-10 18:49
funny_hacks25-Apr-10 18:49 
AnswerRe: after using the above code for creating a scrollable control surface, how do I enable click? Pin
Sacha Barber25-Apr-10 20:09
Sacha Barber25-Apr-10 20:09 
AnswerRe: after using the above code for creating a scrollable control surface, how do I enable click? Pin
nicksaxelby21-Nov-11 4:26
nicksaxelby21-Nov-11 4:26 
GeneralRe: after using the above code for creating a scrollable control surface, how do I enable click? Pin
Ganther825-Nov-19 21:37
Ganther825-Nov-19 21:37 
GeneralGood Stuff! Pin
martin martinez abadi12-Mar-10 7:14
martin martinez abadi12-Mar-10 7:14 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.