Calculating number of visible items in ListBox

2019-04-14 06:11发布

问题:

I have a class called Book;

class Book
{
    public string Name { get; set; }
    public string Author { get; set; }
    public int PagesCount { get; set; }
    public int Category { get; set; }
}

The ListBox displays a list of Books and the ItemTemplate has been modified so as to visually represent the Book. The text shows the book's Name, author and the number of pages. The category, however is represented by a certain color (for example, history is blue, romance is red etc.) Now, the text has an OuterGlowBitmap Effect and a value converter from the Category (int) to the appropriate Color. Everything is bound in DataTemplate for ListBoxItem. Technically, everything works fine.

The problem, however, is performance. It seems that the outerGlow bitmap effect is heavy on processor, so when I have a list of about 500 books, it takes about 500ms to retreive the data from the database but around 10 seconds to actually load the items into the ListBox. And even when the loading is done, scrolling is very laggy. I've tried to set the VirtualizingStackPanel.IsVirtualizing to True, but to no avail. (The maximum number of books that can be in the database at any given time is about 30000.)

However, even when there is more than 100 items in the listbox, human mind can't process that much quickly, so I don't aim to load and list to the user all the books that are searched for. That is why I have created a wrapper navigation class BookNavigator that actually binds the listbox to its ObservableCollection object. All the books are loaded into this BookNavigator, but only X of them are displayed in the listbox (by adding them to the observableCollection).

The problem with this is that I want that number of displayed books to be small enough for listbox not to display the scrollbar, so i can implement my own methods of scrolling (First, Previous, Next, Last, or simply my own scrollbar, doesn't matter).

How can I calculate how many items to display so the scrollbar is not shown?

Two problems that pop up: - Resizing the application can change the listbox's size - Not all the listbox items are of the same height (depending on the number of authors).

Is there any way to achieve what I am trying to do?


EDIT (as a reply to Martin Harris)

The problem with the code Martin Harris suggested is that the foreach loop uses FrameworkElement, but the listbox is filled with objects of type Book which does not inherit from FrameworkElement, nor it has any other mean of calculating its height. The ListBoxItem's root element is a grid, so maybe it would be possible to retreive this grid, but I don't know how to do that?

Is there any way at all to get the actual UI elements that are created to represent the listbox item?


EDIT

I found this Q/A which seems to be what I need.. ItemContainerGenerator

回答1:

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. The count of this list would give you the number of visible items in the listbox.

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;
}

This would then contain the list of items currently shown in the listbox (including those hidden by scrolling or something similar).



回答2:

Will you probably could figure out the sizes of all the books before adding them to the list box (possibly by parsing and populating the template XAML in code behind then asking the outer control for its size) this would be a lot of work for not much gain. Can't you just simply select a number of books which will be enough to fill the list box, but not so many to slow the rendering down and turn the scroll bar off? This could still be linked to the size of the list box so that as it grew more items were added, but the performance cost for adding a few extra items should be less that the cost of calculating all the sizes again.

Quick example: Let's say that the size of a book with one author is 150 pixels. You could take the size of the listbox and divide it by 125 to get a rough estimate of the number of items which would be in-the-ballpark, but not costly to calculate. What you want to avoid is too few items since that will leave empty space.


Edited in light of your comment.

In that case you could add the items to the list box (using the method above to get a close guess), then calculate which the last completely visible item is and then remove the extra items.

This extension method will get the last element that is completely shown in a listbox or null if no items are visible:

public static class ListBoxExtensions
{
    public static FrameworkElement GetLastItem(this ListBox listBox)
    {
        double height = listBox.ActualHeight;

        double currentHeight = 0;
        FrameworkElement previous = null;

        foreach (FrameworkElement item in listBox.Items)
        {
            currentHeight += item.ActualHeight;
            if (currentHeight > height)
            {
                return previous;
            }

            previous = item;
        }

        return previous;
    }
}

If the list is made larger then you can add enough items to the collection that you fill in the gap and re-run the process.



回答3:

"How can I calculate how many items to display so the scrollbar is not shown? "

Short answer: (listBox1.Height/ listBox1.ItemHeight)

This gives you the number of displayed/available lines, so you can read only this number of lines and will fill all the listbox.

Now, the demo:

        int listbox_total_visible_lines = 2;
        listBox1.Height = (listbox_total_visible_lines + 1) * listBox1.ItemHeight;
        listBox1.Items.Add("um");
        listBox1.Items.Add("dois");
        listBox1.Items.Add("tres");
        listBox1.Items.Add("quatro");
        listBox1.Items.Add("cinco");
        listBox1.SelectedIndex = listBox1.Items.Count - 1;
        this.Text = (listBox1.Height/ listBox1.ItemHeight).ToString();

This example let's you choose the number of items that are going to be visible, so it's the number os lines that are actualy available to show. Therefore, you add only the "listbox_total_visible_items" items and the listbox will be full and not show the scrollbars.

Code explain:

  1. listbox_total_visible_items contains the number of lines to be shown

  2. setup listbox with the proper size, 2 lines only

3-7. add some lines

  1. go to the last line, just for fun

  2. show on the form text bar, the number of the listbox lines, based on the listbox height divided by the size of each item.

That's it.



回答4:

The solution can implicitly be found here: Solution