如何防止 QListView 调用每个项目的 sizeHint?

How to prevent QListView from calling sizeHint of each item?

我有一个 QListView,其中包含许多高度不同的项目。我实现了一个用于绘制项目的自定义委托,并将布局模式设置为 Batched。

但是,当分配模型时,列表视图会预先为模型中的每个项目请求 sizeHint,忽略 Batched 设置,从而破坏性能,因为要计算大小,委托必须布局大量文本(这并不快)。

可能是为了计算滚动条的位置,但是我估计当item数量多的时候,滚动条的位置可以只根据item的索引,不考虑item的高度。然而,这似乎不是 QListView 的工作方式。

我也试过在模型中使用canFetchMore/fetchMore,但这导致用户体验不好——滚动条位置不再准确,加载更多项目时列表跳来跳去,不流畅完全没有。

所以,问题是:

  1. 有没有办法阻止 QListView 为不可见的项目调用 sizeHint?
  2. 如果只能使用canFetchMore/fetchMore,如何实现流畅的滚动和稳定准确的滚动条?

非常感谢!

UPD: 下面是重现此行为的最小示例: https://github.com/ajenter/qt_hugelistview

请注意巨大的启动延迟和调试消息显示预先请求了所有 5000 个项目的 sizeHint。

好吧,看来我找到了解决方案,所以我会在这里分享它,以供遇到相同问题并搜索此线程的任何人使用。

首先,我发现这实际上是 Qt 中的一个错误,早在 2011 年就已注册,并且仍然存在: https://bugreports.qt.io/browse/QTBUG-16592

我已经投了票(你也应该投!)。 然后决定尝试使用 QTableView 而不是 QListView - 而且,令人惊讶的是,我设法让它工作,或者看起来是这样。

与 QListView 不同,QTableView 仅根据明确请求通过调用 resizeRowToContents(rowNum) 调整行的大小。因此诀窍是以 just-in-time 方式调用它以显示在视口中可见的行。

这是我所做的:

  1. 继承自QTableView(姑且称之为MyTableView)

  2. 用MyTableView替换QListView,在构造函数中这样初始化。这将分配自定义项目委托,隐藏 table headers 并应用“按行”选择模式:

   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. 在 MyTableView 中,添加一个 QItemSelection 私有字段和一个计算实际行高的 public 函数,但仅计算当前可见的行高:
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. 注意:如果项目高度取决于控件的宽度,您需要覆盖 resizeEvent(),清除 _itemsWithKnownHeights,然后再次调用 updateVisibleRowsHeight()。

  2. 在将模型分配给 MyTableView 实例后调用 updateVisibleRowHeights(),以便初始视图正确:

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

事实上,它应该在 MyTableView 的一些响应模型变化的方法中完成,但我会把它留作练习。

  1. 现在剩下的就是在 table 的垂直滚动位置发生变化时调用 updateRowHeights。所以我们需要在MyTableView的构造函数中加入如下内容:
connect(verticalScrollBar(), &QScrollBar::valueChanged, [this](int) {
        updateRowHeights();
    });

完成 - 即使使用 100,000 件物品的模型,它的运行速度也非常快!而且启动是即时的!

此技术的基本 proof-of-concept 示例(使用纯 QTableView 而不是子类)可在此处找到: https://github.com/ajenter/qt_hugelistview/blob/tableview-experiment/src/main.cpp

警告:此技术尚未经过实战验证,可能包含一些未知问题。使用风险自负!