具有数百万个项目的 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
:
时会发生什么
注意到底部附近的减速了吗?这是我问题的重点。让我解释一下我是如何浏览列表的。
解释:
- 从顶部开始,列表中的第一项是 selected。
- 按下并按住向下箭头键,当前项目(selection)更改为下一个项目。
- 更改 selection 对于当前可见的所有项目都很快。
- 只要列表需要显示下一项,selection rate 就会显着减慢。
我希望列表的滚动速度应该与我的键盘输入速度一样快——换句话说,select下一个项目所花费的时间不应在列表滚动时变慢已滚动。
使用鼠标快速滚动
这是我使用鼠标时的样子:
解释:
- 使用鼠标,我 select 滚动条手柄。
- 上下快速拖动滚动条手柄,列表相应滚动。
- 所有动作都极快
- 请注意,没有select生成离子。
这主要证明了两点:
模型不是问题。如您所见,模型在性能方面没有任何问题。它可以比显示元素更快地传送元素。
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)
.
这不是问题
总结性问题
- 为什么滚动列表时使用键盘导航
QListView
(模型中有数百万个项目)这么慢?
- 为什么 select 浏览和滚动 项的组合会导致速度变慢?
- 我是否遗漏了任何实施细节,或者我是否达到了
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;
然后我在 WordListModel
的 index
、parent
和 data
方法中插入对 NewCall
的调用。最后,我在对话框中添加了一个 QPushButton
,clicked
信号链接到一个调用 Test::Summary
.
的方法
测试步骤如下:
- Select 列表中最后显示的项目
- 按摘要按钮清除通话列表
- 再次用tab键select列表查看
- 使用方向键滚动
- 再次按摘要按钮
打印的列表显示了问题。 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 ▲被按下而不是▼。您还必须添加溢出和下溢检查。
您还可以通过更改 Timer
的 interval
来选择滚动速度。然后只需修改所选元素的颜色等即可显示它已被选中。
在此基础上,您可以使用 cacheBuffer
和 ListView
来缓存更多元素,但我认为没有必要。
如果你想使用 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,我很确定这将解决问题(但未经过测试)。但是如果有一天你隐藏了一些项目......例如支持过滤,你会看到新的问题!
最有可能的正确解决方法是让视图同时维护 flowPositions
和 visibleFlowPositions
以避免动态创建它...
我正在使用 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
:
注意到底部附近的减速了吗?这是我问题的重点。让我解释一下我是如何浏览列表的。
解释:
- 从顶部开始,列表中的第一项是 selected。
- 按下并按住向下箭头键,当前项目(selection)更改为下一个项目。
- 更改 selection 对于当前可见的所有项目都很快。
- 只要列表需要显示下一项,selection rate 就会显着减慢。
我希望列表的滚动速度应该与我的键盘输入速度一样快——换句话说,select下一个项目所花费的时间不应在列表滚动时变慢已滚动。
使用鼠标快速滚动
这是我使用鼠标时的样子:
解释:
- 使用鼠标,我 select 滚动条手柄。
- 上下快速拖动滚动条手柄,列表相应滚动。
- 所有动作都极快
- 请注意,没有select生成离子。
这主要证明了两点:
模型不是问题。如您所见,模型在性能方面没有任何问题。它可以比显示元素更快地传送元素。
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)
.
总结性问题
- 为什么滚动列表时使用键盘导航
QListView
(模型中有数百万个项目)这么慢? - 为什么 select 浏览和滚动 项的组合会导致速度变慢?
- 我是否遗漏了任何实施细节,或者我是否达到了
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;
然后我在 WordListModel
的 index
、parent
和 data
方法中插入对 NewCall
的调用。最后,我在对话框中添加了一个 QPushButton
,clicked
信号链接到一个调用 Test::Summary
.
测试步骤如下:
- Select 列表中最后显示的项目
- 按摘要按钮清除通话列表
- 再次用tab键select列表查看
- 使用方向键滚动
- 再次按摘要按钮
打印的列表显示了问题。 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 ▲被按下而不是▼。您还必须添加溢出和下溢检查。
您还可以通过更改 Timer
的 interval
来选择滚动速度。然后只需修改所选元素的颜色等即可显示它已被选中。
在此基础上,您可以使用 cacheBuffer
和 ListView
来缓存更多元素,但我认为没有必要。
如果你想使用 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,我很确定这将解决问题(但未经过测试)。但是如果有一天你隐藏了一些项目......例如支持过滤,你会看到新的问题!
最有可能的正确解决方法是让视图同时维护 flowPositions
和 visibleFlowPositions
以避免动态创建它...