views:

777

answers:

3

How to autoscroll a Listbox after adding a new item, but only if the scrollbar is at the bottom before the item is added?

+2  A: 

This sample code should help you out. I've done this many times with a TextBox but it took awhile to figure it out for a ListBox

Obviously, it's just a Form with a Button and a ListBox. Modify to fit your needs:

private void button1_Click(object sender, EventArgs e)
{
    listBox1.Items.Add("Some Text " + listBox1.Items.Count.ToString());

    //The max number of items that the listbox can display at a time
    int NumberOfItems = listBox1.ClientSize.Height / listBox1.ItemHeight;

    if (listBox1.TopIndex == listBox1.Items.Count - NumberOfItems - 1)
    {
        //The item at the top when you can just see the bottom item
        listBox1.TopIndex = listBox1.Items.Count - NumberOfItems + 1;
    }
}
colithium
A: 

I solved this problem using a similar method to colithium, except I then realised there was a bug with concurrent updates. So there is a class member called m_previousCount which stores the number of items in the ListBox before this update happened.

I did this with a ListView, but it should work the same for a ListBox.

In this case, my listView1 is content-bound to listViewModel1.Entries.

private void EventMessageViewModel_PropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
{
    listView1.Dispatcher.BeginInvoke(DispatcherPriority.Background, new ScrollToLastItemDelegate(ScrollToLastItem)); 
}

/// <summary>
/// Scroll to last item, unless you have scrolled up the list
/// </summary>
private void ScrollToLastItem()
{
    // Get scrollviewer
    Decorator border = System.Windows.Media.VisualTreeHelper.GetChild(listView1, 0) as Decorator;
    ScrollViewer scrollViewer = border.Child as ScrollViewer;

    double vo = scrollViewer.VerticalOffset;

    // assume they are all the same height
    ListBoxItem lbi = listView1.ItemContainerGenerator.ContainerFromIndex(0) as ListBoxItem;

    //The max number of items that the listbox can display at a time
    double NumberOfItemsOnScreen = listView1.ActualHeight / lbi.ActualHeight;

    // use previousCount in case we get multiple updates in one go
    if (m_previousCount > NumberOfItemsOnScreen) // scrollbar should be active
    {
        if (vo < (m_previousCount - NumberOfItemsOnScreen)) // you're not at the bottom
        {
            return; // don't scroll to the last item
        }
    }

    m_previousCount = listView1.Items.Count;

    // scroll to the last item
    listView1.SelectedItem = listView1.Items.GetItemAt(listViewModel1.Entries.Count - 1);

    listView1.ScrollIntoView(listView1.SelectedItem);
}
metao
A: 

I came up with the following solution which will also work for owner drawn listboxes with variable height items.

The basic idea is that it figures out if it's scrolled to the bottom by using the IndexToPoint method and a point at the bottom of the listbox to see if the last item is at that position. This has a slight flaw in that if the last item is at the bottom but not fully visible it will still think it's scrolled to the bottom.

It uses the TopIndex property to scroll the list box. Note that when setting the TopIndex to be the last item in the listbox it won't actually put it at the top if there is plenty of room for other items. In that case it will put it at the bottom, which is what you want.

It also has some extra code to keep the number of items in the list to a maximum amount (defined elsewhere by the constant MAX_LISTBOX_ITEMS) by deleting the top one after it gets full. When it does this it works out where it will need to scroll the list to to keep the same items showing even after one is deleted. If you don't care about controlling the number of items added to the listbox you should be able to remove the middle if clause from the code and any mentions of the scrollToIndex variable.

private void AddItemToListBox(ListBox listBox, object newItem)
    {
        int scrollToIndex = -1;
        bool scrollToEnd = false;

        //Work out if we should scroll to the end after adding a new item
        int lastIndex = listBox.IndexFromPoint(4, listBox.ClientSize.Height - 4);
        if ((lastIndex < 0) || (lastIndex == listBox.Items.Count - 1))
        {
            scrollToEnd = true;
        }

        //To prevent listbox jumping about as we delete and scroll
        listBox.BeginUpdate();

        //Work out if we have too many items and have to delete
        if (listBox.Items.Count >= MAX_LISTBOX_ITEMS)
        {
            //If deleting an item, and not scrolling to the end when new item added anyway,
            //then we will need to scroll to keep in the same place, work out new scroll index
            if (!scrollToEnd)
            {
                scrollToIndex = listBox.TopIndex - 1;
                if (scrollToIndex < 0)
                {
                    scrollToIndex = 0;
                }
            }

            //Remove top item
            listBox.Items.Remove(listBox.Items[0]);
        }

        //Add new item
        listBox.Items.Add(newItem);

        //Scroll if necessary
        if (scrollToEnd)
        {
            listBox.TopIndex = listBox.Items.Count - 1;
        }
        else if (scrollToIndex >= 0)
        {
            listBox.TopIndex = scrollToIndex;
        }

        listBox.EndUpdate();
    }
dunni