我使用QListView
来自QAbstractItemModel
的自定义模型。我有数百万件物品。我已调用listView->setUniformItemSizes(true)
以防止在我向模型添加项目时调用一堆布局逻辑。到目前为止,一切都按预期工作。
问题是使用键盘导航列表很慢。如果我在列表中选择一个项目,然后按向上/向下,选择将快速移动直到选择需要滚动列表。然后变得非常迟钝。按向上翻页或向下翻页也非常滞后。问题似乎是当用键盘选择了一个项目(也就是"当前项目")时,列表也会向上/向下滚动。
如果我使用鼠标,导航列表很快。我可以使用快速的鼠标滚轮。我可以按照我想要的速度向上/向下拖动滚动条 - 从列表顶部到底部 - 列表视图快速更新。
关于为什么更改选择和滚动列表的组合如此缓慢的任何想法?有可行的解决方法吗?
为了更好地说明问题,我在此次更新中提供了放大信息。
这主要是性能问题,尽管它确实与用户体验(UX)有关。看看当我使用键盘滚动QListView
:
注意底部附近的减速?这是我的问题的焦点。让我解释一下我如何浏览列表。
解释
我希望列表能够像键盘的打字速度一样快地滚动 - 换句话说,选择下一个项目所需的时间不应该在滚动列表时减慢。
这是我使用鼠标时的样子:
解释
这证明了两个要点:
模型不是问题。正如您所看到的,模型在性能方面没有任何问题。它可以比显示元素更快地传递元素。
选择和滚动时性能下降。"完美风暴"选择和滚动(如使用键盘在列表中导航所示)会导致减速。因此,我推测,在正常执行的滚动期间进行选择时,Qt会以某种方式进行大量处理。
我想指出,我的问题似乎与Qt有关。
在使用不同的框架之前,我已经实现了这种类型的东西。我想要做的是在模型视图理论的范围内。我可以使用juce::ListBoxModel使用juce::ListBox以极快的速度完成我所描述的内容。它很快愚蠢(另外,当每个项目已经有唯一索引时,不需要为每个项目创建重复索引,例如QModelIndex
)。我认为Qt的模型 - 视图架构需要QModelIndex
每个项目,虽然我不喜欢开销成本,但我认为我理性,我可以忍受它。无论哪种方式,我都不会怀疑这些QModelIndex
是导致我的表现减慢的原因。
通过JUCE实现,我甚至可以使用Page-up&用于导航列表的向下翻页键,它只是在列表中闪现。使用Qt QListView
实现,即使使用发布版本,它也会突然出现并且很滞后。
使用JUCE framework的模型视图实现速度非常快。 为什么Qt QListView
实施这样的狗?!
难以想象为什么你在列表视图中需要这么多项目?好吧,我们以前都见过这种事:
这是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)
而不是问题。
QListView
(模型中有数百万项)?QListView
的效果阈值?答案 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,我非常确定这会解决问题(但未经过测试)。但是,如果你有一天隐藏某些项目......例如支持过滤,那么你会发现新的问题!
最有可能正确的解决方法是让视图同时维护flowPositions
和visibleFlowPositions
以避免动态创建...
答案 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}},index
和parent
方法中插入data
的来电。最后,我在对话框中添加WordListModel
,QPushButton
信号链接到调用clicked
的方法。
测试的步骤是下一步:
打印列表显示问题。 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方法可以在大数据集中保持性能。它允许您在滚动时填充列表。