6

Goal: To have a scrollable list of custom widgets amounting hunderts of thousands (and possibly more) in a Qt5 C++ application under Windows 7, 10.

Problem: The program stops responding after minimizing the window to the task bar and restoring it again. It doesn't crash though. The CPU usage constants 25%. The GUI doesn't become responsive again even after several minutes of waiting. Furthermore, a large amount of memory is being consumed in general (more than 200M), which I think is too much even for 100k QLabels (aprox 2k per QLabel).

Here are some suggested solutions to a similar problem, which I don't find suitable for my case.

Example: The following example illustrates the problem. For the sake of the demonstration a list of QLabels is used, but it could be any class derived from QWidget.

MainWindow.h

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include <QMainWindow>
#include <QScrollArea>
#include <QVBoxLayout>
#include <QLabel>

class MainWindow : public QMainWindow
{
    Q_OBJECT
public:
    explicit MainWindow(QWidget *parent = 0);
};

#endif // MAINWINDOW_H

MainWindow.cpp

#include "MainWindow.h"

MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent)
{
    QScrollArea *scrollArea = new QScrollArea(this);
    QFrame *frame = new QFrame();
    QVBoxLayout *l = new QVBoxLayout(frame);
    int N = 121004;

    scrollArea->setWidget(frame);
    scrollArea->setWidgetResizable(true);

    for (int n = 0; n < N; n++) { l->addWidget(new QLabel(QString::number(n), this)); }

    resize(640, 480);
    setCentralWidget(scrollArea);
}
scopchanov
  • 7,966
  • 10
  • 40
  • 68

2 Answers2

12

I have some bad news and some good news:

The bad news: You cannot do this with Qt Widgets directly.

The good news: There's a way to do it, that's independent of the number of items you have in the list (even billions), but you need to give yourself the time to learn how to do this.

So, first thing: QScrollArea is SO not the way to do this. The correct way to do this is using the Model/View/Controller programming paradigm. The data model that has the information to be displayed has to be completely separated from the view, so that Qt could only worry about displaying the items the user is trying to view. Think about it: If you have a billion elements to put in that list, does that mean the user has to see them all at once? Does that mean Qt has to render them all? In your code, that's what you're asking Qt to do. Are you surprised it's slow?

Advice Nr. 1: Read how Qt manages Model/View programming, then choose the correct viewing tool. I suggest QListView for what you described. QTableView will make things easier for you if you can put things in a table.

The control on the list is done through delegates. A delegate is the class responsible for drawing widgets in the view. The default will just do text vs icons.

Advice Nr. 2: Forget about creating Qt widgets for every element. I just finished answering another guy's question on why this won't work, even when using delegates. Take a look at Qt Torrent Example to see how controls are drawn there.

What you can do is draw controls, not widgets. This is because every widget you create has to go to the main event loop in Qt, which will make your program slow (and you experienced that already). If you loop from one to million just to add numbers it'll take a significant amount of time. Do you really want Qt's event loop to loop over all your widgets to process every one of them?

Advice Nr. 3: Start simple! You seem to have a lot to do. Start with model/view, then add a delegate that will paint a custom control, then expand it. Give yourself the time to learn all this.

Good luck!

