Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Creating a Scrollable Control Surface In WPF

0.00/5 (No votes)
17 Jun 2009 1  
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 has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here