Creating a Scrollable Control Surface In WPF






4.44/5 (10 votes)
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).
Here is a link to the demo app (Frictionless
).
Here is a link to the demo app (Friction
).