1

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).

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!

UPD: Here is the minimal example that reproduces this behavior: https://github.com/ajenter/qt_hugelistview

Note the huge startup delay and the debug messages showing that sizeHint of all 5000 items is requested upfront.

Alex Jenter
  • 4,112
  • 3
  • 33
  • 59
  • If you use `QAbstractItemView::ScrollPerItem` and provide the number of items beforehand (even with `canFetchMore()`)? Concerning jumping scroll bar... I once made a demo to keep the current item at stable view position - regardless whether items are inserted before or after it: [Stop QTableView from scrolling as data is added above current position](https://stackoverflow.com/a/42460216/7478597) (Though, it depends on using `ScrollPerPixel`.) :-( – Scheff's Cat Nov 20 '20 at 07:13
  • @Scheff: 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. Can it be a bug in Qt 6 that I'm using? – Alex Jenter Nov 20 '20 at 07:32
  • Regarding canFetchMore, thanks for the link! As far I see it requires a QTableView - do you suggest to switch to QTableView instead of QListView? – Alex Jenter Nov 20 '20 at 07:35
  • 1
    I'm struggling with performance issues in `QTreeView` and `QTableView` for a long time. (With `QListView`, I actually have very few experience.) Layouting (i.e. determining the size for a large amount of items) is something I feel as one of the essential issues as well. I would think it cannot be done better but, actually, I once saw it better in gtkmm from which we switched to Qt when the Windows support became unreliable for gtkmm 3. Sorry, that I'm unable to provide anything like an answer but I'd liked to share my thoughts as feeling with you. ;-) – Scheff's Cat Nov 20 '20 at 08:18
  • I see, thanks for sharing your experience anyway! – Alex Jenter Nov 20 '20 at 08:38
  • Can you please post a minimal reproducible example of your code? I'd like to play around with it. – aalimian Nov 20 '20 at 21:38
  • @aalimian: Sure, here it is: https://github.com/ajenter/qt_hugelistview – Alex Jenter Nov 21 '20 at 07:01

1 Answers1

2

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 you should, too!). 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/blob/tableview-experiment/src/main.cpp

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

Dharman
  • 21,838
  • 18
  • 57
  • 107
Alex Jenter
  • 4,112
  • 3
  • 33
  • 59