5

Let me introduce first the problem. I will present a simplification of my QT6 project app (in C++) to not make it too difficult to read, as the original code has lots of unnecesary things for this problem in particular, so maybe something is missing but I tried to put everything needed here.

I have a an abstract class, let us call it Event

class EventList; // "Shortcut" for std::vector<Event*> 
class Event
{
protected: 
  uint16_t _code;
  std::shared_ptr<std::vector<EventList*>> _children_lists;
  // More things...
public:
  // More things...
  static Event* create(VALUE obj);
  inline virtual uint16_t code() const { return _code; }
};

that represents an Event, that can be of very different types depending on some parameter of the argument VALUE obj of the create method, each one of this being a derived class, e.g.:

class Event_1 : public Event
{
public: 
  Event_1(...);
  static Event_1* create(VALUE obj);
  inline uint16_t code() const override { return 1; }
  // More things...
}

Now, I want to create a QT6 Widget to represent each of this Event classes in a GUI. Key things to have in mind:

  • Each Event may have 0 or any number of childs of any type associated to it, and I want to display the children widgets "inside" the parent widget (like a tree structure) as this example:
    • Event #1 (Type 1)
      • Child #1.1 of Event #1 (Type 1)
        • Child #1.1.1 of Child #1.1 (Type 2)
        • Child #1.1.2 of Child #1.1 (Type 3)
      • Child #1.2 of Event #1 (Type 2)
      • etc
  • Each derived class Event_X should have a different widget (Widget_X)

My approach is to have an abstract Widget class "equivalent" to the Event one and a subclass for each Ecvent_X that will be Widget_X

class WidgetViewer; // Stores and draws a vector of widgets, used for children. 
class Widget : public QWidget
{
  Q_OBJECT;
public:
  Widget(QWidget* parent);
public slots:
  virtual void setEvent(Event*) = 0;
  // More things...
protected:
  WidgetViewer * addChildren(EventList* list);
  virtual uint16_t code() const = 0; // Similar to the prev. one
};

class Widget_1 : public Widget
{
  Widget_1(QWidget* parent); // Constructor that will call Widget::Widget(parent)
  void setEvent(Event* ev) override 
  {
    Event_1* event = dynamic_cast<Event_1*>(ev);
    // Do things
  }
  // More things...
};

This classes constructors typically initializate one or more smaller widgets, like labels, this is an example of one of them:

Widget_1::Widget_1(QWidget *parent)
    : WidgetViewer(parent),
    label(new QLabel(this))
{
// Custom functions to customize a header widget every Widget_X shares.
    setHeaderText("Show Text"); 
    setHeaderColor("#b6d7a8");
    setBackgroundColor("#d9ead3");
    setBorderColor("#a2be96");

    label->setWordWrap(true);
    contents->layout()->addWidget(label);
}

Then, I have a factory method to create the correct instance of one of the Widget_X from the Event class based on code,

class WidgetFactory
{
public:
    static Widget* createWidget(WidgetViewer* parent, Event* event);
  // More things...
};

The class WidgetViewer is the responsable for creating the Widgets itself, using this function:

Widget* WidgetViewer::createWidget(Event* event)
{
    QElapsedTimer timer;
    timer.start();
    Widget* widget = WidgetFactory::createWidget(this, event);
    qDebug() << "Took " << timer.elapsed() << "ms to create event widget for event " << event->code();

    timer.restart();
    widget->setEvent(event);
    qDebug() << "Took " << timer.elapsed() << "ms to set event event for event widget " << _id;

    return widget;
}

And for each Event in a EventList, this process in done recursively for each children. It is not visible in this 'simplified' version, but most of the implementations of Widget_X::setEvent pure abstract function call the following function over children of Widget_X:

void WidgetViewer::setList(WidgetList* _list)
{
  for(auto it = list->begin(); it < list->end(); it++)
  {
      Event* event = (*it);
      if(event == nullptr)
        continue;
      widgets.push_back(createWidget(event));
      ui->verticalLayout->addWidget(widgets.last());
  }
}

creating therefore the recursion.

This is my approach to do it. The problem is that this lists can be very long, and creating a new Widget class each time takes so much time than expected (sometimes up to 2-3 secs, in which my app is stopped). I have tried some things to speed it up, but I would like to know if there is a better way, as the result is not very satisfactory.


The first attempt I tried to solve the slowness issue was to use multithreading, however, since GUI operations cannot be made on other threads than the main one, I dind't win anything but problems inherited by the threading stuff.

