QListView with millions of items slow with keyboard - c++

I'm using a QListView with a custom model derived from QAbstractItemModel. I have on the order of millions of items. I have called listView->setUniformItemSizes(true) to prevent a bunch of layout logic from being called when I'm adding items to the model. So far, everything works as expected.
The problem is that using the keyboard to navigate the list is slow. If I select an item in the list, then press up/down, the selection moves fast until the selection needs to scroll the list. Then it becomes extremely laggy. Pressing page-up or page-down is also very laggy. The problem seems to be when an item is selected (aka the "current item") with the keyboard and the list is also scrolled up/down.
If I use the mouse, navigating the list is fast. I can use the mouse wheel, which is fast. I can drag the scroll bar up/down as fast as I want--from the top of the list to the bottom--and the list view updates wickedly fast.
Any ideas on why the combination of changing selections and scrolling the list is so slow? Is there a viable work-around?
Update 9/9/15
In order to better illustrate the issue, I'm providing amplifying information in this update.
Performance Issues with KEYBOARD + SCROLLING
This is mostly a performance question, although it does tie in with the user experience (UX) somewhat. Check out what happens as I use the keyboard to scroll through a QListView:
Notice the slow-down near the bottom? This is the focal point of my question. Let me explain how I am navigating the list.
Explanation:
Starting at the top, the first item in the list is selected.
Pressing and holding the down arrow key, the current item (selection) is changed to the next item.
Changing selection is fast for all of the items that are currently in view.
As soon as the list needs to bring the next item into view, the selection rate slows down significantly.
I expect that the list should be able to scroll as fast as the typematic rate of my keyboard--in other words, the time it takes to select the next item should not slow down when the list is scrolled.
Fast Scrolling with MOUSE
Here's what it looks like when I use the mouse:
Explanation:
Using the mouse, I select the scroll bar handle.
Quickly dragging the scroll bar handle up and down, the list is scrolled accordingly.
All movements are extremely fast.
Note that no selections are being made.
This proves two main points:
The model is not the problem. As you can see, the model has no problem whatsoever performance-wise. It can deliver the elements faster than they can be displayed.
Performance is degraded when selecting AND scrolling. The "perfect storm" of selecting and scrolling (as illustrated by using the keyboard to navigate through the list) causes the slowdown. As a result, I surmise that Qt is somehow doing a lot of processing when selections are being made during scrolling that aren't normally performed.
Non-Qt Implementation is FAST
I want to point out that my issue seems to be specific to Qt.
I have already implemented this type of thing before using a different framework. What I am trying to do is within the scope of model-view theory. I can do exactly what I am describing at blazing fast speeds using juce::ListBoxModel with a juce::ListBox. It's stupid fast (plus, there's no need to create a duplicate index such as a QModelIndex for every single item when each item already has a unique index). I get that Qt needs a QModelIndex for each item for its model-view architecture, and although I don't like the overhead cost, I think I get the rational and I can live with it. Either way, I don't suspect that these QModelIndexes are what is causing my performance slow-down.
With a JUCE implementation, I can even use the page-up & page-down keys to navigate the list, and it just blazes through the list. Using the Qt QListView implementation, it chugs along and is laggy, even with a release build.
A model-view implementation using the JUCE framework is extremely fast. Why is the Qt QListView implementation such a dog?!
Motivating Example
Is it hard to imagine why you'd need so many items in a list view? Well, we've all seen this kind of thing before:
This is the Visual Studio Help Viewer index. Now, I haven't counted all of the items--but I think we'd agree that there are a lot of them! Of course to make this list "useful," they added a filter box that narrows down what is in the list view according to an input string. There aren't any tricks here. It's all practical, real-world stuff we've all seen for decades in desktop applications.
But are there millions of items? I'm not sure it matters. Even if there were "only" 150k items (which is roughly accurate based on some crude measurements), it's easy to point out that you have to do something to make it useable--which is what a filter will do for you.
My specific example uses 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 that was used to assemble this list. For linguistic study, this is a reasonable use case.
Concerns about improving the UX (user experience) or filtering are great design goals, but they are out of the scope of this question (I'll certainly address them later in the project).
Code
Want a code example? You got it! I'm not sure how useful it will be; it's as vanilla as it gets (about 75% boilerplate), but I suppose it will provide some context. I realize that I'm using a QStringList and that there is a QStringListModel for this, but the QStringList that I'm using to hold the data is a placeholder--the model will eventually be somewhat more complicated, so in the end, I need to use a custom model derived from 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);
}
As noted, I've already reviewed and taken Kuba Ober's advice:
QListView takes too long to update when given 100k items
My question is not a duplicate of that question! In the other question, the OP was asking about loading speed, which as I've noted in my code above, is not a problem due to the call to setUniformItemSizes(true).
Summary Questions
Why is navigating a QListView (with millions of items in the model) using the keyboard so slow when the list is scrolled?
Why does the combination of selecting and scrolling items cause a slow-down?
Are there any implementation details that I am missing, or have I reached a performance threshold for QListView?

