具有数百万个项目的 QListView 键盘速度慢

QListView with millions of items slow with keyboard

我正在使用 QListView 和派生自 QAbstractItemModel 的自定义模型。我有数百万件商品的订单。我已调用 listView->setUniformItemSizes(true) 以防止在向模型添加项目时调用一堆布局逻辑。到目前为止,一切都按预期进行。

问题是使用键盘在列表中导航很慢。如果我 select 列表中的一个项目,然后按 up/down,selection 移动得很快 until selection 需要滚动列表。然后它变得非常滞后。按向上翻页或向下翻页也很滞后。问题似乎是当使用键盘 selected(又名 "current item")并且列表也滚动时 up/down。

如果我使用鼠标,浏览列表的速度很快。我可以使用鼠标滚轮,速度很快。我可以尽可能快地拖动滚动条 up/down——从列表的顶部到底部——列表视图的更新速度非常快。

关于为什么更改 selection 和滚动列表的组合如此缓慢的任何想法?有可行的解决方法吗?

2015 年 9 月 9 日更新

为了更好地说明问题,我在本次更新中提供了更详细的信息。

KEYBOARD + SCROLLING 的性能问题

这主要是一个性能问题,尽管它在某种程度上与用户体验 (UX) 相关。看看当我使用键盘滚动 QListView:

时会发生什么

注意到底部附近的减速了吗?这是我问题的重点。让我解释一下我是如何浏览列表的。

解释:

  1. 从顶部开始,列表中的第一项是 selected。
  2. 按下并按住向下箭头键,当前项目(selection)更改为下一个项目。
  3. 更改 selection 对于当前可见的所有项目都很快。
  4. 只要列表需要显示下一项,selection rate 就会显着减慢。

我希望列表的滚动速度应该与我的键盘输入速度一样快——换句话说,select下一个项目所花费的时间不应在列表滚动时变慢已滚动。

使用鼠标快速滚动

这是我使用鼠标时的样子:

解释:

  1. 使用鼠标,我 select 滚动条手柄。
  2. 上下快速拖动滚动条手柄,列表相应滚动。
  3. 所有动作都极快
  4. 请注意,没有select生成离子

这主要证明了两点:

  1. 模型不是问题。如您所见,模型在性能方面没有任何问题。它可以比显示元素更快地传送元素。

  2. select 滚动和滚动时性能下降。 select 滚动和滚动的 "perfect storm"(如通过使用键盘在列表中导航来说明)会导致速度减慢。因此,我推测当 select 离子在滚动 期间通常不会执行时,Qt 会以某种方式进行大量处理。

非 Qt 实现速度很快

我想指出,我的问题似乎是 Qt 特有的。

在使用不同的框架之前,我已经实现了这种类型的东西。我正在尝试做的是在模型视图理论的范围内。我可以使用 juce::ListBoxModel with a juce::ListBox 以极快的速度准确地完成我所描述的事情。它的速度非常快(另外,当每个项目已经具有唯一索引时,无需为每个项目创建重复索引,例如 QModelIndex)。我知道 Qt 的模型视图架构的每个项目都需要一个 QModelIndex,虽然我不喜欢间接费用,但我认为我明白了,我可以接受它。无论哪种方式,我都不怀疑这些 QModelIndex 是导致我的性能下降的原因。

使用 JUCE 实现,我什至可以使用向上翻页和向下翻页键在列表中导航,而且它可以快速浏览列表。使用 Qt QListView 实现,即使是发布版本,它也会卡顿和滞后。

使用 JUCE framework 的模型视图实现非常快。 为什么Qt QListView实现这么狗?!

激励示例

很难想象为什么在列表视图中需要这么多项目?好吧,我们以前都见过这种事情:

这是 Visual Studio 帮助查看器索引。现在,我还没有计算所有的项目——但我想我们会同意其中有很多!当然,为了制作此列表 "useful,",他们添加了一个筛选框,可根据输入字符串缩小列表视图中的内容。这里没有任何技巧。这些都是我们几十年来在桌面应用程序中看到的所有实用的、真实的东西。

但是有 百万 项吗?我不确定这是否重要。即使有 "only" 150k 个项目(根据一些粗略的测量结果大致准确),也很容易指出您必须做一些事情才能使其可用——这就是过滤器将为您做的事情。

我的具体示例使用 a list of German words as a plain text file with slightly more than 1.7 million entries (including inflected forms). This is probably only a partial (but still significant) sample of words from the German text corpus 用于 assemble 此列表。对于语言学研究,这是一个合理的用例。

