disable mouse wheel on itemscontrol in wpf
Asked Answered
C

8

30

I have a usercontrol that has a scrollviewer, then a bunch of child controls like text boxes, radio buttons, and listboxes, etc inside of it. I can use the mouse wheel to scroll the parent scrollviewer until my mouse lands inside a listbox then, the mouse wheel events start going to the listbox.

Is there any way to have the listbox send those events back up to the parent control? Removing the listbox from within side the parent control like this question suggests (Mouse wheel not working when over ScrollViewer's child controls) isnt a solution.

I have tried

private void ListBox_PreviewMouseWheel(object sender, MouseWheelEventArgs e)
{
    e.Handled = true;
}

but that didnt work either.

Thanks

Calk answered 3/2, 2010 at 1:19 Comment(0)
J
40

The answer you have referenced is exactly what is causing your problem, the ListBox (which is composed of among other things a ScrollViewer) inside your ScrollViewer catches the MouseWheel event and handles it, preventing it from bubbling and thus the ScrollViewer has no idea the event ever occurred.

Use the following extremely simple ControlTemplate for your ListBox to demonstrate (note it does not have a ScrollViewer in it and so the MouseWheel event will not be caught) The ScrollViewer will still scroll with the mouse over the ListBox.

<UserControl.Resources>
     <ControlTemplate x:Key="NoScroll">
         <ItemsPresenter></ItemsPresenter>
     </ControlTemplate>
</UserControl.Resources>

<ScrollViewer>
    <SomeContainerControl>
        <.... what ever other controls are inside your ScrollViewer>
        <ListBox Template="{StaticResource NoScroll}"></ListBox>
    <SomeContainerControl>
</ScrollViewer>

You do have the option of capturing the mouse when it enters the ScrollViewer though so it continues to receive all mouse events until the mouse is released, however this option would require you to delgate any further mouse events to the controls contained within the ScrollViewer if you want a response...the following MouseEnter MouseLeave event handlers will be sufficient.

private void ScrollViewerMouseEnter(object sender, MouseEventArgs e)
{
    ((ScrollViewer)sender).CaptureMouse();
}

private void ScrollViewerMouseLeave(object sender, MouseEventArgs e)
{
    ((ScrollViewer)sender).ReleaseMouseCapture();
}

Neither of the workarounds I have provided are really preferred however and I would suggest rethinking what you are actually trying to do. If you explain what you are trying to achieve in your question I'm sure you will get some more suggestions...

Jeraldjeraldine answered 3/2, 2010 at 2:10 Comment(11)
I followed Simon's suggestion but with an implementation variant: I used Blend to get a copy of the current ListBox template, then I eliminated the ListBox's ScrollViewer.Shu
Great! This solved no less the three minor UI quirks for me. Thanks.Wispy
<ItemsPresenter> removed the column headerMarla
I don't understand why there was any confusion about what the OP wanted to do. Having a list box with all elements fully visible and that is embedded within a stack panel with other items seems like a fairly common design to me. The idea is that you want the whole thing to scroll with the mouse over the list box or any other control within the stackpanel. The stackpanel would be within a scrollviewer and the vertical stackpanel would have a listbox, text, textboxes, etc. I can't really vote for this one because the writer says that neither workarounds are preferred, which is confusing.Gelsenkirchen
@Gelsenkirchen why would you "want the whole thing to scroll with the mouse over the list box"? This would make the list box pointless as it would not be scrollable (instead, the whole thing would be scrolling)Jeraldjeraldine
In my current situation, list box is being used as a container for controls generated via data templates from a dynamic collection, and its scrolling behavior needs to be bound to the scrolling of another control. That second part doesn't seem to be possible using ListBox's internal ScrollViewer because the functionality that's exposed is very limited.Kat
@Kat sounds like an ItemsControl would suffice. You could capture the events generated by "scrolling of another control" and adjust offsets of the ItemsControl as they arrive.Jeraldjeraldine
Yes, ItemsControl is what I'm actually using in some places, but my situation is much more complicated than is reasonable to explain here. However, for both ItemsControl and ListBox the internal scrolling behavior seems to be the result of the default ItemPanel being a StackPanel, which has it's own internal ScrollViewer, which isn't really exposed in a useful way.Kat
@Kat no, a StackPanel does not have an "internal ScrollViewer". As described in my answer, the default template of a ListBox has a ScrollViewer in it, but that is not the case for ItemsControl so requires a custom solution to make it scrollable.Jeraldjeraldine
Fine. Everything you say is true and the behavior I'm actually observing in my application is wrong.Kat
This answer give me a good direction to solve my problem, great thx.Cherin
S
42

This can be accomplished via attached behaviors.

So instead I came up with the following IgnoreMouseWheelBehavior. Technically it’s not ignoring the MouseWheel, but it is “forwarding” the event back up and out of the ListBox. Check it.

/// <summary>
/// Captures and eats MouseWheel events so that a nested ListBox does not
/// prevent an outer scrollable control from scrolling.
/// </summary>
public sealed class IgnoreMouseWheelBehavior : Behavior<UIElement>
{

  protected override void OnAttached( )
  {
     base.OnAttached( );
      AssociatedObject.PreviewMouseWheel += AssociatedObject_PreviewMouseWheel ;
  }

  protected override void OnDetaching( )
  {
      AssociatedObject.PreviewMouseWheel -= AssociatedObject_PreviewMouseWheel;
      base.OnDetaching( );
  }

  void AssociatedObject_PreviewMouseWheel(object sender, MouseWheelEventArgs e)
  {

      e.Handled = true;

      var e2 = new MouseWheelEventArgs(e.MouseDevice,e.Timestamp,e.Delta);
      e2.RoutedEvent = UIElement.MouseWheelEvent;

      AssociatedObject.RaiseEvent(e2);

  }

}

And here’s how you would use it in XAML.

<ScrollViewer Name="IScroll">
    <ListBox Name="IDont">
        <i:Interaction.Behaviors>
            <local:IgnoreMouseWheelBehavior />
        </i:Interaction.Behaviors>
    </ListBox>
</ScrollViewer>

Where the i namespace is:

 xmlns:i="http://schemas.microsoft.com/xaml/behaviors"

Note that you will need the Microsoft.Xaml.Behaviors.Wpf nuget package to use this.

Snowshoe answered 9/8, 2011 at 21:40 Comment(4)
Work for me! Simply hiding Vertical and Horizontal scroll are not enough, because MouseWheel is still scrolling the items.Hungarian
It doesn't work in Visual Studio 2017 because the interactivity XML namespace is missing.Tobacconist
@Sasinosoft the interactivity DLLs were always separate from VS, from what I remember. Originally you had to have Blend installed. Now it looks like you can get these through a NuGet package (Microsoft.Xaml.Behaviors.Wpf). I am not setup to verify this personally, though.Snowshoe
Works Perfect for a Listbox! Thnx.Tokay
J
40

The answer you have referenced is exactly what is causing your problem, the ListBox (which is composed of among other things a ScrollViewer) inside your ScrollViewer catches the MouseWheel event and handles it, preventing it from bubbling and thus the ScrollViewer has no idea the event ever occurred.

Use the following extremely simple ControlTemplate for your ListBox to demonstrate (note it does not have a ScrollViewer in it and so the MouseWheel event will not be caught) The ScrollViewer will still scroll with the mouse over the ListBox.

<UserControl.Resources>
     <ControlTemplate x:Key="NoScroll">
         <ItemsPresenter></ItemsPresenter>
     </ControlTemplate>
</UserControl.Resources>

<ScrollViewer>
    <SomeContainerControl>
        <.... what ever other controls are inside your ScrollViewer>
        <ListBox Template="{StaticResource NoScroll}"></ListBox>
    <SomeContainerControl>
</ScrollViewer>

You do have the option of capturing the mouse when it enters the ScrollViewer though so it continues to receive all mouse events until the mouse is released, however this option would require you to delgate any further mouse events to the controls contained within the ScrollViewer if you want a response...the following MouseEnter MouseLeave event handlers will be sufficient.

private void ScrollViewerMouseEnter(object sender, MouseEventArgs e)
{
    ((ScrollViewer)sender).CaptureMouse();
}

private void ScrollViewerMouseLeave(object sender, MouseEventArgs e)
{
    ((ScrollViewer)sender).ReleaseMouseCapture();
}

Neither of the workarounds I have provided are really preferred however and I would suggest rethinking what you are actually trying to do. If you explain what you are trying to achieve in your question I'm sure you will get some more suggestions...

Jeraldjeraldine answered 3/2, 2010 at 2:10 Comment(11)
I followed Simon's suggestion but with an implementation variant: I used Blend to get a copy of the current ListBox template, then I eliminated the ListBox's ScrollViewer.Shu
Great! This solved no less the three minor UI quirks for me. Thanks.Wispy
<ItemsPresenter> removed the column headerMarla
I don't understand why there was any confusion about what the OP wanted to do. Having a list box with all elements fully visible and that is embedded within a stack panel with other items seems like a fairly common design to me. The idea is that you want the whole thing to scroll with the mouse over the list box or any other control within the stackpanel. The stackpanel would be within a scrollviewer and the vertical stackpanel would have a listbox, text, textboxes, etc. I can't really vote for this one because the writer says that neither workarounds are preferred, which is confusing.Gelsenkirchen
@Gelsenkirchen why would you "want the whole thing to scroll with the mouse over the list box"? This would make the list box pointless as it would not be scrollable (instead, the whole thing would be scrolling)Jeraldjeraldine
In my current situation, list box is being used as a container for controls generated via data templates from a dynamic collection, and its scrolling behavior needs to be bound to the scrolling of another control. That second part doesn't seem to be possible using ListBox's internal ScrollViewer because the functionality that's exposed is very limited.Kat
@Kat sounds like an ItemsControl would suffice. You could capture the events generated by "scrolling of another control" and adjust offsets of the ItemsControl as they arrive.Jeraldjeraldine
Yes, ItemsControl is what I'm actually using in some places, but my situation is much more complicated than is reasonable to explain here. However, for both ItemsControl and ListBox the internal scrolling behavior seems to be the result of the default ItemPanel being a StackPanel, which has it's own internal ScrollViewer, which isn't really exposed in a useful way.Kat
@Kat no, a StackPanel does not have an "internal ScrollViewer". As described in my answer, the default template of a ListBox has a ScrollViewer in it, but that is not the case for ItemsControl so requires a custom solution to make it scrollable.Jeraldjeraldine
Fine. Everything you say is true and the behavior I'm actually observing in my application is wrong.Kat
This answer give me a good direction to solve my problem, great thx.Cherin
U
16

I followed Amanduh's approach to solve the same problem I had with multiple datagrids in a scrollviewer but in WPF:

public sealed class IgnoreMouseWheelBehavior 
{
    public static bool GetIgnoreMouseWheel(DataGrid gridItem)
    {
        return (bool)gridItem.GetValue(IgnoreMouseWheelProperty);
    }

    public static void SetIgnoreMouseWheel(DataGrid gridItem, bool value)
    {
        gridItem.SetValue(IgnoreMouseWheelProperty, value);
    }

    public static readonly DependencyProperty IgnoreMouseWheelProperty =
        DependencyProperty.RegisterAttached("IgnoreMouseWheel", typeof(bool),
        typeof(IgnoreMouseWheelBehavior), new UIPropertyMetadata(false, OnIgnoreMouseWheelChanged));

    static void OnIgnoreMouseWheelChanged(DependencyObject depObj, DependencyPropertyChangedEventArgs e)
    {
        var item = depObj as DataGrid;
        if (item == null)
            return;

        if (e.NewValue is bool == false)
            return;

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

    static void OnPreviewMouseWheel(object sender, MouseWheelEventArgs e)
    {
        e.Handled = true;

        var e2 = new MouseWheelEventArgs(e.MouseDevice, e.Timestamp, e.Delta)
                     {RoutedEvent = UIElement.MouseWheelEvent};

        var gv = sender as DataGrid;
        if (gv != null) gv.RaiseEvent(e2);
    }
}
Unlisted answered 8/2, 2012 at 6:59 Comment(3)
Works great. For greater portability, you can replace the DataGrid type with UIElement. I'd also suggest naming it PassthroughMouseWheel since that's what it does. Especially it doesn't eat up (finally ignore) the wheel event. – Oh, and this is an attached property, not a behavior. The class name is misleading here.Biz
Thanks, great solution. But, can anyone explain why it works?Raindrop
Thanks for your answer. I used it for a ListView that has multiple expanders. The scroll viewer would not scroll if the mouse was over the list view. Your solution worked perfect :)Hilversum
M
9

As Simon said, it's the ScrollViewer in the standard ListBox template that's catching the event. To bypass it you can provide your own template.

<ControlTemplate x:Key="NoWheelScrollListBoxTemplate" TargetType="ListBox">
    <Border BorderThickness="{TemplateBinding Border.BorderThickness}" Padding="1,1,1,1" BorderBrush="{TemplateBinding Border.BorderBrush}" Background="{TemplateBinding Panel.Background}" Name="Bd" SnapsToDevicePixels="True">
        <!-- This is the new control -->
        <l:NoWheelScrollViewer Padding="{TemplateBinding Control.Padding}" Focusable="False">
            <ItemsPresenter SnapsToDevicePixels="{TemplateBinding UIElement.SnapsToDevicePixels}" />
        </l:NoWheelScrollViewer>
    </Border>
    <ControlTemplate.Triggers>
        <Trigger Property="UIElement.IsEnabled" Value="False">
            <Setter TargetName="Bd" Property="Panel.Background" Value="{DynamicResource {x:Static SystemColors.ControlBrushKey}}" />
        </Trigger>
        <Trigger Property="ItemsControl.IsGrouping" Value="True">
            <Setter Property="ScrollViewer.CanContentScroll" Value="False" />
        </Trigger>
    </ControlTemplate.Triggers>
</ControlTemplate>

And the implementation for NoWheelScrollViewer is pretty simple.

public class NoWheelScrollViewer : ScrollViewer
{
    protected override void OnMouseWheel(MouseWheelEventArgs e)
    {
        // Do nothing
    }
}

Then, whenever you want a listbox to not handle the mouse wheel.

<ListBox Template="{StaticResource NoWheelScrollListBoxTemplate}">
Mackenzie answered 3/2, 2010 at 9:21 Comment(2)
A more detailed explanation would have been helpful. For instance, why isn't setting CanContentScroll = false on the Listbox good enough? Why do we have to do this relatively complex template just to have listbox ignore the events, and then on top of that have to code up the event handler?Gelsenkirchen
@Gelsenkirchen The answer to all your questions is, because that's how WPF works. To override WPF you have to provide a custom template, that uses the custom control.Mackenzie
B
3

A simple solution which worked for me is to override the inner control template to remove the scroll viewer (whichever required) like this

For example I have a structure like this

  • ListView (a)

    • ListView (b)

      • ListView (c)

I wanted to bubble the mouse wheel scroll of (b) to (a), however wanted to keep the mouse wheel scroll of (c) available. I simply overridden the Template of (b) like this. This allowed me to bubble contents of (b) except (c) to (a). Also, I can still scroll the contents of (c). If i want to remove even for (c) then i have to repeat the same step.

<ListView.Template>
  <ControlTemplate>
     <ItemsPresenter />
  </ControlTemplate>
</ListView.Template>
Bookbinder answered 7/4, 2016 at 23:20 Comment(0)
P
2

I was trying to adapt Simon Fox's answer for a DataGrid. I found the the template hid my headers, and I never got the mouseLeave event by doing it in C#. This is ultimately what worked for me:

    private void DataGrid_PreviewMouseWheel(object sender, MouseWheelEventArgs e)
    {
        ((DataGrid)sender).CaptureMouse();
    }

    private void DataGrid_MouseWheel(object sender, MouseWheelEventArgs e)
    {
        ((DataGrid)sender).ReleaseMouseCapture();
    }
Pushkin answered 20/8, 2014 at 18:13 Comment(0)
C
1

A modified Simon Fox's solution if the original doesn't work:

public sealed class IgnoreMouseWheelBehavior : Behavior<UIElement>
{
    protected override void OnAttached()
    {
        base.OnAttached();
        AssociatedObject.PreviewMouseWheel += AssociatedObject_PreviewMouseWheel;
    }

    protected override void OnDetaching()
    {
        AssociatedObject.PreviewMouseWheel -= AssociatedObject_PreviewMouseWheel;
        base.OnDetaching();
    }

    static void AssociatedObject_PreviewMouseWheel(object sender, MouseWheelEventArgs e)
    {
        if (!(sender is DependencyObject))
        {
            return;
        }

        DependencyObject parent = VisualTreeHelper.GetParent((DependencyObject) sender);
        if (!(parent is UIElement))
        {
            return;
        }

        ((UIElement) parent).RaiseEvent(
            new MouseWheelEventArgs(e.MouseDevice, e.Timestamp, e.Delta) { RoutedEvent = UIElement.MouseWheelEvent });
        e.Handled = true;
    }
}
Castaway answered 24/12, 2014 at 16:27 Comment(0)
S
1

You must listening PreviewMouseWheel from ScrollViewer (it works), but not from listbox.

Sacculate answered 19/2, 2015 at 14:28 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.