Reputation: 4432
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:
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.
Upvotes: 2
Views: 592
Reputation: 4432
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:
Inherit from QTableView (let's call it MyTableView)
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
}
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";
}
}
}
Note: if item heights depend on control's width, you need to override resizeEvent()
, clear _itemsWithKnownHeights
, and call updateVisibleRowsHeight() again.
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.
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!
Upvotes: 3