使用键盘

时间:2015-09-08 05:09:46

标签: c++ qt qlistview qabstractitemmodel model-view

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

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

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

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

2015年9月9日更新

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

KEYBOARD + SCROLLING的性能问题

这主要是性能问题,尽管它确实与用户体验(UX)有关。看看当我使用键盘滚动QListView

时会发生什么

Slow Scrolling Issue

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

解释

  1. 从顶部开始,选择列表中的第一项。
  2. 按住按住向下箭头键,当前项目(选择)将更改为下一个项目。
  3. 对于当前正在查看的所有项目,快速更改选择。
  4. 一旦列表需要显示下一个项目,选择率就会显着减慢。
  5. 我希望列表能够像键盘的打字速度一样快地滚动 - 换句话说,选择下一个项目所需的时间不应该在滚动列表时减慢。

    使用鼠标快速滚动

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

    Fast Mouse Navigation

    解释

    1. 使用鼠标,选择滚动条手柄。
    2. 快速向上和向下拖动滚动条手柄,相应地滚动列表。
    3. 所有动作都非常快。
    4. 请注意没有选择
    5. 这证明了两个要点:

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

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

      3. 非Qt实施是快速的

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

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

        通过JUCE实现,我甚至可以使用Page-up&用于导航列表的向下翻页键,它只是在列表中闪现。使用Qt QListView实现,即使使用发布版本,它也会突然出现并且很滞后。

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

        激励示例

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

        Visual Studio Index

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

        但是数百万项目?我不确定这是否重要。即使只有"只有" 150k项目(根据一些原始测量值大致准确),很容易指出你必须做一些事情才能使它成为可能 - 这就是过滤器将为你做的事情。

        我的具体示例使用a list of German words as a plain text file with slightly more than 1.7 million entries (including inflected forms).这可能只是用于汇编此列表的德语text corpus中的部分(但仍然很重要)样本。对于语言学习,这是一个合理的用例。

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

        代码

        想要一个代码示例吗?你说对了!我不确定它会有多大用处;它就像香草一样(大约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询问加载速度,正如我在我的注意事项中所说的那样上面的代码,由于调用setUniformItemSizes(true)而不是问题。

        摘要问题

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

3 个答案:

答案 0 :(得分:4)

<强> 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。为什么选择和滚动项目的组合会导致速度变慢?

因为&#34;选择和滚动&#34; Qt调用QListView::scrollTo(将视图滚动到特定项目),最终调用QListModeViewBase::perItemScrollToValue。使用滚动条滚动时,系统无需要求视图滚动到特定项目。

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

我担心你做的事情是对的。这绝对是一个Qt错误。必须完成错误报告,以便在以后的版本中修复此问题。 I submitted a Qt bug here

由于此代码是内部(私有数据类)而不是任何QListView设置的条件,我认为除了通过修改和重新编译Qt源代码之外无法修复它(但我不知道究竟如何,这将需要更多的调查)。堆栈中第一个可以覆盖的函数是QListView::scrollTo,但是我怀疑在不调用QListViewPrivate::verticalScrollToValue的情况下取代它会很容易...

注意:当this bug被修复(see changes)时,这个函数遍历视图的所有项目这一事实显然已在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;

然后你必须重新编译Qt,我非常确定这会解决问题(但未经过测试)。但是,如果你有一天隐藏某些项目......例如支持过滤,那么你会发现新的问题!

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

答案 1 :(得分:1)

我做了以下测试:

首先,我创建一个类来检查调用:

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;

然后,我在NewCall的{​​{1}},indexparent方法中插入data的来电。最后,我在对话框中添加WordListModelQPushButton信号链接到调用clicked的方法。

测试的步骤是下一步:

  1. 选择列表中最后显示的项目
  2. 摘要按钮清除通话清单
  3. 使用标签键再次选择列表视图
  4. 使用方向键执行滚动
  5. 再次按摘要按钮
  6. 打印列表显示问题。 Test::Summary小部件会拨打大量电话。似乎小部件正在重新加载模型中的所有数据。

    我不知道它是否可以改进,但除了过滤列表以限制要显示的项目数之外,你不能做任何事情。

答案 2 :(得分:-1)

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

虽然您可以使用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来模拟滚动,当然你可以打开或关闭那个定时器,具体取决于是否按下按键以及如果▲而不是。您还必须添加上溢和下溢检查。

您还可以通过更改interval的{​​{1}}来选择滚动速度。然后,只需修改所选元素的颜色等就可以显示它已被选中。

最重要的是,您可以Timer使用cacheBuffer来缓存更多元素,但我认为没有必要。

如果您想使用ListView,请查看此示例:http://doc.qt.io/qt-5/qtwidgets-itemviews-fetchmore-example.html 使用fetch方法可以在大数据集中保持性能。它允许您在滚动时填充列表。