16

I cannot determine how to scroll horizontally using the mouse wheel. Vertical scrolling works well automatically, but I need to scroll my content horizontally. My code looks like this:

<ListBox x:Name="receiptList"
         Margin="5,0"
         Grid.Row="1"
         ItemTemplate="{StaticResource receiptListItemDataTemplate}"
         ItemsSource="{Binding OpenReceipts}"
         ScrollViewer.VerticalScrollBarVisibility="Disabled">
    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <StackPanel Orientation="Horizontal"
                        ScrollViewer.HorizontalScrollBarVisibility="Visible"/>
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>
</ListBox>

My item template looks like this:

<DataTemplate x:Key="receiptListItemDataTemplate">
    <RadioButton GroupName="Numbers"
                 Command="{Binding Path=DataContext.SelectReceiptCommand,RelativeSource={RelativeSource FindAncestor,AncestorType={x:Type POS:PointOfSaleControl}}}"
                 CommandParameter="{Binding }"
                 Margin="2,0"
                 IsChecked="{Binding IsSelected}">
        <RadioButton.Template>
            <ControlTemplate TargetType="{x:Type RadioButton}" >
                <Grid x:Name="receiptGrid" >
                    <Grid>
                        <Border BorderThickness="2" 
                                BorderBrush="Green" 
                                Height="20" 
                                Width="20">
                            <Grid x:Name="radioButtonGrid" 
                                  Background="DarkOrange">
                                <TextBlock x:Name="receiptLabel"
                                           HorizontalAlignment="Center"
                                           VerticalAlignment="Center"
                                           Text="{Binding Path=NumberInQueue, Mode=OneWay}"
                                           FontWeight="Bold"
                                           FontSize="12"
                                           Foreground="White">
                                </TextBlock>
                            </Grid>
                        </Border>
                    </Grid>
                </Grid>

                <ControlTemplate.Triggers>
                    <Trigger Property="IsChecked" Value="True">
                        <Setter Property="Margin" 
                                TargetName="receiptGrid" 
                                Value="2,2,-1,-1"/>
                        <Setter Property="Background"
                                TargetName="radioButtonGrid" 
                                Value="Maroon"/>
                    </Trigger>
                </ControlTemplate.Triggers>
            </ControlTemplate>
        </RadioButton.Template>
    </RadioButton>          
</DataTemplate>

Is there another method or control that I need to add to get that functionality?

Esoteric Screen Name
  • 6,082
  • 4
  • 29
  • 38
vts123
  • 1,736
  • 6
  • 27
  • 41
  • You should make it clear (by reformulating) in your question you need a solution for the *mouse wheel* to work. Some like me don't get it easily. – SandRock Jan 30 '13 at 10:36

6 Answers6

24

Here's a complete behaviour. Add the below class to your code, then in your XAML set the attached property to true on any UIElement that contains a ScrollViewer as a visual child.

<MyVisual ScrollViewerHelper.ShiftWheelScrollsHorizontally="True" />

The class:

public static class ScrollViewerHelper
{
    public static readonly DependencyProperty ShiftWheelScrollsHorizontallyProperty
        = DependencyProperty.RegisterAttached("ShiftWheelScrollsHorizontally",
            typeof(bool),
            typeof(ScrollViewerHelper),
            new PropertyMetadata(false, UseHorizontalScrollingChangedCallback));

    private static void UseHorizontalScrollingChangedCallback(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        var element = d as UIElement;

        if (element == null)
            throw new Exception("Attached property must be used with UIElement.");

        if ((bool)e.NewValue)
            element.PreviewMouseWheel += OnPreviewMouseWheel;
        else
            element.PreviewMouseWheel -= OnPreviewMouseWheel;
    }

    private static void OnPreviewMouseWheel(object sender, MouseWheelEventArgs args)
    {
        var scrollViewer = ((UIElement)sender).FindDescendant<ScrollViewer>();

        if (scrollViewer == null)
            return;

        if (Keyboard.Modifiers != ModifierKeys.Shift)
            return;

        if (args.Delta < 0)
            scrollViewer.LineRight();
        else
            scrollViewer.LineLeft();

        args.Handled = true;
    }

    public static void SetShiftWheelScrollsHorizontally(UIElement element, bool value) => element.SetValue(ShiftWheelScrollsHorizontallyProperty, value);
    public static bool GetShiftWheelScrollsHorizontally(UIElement element) => (bool)element.GetValue(ShiftWheelScrollsHorizontallyProperty);