My second attempt was (and is my current best) to create a WidgetPool so that there are Widget_X of any X type already stored in memory, so there is no need of creating them via constructor, what takes a long time (up to 10ms for some of them). I did this by creating a new class,

class WidgetPool
{
public:
    static WidgetPool& instance()
    {
        static WidgetPool instance;
        return instance;
    }

    Widget* getWidget(WidgetViewer* parent, Event* event); // Gets a widget from the pool, if there is no one, uses the WidgetFactory to create one.
    void returnWidget(Widget* widget); // Releases a widget to the pool again

private:
    WidgetPool();
    ~WidgetPool();
    WidgetPool(const WidgetPool&) = delete;
    WidgetPool& operator=(const WidgetPool&) = delete;

    QMap<uint16_t, QVector<Widget*>> widget_pool;
};

and modifying WidgetViewer::createWidget as follows:

Widget* WidgetViewer::createWidget(Event* event)
{
    QElapsedTimer timer;
    timer.start();
    Widget* widget = WidgetPool::instance().getWidget(this, event);
    qDebug() << "Took " << timer.elapsed() << "ms to create command widget for command " << event->code();

    timer.restart();
    widget->setEvent(event);
    qDebug() << "Took " << timer.elapsed() << "ms to set event command for command widget " << _id;

    return widget;
}

In this way, the creation of the WidgetViwer is faster, but the RAM usage is significantly bigger (from 400-500 MB to roughly 1 GB), and it is not that faster, there is still some time before it fully loades big EventLists.

Another solution I have thought of is to have stored in memory "patterns" for each Widget_X and then copy them instead of needing to use new Widget_X, to then just call setEvent as above, but I don't know if this will be any better than just creating a fully new instance with new.

So, if anyone knows/can think of a better approach than these ones, or how to improve one of these, it is greatly appreciated.

Thanks.

9
  • There's a lot to digest here, but my first notice is that your comments mention WidgetViewer creates a list of widgets. Generally an operation like push_back is O(n) on a list, and you have this inside a loop. Commented Aug 15 at 15:04
  • 1
    "WidgetViewer creates a list of widgets." everything in this screams poor design or inexpirience. Widget is a GUI event client, aka "window" in Windows API (in X they are called 'widgets", that's where name is from). It's a quite heavy class describing abstraction of everyhing a window can do in supported OSes. What you need is a Document-View implementation and there are some examples in Framework ready. Main thing is that tables, tree views and alike "cheat" - their visual elements aren't widgets. Commented Aug 15 at 15:24
  • 1
    » not a good idea: you could simply use a tree widget (QTreeWidget or QTreeView with an appropriate model) and a specific delegate that eventually implements its painting and size hint depending on the situation. That doesn't automatically prevent interaction, though, as you could always switch to an "edit" mode by using delegate's editors (even custom ones) or through signals. The benefit of this approach is that no widgets are created unnecessarily, painting is much faster and memory/CPU impact will be much better, both at creation and when "browsing" through the structure. Commented Aug 15 at 16:24
  • 1
    I agree with above comments questioning if creating/showing so many widgets at once is necessary or practical (will it really result in good UX?). And agree this sounds potentially like a good place for a model/view application. I'll add that it may be useful to identify where exactly the slowness is coming from. If caching widgets doesn't change the speed so much, maybe creation is not where the expense is. It could instead be taken by the layout(s) handling all these widgets, or any number of other operations on the GUI side when trying to actually show them all in a parent window/widget. Commented Aug 15 at 18:42
  • 3
    @MaximPaperno You're absolutely right, layouts may be a further and quite important issue to consider, also depending on when widgets are actually added: even if the layout request is done for the top level widget only once all children have been added, this doesn't mean that the underlying and recursive computations of each child (and, eventually, their parents) may be fast/optimal anyway. Interestingly enough, if the parent widget/layout is already active, adding widgets at a later point (in batches or waiting for queued event processing) may worsen the situation, even exponentially. Commented Aug 15 at 21:43

2 Answers 2

9

To be honest, it would be near-impossible to optimize it better than a "bunch of widgets". QWidgets are heavy and most of their cost is beyond your reach or would involve non-portable solutions. They were never designed to be. You have to move to Document-View ideology and cheaper representation.

Use of QWidgets is costly because they are just front - there is a private object behind them, they aren't copyable and the cost of creation is complex - it's OS-side preparation, resource preparation, rendering, etc.

Note, there are a few common sources of extreme times: e.g. use of style sheets. Compiling stylesheets for a widget can take up to a second, especially if you had made mistakes there. It never would complain about mistakes, but can work incorrectly. Use of multiple fonts, misspelled or misused properties, large resource pictures, etc. One of the egregious mistakes I saw was QSS looking like this:

  { font: bold 14 px; }

