Posted by: beatkiener | April 6, 2009

How to enable mouse wheel scrolling in Silverlight without extending controls

Introduction

Silverlight currently does not support mouse wheel events. However, you can attach an event to capture the mouse wheel movement through the HtmlPage object, find the control beneath the mouse position with the VisualTreeHelper class and scrolling the region (DataGrid, Combobox, ScrollViewer, TreeView, etc) through the Automation API.

Source Code and Live Demo

Mouse wheel event

There are several examples on the net how to capture the mouse wheel event. The following code snippet shows this very shortly. You can find a full example here (Thanks to Mike Snow): http://silverlight.net/blogs/msnow/archive/2008/07/29/tip-of-the-day-23-how-to-capture-the-mouse-wheel-event.aspx

public TestPage()
{
	InitializeComponent();
	HtmlPage.Window.AttachEvent("DOMMouseScroll", OnMouseWheel);
	HtmlPage.Window.AttachEvent("onmousewheel", OnMouseWheel);
	HtmlPage.Document.AttachEvent("onmousewheel", OnMouseWheel);
}

private void OnMouseWheel(object sender, HtmlEventArgs args)
{
	// code goes here…
}

Scrolling with the UI Automation API

As next step we have to implement programmatically scrolling for different types of controls, such as Combobox, Treeview, DataGrid, ItemsControl, ScrollViewer, etc. But how do we programmatically scroll those controls without implementing special derived classes from it. Furthermore the scrolling support for the DataGrid is unfortunately not implemented by using ScrollViewers to do scrolling. Instead the DataGrid implements a method called ScrollIntoView which is not really helpfully here.

As I played with the Silverlight UI Automation API (UIA), I found a very interesting interface called IScrollProvider. This interface is exposed through the UIA and enables us scrolling out of the box. The UIA Provider interfaces are meant for Screen readers and general automation (testing) but can equally be used at runtime.

// get automation peer
AutomationPeer automationPeer = FrameworkElementAutomationPeer.FromElement(element);
if (automationPeer == null)
{
    // create automation peer for element
    automationPeer = FrameworkElementAutomationPeer.CreatePeerForElement(element);
}
// try to get scroll provider
IScrollProvider scrollProvider = automationPeer.GetPattern(PatternInterface.Scroll) as IScrollProvider;
if (scrollProvider != null)
{
    scrollProvider.Scroll(horizontalScrollAmount, verticalScrollAmount);
}

That’s it!

Connect wheel event to the controls

How do we route the mouse turned event to the right control? You can implement the code above for every control where we need mouse scrolling and enable/disable it with the MouseEnter/MouseLeave event. But this brings us a lot of plumbing code and special controls.

The VisualTreeHelper class provides the method FindElementsInHostCoordinates. This method returns a list of UIElements beneath a specified point (the current mouse position).

Our mouse wheel class (see sample code here) always knows the current mouse position througth the subscribed MouseMove event on the root UIElement.

Here the code (reduced to the important lines):

// go through all element beneath the current mouse position
IEnumerable<UIElement> elements = VisualTreeHelper.FindElementsInHostCoordinates(currentPoint, s_rootElement);
foreach (UIElement element in elements)
{
    // get automation peer (if already created for this control)
    AutomationPeer automationPeer = FrameworkElementAutomationPeer.FromElement(element);
    if (automationPeer == null)
    {
        // create automation peer for element
        automationPeer = FrameworkElementAutomationPeer.CreatePeerForElement(element);
    }

    //expect null: some elements doesn't have an automation peer implemented
    if (automationPeer != null)
    {
        // try to get scroll provider
        IScrollProvider scrollProvider = automationPeer.GetPattern(PatternInterface.Scroll) as IScrollProvider;
        if (scrollProvider != null)
        {
            if (scrollProvider.VerticallyScrollable)
            {
                scrollProvider.Scroll(ScrollAmount.NoAmount, scrollAmount);

                // break the further search in the uielement collection
                break;
            }

            // don't break here, because of encapsulated scroll viewers such as in the treeview from the sl-toolkit
            //break;
        }
    }
}

 

Misc

With pressing the control key, horizontal scrolling is possible also:

// horizontal scrolling?
bool ctrlKey = (Keyboard.Modifiers & ModifierKeys.Control) == ModifierKeys.Control;

if (scrollProvider.HorizontallyScrollable && ctrlKey)
{
    scrollProvider.Scroll(scrollAmount, ScrollAmount.NoAmount);
}

Conclusion

The UIA (UI Autamation API) provide an eas way to do scrolling without extending the existing controls and the VisualTreeHelper gets the possibility to dispatch the mouse wheel-turned event to the right control.

Most of the controls with scrolling regions implements the IScrollProvider API, unfortunately Textbox not. I’m trying to get another way to enable scrolling for textbox.


