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)

image

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

34 Comments so far »

  1. 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 :)

  2. sacha barber said

    am April 10 2008 @ 10:33 am

    Thanks Rudi. Yeah I think this is handy for sure

  3. 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 !

  4. sacha said

    am April 11 2008 @ 6:57 am

    Thanks Marlon. Friction is now there, have a play.

  5. 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. :)

  6. 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.

  7. 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!

  8. 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.

  9. 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.

  10. 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.

  11. fcchan said

    am March 26 2009 @ 8:18 am

    i wish i am as good programmer as you are. cool code. thanks man

  12. sacha said

    am March 26 2009 @ 8:30 am

    Cheers man

  13. 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

  14. 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.

  15. 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

  16. 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.

  17. fcchan said

    am March 26 2009 @ 11:57 am

  18. 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_

  19. 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

  20. 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.

  21. fcchan said

    am March 26 2009 @ 12:59 pm

    hmmmm… u look a bit like Jack Sheppard of Lost

  22. 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 [...]

  23. André Knuth said

    am April 17 2009 @ 2:32 pm

    very nice idea, going to port this to silverlight…
    thanks!

  24. sacha said

    am April 17 2009 @ 4:20 pm

    Andre

    Here is a Silverlight version (Sl3 though)

    http://sachabarber.net/?p=481

  25. André Knuth said

    am April 17 2009 @ 7:06 pm

    Thanks, mate. That will do!

  26. Mark Pearl said

    am May 27 2009 @ 10:19 am

    Thanks so much. This really helped me with ym WPF project.

  27. sacha said

    am May 27 2009 @ 10:31 am

    You are welcome

  28. 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.

  29. sacha said

    am September 9 2009 @ 4:20 pm

    Thanks Sandra

  30. sachabarber.net » Friction Scrolling Now An WPF Attached Behaviour Too said

    am December 24 2009 @ 7:01 am

  31. Brian said

    am May 12 2010 @ 1:03 am

    Hi,

    I’ve currently encountered laggy when i tried to change the code in “scrollablearea.zip”

    for (int i = 0; i < 5000; i++)
    {
    Rectangle rect = new Rectangle();
    rect.Width = 3;
    rect.Height = 3;
    rect.Margin = new Thickness(1);
    rect.Fill = i % 2 == 0 ? Brushes.Black : color;
    sp.Children.Add(rect);
    }

    Do you have any idea why is it has a poor performance if I try to change the size of the rectangle ?

  32. sacha said

    am May 12 2010 @ 5:56 am

    No idea about that actually. Obviously the rectangles were just simulation, I would never create a UI that had 5000 rectangles on it.

  33. sacha said

    am May 12 2010 @ 5:57 am

    cant recall but the problem could be no virtualization, if it is using a standard StackPanel in the XAML try and swap it for a VirtualizingStackPanel and try that.

  34. Brian said

    am May 13 2010 @ 12:01 am

    oh ~
    I need to have around 5k ~ 10k rectangle…
    is there any better way you can suggest me to improve its performance ?
    i’ve ask in the msdn>wpf forum
    some suggest me to draw it in Viewport3D
    is there any other way that i can do it ?
    your reply would be greatly appreciated.. sorry i have to post in ur comment here..

Comment RSS · TrackBack URI

Leave a comment

Name: (Required)

eMail: (Required)

Website:

Comment: