Important: Please read the Qt Code of Conduct - https://forum.qt.io/topic/113070/qt-code-of-conduct

How to prevent QListView from calling sizeHint of each item?



  • I have a QListView with a lot of items that are of various heights. I implement a custom delegate for painting items and set layout mode to Batched.

    However, when the model is assigned, the list view requests sizeHint for every item in the model upfront, ignoring the Batched setting and thus ruining the performance because to calculate the size, the delegate has to layout a lot of text (which is not fast). The behavior is similar regardless of the vertical scroll mode - sizeHints for all items are requested upfront (!), which simply doesn't make sense for me, given that I've explicitly set the batch size to only 10 items. Tried in Qt 5.15.2 and 6.0.0 beta5.

    Probably it does this to calculate the scrollbar position, but I reckoned that when the number of items is large, the scrollbar position can be based on item indices only, not taking into consideration item heights. However it seems that this is not how QListView works.

    I also tried to use canFetchMore/fetchMore in the model, but this leads to bad user experience - the scrollbar position is no longer accurate, and the list jumps around when more items are loaded, it was not smooth at all.

    So, the question is:

    1. Is there a way to prevent QListView from calling sizeHint for invisible items?
    2. If the only way is to use canFetchMore/fetchMore, how to get smooth scrolling and stable and accurate scrollbar?

    Thanks a lot!


  • Qt Champions 2019

    @Alex-Jenter said in How to prevent QListView from calling sizeHint of each item?:

    Is there a way to prevent QListView from calling sizeHint for invisible items?

    No

    If the only way is to use canFetchMore/fetchMore, how to get smooth scrolling and stable and accurate scrollbar?

    Don't calculate the size on every sizeHint() but cache it. Since the scrollbar has to be at the correct position the size of all items has to be known

    ignoring the Batched setting

    This has nothing to do with the size of an item.



  • Hi Christian, thanks for your reply!

    Don't calculate the size on every sizeHint() but cache it. Since the scrollbar has to be at the correct position the size of all items has to be known

    I do cache them, but this doesn't help prevent a huge startup delay when it is being calculated for the first time. This is a no-go. How to eliminate it? The scrollbar position can use item indices and safely ignore item heights, especially when the dataset is large, it won't make any noticeable difference!

    Here is the minimal example that demonstrates the current behavior: https://github.com/ajenter/qt_hugelistview
    Note that it has caching, but it doesn't help.

    There should be a way to make a listview that is a "window" to a huge data set, I'm surprised this is not possible!

    This has nothing to do with the size of an item.

    Could you please explain then what it does? The docs say it is about item layouting, and item layouting depends very much on item size when the items are not uniformly sized.

    Thanks!



  • Well it seems that I've found a solution, so I'll share it here for sake of anyone who has the same problem and googles this thread.

    First of all, I've found that this is actually a bug in Qt registered back in 2011 and still open:
    https://bugreports.qt.io/browse/QTBUG-16592

    I've added my vote to it, and then decided to try out using QTableView instead of QListView - and, surpise, I managed to make it work, or so it seems.

    Unlike QListView, QTableView only resizes rows upon explicit request, by calling resizeRowToContents(rowNum). So the trick is to call it in a just-in-time fashion for rows that become visible in the viewport.

    Here's what I did:

    1. Inherit from QTableView (let's call it MyTableView)

    2. Replace QListView with MyTableView and initialize it like this in the constructor. This assigns custom item delegate, hides table headers and applies "by row" selection mode:

       MyTableView::MyTableView(QWidget* parent) : QTableView(parent)
        {
           setSelectionBehavior(QAbstractItemView::SelectRows);
           horizontalHeader()->setStretchLastSection(true);	
           horizontalHeader()->hide();
           verticalHeader()->hide();
           setItemDelegateForColumn(0, new CustomDelegate(&table)); // for custom-drawn items
        }
    
    1. In MyTableView, add a QItemSelection private field and a public function that calculates real heights of rows, but only those that are currently visible:
    QItemSelection _itemsWithKnownHeight; // private member of MyTableView
    
    void MyTableView::updateVisibleRowHeights()
    {
    	const QRect viewportRect = table.viewport()->rect();
    
    	QModelIndex topRowIndex = table.indexAt(QPoint(viewportRect.x() + 5, viewportRect.y() + 5));
    	QModelIndex bottomRowIndex = table.indexAt(QPoint(viewportRect.x() + 5, viewportRect.y() + viewportRect.height() - 5));
    	qDebug() << "top row: " << topRowIndex.row() << ", bottom row: " << bottomRowIndex.row();
    
    	for (auto i = topRowIndex.row() ; i < bottomRowIndex.row() + 1; ++i)
    	{
    		auto index = model()->index(i, 0);
    		if (!_itemsWithKnownHeights.contains(index))
    		{
    			resizeRowToContents(i);
    			_itemsWithKnownHeights.select(index, index);
    			qDebug() << "Marked row #" << i << " as resized";
    		}
    	}
    }
    
    1. Note: if item heights depend on control's width, you need to override resizeEvent(), clear _itemsWithKnownHeights, and call updateVisibleRowsHeight() again.

    2. Call updateVisibleRowHeights() after assigning a model to MyTableView instance, so that initial view is correct:

       table.setModel(&myModel);
       table.updateVisibleRowHeights();
    

    In fact it should be done in some MyTableView's method that reacts to model changes, but I'll leave it as an exercise.

    1. Now all that's left is to have something call updateRowHeights whenever table's vertical scroll position changes. So we need to add the following to MyTableView's constructor:
    connect(verticalScrollBar(), &QScrollBar::valueChanged, [this](int) {
    		updateRowHeights();
    	});
    

    Done - it works really fast even with model of 100,000 items! And startup is instantenious!

    A basic proof-of-concept example of this technique (using pure QTableView instead of subclass) can be found here:
    https://github.com/ajenter/qt_hugelistview/tree/tableview-experiment

    Warning: this technique is not battle proven yet and may contain some yet unknown issues. Use at own risk!

    Hope this helps!


Log in to reply