关于改善 UX(用户体验)或过滤的问题是很好的设计目标,但它们超出了这个问题的范围(我肯定会在项目的稍后部分解决它们)。

代码

想要代码示例吗?你说对了!我不确定它会有多大用处;它很普通(大约 75% 的样板),但我想它会提供一些背景信息。我意识到我正在使用 QStringList 并且为此有一个 QStringListModel,但是我用来保存数据的 QStringList 是一个占位符——模型将最终会稍微复杂一些,所以最后,我需要使用派生自 QAbstractItemModel.

的自定义模型
//
// wordlistmodel.h ///////////////////////////////////////
//
class WordListModel : public QAbstractItemModel
{
    Q_OBJECT
public:
    WordListModel(QObject* parent = 0);

    virtual QModelIndex index(int row, int column, const QModelIndex& parent = QModelIndex()) const;
    virtual QModelIndex parent(const QModelIndex& index) const;
    virtual int rowCount(const QModelIndex& parent = QModelIndex()) const;
    virtual int columnCount(const QModelIndex & parent = QModelIndex()) const;
    virtual QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const;

public slots:
    void loadWords();

signals:
    void wordAdded();

private:
    // TODO: this is a temp backing store for the data
    QStringList wordList;
};


//
// wordlistmodel.cpp ///////////////////////////////////////
//
WordListModel::WordListModel(QObject* parent) :
    QAbstractItemModel(parent)
{
    wordList.reserve(1605572 + 50); // testing purposes only!
}

void WordListModel::loadWords()
{
    // load items from file or database

    // Due to taking Kuba Ober's advice to call setUniformItemSizes(true),
    // loading is fast. I'm not using a background thread to do
    // loading because I was trying to visually benchmark loading speed.
    // Besides, I am going to use a completely different method using
    // an in-memory file or a database, so optimizing this loading by
    // putting it in a background thread would obfuscate things.
    // Loading isn't a problem or the point of my question; it takes
    // less than a second to load all 1.6 million items.

    QFile file("german.dic");
    if (!file.exists() || !file.open(QIODevice::ReadOnly))
    {
        QMessageBox::critical(
            0,
            QString("File error"),
            "Unable to open " + file.fileName() + ". Make sure it can be located in " +
                QDir::currentPath()
        );
    }
    else
    {
        QTextStream stream(&file);
        int numRowsBefore = wordList.size();
        int row = 0;
        while (!stream.atEnd())
        {
            // This works for testing, but it's not optimal.
            // My real solution will use a completely different
            // backing store (memory mapped file or database),
            // so I'm not going to put the gory details here.
            wordList.append(stream.readLine());    

            ++row;

            if (row % 10000 == 0)
            {
                // visual benchmark to see how fast items
                // can be loaded. Don't do this in real code;
                // this is a hack. I know.
                emit wordAdded();
                QApplication::processEvents();
            }
        }

        if (row > 0)
        {
            // update final word count
            emit wordAdded();
            QApplication::processEvents();

            // It's dumb that I need to know how many items I
            // am adding *before* calling beginInsertRows().
            // So my begin/end block is empty because I don't know
            // in advance how many items I have, and I don't want
            // to pre-process the list just to count the number
            // of items. But, this gets the job done.
            beginInsertRows(QModelIndex(), numRowsBefore, numRowsBefore + row - 1);
            endInsertRows();
        }
    }
}

QModelIndex WordListModel::index(int row, int column, const QModelIndex& parent) const
{
    if (row < 0 || column < 0)
        return QModelIndex();
    else
        return createIndex(row, column);
}

QModelIndex WordListModel::parent(const QModelIndex& index) const
{
    return QModelIndex(); // this is used as the parent index
}

int WordListModel::rowCount(const QModelIndex& parent) const
{
    return wordList.size();
}

int WordListModel::columnCount(const QModelIndex& parent) const
{
    return 1; // it's a list
}

QVariant WordListModel::data(const QModelIndex& index, int role) const
{
    if (!index.isValid())
    {
        return QVariant();
    }    
    else if (role == Qt::DisplayRole)
    {
        return wordList.at(index.row());
    }
    else
    {    
        return QVariant();
    }
}


//
// mainwindow.h ///////////////////////////////////////
//    
class MainWindow : public QMainWindow
{
    Q_OBJECT

public:
    explicit MainWindow(QWidget *parent = 0);
    ~MainWindow();

public slots:
    void updateWordCount();

private:
    Ui::MainWindow *ui;
    WordListModel* wordListModel;
};

