Click here to Skip to main content
Click here to Skip to main content
Technical Blog

Tagged as

Creating A Scrollable Control Surface In WPF

, 17 Jun 2009 CPOL
Rate this:
Please Sign up or sign in to vote.
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 [...]

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:      /// <span class="code-SummaryComment"><summary></span>
  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:      /// <span class="code-SummaryComment"></summary></span>
  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)

image-thumb.png

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

License

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

Share

About the Author

Sacha Barber
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 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

 
Questionmany many thanks PinmemberKapistiotis30-Aug-12 21:57 
Generalconvert to a Canvas Pinmembertcpatiii30-May-11 11:49 
GeneralRe: convert to a Canvas PinmvpSacha Barber30-May-11 19:54 
GeneralRe: convert to a Canvas Pinmembertcpatiii3-Jun-11 4:45 
GeneralRe: convert to a Canvas Pinmembertcpatiii3-Jun-11 4:46 
GeneralRe: convert to a Canvas PinmvpSacha Barber3-Jun-11 5:17 
GeneralRe: convert to a Canvas Pinmembertcpatiii3-Jun-11 6:38 
GeneralRe: convert to a Canvas PinmvpSacha Barber5-Jun-11 19:54 
GeneralHow can we scroll content in loop - like coverflow Pinmemberamer chaudry29-May-11 4:14 
GeneralRe: How can we scroll content in loop - like coverflow PinmvpSacha Barber29-May-11 22:47 
Generalhow would you apply this to a wpf listbox? Pinmemberfaggus214-May-11 6:11 
GeneralRe: how would you apply this to a wpf listbox? PinmvpSacha Barber14-May-11 21:26 
Questionafter using the above code for creating a scrollable control surface, how do I enable click? Pinmemberfunny_hacks25-Apr-10 18:49 
AnswerRe: after using the above code for creating a scrollable control surface, how do I enable click? PinmvpSacha Barber25-Apr-10 20:09 
AnswerRe: after using the above code for creating a scrollable control surface, how do I enable click? Pinmembernicksaxelby21-Nov-11 4:26 
GeneralGood Stuff! Pinmembermartin martinez abadi12-Mar-10 7:14 
GeneralRe: Good Stuff! PinmvpSacha Barber13-Mar-10 9:25 

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

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

| Advertise | Privacy | Mobile
Web02 | 2.8.141015.1 | Last Updated 17 Jun 2009
Article Copyright 2009 by Sacha Barber
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid