WPF Scrolling Content with Flicks and Gestures - C#

by Travis Feirtag, copyright 2009

I was looking for a way to scroll UI elements in WPF similar to how the popular iPhone works or the HP MediaSmart software.  I couldn't find what I was looking for so it seemed like a good idea to work on a solution and write an article.  I'll preface this by saying that this is one possible solution.  I'm sure that there are some other ways to do the same thing in WPF but this is the way I solved it.  For this article, I wanted to demonstrate how to use the ScrollViewer control in WPF combined with gestures and flicks to create a simple image catalog. I'll use the same code for accessing flicks and gestures from the C# libraries I already wrote in a previous article.  I hope you find it fun and useful.

ScrollViewer Control

WPF provides a ScrollViewer control that is a container for other controls and creates a scrollable area.  It is simple to create and add to a XAML file.  The developer can choose to display a horizontal and/or vertical scroll bar.  For this article, I chose to hide the scoll bar so that the user must scroll by clicking on the content in the ScrollViewer.

WPF Scroll Content App

First I created a new WPF application and added a ScrollViewer control to the XAML.  I also want to populate the ScrollViewer with image thumbnails.  So I wrote a method called GetImageList that finds all image files in a given directory path and loads them into a list.  That list is then used to populate a StackPanel control which is a really useful control for ordering UI elements into a single row.  This is called in the constructor of the WPF window.

 
        public WinMain()
        {
            InitializeComponent();
 
            // Get images
            List<Image> imageList = GetImageList(@"C:\Users\Public\Pictures\Sample Pictures");
 
            // Create StackPanel and set child elements to horizontal orientation
            StackPanel imgStackPnl = new StackPanel();
            imgStackPnl.HorizontalAlignment = HorizontalAlignment.Left;
            imgStackPnl.VerticalAlignment = VerticalAlignment.Top;
            imgStackPnl.Orientation = Orientation.Horizontal;
 
            // Iterate through each image and add to StackPanel
            foreach (Image curImage in imageList)
            {
                imgStackPnl.Children.Add(curImage);
            }
 
            // Set content of ScrollViewer to StackPanel
            this.myScrollViewer.Content = imgStackPnl;
        }
 
 
        private List<Image> GetImageList(string strPath)
        {
            List<Image> imageList = new List<Image>();
            string strFilePath = "";
 
            try
            {
                // supported image files
                List<string> formatList = new List<string>();
                formatList.Add(".jpg");
                formatList.Add(".png");
                formatList.Add(".bmp");
                formatList.Add(".gif");
                formatList.Add(".tif");
 
                DirectoryInfo dirInfo = new DirectoryInfo(strPath);
                FileInfo[] files = dirInfo.GetFiles();
 
                foreach (FileInfo curFile in files)
                {
                    // only look for image files 
                    if (formatList.Contains(curFile.Extension.ToLower()) == false)
                        continue;
 
                    strFilePath = curFile.FullName;
 
                    Image curImage = new Image();
                    BitmapImage bmpImage = new BitmapImage();
                    bmpImage.BeginInit();
                    bmpImage.UriSource = new Uri(curFile.FullName, UriKind.Absolute);
                    bmpImage.EndInit();
 
                    curImage.Height = 140;
                    curImage.Stretch = Stretch.Fill;
 
                    curImage.Source = bmpImage;
                    curImage.Margin = new Thickness(10);
 
                    imageList.Add(curImage);
                }
            }
            catch (Exception ex)
            {
                MessageBox.Show(string.Format("{0}-{1}", ex.Message, strFilePath));
            }
 
            return imageList;
        }

The application should look like this so far.  As you can see the scrollable content is at the bottom of the window.  I used the /Sample Pictures directory found on Vista OS for example images.  However there are no scroll bars so if you click on the content, it will not scroll.  We have to add some event handlers first.

MouseDown, MouseUp and MouseMove Handlers

We'll start by hooking the MouseDown and MouseUp events of the Image elements.  We'll do it in the foreach loop when the Image elements are being added to the StackPanel.  We don't hook the MouseDown of the ScrollViewer because the child elements will get the MouseDown event first.

 
            // Iterate through each image and add to StackPanel
            foreach (Image curImage in imageList)
            {
                curImage.MouseDown += new MouseButtonEventHandler(CurImage_MouseDown);
                curImage.MouseUp += new MouseButtonEventHandler(CurImage_MouseUp);
                imgStackPnl.Children.Add(curImage);
            }
 

The event handlers will be used to set / reset the offset starting mouse position.  The variable _dbMousePosition will be used to hold the current postion of the mouse.

 
        private void CurImage_MouseDown(object sender, MouseButtonEventArgs mouseButtonEventArgs)
        {
            this._dbMousePosition= mouseButtonEventArgs.GetPosition(this.myScrollViewer).X;
        }
 
        private void CurImage_MouseUp(object sender, MouseButtonEventArgs mouseButtonEventArgs)
        {
            this._dbMousePosition= -1;
        }
 

Now we hook the MouseMove event of the ScrollViewer control.  This will be used to move the StackPanel content in the ScrollViewer based on the initial position of the mouse from the MouseDown event of the image.

 
        private void MyScrollViewer_MouseMove(object sender, MouseEventArgs mouseEventArgs)
        {
            if (this._dbMousePosition > 0)
            {
                double dbX = mouseEventArgs.GetPosition(this.myScrollViewer).X;
                double dbDelta = dbX - this._dbMousePosition;
                double dbCurOffset = this.myScrollViewer.ContentHorizontalOffset;
 
                double dbNew = dbCurOffset - dbDelta;
                this.myScrollViewer.ScrollToHorizontalOffset(dbNew);
 
                this._dbMousePosition = dbX;
            }
        }
 

At this point, you can run the program and click on any of the images in the ScrollViewer, drag them left and right to see the content scroll the way we want it to.  But we're not done yet, now we want to scroll the content by using a tablet flick.

NOTE: I've provided a copy of the code that can be used without the tablet flicks. You can scroll the content simply by clicking on the image and dragging. This code is useful for anyone who doesn't have a tablet but still wants to scroll content. The rest of the code in this article will use the tablet flicks / gestures and will require you to have a compatible tablet or touch panel.

WPF Scrolling Content without flicks, C# Source Code WpfScrollContent_no_flicks.zip

Animating the ScrollViewer Control

In WPF we have the class called DoubleAnimation which should be perfect for what we want to do.  Set the From value, To value and the Duration, and let it go.  However what I found is that it unfortunately will not work.  I wrote the following code to animate the HorizontalOffset of the ScrollViewer control.

 
            DoubleAnimation animScrollLeft = new DoubleAnimation();
            animScrollLeft.From = dbCurOffset;
            animScrollLeft.To = dbCurOffset - 200;
            animScrollLeft.Duration = new Duration(TimeSpan.FromSeconds(3));
            animScrollLeft.AccelerationRatio = 0.5;
 
            // THIS DOESN'T WORK !!!
            this.myScrollViewer.BeginAnimation(ScrollViewer.ContentHorizontalOffsetProperty, animScrollLeft);
 

Everything will compile fine and the application will seemingly run.  But when I try to execute this code, I get a runtime exception that states, "'ContentHorizontalOffset' property is not animatable on 'System.Windows.Controls.ScrollViewer' class because the IsAnimationProhibited flag has been set on the UIPropertyMetadata used to associate the property with the class."  What the..?!?  I don't understand why but this dependency property cannot be set using an Animation class.  Next I attempted to override the metadata of the dependency property by creating a custom class that inherits from ScrollViewer to set the IsAnimationProhibited value to false.  In the static constructor, I tried running the following code.

 
        static CustomScrollViewer()
        {
            UIPropertyMetadata uiPropMetadata = new UIPropertyMetadata();
            uiPropMetadata.IsAnimationProhibited = false;
            // THIS DOESN'T WORK !!!
            ContentHorizontalOffsetProperty.OverrideMetadata(typeof(MyScrollViewer), uiPropMetadata);
        }
 

This also throws a runtime exception stating that this is not allowed. So I needed a new approach to animating the content.  At this point I decided to create my own animation class.  Instead of animating using a dependency property, it uses a callback method to tell the UI that a new value has been created.  It is up to the main window to update the ScrollViewer control's HorizontalOffset.  This turned out to work very well.  It was important to be certain that the value was being set in the UI thread instead of a separate thread so I made good use of the Dispatcher class.

I created a class called ScrollAnimation.  You can set From, To and Duration values just like the DoubleAnimation class.  The difference is that you hook the UpdateAnimation event and call Start to begin the animation.  The class calculates all the needed values, creates a thread and runs the animation for the specified duration.  I won't show all the code here because it would take up too much space.  I'll leave it up to you to open the ScrollAnimation.cs file and see how it works.  In addition, I created a custom EventArgs class called AnimationEventArgs.  All it does is adds a Value property to pass back to the subscriber of the event.  I will show how the WPF app calls the animation object and handles the callback.

To get the animation to start moving right, you simply get the current offset, set the values of the animation and tell it to Start.

 
        private void ScrollContentRight()
        {
            double dbCurOffset = this.myScrollViewer.ContentHorizontalOffset;
 
            this._animScroll.From = dbCurOffset;
            this._animScroll.To = dbCurOffset + ANIM_SCROLL_AMOUNT;
            this._animScroll.Duration = TimeSpan.FromSeconds(1);
            this._animScroll.Start();
        }
 

The animation object callback event is hooked in the Window constructor. So then each time a value is updated from the animation, the callback is sent from the other thread.  It is important to check the current Dispatcher to make certain you can set UI elements from the correct thread.

        {
            // Hook the update animation callback event in the Window constructor
            this._animScroll.UpdateAnimation += new UpdateAnimationHandler(AnimScroll_UpdateAnimation);
        }
 
        private void AnimScroll_UpdateAnimation(object objSender, AnimationEventArgs animationArgs)
        {
            if (this.Dispatcher == System.Windows.Threading.Dispatcher.CurrentDispatcher)
            {
                this.myScrollViewer.ScrollToHorizontalOffset(animationArgs.Value);
            }
            else
            {
                this.Dispatcher.BeginInvoke(new UpdateAnimationHandler(AnimScroll_UpdateAnimation), 
                                            new object[] { objSender, animationArgs });
            }
        }
 

Adding the Flicks

Now that we have a scroll animation, we need to add the flick references and hook the flick actions.  I used the FlickLib project that I created for the previous article.  In WPF, we need to access the message pump to find the flick and gesture messages to do something with them.  This is done using the WindowsInteropHelper class to hook a MessageProc method.

 
        private void WinMain_Loaded(object sender, RoutedEventArgs routedEventArgs)
        {
            WindowInteropHelper winHelper = new WindowInteropHelper(this);
            try
            {
                HwndSource hwnd = HwndSource.FromHwnd(winHelper.Handle);
                hwnd.AddHook(new HwndSourceHook(this.MessageProc));
            }
            catch (Exception ex)
            {
                MessageBox.Show(ex.Message);
            }
        }
 

The MessageProc simply looks at the message ID and finds the flick messages.

 
        private IntPtr MessageProc(IntPtr hwnd, int nMsgID, IntPtr wParam, IntPtr lParam, ref bool bHandled)
        {
            if (nMsgID == Flicks.WM_TABLET_FLICK)
            {
                Flick flick = Flicks.ProcessMessage(lParam, wParam);
                if (flick != null)
                {
                    ProcessFlick(flick);
                    bHandled = true;
                }
            }
 
            return IntPtr.Zero;
        }
 

