WPF Listbox auto scroll while dragging
Asked Answered
P

3

31

I have a WPF app that has a ListBox. The drag mechanism is already implemented, but when the list is too long and I want to move an item to a position not visible, I can't.

For example, the screen shows 10 items. And I have 20 items. If I want to drag the last item to the first position I must drag to the top and drop. Scroll up and drag again.

How can I make the ListBox auto scroll?

Phenoxide answered 22/8, 2009 at 15:47 Comment(0)
P
34

Got it. Used the event DragOver of the ListBox, used the function found here to get the scrollviewer of the listbox and after that its just a bit of juggling with the Position.

private void ItemsList_DragOver(object sender, System.Windows.DragEventArgs e)
{
    ListBox li = sender as ListBox;
    ScrollViewer sv = FindVisualChild<ScrollViewer>(ItemsList);

    double tolerance = 10;
    double verticalPos = e.GetPosition(li).Y;
    double offset = 3;

    if (verticalPos < tolerance) // Top of visible list?
    {
        sv.ScrollToVerticalOffset(sv.VerticalOffset - offset); //Scroll up.
    }
    else if (verticalPos > li.ActualHeight - tolerance) //Bottom of visible list?
    {
        sv.ScrollToVerticalOffset(sv.VerticalOffset + offset); //Scroll down.    
    }
}