Responses

  1. This is just what I was looking for. It looks like the source code zip file is missing the MouseWheelScrollingWeb project – any chance you can update the zip file?

    Thanks!

  2. Sorry, I misunderstood your question.
    Of course, the web project is missing, but you should be able to start the Silverlight project directly. Visual studio will create a TestPage.html on the fly.

  3. How to change this so it works in SL3 OOB application also ? Shoudl this changed to use SL3 OnMouseWheel events ?

  4. beatkiener,
    Thank you very much! This is by far the most elegant and useful solution I have seen to this problem. Bravo.

    rogueiv

  5. Using Ctrl+Wheel for horizontal scrolling is nice idea.

    How to add zoom it / zoom out feature for a scrolling with ctrl+shift+wheel presses just like Internet Explorer Ctrl ++ / – or Ctrl+wheel command ?

    • I think one way is to use a ScaleTransform for zooming in/out the a specify element of the application.

      I’ve just added the following code to the MouseWheelService class and it works pretty good.

      // zoom?
      bool zooming = ctrlKey && (Keyboard.Modifiers & ModifierKeys.Shift) == ModifierKeys.Shift;
      
      if (zooming)
      {
             // go through all element beneath the current mouse position
          IEnumerable elements = VisualTreeHelper.FindElementsInHostCoordinates(currentPoint, s_rootElement);
          foreach (UIElement element in elements)
          {
              IMouseWheelZoom mouseZooming = element as IMouseWheelZoom;
      
              if (mouseZooming != null)
              {
                  mouseZooming.OnMouseWheelZooming( new MouseWheelZoomingEventArgs(currentPoint, e.Delta));
                  break;
              }
          }
      
      }
      else
      {
         // normal....
      }
      

      The code searches for a UIElement beneath the current mouse position which implements the interface IMouseWheelZoom. In this way you can simply delegate the functionality to any UIElement which implement IMouseWheelZoom. You can find a quick implementation of IMouseWheelZoom in the Page.cs file.

      Here is the code: http://files.thekieners.com/blogcontent/MouseWheelScrolling/MouseWheelWithZooming.zip

      Cheers,
      Beat Kiener

      • Nice but not sure is this useful: If DataGrid is zoomed, its scrollbars move out of screen. How to force only Text inside DataGrid to zoom so that DataGrid scrollbars remain visible ?

  6. Thank you very much for this code. It works very nice and I don’t think I could have figured it out myself.
    Would be nice for MouseWheel scrolling to be natively supported on the microsoft silverlight controls, but this works nicely.

  7. [...] How to enable mouse wheel scrolling in Silverlight without extending controls [...]

  8. How can I scroll all the datagrid whenever I am scrolling any one of the datagrid in my xaml page?
    I have 3 Datagrid on my xaml page and whenever user scrolls first datagrid then other 2 datagrid also scroll.
    Thanks

    • Hi,
      Sorry for delay, I was very busy last days. Did you find a solution to parallel scroll all three data grids?

      I think there are two cases:
      First case is when the user scrolls a data grid with mouse wheel. In that case you can grab the IScrollProvider from each data grid and delegate your scroll command to all there grids.

      In the second case the user manually drags the scrollbar. In that case you can grab a reference to the vertical scrollbar and delegate the scroll event to the two other data grids and scroll them via IScrollProvider.
      I didn’t try this out, but I would start with this class:

      public class MyDataGrid : DataGrid
      {
      
          private ScrollBar verticalScrollbar;
      
          public MyDataGrid()
          {
          }
      
          public override void OnApplyTemplate()
          {
              base.OnApplyTemplate();
      
              verticalScrollbar = base.GetTemplateChild("VerticalScrollbar") as ScrollBar;
              verticalScrollbar.Scroll += new ScrollEventHandler(verticalScrollbar_Scroll);
          }
      
          void verticalScrollbar_Scroll(object sender, ScrollEventArgs e)
          {
              // Here access the two other data grids an scroll them via IScrollProvider
              IScrollProvider datagrid2 = null; // todo
      
              // scroll based on the scroll origin
              switch (e.ScrollEventType)
              {
                  case ScrollEventType.First:
                      datagrid2.SetScrollPercent(System.Windows.Automation.ScrollPatternIdentifiers.NoScroll, 0); // 0%
                      break;
      
                  case ScrollEventType.LargeDecrement:
                      datagrid2.Scroll(System.Windows.Automation.ScrollAmount.NoAmount, System.Windows.Automation.ScrollAmount.LargeDecrement);
                      break;
      
                  case ScrollEventType.LargeIncrement:
                      datagrid2.Scroll(System.Windows.Automation.ScrollAmount.NoAmount, System.Windows.Automation.ScrollAmount.LargeIncrement);
                      break;
      
                  case ScrollEventType.Last:
                      datagrid2.SetScrollPercent(System.Windows.Automation.ScrollPatternIdentifiers.NoScroll, 100); // 100%
                      break;
      
                  case ScrollEventType.SmallDecrement:
                      datagrid2.Scroll(System.Windows.Automation.ScrollAmount.NoAmount, System.Windows.Automation.ScrollAmount.SmallDecrement);
                      break;
      
                  case ScrollEventType.SmallIncrement:
                      datagrid2.Scroll(System.Windows.Automation.ScrollAmount.NoAmount, System.Windows.Automation.ScrollAmount.SmallIncrement);
                      break;
      
                  case ScrollEventType.ThumbPosition:
                  case ScrollEventType.ThumbTrack:
                  case ScrollEventType.EndScroll:
      
                      double percent = e.NewValue / this.verticalScrollbar.Maximum;
                      datagrid2.SetScrollPercent(System.Windows.Automation.ScrollPatternIdentifiers.NoScroll, percent);
      
                      break;
                  default:
                      break;
              }
      
          }
      
      }
      
  9. This code doesn’t work when the application is in full screen mode. Any ideas?

    • Hi,

      No. But I’m going to check it….

  10. Hi, thanks for the reply but I want the scrolling for both Horizontal as well as Vertical. Can we do this?
    Thanks again.

  11. Hi,
    How do you make this work in full screen mode?

  12. Very sweet, Kiener. Thanks for taking the time to share this.


Leave a response

Your response:

Categories