//
// mainwindow.cpp ///////////////////////////////////////
//
MainWindow::MainWindow(QWidget *parent) :
    QMainWindow(parent),
    ui(new Ui::MainWindow)
{
    ui->setupUi(this);
    ui->listView->setModel(wordListModel = new WordListModel(this));

    // this saves TONS of time during loading,
    // but selecting/scrolling performance wasn't improved
    ui->listView->setUniformItemSizes(true);

    // these didn't help selecting/scrolling performance...
    //ui->listView->setLayoutMode(QListView::Batched);
    //ui->listView->setBatchSize(100);

    connect(
        ui->pushButtonLoadWords,
        SIGNAL(clicked(bool)),
        wordListModel,
        SLOT(loadWords())
    );

    connect(
        wordListModel,
        SIGNAL(wordAdded()),
        this,
        SLOT(updateWordCount())
    );
}

MainWindow::~MainWindow()
{
    delete ui;
}

void MainWindow::updateWordCount()
{
    QString wordCount;
    wordCount.setNum(wordListModel->rowCount());
    ui->labelNumWordsLoaded->setText(wordCount);
}

如前所述,我已经阅读并采纳了 Kuba Ober 的建议:

QListView takes too long to update when given 100k items

我的问题不是那个问题的重复! 在另一个问题中,OP 询问的是 loading 速度,作为我在上面的代码中注意到,由于调用 setUniformItemSizes(true).

这不是问题

总结性问题

  1. 为什么滚动列表时使用键盘导航 QListView(模型中有数百万个项目)这么慢?
  2. 为什么 select 浏览和滚动 项的组合会导致速度变慢?
  3. 我是否遗漏了任何实施细节,或者我是否达到了 QListView 的性能阈值?

我做了以下测试:

首先,我创建了一个 class 来检查通话:

struct Test
{
  static void NewCall( QString function, int row )
  {
    function += QString::number( row );

    map[ function ]++;
  }

  static void Summary( )
  {
    qDebug() << "-----";
    int total = 0;
    QString data;
    for( auto pair : map )
    {
      data = pair.first + ": " + QString::number( pair.second );
      total += pair.second;
      qDebug( ) << data;
    }

    data = "total: " + QString::number( total ) + " calls";
    qDebug() << data;
    map.clear();
  }

  static std::map< QString, int > map;
};

std::map<QString,int> Test::map;

然后我在 WordListModelindexparentdata 方法中插入对 NewCall 的调用。最后,我在对话框中添加了一个 QPushButtonclicked 信号链接到一个调用 Test::Summary.

的方法

测试步骤如下:

  1. Select 列表中最后显示的项目
  2. 摘要按钮清除通话列表
  3. 再次用tab键select列表查看
  4. 使用方向键滚动
  5. 再次按摘要按钮

打印的列表显示了问题。 QListView 小部件发出大量调用。小部件似乎正在重新加载模型中的所有数据。

我不知道它是否可以改进,但您只能过滤列表以限制要显示的项目数。

不幸的是,我相信你对此无能为力。 我们对小部件没有太多控制权。

尽管您可以改用 ListView 来避免该问题。 如果您尝试下面我的快速示例,您会发现即使使用代价高昂的委托也能多快。

示例如下:

Window{
    visible: true
    width: 200
    height: 300

    property int i: 0;

    Timer {
        interval: 5
        repeat: true
        running: true
        onTriggered: {
            i += 1
            lv.positionViewAtIndex(i, ListView.Beginning)
        }
    }

    ListView {
        id:lv
        anchors.fill: parent
        model: 1605572
        delegate: Row {
            Text { text: index; width: 300; }
        }
    }
}

我放了一个Timer来模拟滚动,但是当然你可以打开或关闭那个定时器,这取决于是否按下了键,如果是,也可以通过 i += -1 改变 i += 1 被按下而不是。您还必须添加溢出和下溢检查。

您还可以通过更改 Timerinterval 来选择滚动速度。然后只需修改所选元素的颜色等即可显示它已被选中。

在此基础上,您可以使用 cacheBufferListView 来缓存更多元素,但我认为没有必要。

如果你想使用 QListView,请看这个例子:http://doc.qt.io/qt-5/qtwidgets-itemviews-fetchmore-example.html 使用 fetch 方法即使在大数据集上也能保持性能。它允许您在滚动时填充列表。

1.为什么要导航 QListView(模型中有数百万个项目) 滚动列表时使用键盘很慢?