1. Why is navigating a QListView (with millions of items in the model)
using the keyboard so slow when the list is scrolled?
Because when you navigate through your list using the keyboard, you enter the internal Qt function QListModeViewBase::perItemScrollToValue, see stack:
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++
And this function does:
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));
}
Where flowPositions contains as many items as your QListView, so this basically iterates through all your items, and this will definitely take a while to process.
2. Why does the combination of selecting and scrolling items cause a slow-down?
Because "selecting and scrolling" makes Qt call QListView::scrollTo (to scroll the view to a specific item) and this is what ends up calling QListModeViewBase::perItemScrollToValue. When you scroll using the scroll bar, the system does not need to ask the view to scroll to a specific item.
3. Are there any implementation details that I am missing, or have I reached a performance threshold for QListView?
I'm afraid you are doing the things right. This is definitely a Qt bug. A bug report must be done to hope having this fixed in later releases. I submitted a Qt bug here.
As this code is internal (private data classes) and not conditionnal to any QListView setting, I see no way to fix it except by modifying and recompiling the Qt source code (but I don't know exactly how, this would require more investigation). The first function overidable in the stack is QListView::scrollTo but I doubt it would be easy to oevrride it without calling QListViewPrivate::verticalScrollToValue...
Note: The fact that this function goes through all items of the view was apparently introduced in Qt 4.8.3 when this bug was fixed (see changes). Basically, if you don't hide any items in your view, you could modify Qt code as below:
/*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;
Then you'll have to recompile Qt and I'm pretty sure this will fix the issue (not tested however). But then you'll see new problems if you one day hide some items...to support filtering for instance!
Most likely the right fix would have been to have the view maintain both flowPositions and visibleFlowPositions to avoid creating it on the fly...

I have made the following test:
First of all i create a class to check in the calls:
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;
Then I insert a call to NewCall in index, parent and data methods of WordListModel. Finally i add a QPushButton in the dialog, the clicked signal is linked to a method which call to Test::Summary.
The steps of the test are the next:
Select the last showed item of the list
Press the Summary button to clear the calling list
With tab key select the list view again
Perform a scroll with the direction keys
Press Summary button again
The printed list shows the problem. QListView widget makes a big number of calls. It seems the widget is reloading all the data from the model.
I don't know if it can be improved but you can't do anything but filter the list to limit the number of items to show.

Unfortunately, I believe that you can't do much about this.
We don't have much control over widgets.
Although you can avoid that issue by using ListView instead.
If you try my quick example below you'll notice how fast it can be even using delegates which is costly.
Here is the example:
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; }
}
}
}
I put a Timer to simulate the scrolling, but of course you can turn on or off that timer depending on whether keys are pressed as well as changing i += 1 by i += -1 if ▲ is pressed instead of ▼. You'd have to add overflow and underflow checks too.
You can also choose the scrolling speed by changing interval of Timer. Then it's just a matter of modifying the selected element's color etc. to show it's selected.
On top of which you can use cacheBuffer with ListView to cache more elements but I don't think it is necessary.
If you want to use QListView anyway take a look at this example: http://doc.qt.io/qt-5/qtwidgets-itemviews-fetchmore-example.html
Using the fetch method allow to keep performance even with big datasets. It allows you to fill the list as you scroll.

Related

QTableView, glitch on showing hidden rows

I'm not sure either it's some kind of bug\glitch or I'm doing something wrong.
I have some hidden rows in the table and on click I want them to appear but result you can see on the picture below.
This effect occurs only when slider is scrolled till the end. Glitch disappears if I change focus to another window or when I scroll table up until it goes out of view.
Here is my code:
connect(tableView_, SIGNAL(clicked(const QModelIndex &)), this,
SLOT(onTableClicked(const QModelIndex &)));
for(int i = 0; i < table_->rowCount(); i++)
{
if(i%table_->typesNumber())
tableView_->hideRow(i);
}
...
void DumpsComparisonWindow::onTableClicked(const QModelIndex& index)
{
...
tableView_->showRow(index.row() + i);
...
}
UPDATE: to make it possible to test I have created cut version of my project, you can find it here: https://gitlab.com/JuicyPussy/qtableview_glitch
After showing or hiding any rows make the model notify about the change in the layout to all views. You do that by calling layoutChanged.
void DumpsComparisonWindow::onTableClicked(const QModelIndex& index)
{
...
tableView_->showRow(index.row() + i);
...
table_->layoutChanged(); //add this
}

Stop QTextCursor::insertText() from modifying QTextDocument scrollbar range

I have a QTextEdit that contains a QTextDocument, which is being programatically edited using the QTextCursor interface. The document is being edited with QTextCursor::insertText().
I load the text file being edited in chunks, so the initial size of the QTextDocument might only be 20 lines even though the document is 100,000 lines. However, I want the QTextEdit scrollbar to reflect the full size of the document instead of just the 20 line document it's currently displaying.
The QTextEdit's scrollbar range is set with QScrollBar::setMaximum() which adjusts the scrollbar to the proper size on the initial opening of the file, but when QTextCursor::insertText() is called the QScrollBar's range is recalculated.
I've already tried calling QScrollBar::setMaximum() after each QTextCursor::insertText() event, but it just makes the whole UI jerky and sloppy.
Is there any way to keep the range of the QScrollBar while the QTextDocument is being modified?
Yes. You'd depend on the implementation detail. In QTextEditPrivate::init(), the following connection is made:
Q_Q(QTextEdit);
control = new QTextEditControl(q);
...
QObject::connect(control, SIGNAL(documentSizeChanged(QSizeF)), q, SLOT(_q_adjustScrollbars()))
Here, q is of the type QTextEdit* and is the Q-pointer to the API object. Thus, you'd need to disconnect this connection, and manage the scroll bars on your own:
bool isBaseOf(const QByteArray &className, const QMetaObject *mo) {
while (mo) {
if (mo->className() == className)
return true;
mo = mo->superClass();
}
return false;
}
bool setScrollbarAdjustmentsEnabled(QTextEdit *ed, bool enable) {
QObject *control = {};
for (auto *ctl : ed->children()) {
if (isBaseOf("QWidgetTextControl", ctl->metaObject()) {
Q_ASSERT(!control);
control = ctl;
}
}
if (!control)
return false;
if (enable)
return QObject::connect(control, SIGNAL(documentSizeChanged(QSizeF)), ed, SLOT(_q_adjustScrollbars()), Qt::UniqueConnection);
else
return QObject::disconnect(control, SIGNAL(documentSizeChanged(QSizeF)), ed, SLOT(_q_adjustScrollbars()));
}
Hopefully, this should be enough to prevent QTextEdit from interfering with you.

QListView: how to automatically scroll the view and keep current selection on correct items in view when removing items from the top

I have a list view with a custom model. The model allows me to add text to the bottom of the list (using 'addText(const QString&)') and to remove items from the top of the list (using 'removeItemsFromTop(int _iCount)').
What is the best way to add text to the view and keep the model size under some maximum (lets say 'MAX_LIST_SIZE'), while always maintaining the view (i.e. current selection and items in view should not change when items are removed).
The solution should preferably be a function that I can use wherever I'm using my custom model.
I have looked at indexAt(...), scrollTo(...), currentIndex(...) and setCurrentIndex(...) methods on QListView, but I can't figure out how to put all of this together.
So far I have (for auto scrolling the view)
// add items here ...
// cleanup
QModelIndex indexViewTop = listView->indexAt(QPoint(8, 8));
if (listModel->rowCount() > MAX_SIZE)
{
int iRemoveCount = (listModel->rowCount() - MAX_SIZE) + MAX_SIZE/10;
listModel->clearTextFromFront(iRemoveCount);
listView->scrollTo(indexViewTop.sibling(indexViewTop.row() - iRemoveCount, 0), QAbstractItemView::PositionAtTop);
}
This is supposed to scroll the list view as items are removed to keep the view consistent, but indexAt(...) always returns an invalid index.
For keeping the selection consistent I tried:
// add items her ...
// cleanup
if (listModel->rowCount() > MAX_SIZE)
{
int iCurrentViewIndex = listView->currentIndex().row();
int iRemoveCount = (listModel->rowCount() - MAX_SIZE) + MAX_SIZE/10;
listModel->clearTextFromFront(iRemoveCount);
listView->setCurrentIndex(listModel->index(iCurrentViewIndex - iRemoveCount, 0));
}
This seems to work, but I'm still stuck on the auto scrolling.
I did a queue-like table model implementation. I think it is similar for QAbstractItemModel. Best way would be to use QQueue to store data.
Now, this is a snipped for QAbstractTableModel (which is subclass of QAbstractItemModel so it should work; mEvents is QQueue):
// custom table for inserting events
void EventPreviewTableModel::insertEvent(const DeviceEvent &event) {
beginInsertRows(QModelIndex(), 0, 0);
mEvents.enqueue(event);
endInsertRows();
if (mEvents.size() > SIZE) {
beginRemoveRows(QModelIndex(), mEvents.size(), mEvents.size());
mEvents.dequeue();
endRemoveRows();
}
}
And also override data() and rowCount() to serve correct data.
For second part using ItemIsSelected flag for items you want to have selected is done through: Qt::ItemFlags QAbstractItemModel::flags(const QModelIndex & index)
This is my current approach and it seems to work well:
void addTitlesToList(Model *model, QListView *view, std::vector<Object*> &items)
{
QScrollBar *pVerticalScrollBar = view->verticalScrollBar();
bool bScrolledToBottom = pVerticalScrollBar->value() == pVerticalScrollBar->maximum();
QModelIndex indexViewTop = view->indexAt(QPoint(8, 8));
// add to model
model->pushItems(items);
// cleanup if model gets too big
if (model->rowCount() > model->maxListSize())
{
int iCurrentViewIndex = view->currentIndex().row();
int iRemoveCount = (int)(model->rowCount() - model->maxListSize()) + (int)model->maxListSize()/10;
model->removeItemsFromFront(iRemoveCount);
// scrolls to maintain view on items
if (bScrolledToBottom == false)
{
_pView->scrollTo(indexViewTop.sibling(indexViewTop.row() - iRemoveCount, 0), QAbstractItemView::PositionAtTop);
}
// maintain selection
if (iCurrentViewIndex >= iRemoveCount)
{
view->setCurrentIndex(_pModel->index(iCurrentViewIndex - iRemoveCount, 0));
}
else
{
view->setCurrentIndex(QModelIndex());
}
}
// move scroll bar to keep new items in view (if scrolled to the bottom)
if (bScrolledToBottom == true)
{
view->scrollToBottom();
}
}
One issue I had with indexAt(QPoint(...)) is that I was calling it after adding the items to the list, which seems to cause it to always return an invalid index. Calling indexAt before anything is added to the model seems to work.
I also added automatic 'scroll to bottom' if already there (i.e. the view either stays fixed on specific items or sticks to the latest items if scrolled all the way to the bottom).

QSortFilterProxyModel and lazily populated treeviews

I have implemented a lazily populated treeview by subclassing QAbstractItemModel. The implementation looks something like:
https://gist.github.com/gnufied/db9c4d805e2bb24d8c23
(I am not pasting code inline, so as to mess with messaging)
It is basically a tree representation of hierarchical data stored in table. Now, I want users to be able to sort the rows based on columns. Where columns are, "count" or "reference count". These values are basically integers.
The implementation on its own works, until I throw in QSortFilterProxyModel and I start to get lots of empty rows in the view. The hard problem is, this tends to happen only when I have lots of rows (like thousands or so).
The code for implementing sorting proxy is:
rootItem = RBKit::SqlConnectionPool::getInstance()->rootOfSnapshot(snapShotVersion);
model = new RBKit::HeapDataModel(rootItem, this);
proxyModel = new SortObjectProxyModel(this);
proxyModel->setSourceModel(model);
ui->treeView->setModel(proxyModel);
ui->treeView->setSortingEnabled(true);
I have subclassed QSortFilterProxyModel class and subclass implementation is really simple:
https://gist.github.com/gnufied/115f1a4fae3538534511
The documentation does say -
"This simple proxying mechanism may need to be overridden for source models with more complex behavior; for example, if the source model provides a custom hasChildren() implementation, you should also provide one in the proxy model."
But beyond that, I am not sure - what I am missing.
So, I think I have found the solution and the fix appears to be reimplementing fetchMore in proxy model subclass, because inserted rows reported by source model, do not match place where rows actually exist in view (Rows in view are owned by proxy model), so this seems to have fixed it for me:
#include "sortobjectproxymodel.h"
SortObjectProxyModel::SortObjectProxyModel(QObject *parent) :
QSortFilterProxyModel(parent)
{
}
bool SortObjectProxyModel::hasChildren(const QModelIndex &parent) const
{
const QModelIndex sourceIndex = mapToSource(parent);
return sourceModel()->hasChildren(sourceIndex);
}
int SortObjectProxyModel::rowCount(const QModelIndex &parent) const
{
const QModelIndex sourceIndex = mapToSource(parent);
return sourceModel()->rowCount(sourceIndex);
}
bool SortObjectProxyModel::canFetchMore(const QModelIndex &parent) const
{
if(!parent.isValid())
return true;
else {
const QModelIndex sourceIndex = mapToSource(parent);
return sourceModel()->canFetchMore(sourceIndex);
}
}
void SortObjectProxyModel::fetchMore(const QModelIndex &parent)
{
if (parent.isValid() && parent.column() == 0) {
int row = parent.row();
int startRow = row + 1 ;
const QModelIndex sourceIndex = mapToSource(parent);
RBKit::HeapItem *item = static_cast<RBKit::HeapItem *>(sourceIndex.internalPointer());
if (!item->childrenFetched) {
qDebug() << "Insert New Rows at :" << startRow << " ending at : " << startRow + item->childrenCount();
beginInsertRows(parent, startRow, startRow + item->childrenCount());
item->fetchChildren();
endInsertRows();
}
}
}
Thank you for responding. At this point I really don't care about resorting rows that were lazy loaded (when a node was expanded), so I went ahead and disabled sortingEnabled and disabled dynamicSortFiltertoo.
The new code looks like:
rootItem = RBKit::SqlConnectionPool::getInstance()->rootOfSnapshot(snapShotVersion);
model = new RBKit::HeapDataModel(rootItem, this);
proxyModel = new SortObjectProxyModel(this);
proxyModel->setSourceModel(model);
proxyModel->sort(2, Qt::DescendingOrder);
proxyModel->setDynamicSortFilter(false);
ui->treeView->setModel(proxyModel);
That still leaves with empty rows though.
In my oppinion You don't need to subclass QSortFilterProxyModel for sorting on the top layer. If sortingEnabled == true for your view then the view will perform sorting on the proxy model, which is not desirable as the model should sort itself. What you need is to to call proxyModel->sort(desiredColumn) and that will display your model sorted in the view without altering your data. By default the QSortFilterProxyModel has its dynamicSortFilter property on, which will cause the proxy model to automatically re-sort when data changes or row is inserted or removed. I didn't see emitting dataChanged signal anywhere in your HeapDataModel, so maybe that could be a hint for you to get dynamically sorted rows. If you need to sort subitems then it goes little more complicated and then maybe you'll need to subclasss QSortFilterProxyModel. These model-view abstractions are hard to learn but once you get it you can do miracles rapidly.
rootItem = RBKit::SqlConnectionPool::getInstance()->rootOfSnapshot(snapShotVersion);
model = new RBKit::HeapDataModel(rootItem, this);
proxyModel = new SortObjectProxyModel(this);
proxyModel->setSourceModel(model);
proxyModel->sort(column);
ui->treeView->setModel(proxyModel);

Restore original order in QTableView/QSortFilterProxy

I have a QTableView with QSortFilterProxyModel between the view and the model (QStandardItemModel). The problem is when I call sort() I'm unable to restore original order of lines in a table. I was trying to acheve that by changing model proxy to QIdentityProxy on-the-fly but to no avail, as the only change is that lines are renumbered but order is kept sorted.
Is it possible to somehow "unsort" data? I think, that code is unnecessary in this case, but will post if asked.
I'm using Qt5 on Win x64
P.S.: The same problem was posted here back in 2009 but never was answered.
To restore initial unsorted state ( tested )
sortModel->setSortRole(Qt::InitialSortOrderRole);
sortModel->invalidate();
QSortFilterProxyModel::​setSortRole(int role)
The point is to sort manually deciding between sort by column -1 (restore) and normal column number, and intercept communication between QHeaderView and QSortFilterProxyModel somehow.
So, using some insight from #vahancho's answer, I've managed to implement sorting like this:
class ProxyModel : public QSortFilterProxyModel
{
Q_OBJECT
public:
ProxyModel(QObject* parent = 0);
signals:
void askOrder(int column, Qt::SortOrder order);
public slots:
//! override of automatically called function
void sort(int column, Qt::SortOrder order)
{
emit askOrder(column, order);
}
//! real sorting happens here
void doSort(int column, Qt::SortOrder order)
{
QSortFilterProxyModel::sort(column, order);
}
};
and on parent's side I made proper connection and checks:
ResultsTable::ResultsTable(QWidget *parent) : QTableView(parent)
{
/*...*/
p_Header = new QHeaderView(this);
p_Sort = new ProxyModel(this);
connect(this, &ResultsTable::doSort, p_Sort, &ProxyModel::doSort);
connect(p_Sort, &ProxyModel::askOrder, this, &ResultsTable::setSorting);
/*...*/
setSortingEnabled(true);
}
void ResultsTable::setSorting(int column, Qt::SortOrder order)
{
if (p_Header->sortIndicatorOrder() == Qt::AscendingOrder && p_Header->isSortIndicatorShown() && m_PreviousSort == column)
{
p_Header->setSortIndicator(column, Qt::DescendingOrder);
p_Header->setSortIndicatorShown(false);
column = -1;
}
else
{
p_Header->setSortIndicatorShown(true);
}
m_PreviousSort = column;
emit doSort(column, order);
}
this way I can use automatic sorting treating done by QTableView when sortingEnabled is true. I've tried to research what happens inside of Qt when table header is clicked to induce sorting, but failed, so stopped with this solution.
I'm still unsure if it's right that this way QTableView is responsible for setting correct sort indication, and not QHeaderView itself (as I thought this functionality should to belong to the header).
My understanding is that you need to return your sorting to its default state? What if you override the QSortFilterProxyModel::lessThan() function in the way that it returns the default value when you want to reset sorting, i.e.:
return QSortFilterProxyModel::lessThan(left, right);
,and custom sorting results when sorting "enabled"? I think you will also need to reset your proxy model to its original state with QAbstractItemModel::reset(). However, it will repopulate the whole model data and you will lost selection information.
I like to use top-left corner button to restore order (that is, sort by row number to which that button is the header). This works with standard classes, in pyqt 5.9:
def __init__(self):
#...
tb = self.tableView # has sorting enabled and SortIndicator shown
proxy = QSortFilterProxyModel(self)
proxy.setSourceModel(self.model)
tb.setModel(proxy)
btn = tb.findChild(QAbstractButton)
if btn:
btn.disconnect()
btn.clicked.connect(self.disableSorting)
tb.horizontalHeader().setSortIndicator(-1, 0)
def disableSorting(self):
self.tableView.model().sort(-1)
self.tableView.horizontalHeader().setSortIndicator(-1, 0)
This is what worked best for me:
QSortFilterProxyModel *proxyModel = myTableView->model ();
proxyModel->sort (-1);
Note that, this won't update the header indicator of a TableView, so before calling the code above, I'm also calling:
myTableView->horizontalHeader ()->setSortIndicator (0, Qt::DescendingOrder);
I don't really like it, but I haven't found a way of using the TableView or HorizontalHeader to reset the QSortFilterProxyModel sorting.
This is the full code:
// Block QHeaderView signals so sorting doesn't happen twice
myTableView->horizontalHeader ()->blockSignals (true);
// Update the sort indicator to be the same as it was when the TableView was created
myTableView->horizontalHeader ()->setSortIndicator (0, Qt::DescendingOrder);
// Reset sorting
QSortFilterProxyModel *proxyModel = myTableView->model ();
proxyModel->sort (-1);
// Unblock QHeaderView signals
myTableView->horizontalHeader ()->blockSignals (false);
NOTE: I'm blocking the horizontal header signals temporarily to prevent QSortFilterProxyModel sort from executing twice.
From the docs:
QSortFilterProxyModel can be sorted by column -1, in which case it returns to the sort order of the underlying source model.
So, as #eTHP says, sort(-1) does the trick!