There is no font named "bold" or "14" or "" (empty string). But Qt would read that line like that and will search EVERYWHERE for them (on Windows there are 6 different sources it can look for fonts, including two in .exe file). Cost? I saw 0.03-0.05 seconds spent for looking... per widget. 12000 widgets in that app slowed down starting time by nearly a minute. It also was a waste of memory. What has to be written:

   { font-weight: bold; font-size: 14 px; }

Better once and for all widgets using a class selector. If you have performance issues or you change the look of widgets dynamically, do not use styles. Create custom rendered elements. That's faster, by several magnitudes.

One possible way is to consider QGraphicsScene as a ready system of lightweight visual elements (if something is required, a QWidget can be used there too). The problem with that is the requirement is completely rewriting the representation.

Sign up to request clarification or add additional context in comments.

9 Comments

If I may, I appreciate your input, but can you please take your time to improve the syntax and spelling of this post? While some misspelled words may be understandable from their context, it's a bit difficult to immediately understand some phrases, especially for non native English speakers (eg: "optimize it bete"). OTOH, while QSS efficiency aspects may be important, the OP never mentioned their usage, therefore it may not be necessary to stress about their specific usage that much (unless the OP eventually clarifies the complexity of those "widgets").
@musicamante On note about widgets: even if they don't use QSS, QSS can be foreced on top, by Framework or desktop setup (e.g.in OpenDesktop environment), aka "visual theme". It's an example of how complex QWidget is. It got visual policies outside QSS as well. QWidget is an extremely expensive visual tool if start to investigate Qt code.
@musicamante just example here, I wrote " is" and keyboard software automatically changes is to "ot". Thta's not an English word, heh. I see someone fixed same typo in text (and it replaced typo with type).
I normally do edits, I didn't do it here as I simply wasn't sure about many words and didn't want to risk changing what you wrote just by guessing. I've never heard of systems that enforce QSS on top of any QApplication they run, what usually happens is that they use custom styles set as default (such as Breeze or Oxygen), but those are fully implemented in C++ like the standard ones as pure QStyle subclasses, which is not the same as using QSS (although using QSS does imply the private QStyleSheetStyle being installed on affected widgets, but that's done internally by Qt).
@musicamante Not sure how common it is, but I'm runnin an Debian branch called AstraLinux whch got a branch of OpenDesktop, but with fully written in Qt desktop manager. As Desktop launches Qt applications via QProcess, they inherit styles, so at least such mechanics appear to exist).It can be broken by installing own QSS style in central widget. The visual styles you talk designed differently, they are dynamically chosen. Widgets can be native or non-native, that's different renderer as well..
|
-1

The problem you're describing can be simplified with a nested Javascript object and a well crafted EventView.qml that recursively walks the Javascript object, e.g.

import QtQuick
import QtQuick.Controls
Page {
    EventView {
        props: {
            "eventName": "Event #1",
            "eventType": 1,
            "eventChildren": [
                {
                    "eventName": "Child #1.1 of Event #1",
                    "eventType": 1,
                    "eventChildren": [
                        {
                            "eventName": "Child #1.1.1 of Child #1.1",
                            "eventType": 2,
                        },{
                            "eventName": "Child #1.1.2 of Child #1.1",
                            "eventType": 3,
                        }
                    ]
                },{
                    "eventName": "Child #1.2 of Event #1",
                    "eventType": 2
                }
            ]
        }
    }
}

// EventView.qml
import QtQuick
import QtQuick.Controls
Loader {
    property var props
    onPropsChanged: setSource("EventViewInner.qml", props)
}

// EventViewInner.qml
import QtQuick
import QtQuick.Controls
Column {
    property string eventName: ""
    property int eventType: 0
    property list<var> eventChildren: ([])
    Label { text: `${eventName} (type ${eventType})` }
    Repeater {
        model: eventChildren
        delegate: EventView { x: 20; props: modelData }
    }
}

If you want to instantiate a different UI based on eventType, we can consider adding a DelegateChooser, otherwise, we can implement a dynamic logic in our Loader, e.g.


// EventView.qml
import QtQuick
import QtQuick.Controls
Loader {
    property var props
    onPropsChanged: switch (props.eventType)
    {
        case 1: setSource("EventView1Inner.qml", props); break;
        case 2: setSource("EventView2Inner.qml", props); break;
        case 3: setSource("EventView3Inner.qml", props); break;
        default: setSource("")
    }
}

You can Try it Online!

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.