带有键盘数百万项的QListView

Mat*_*aus 7 c++ qt qlistview qabstractitemmodel model-view

我正在使用一个QListView源自的自定义模型QAbstractItemModel.我有数百万件物品.我打电话listView->setUniformItemSizes(true)来阻止在向模型添加项目时调用一堆布局逻辑.到目前为止,一切都按预期工作.

问题是使用键盘导航列表很慢.如果我在列表中选择一个项目,然后按向上/向下,选择将快速移动,直到选择需要滚动列表.然后变得非常迟钝.按向上翻页或向下翻页也非常滞后.问题似乎是当使用键盘选择项目(也就是"当前项目")时,列表也会向上/向下滚动.

如果我使用鼠标,导航列表很快.我可以使用快速的鼠标滚轮.我可以按照我想要的速度向上/向下拖动滚动条 - 从列表顶部到底部 - 列表视图快速更新.

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

2015年9月9日更新

为了更好地说明问题,我在此更新中提供了放大信息.

KEYBOARD + SCROLLING的性能问题

这主要是性能问题,尽管它确实与用户体验(UX)有关.看看当我使用键盘滚动浏览时会发生什么QListView:

慢滚动问题

注意底部附近的减速?这是我的问题的焦点.让我解释一下我如何浏览列表.

说明:

  1. 从顶部开始,选择列表中的第一个项目.
  2. 按下并保持向下箭头键,当前项(选择)更改到下一个项目.
  3. 对于当前查看的所有项目,快速更改选择.
  4. 一旦列表需要将下一个项目放入视图中,选择速率就会显着降低.

我希望列表能够像键盘的打字速度一样快地滚动 - 换句话说,选择下一个项目所花费的时间不应该在滚动列表时减慢.

使用MOUSE快速滚动

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

快速鼠标导航

说明:

  1. 使用鼠标,我选择滚动条手柄.
  2. 快速向上和向下拖动滚动条手柄,相应地滚动列表.
  3. 所有动作都非常快.
  4. 请注意,没有选择.

这证明了两个要点:

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

  2. 选择和滚动时性能会降低.选择和滚动的"完美风暴"(如使用键盘在列表中导航所示)导致减速.因此,我推测,在滚动期间正常执行的选择时,Qt以某种方式进行了大量处理.

非Qt实施是快速的

我想指出,我的问题似乎与Qt有关.

在使用不同的框架之前,我已经实现了这种类型的东西.我想做的是在模型 - 视图理论的范围内.我可以使用带有juce :: ListBox的juce :: ListBoxModel以极快的速度完成我所描述的内容.这是愚蠢的快速(此外,当每个项目已经具有唯一索引时,不需要为每个项目创建重复索引,例如a ).我认为Qt需要针对其模型 - 视图架构的每个项目,虽然我不喜欢开销成本,但我认为我理性,我可以忍受它.无论哪种方式,我都不怀疑这些因素导致我的表现减慢.QModelIndexQModelIndexQModelIndex

通过JUCE实现,我甚至可以使用向上翻页和向下翻页键来导航列表,它只是在列表中闪现.使用Qt QListView实现,即使使用发布版本,它也会突然出现并且很迟钝.

使用JUCE框架的模型视图实现非常快.为什么Qt QListView实现这样的狗?!

激励范例

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

Visual Studio索引

这是Visual Studio帮助查看器索引.现在,我没有计算所有项目 - 但我认为我们同意它们中有很多!当然为了使这个列表"有用",他们添加了一个过滤器框,根据输入字符串缩小列表视图中的内容.这里没有任何技巧.这是我们几十年来在桌面应用程序中看到的所有实用的,现实世界的东西.

但是有数百万件物品吗?我不确定这很重要.即使有"仅"150k项目(根据一些原始测量结果大致准确),也很容易指出你必须做些什么来使它可用 - 这就是过滤器将为你做的事情.

我的具体示例使用德语单词列表作为纯文本文件,条目略多于170万条(包括变形形式).这可能只是德国文本语料库中用于组合此列表的部分(但仍然很重要)单词样本.对于语言学习,这是一个合理的用例.

关于改进用户体验(用户体验)或过滤的担忧是很好的设计目标,但它们超出了这个问题的范围(我当然会在项目的后期解决它们).

想要一个代码示例?你说对了!我不确定它会有多大用处; 它就像它获得的香草(大约75%样板),但我想它会提供一些背景.我意识到我正在使用a 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);
}
Run Code Online (Sandbox Code Playgroud)

如上所述,我已经审查并采纳了Kuba Ober的建议:

给定100k项目时,QListView需要很长时间才能更新

我的问题不是那个问题的重复!在另一个问题中,OP询问加载速度,正如我在上面的代码中所提到的,由于调用而没有问题setUniformItemSizes(true).

总结问题

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

jpo*_*o38 5

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++
Run Code Online (Sandbox Code Playgroud)

此功能执行以下操作:

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));
}
Run Code Online (Sandbox Code Playgroud)

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

2、为什么选择和滚动项结合会导致速度变慢?

因为“选择和滚动”使 Qt 调用QListView::scrollTo(将视图滚动到特定项目),这就是最终调用QListModeViewBase::perItemScrollToValue. 当您使用滚动条滚动时,系统不需要要求视图滚动到特定项目。

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

恐怕你做对了。这绝对是一个 Qt 错误。必须完成错误报告以希望在以后的版本中修复此问题。我在这里提交了一个 Qt 错误

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

注意:当这个错误被修复时,这个函数遍历视图的所有项目的事实显然是在 Qt 4.8.3 中引入的(请参阅更改)。基本上,如果您不隐藏视图中的任何项目,您可以修改 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;
Run Code Online (Sandbox Code Playgroud)

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

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