    [CanBeNull]
    private static T FindDescendant<T>([CanBeNull] this DependencyObject d) where T : DependencyObject
    {
        if (d == null)
            return null;

        var childCount = VisualTreeHelper.GetChildrenCount(d);

        for (var i = 0; i < childCount; i++)
        {
            var child = VisualTreeHelper.GetChild(d, i);

            var result = child as T ?? FindDescendant<T>(child);

            if (result != null)
                return result;
        }

        return null;
    }
}

This answer fixes a few bugs in Johannes' answer, such as not filtering by Shift key, scrolling both horizontally and vertically at the same time (motion was diagonal) and the inability to disable the behaviour by setting the property to false.

DAG
  • 867
  • 10
  • 16
Drew Noakes
  • 300,895
  • 165
  • 679
  • 742
  • Doesn't compile. The class and its property can't be found, this is three errors in the XAML file. The C# file has no own errors. VS2015 – ygoe Apr 29 '17 at 11:47
  • @ygoe, you'll need to set up a xmlns that maps to the CLR namespace of the class. – Drew Noakes Apr 29 '17 at 19:17
  • I've put the class into my own namespace and already use other classes from there. That's not the problem. – ygoe May 01 '17 at 21:03
  • @ygoe what is the error you get? This exact code works on my computer. – Drew Noakes May 02 '17 at 08:36
  • Three errors in the XAML file: 1. "Static properties are not supported in markup", 2. "The 'ShiftWheelScrollsHorizontallyProperty' property that can be attached was not found in the type 'ScrollViewerHelper'", 3. "The property "ScrollViewerHelper.ShiftWheelScrollsHorizontallyProperty" does not exist in the XML namespace "clr-namespace:Unclassified.UI"" (that's my namespace). When I fix your code and remove the "Property" suffix from the XAML attached property, I get the same errors. – ygoe May 02 '17 at 20:59
  • The property suffix is standard naming convention. Are you using WPF or UWP? – Drew Noakes May 02 '17 at 21:02
  • WPF. You usually specify the name from the string (without `Property`) in XAML, not from the code (with `Property`). This is the first time I see it different. – ygoe May 03 '17 at 06:59
  • Ok I see now what you mean. Updated the answer. Thanks. – Drew Noakes May 03 '17 at 09:37
  • Well, like I said, I already tried that and it has the same errors. – ygoe May 03 '17 at 13:06
  • 1
    The Property not found issue is caused by the wrong name of the Get and Set properties. They need to be `SetShiftWheelScrollsHorizontally` and `GetShiftWheelScrollsHorizontally` instead of `SetUseHorizontalScrolling` and `GetUseHorizontalScrolling` – Kornelis Dec 01 '17 at 09:01
  • Using `LineRight()` and `LineLeft()` can be a bit slow depending on your content, you can use the following instead: `scrollViewer.ScrollToHorizontalOffset(scrollViewer.HorizontalOffset + -args.Delta);` – Pedro Henrique Oct 21 '18 at 21:49
  • This really needs to be fixed. It's referencing ItemsControl in the code but the answer says "set the attached property to true on any UIElement that contains a ScrollViewer as a visual child". To fix this simply swap UIElement in place of ItemsControl. – DAG Jun 04 '21 at 01:56
10

I wrote an Attached Property for this purpose to reuse it on every ItemsControl containing an ScrollViewer. FindChildByType is an Telerik extension but can also be found here.

 public static readonly DependencyProperty UseHorizontalScrollingProperty = DependencyProperty.RegisterAttached(
            "UseHorizontalScrolling", typeof(bool), typeof(ScrollViewerHelper), new PropertyMetadata(default(bool), UseHorizontalScrollingChangedCallback));

        private static void UseHorizontalScrollingChangedCallback(DependencyObject dependencyObject, DependencyPropertyChangedEventArgs dependencyPropertyChangedEventArgs)
        {
            ItemsControl itemsControl = dependencyObject as ItemsControl;

            if (itemsControl == null) throw new ArgumentException("Element is not an ItemsControl");

            itemsControl.PreviewMouseWheel += delegate(object sender, MouseWheelEventArgs args)
            {
                ScrollViewer scrollViewer = itemsControl.FindChildByType<ScrollViewer>();

                if (scrollViewer == null) return;

                if (args.Delta < 0)
                {
                    scrollViewer.LineRight();
                }
                else
                {
                    scrollViewer.LineLeft();
                }
            };
        }


        public static void SetUseHorizontalScrolling(ItemsControl element, bool value)
        {
            element.SetValue(UseHorizontalScrollingProperty, value);
        }

        public static bool GetUseHorizontalScrolling(ItemsControl element)
        {
            return (bool)element.GetValue(UseHorizontalScrollingProperty);
        }
Community
  • 1
  • 1
Johannes Wanzek
  • 2,825
  • 2
  • 29
  • 47
  • This should be the accepted answer, +1 for including code and links to places to get more code. This implementation worked on the first try and made it extremely simple to add the behavior to only the controls I wanted to add it to. – tpartee Jun 14 '16 at 21:16
7

The easiest way is to add PreviewMouseWheel listener to the ScrollViewer, check for shift (or whatever you want to do to indicate horizontal scroll), and then call LineLeft or LineRight (or PageLeft / PageRight) depending on the value of the Delta value of the MouseWheelEventArgs

cKNet
  • 635
  • 1
  • 10
  • 22
John Gardner
  • 24,225
  • 5
  • 58
  • 76
  • I dont' understand this answer: How should I add PreviewMouseWheel when there is no ScrollViewer at all? – springy76 May 25 '14 at 14:54
  • i believe the listbox's control template has a scrollviewer inside of it. hence the reason that the attached properties like `ScrollViewer.VerticalScrollBarVisibility="Disabled"` work. – John Gardner May 27 '14 at 23:39
  • Yes, but these are attached properties. But how do I attach PreviewMouseWheel of the ScrollViewer without rewriting the control template (which I won't do because this will break the automatic operating system dependent theming)? – springy76 May 28 '14 at 06:48
  • i presume you walk the visual tree to find it? (don't remember, this answer is 3 years old now :)) – John Gardner May 28 '14 at 21:46
6

I was kinda looking for the most simple way to make any ScrollViewer scroll left-right instead of up-down. So here is the simplest combination of the other answers.

<ScrollViewer HorizontalScrollBarVisibility="Visible"
              PreviewMouseWheel="ScrollViewer_PreviewMouseWheel"> 

and:

private void ScrollViewer_PreviewMouseWheel(object sender, MouseWheelEventArgs e)
{
    ScrollViewer scrollViewer = (ScrollViewer)sender;
    if (e.Delta < 0)
    {
        scrollViewer.LineRight();
    }
    else
    {
        scrollViewer.LineLeft();
    }
    e.Handled = true;
}

This is basically what John Gardner suggested just with the actual code. I'm also quoting my answer from similar question here.

kub1x
  • 3,272
  • 37
  • 38
4
(sender as ScrollViewer).ScrollToHorizontalOffset( (sender as ScrollViewer).ContentHorizontalOffset    + e.Delta);
Henka Programmer
  • 433
  • 6
  • 18
  • I used this but had to subtract the delta value to get the expected behavior (i.e. content scrolls to the right when mouse-wheel down/towards user). – Sean Hanley Jul 09 '14 at 20:34
0

Try this:

<ListBox x:Name="receiptList" 
                       Margin="5,0" 
                       Grid.Row="1" 
                       ItemTemplate="{StaticResource receiptListItemDataTemplate}" 
                       ItemsSource="{Binding OpenReceipts}" 
                       ScrollViewer.VerticalScrollBarVisibility="Disabled" 
                       ScrollViewer.HorizontalScrollBarVisibility="Visible"
                           > 
                <ItemsControl.ItemsPanel> 
                  <ItemsPanelTemplate> 
                    <StackPanel Orientation="Horizontal" /> 
                  </ItemsPanelTemplate> 
                </ItemsControl.ItemsPanel> 
</ListBox> 

UPDATE Ooops, missed the part about the mouse wheel! Sorry

To make the mouse wheel work, you will have to subscribe to the mouse wheel event and manuaaly move the scroll bar... This can nicelly be encapsulated by a behavior but I think thats the only way to make it work!

rudigrobler
  • 17,045
  • 12
  • 60
  • 74
  • This does not works. it works only with keyborad "->" and "<-". Moruse wheel scroll does not work. – vts123 Sep 17 '10 at 09:55