Get First Visible Item in WPF ListView C#
Asked Answered
B

5

6

Anyone know how to get a ListViewItem by grabbing the first visible item in the ListView? I know how to get the item at index 0, but not the first visible one.

Boer answered 28/5, 2010 at 4:56 Comment(0)
B
4

This was so painful to get working:

HitTestResult hitTest = VisualTreeHelper.HitTest(SoundListView, new Point(5, 5));
System.Windows.Controls.ListViewItem item = GetListViewItemFromEvent(null, hitTest.VisualHit) as System.Windows.Controls.ListViewItem;

And the function to get the list item:

System.Windows.Controls.ListViewItem GetListViewItemFromEvent(object sender, object originalSource)
    {
        DependencyObject depObj = originalSource as DependencyObject;
        if (depObj != null)
        {
            // go up the visual hierarchy until we find the list view item the click came from  
            // the click might have been on the grid or column headers so we need to cater for this  
            DependencyObject current = depObj;
            while (current != null && current != SoundListView)
            {
                System.Windows.Controls.ListViewItem ListViewItem = current as System.Windows.Controls.ListViewItem;
                if (ListViewItem != null)
                {
                    return ListViewItem;
                }
                current = VisualTreeHelper.GetParent(current);
            }
        }

        return null;
    }
Boer answered 28/5, 2010 at 8:2 Comment(0)
C
3

After trying to figure out something similar, I thought I would share my result here (as it seems easier than the other responses):

Simple visibility test I got from here.

private static bool IsUserVisible(FrameworkElement element, FrameworkElement container)
{
    if (!element.IsVisible)
        return false;

    Rect bounds =
        element.TransformToAncestor(container).TransformBounds(new Rect(0.0, 0.0, element.ActualWidth, element.ActualHeight));
    var rect = new Rect(0.0, 0.0, container.ActualWidth, container.ActualHeight);
    return rect.Contains(bounds.TopLeft) || rect.Contains(bounds.BottomRight);
}

Afterwards you can loop through the listboxitems and use that test to determine which are visible. Since the listboxitems are always ordered the same the first visible one in this list would be the first visible one to the user.

private List<object> GetVisibleItemsFromListbox(ListBox listBox, FrameworkElement parentToTestVisibility)
{
    var items = new List<object>();

    foreach (var item in PhotosListBox.Items)
    {
        if (IsUserVisible((ListBoxItem)listBox.ItemContainerGenerator.ContainerFromItem(item), parentToTestVisibility))
        {
            items.Add(item);
        }
        else if (items.Any())
        {
            break;
        }
    }

    return items;
}
Chinaware answered 28/9, 2012 at 14:18 Comment(0)
D
2

I can't believe there isn't an easier way...

http://social.msdn.microsoft.com/forums/en-US/wpf/thread/2d527831-43aa-4fd5-8b7b-08cb5c4ed1db

Downy answered 28/5, 2010 at 4:59 Comment(1)
Oh my God I think I just threw up in my mouth. What about using the VisualTreeHelper to HitTest the child at the relative point 0,0?Coze
K
2

We only have to calculate the offset of our listbox, and the first visible item will be the item at the index equal to the VerticalOffset...

        // queue is the name of my listbox
        VirtualizingStackPanel panel = VisualTreeHelper.GetParent(queue.Items[0] as ListBoxItem) as VirtualizingStackPanel;
        int offset = (int)panel.VerticalOffset;
        // then our desired listboxitem is:
        ListBoxItem item = queue.Items[offset] as ListBoxItem;

Hope this helps you . . .!

Kwangtung answered 6/1, 2013 at 1:4 Comment(2)
I'm getting the error: An unhandled exception of type 'System.InvalidCastException' occurred. I assume it appears on casting listBox.Items[0]Kalb
I guess it doesn't work in case of grouping, but otherwise it's the fastes way (by far)Zulazulch
O
1

The generality of WPF ListView seems to prevent the class from providing a property like WinForms' TopItem. However, if the instance is configured with a VirtualizingStackPanel, you can still query the topmost index directly. This avoids the searching and iteration required by other approaches. (The approach is based on this post.)

I think the hit-test method used in the accepted answer is more general, but if what you really want is a list index rather than a list item, then this might save an IndexOf call.

My app needed to save and restore the list position after making significant changes to the list contents. The code to set the top position (based on this post) is shown below as well. For convenience, these are implemented as extension methods.

public static class ListViewExtensions {
    public static int GetTopItemIndex(this ListView lv) {
        if (lv.Items.Count == 0) {
            return -1;
        }

        VirtualizingStackPanel vsp = lv.GetVisualChild<VirtualizingStackPanel>();
        if (vsp == null) {
            return -1;
        }
        return (int) vsp.VerticalOffset;
    }

    public static void ScrollToTopItem(this ListView lv, object item) {
        ScrollViewer sv = lv.GetVisualChild<ScrollViewer>();
        sv.ScrollToBottom();
        lv.ScrollIntoView(item);
    }
}

The extremely handy GetVisualChild method comes from an MSDN post:

public static class VisualHelper {
    public static T GetVisualChild<T>(this Visual referenceVisual) where T : Visual {
        Visual child = null;
        for (Int32 i = 0; i < VisualTreeHelper.GetChildrenCount(referenceVisual); i++) {
            child = VisualTreeHelper.GetChild(referenceVisual, i) as Visual;
            if (child != null && child is T) {
                break;
            } else if (child != null) {
                child = GetVisualChild<T>(child);
                if (child != null && child is T) {
                    break;
                }
            }
        }
        return child as T;
    }
}

Usage note on ScrollToTopItem: the ScrollToBottom() call takes effect immediately, but ScrollIntoView() seems to be deferred. So if you call GetTopItemIndex() immediately after ScrollToTopItem(), you'll get the index for an item near the bottom.

Update: just wanted to note that ScrollIntoView() takes 60-100ms on my system, for a list with fewer than 1,000 items. Sometimes it silently fails. I ended up creating a "scroll to index" method that uses sv.ScrollToVerticalOffset() instead.

Overcompensation answered 11/6, 2019 at 16:3 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.