The Quantum Physicist
  • 24,987
  • 19
  • 103
  • 189
  • Did you check the example I've provided? It doesn't take a lot of time to create the widgets and render them on screen. It is definitely slower than handling a couple of elements, but it is done for a reasonable time. The problem occurs when the windows is minimized to the task bar and restored again. THEN and only THEN it takes forever (literally) to repaint the application window. – scopchanov Jul 29 '17 at 23:38
  • Furthermore, what about having a button in all items displayed by the QListView? – scopchanov Jul 29 '17 at 23:54
  • @scopchanov Yes, I saw the code you have up there. You have to understand that there's the right way of doing things and there's a million other wrong ways. Qt, as a library, and as a product, solves every problem in a certain way. If you hack something up together and it works, then good for you! but you can't expect that you render a 100k elements and then complain about it being slow. Again, you're forcing your Qt to do something it's not meant to do. Even if it works for this version or computer, or this OS, it might not work for others. [...] – The Quantum Physicist Jul 30 '17 at 07:34
  • @scopchanov Now about drawing a button, a button is considered a "complex control". Instead of using `drawControl`, like shown in the torrent example, you use [`drawComplexControl`](http://doc.qt.io/qt-5/qstylepainter.html#drawComplexControl), which handles another list of more complex controls (combobox, buttons, etc...). You can see these [here](http://doc.qt.io/qt-5/qstyle.html#ComplexControl-enum) – The Quantum Physicist Jul 30 '17 at 07:38
  • I don't see how performing a custom painting is considered less hack-ish than using the stock widgets to compose custom ones. Keep in mind that the code example is oversimplified just to show the particular issue. The real functionality of each widget in the list consists of signals being emited and animations being played. I am not sure if this could be achieved using QListView and subclassing the QStyledItemDelegate. Anyway, I still don't have an answer why the given code renders the window relatively fast on creation and can't render it after the window is restored back from minimization. – scopchanov Jul 30 '17 at 08:46
  • @scopchanov When you create widgets, you're not only painting. You can literally just paint by creating your own delegate and overriding the `paint()` method. Then, you can create pixmaps of your widgets and paint them. That might work, but there are other issues with it that I can't discuss right here. And whether you can do that with QListView, I don't see why not. I think even the list view is better, because you can literally paint with a delegate there. Check the [Star Delegate Example from Qt](http://doc.qt.io/qt-5/qtwidgets-itemviews-stardelegate-example.html) – The Quantum Physicist Jul 30 '17 at 08:56
  • @scopchanov It's a misunderstanding to think that u're only painting there. You're also adding these controls to the event loop. My guess for that being slow is that when u restore your window that the event loop gets flooded. There's a painful way to verify this, which is to use Qt in debug mode (you have to recompile Qt yourself), and check the event loop in QCoreApplication and see what's happening in there. – The Quantum Physicist Jul 30 '17 at 08:56
  • Let us [continue this discussion in chat](http://chat.stackoverflow.com/rooms/150517/discussion-between-scopchanov-and-the-quantum-physicist). – scopchanov Jul 30 '17 at 09:19
  • After spending a couple of days carefully crafting the customized delegate I've bumped into another issue - drawing text with _QPainter_ is noticably slower than drawing shapes and pixmaps. A bug is reported in that regard: https://www.qtforum.de/viewtopic.php?t=17722, however in my case it is slow even if I don't use `QPainter::setFont()`. – scopchanov Aug 03 '17 at 11:58
  • @scopchanov Interesting... well at least we can hope that this problem will be fixed in the future. On my side, I never noticed a difference. Perhaps my application doesn't require much performance. – The Quantum Physicist Aug 03 '17 at 12:02
  • I've commented out everything in the paint method and put only this: `painter->drawText(10, option.rect.y()+ 22, index.data(Qt::DisplayRole).toString());`. The effect is the same. In contrast, examples like `painter->fillRect(option.rect, Qt::green);` and `painter->drawLine(0, option.rect.y(), option.rect.width(), option.rect.y());` perform as they should. – scopchanov Aug 03 '17 at 12:11
  • Because this goes beyound the scope of my original question, I will open another topic on this. I will also share the code of the delegate I've created. It could be slower than it should, but it could be still useful to someone. – scopchanov Aug 03 '17 at 12:14
  • 1
    https://stackoverflow.com/questions/45485492/performance-issues-when-painting-text-on-custom-item-delegates-in-qt-c – scopchanov Aug 03 '17 at 13:25
  • I will make a separate post on that, but here is a link to the project: https://github.com/scopchanov/CustomList. – scopchanov Aug 03 '17 at 16:32
2

I have implemented a list widget that can show billions of items with an arbitrary number of widgets per item without any performance issues. Unfortunately, I can not share the code.

It is implemented on top of QAbstractScrollArea, and does not use Qt's model/view framework. It only deals with keeping track of the range of items that are in view, calling the appropriate draw function on those items, and keeping track of the overall height of all items combined. That's it.

Every item may have one associated widget. This widget may be arbitrarily complex. Item widgets are made visible when the item is in view. In my implementation, items usually lazily create the widgets, which is one of the reasons why this is fast. In case you have a billion items for example, only a very small subset of those will ever be in view, so it would be a waste to spend any effort in constructing widgets for these items.

Since every item is responsible for the way it looks, this gives a lot of flexibility in terms of what is possible to display with such a very generic list widget.

Ton van den Heuvel
  • 10,157
  • 6
  • 43
  • 82
  • Subclassing `QAbstractScrollArea` might be a valid solution, however I still prefer the model-view approach. – scopchanov Aug 20 '21 at 22:58