The ProcessFlicks method looks for the specific flick directions. It was important to determine if the flick happened inside the ScrollViewer control so that only flicks on the control make it move.  So I used the InputHitTest method from the UIElement class.  It will tell you if the given point is within the control and which child control it was on.  Once I determine that the flick happened over the ScrollViewer control, then I scroll the content left or right.  I also added a section to handle the flick up direction.  If the up flick happens over an image, then it is put onto the main image control.

 
        private void ProcessFlick(Flick flick)
        {
            Object objTest;
            Point ptTest;
 
            // Scroll content left if the left flick was on top of the scroll viewer
            if (flick.Data.Direction == FLICKDIRECTION.FLICKDIRECTION_LEFT)
            {
                ptTest = this.myScrollViewer.PointFromScreen(new Point(flick.Point.X, flick.Point.Y));
                objTest = this.myScrollViewer.InputHitTest(ptTest);
                if (objTest != null)
                    ScrollContentRight();
            }
 
            // Scroll content left if the right flick was on top of the scroll viewer
            if (flick.Data.Direction == FLICKDIRECTION.FLICKDIRECTION_RIGHT)
            {
                ptTest = this.myScrollViewer.PointFromScreen(new Point(flick.Point.X, flick.Point.Y));
                objTest = this.myScrollViewer.InputHitTest(ptTest);
                if (objTest != null)
                    ScrollContentLeft();
            }
 
            // If an up flick happened over an image, then put image on the main image control
            if (flick.Data.Direction == FLICKDIRECTION.FLICKDIRECTION_UP)
            {
                ptTest = this.myScrollViewer.PointFromScreen(new Point(flick.Point.X, flick.Point.Y));
                objTest = this.myScrollViewer.InputHitTest(ptTest);
                if (objTest != null)
                {
                    if (objTest is Image)
                    {
                        Image hitImage = (Image)objTest;
                        this.imgMain.Source = hitImage.Source;
                    }
                }
            }
        } // end of ProcessFlick method
 

At this point, we've accomplished our goal of scrolling the content of a ScrollViewer class using flicks.

Adding Gestures

Now that we have the content scrolling using flicks, then let's add some gesture support for the main image.  The Zoom gesture will be used to enlarge or shrink the image.  The simplest way to accomplish this is to increase or decrease the margin on the main image.  As you can see from the code, I get the current top margin and add ten.  I realize that this is not the most ideal zoom technique because it will cut off part of the image but it is really meant to show proof of concept which I think it does quite nicely.  I'll leave it up to the reader to add their own zoom code to make a better image zoom.  I also left the case statement for the rotate gesture as a placeholder.  Hey, you don't want me to do all your work for you ?!

 
        private void ProcessGesture(GestureData gData)
        {
            double dbTop = this.imgMain.Margin.Top;
            switch (gData.GestureType)
            {
                case ENtrGestureType.Zoom:
                    GestureZoomData gzData = (GestureZoomData)gData;
                    if (gzData.ZoomStruct.mAmount < 0)
                    {
                        dbTop += 10;
                    }
                    else
                    {
                        dbTop -= 10;
                    }
                    this.imgMain.Margin = new Thickness(dbTop);
                    this.InvalidateVisual();
                    break;
 
                case ENtrGestureType.Rotate:
                    GestureRotateData rData = (GestureRotateData)gData;
                    if (rData.RotateStruct.mAmount < 0)
                    {
                    }
                    else
                    {
                    }
                    break;
            }
        }
 

WPF Scrolling Content app with flicks and gestures, C# source code WPF_Scrolling_Content.zip

Conclusion

I wanted to show how easy it was to get the ScrollViewer class to scroll content without the use of the scrollbars.  Improvements could be made to fix zooming and add rotating.  In addition, the flicks are coming from the OS.  Another improvement would be more advanced flicks that move content farther and faster based on length and speed of flick.  The current flicks detected by the OS only give position and direction.  I will have to work on implementing the more advanced flicks.  As always I hope the code I provided was useful and helpful.