public static childItem FindVisualChild<childItem>(DependencyObject obj) where childItem : DependencyObject
{
    // Search immediate children first (breadth-first)
    for (int i = 0; i < VisualTreeHelper.GetChildrenCount(obj); i++)
    {
        DependencyObject child = VisualTreeHelper.GetChild(obj, i);

        if (child != null && child is childItem)
            return (childItem)child;

        else
        {
            childItem childOfChild = FindVisualChild<childItem>(child);

            if (childOfChild != null)
                return childOfChild;
        }
    }

    return null;
}
Phenoxide answered 22/8, 2009 at 16:30 Comment(6)
I tried you method and it works. However, when dragging objects around the same list, after droping, it returns to the original object where I would like to see the dropped item. Did you have this and did you correct it ?Ebullience
@DavidBrunelle I don't remember, sorry.Phenoxide
+1 great answer, though that's a depth-first search, not a breadth-first one as indicated.Amethyst
My "FindVisualChild" never returns a ScrollViewer but I placed the ListBox inside a ScrollViewer in XAML and used "FindVisualParent" instead which did the trick.Equipoise
@Equipoise Hi. I'm trying something similar. Could you post the code please?Carnage
Hi, I just have this changed from the original suggestion: (Sorry it's in VB) Dim lListBoxItem As DragDropListBoxItem = FindVisualParent(Of DragDropListBoxItem)((DirectCast(e.OriginalSource, DependencyObject))) If (lListBoxItem IsNot Nothing) Then DragDrop.DoDragDrop(lListBoxItem, lListBoxItem.DataContext, DragDropEffects.Move) End IfEquipoise
A
19

Based on this I have created an Attached Behavior which can easily be used like this -

<ListView
   xmlns:WpfExtensions="clr-namespace:WpfExtensions" 
   WpfExtensions:DragDropExtension.ScrollOnDragDrop="True"

Here is the code for attached behavior -

/// <summary>
/// Provides extended support for drag drop operation
/// </summary>
public static class DragDropExtension
{
    public static readonly DependencyProperty ScrollOnDragDropProperty =
        DependencyProperty.RegisterAttached("ScrollOnDragDrop",
            typeof(bool),
            typeof(DragDropExtension),
            new PropertyMetadata(false, HandleScrollOnDragDropChanged));

    public static bool GetScrollOnDragDrop(DependencyObject element)
    {
        if (element == null)
        {
            throw new ArgumentNullException("element");
        }

        return (bool)element.GetValue(ScrollOnDragDropProperty);
    }

    public static void SetScrollOnDragDrop(DependencyObject element, bool value)
    {
        if (element == null)
        {
            throw new ArgumentNullException("element");
        }

        element.SetValue(ScrollOnDragDropProperty, value);
    }

    private static void HandleScrollOnDragDropChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
    {
        FrameworkElement container = d as FrameworkElement;

        if (d == null)
        {
            Debug.Fail("Invalid type!");
            return;
        }

        Unsubscribe(container);

        if (true.Equals(e.NewValue))
        {
            Subscribe(container);
        }
    }

    private static void Subscribe(FrameworkElement container)
    {
        container.PreviewDragOver += OnContainerPreviewDragOver;
    }

    private static void OnContainerPreviewDragOver(object sender, DragEventArgs e)
    {
        FrameworkElement container = sender as FrameworkElement;

        if (container == null)
        {
            return;
        }

        ScrollViewer scrollViewer = GetFirstVisualChild<ScrollViewer>(container);

        if (scrollViewer == null)
        {
            return;
        }

        double tolerance = 60;
        double verticalPos = e.GetPosition(container).Y;
        double offset = 20;

        if (verticalPos < tolerance) // Top of visible list? 
        {
            scrollViewer.ScrollToVerticalOffset(scrollViewer.VerticalOffset - offset); //Scroll up. 
        }
        else if (verticalPos > container.ActualHeight - tolerance) //Bottom of visible list? 
        {
            scrollViewer.ScrollToVerticalOffset(scrollViewer.VerticalOffset + offset); //Scroll down.     
        }
    }

    private static void Unsubscribe(FrameworkElement container)
    {
        container.PreviewDragOver -= OnContainerPreviewDragOver;
    }

    private static T GetFirstVisualChild<T>(DependencyObject depObj) where T : DependencyObject
    {
        if (depObj != null)
        {
            for (int i = 0; i < VisualTreeHelper.GetChildrenCount(depObj); i++)
            {
                DependencyObject child = VisualTreeHelper.GetChild(depObj, i);
                if (child != null && child is T)
                {
                    return (T)child;
                }

                T childItem = GetFirstVisualChild<T>(child);
                if (childItem != null)
                {
                    return childItem;
                }
            }
        }

        return null;
    }
}
Abstraction answered 21/12, 2012 at 10:22 Comment(2)
Very nice solution. Don't forget that you can put "ScrollViewer.CanContentScroll="False"" on your ListBox/ListView if you want a smooth scroll.Preventer
@Preventer It's also worth of mentioning that when you will set CanContentScroll to false you will disable virtualization in a same time.Hassanhassell
M
1

Artur's answer was great in that it got me on the right path, but I found a few problems.

First, the code has an error here:

ScrollViewer sv = FindVisualChild<ScrollViewer>(ItemsList);

It should pass in li.

Also, the code above does not properly consider whether the ListBox has a horizontal scroll bar. I've modified the code with both fixes as shown here:

private void MainTreeView_DragOver(object sender, DragEventArgs e)
{
    ListBox li = sender as ListBox;
    ScrollViewer sv = FindVisualChild<ScrollViewer>(li);

    double tolerance = 24;
    double verticalPos = e.GetPosition(li).Y;

    double topMargin = tolerance;
    var bottomMargin = li.ActualHeight - tolerance;
    if(sv.ComputedHorizontalScrollBarVisibility == Visibility.Visible)
    {
        var horizontalScrollBar = sv.Template.FindName("PART_HorizontalScrollBar", sv) as System.Windows.Controls.Primitives.ScrollBar;

        if(horizontalScrollBar != null)
        {
            bottomMargin -= horizontalScrollBar.ActualHeight;
        }
    }

    double distanceToScroll = 3;
    if (verticalPos < topMargin) // Top of visible list?
    {
        sv.ScrollToVerticalOffset(sv.VerticalOffset - distanceToScroll); //Scroll up.
    }
    else if (verticalPos > bottomMargin) //Bottom of visible list?
    {
        sv.ScrollToVerticalOffset(sv.VerticalOffset + distanceToScroll); //Scroll down.    
    }
}

public static childItem FindVisualChild<childItem>(DependencyObject obj) where childItem : DependencyObject
{
    // Search immediate children first (breadth-first)
    for (int i = 0; i < VisualTreeHelper.GetChildrenCount(obj); i++)
    {
        DependencyObject child = VisualTreeHelper.GetChild(obj, i);

        if (child != null && child is childItem)
            return (childItem)child;

        else
        {
            childItem childOfChild = FindVisualChild<childItem>(child);

            if (childOfChild != null)
                return childOfChild;
        }
    }

    return null;
Mcneill answered 3/4, 2023 at 2:27 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.