因为当您使用键盘浏览列表时,您输入了内部 Qt 函数 QListModeViewBase::perItemScrollToValue,请参阅堆栈:

Qt5Widgetsd.dll!QListModeViewBase::perItemScrollToValue(int index, int scrollValue, int viewportSize, QAbstractItemView::ScrollHint hint, Qt::Orientation orientation, bool wrap, int itemExtent) Ligne 2623    C++
Qt5Widgetsd.dll!QListModeViewBase::verticalScrollToValue(int index, QAbstractItemView::ScrollHint hint, bool above, bool below, const QRect & area, const QRect & rect) Ligne 2205  C++
Qt5Widgetsd.dll!QListViewPrivate::verticalScrollToValue(const QModelIndex & index, const QRect & rect, QAbstractItemView::ScrollHint hint) Ligne 603    C++
Qt5Widgetsd.dll!QListView::scrollTo(const QModelIndex & index, QAbstractItemView::ScrollHint hint) Ligne 575    C++
Qt5Widgetsd.dll!QAbstractItemView::currentChanged(const QModelIndex & current, const QModelIndex & previous) Ligne 3574 C++
Qt5Widgetsd.dll!QListView::currentChanged(const QModelIndex & current, const QModelIndex & previous) Ligne 3234 C++
Qt5Widgetsd.dll!QAbstractItemView::qt_static_metacall(QObject * _o, QMetaObject::Call _c, int _id, void * * _a) Ligne 414   C++
Qt5Cored.dll!QMetaObject::activate(QObject * sender, int signalOffset, int local_signal_index, void * * argv) Ligne 3732    C++
Qt5Cored.dll!QMetaObject::activate(QObject * sender, const QMetaObject * m, int local_signal_index, void * * argv) Ligne 3596   C++
Qt5Cored.dll!QItemSelectionModel::currentChanged(const QModelIndex & _t1, const QModelIndex & _t2) Ligne 489    C++
Qt5Cored.dll!QItemSelectionModel::setCurrentIndex(const QModelIndex & index, QFlags<enum QItemSelectionModel::SelectionFlag> command) Ligne 1373    C++

这个函数的作用是:

itemExtent += spacing();
QVector<int> visibleFlowPositions;
visibleFlowPositions.reserve(flowPositions.count() - 1);
for (int i = 0; i < flowPositions.count() - 1; i++) { // flowPositions count is +1 larger than actual row count
    if (!isHidden(i))
        visibleFlowPositions.append(flowPositions.at(i));
}

其中 flowPositions 包含与您的 QListView 一样多的项目,因此这基本上遍历了您的所有项目,这肯定需要一段时间来处理。

2。为什么选择和滚动项目的组合会导致速度变慢?

因为 "selecting and scrolling" 让 Qt 调用 QListView::scrollTo(将视图滚动到特定项目),这就是最终调用 QListModeViewBase::perItemScrollToValue 的原因。当您使用滚动条滚动时,系统不需要要求视图滚动到特定项目。

3。是否有任何我遗漏的实施细节,或者我是否达到了 QListView 的性能阈值?

恐怕你做对了。这绝对是一个 Qt 错误。必须完成错误报告以希望在以后的版本中修复此问题。 I submitted a Qt bug here.

由于此代码是内部代码(私有数据 类)并且不以任何 QListView 设置为条件,我认为除了修改和重新编译 Qt 源代码外没有办法修复它(但我不知道具体如何,这需要更多调查)。堆栈中第一个可覆盖的函数是 QListView::scrollTo 但我怀疑不调用 QListViewPrivate::verticalScrollToValue...

是否容易覆盖它

注意:这个函数遍历视图的所有项目的事实显然是在 Qt 4.8.3 中引入的 this bug was fixed (see changes)。基本上,如果您不在视图中隐藏任何项目,您可以按如下方式修改 Qt 代码:

/*QVector<int> visibleFlowPositions;
visibleFlowPositions.reserve(flowPositions.count() - 1);
for (int i = 0; i < flowPositions.count() - 1; i++) { // flowPositions count is +1 larger than actual row count
    if (!isHidden(i))
        visibleFlowPositions.append(flowPositions.at(i));
}*/
QVector<int>& visibleFlowPositions = flowPositions;

然后您将不得不重新编译 Qt,我很确定这将解决问题(但未经过测试)。但是如果有一天你隐藏了一些项目......例如支持过滤,你会看到新的问题!

最有可能的正确解决方法是让视图同时维护 flowPositionsvisibleFlowPositions 以避免动态创建它...