Evgenii Legotckoi
Evgenii LegotckoiNov. 1, 2017, 2:40 a.m.

QML - Tutorial 032. Create Custom QuickItem from C ++ using OpenGL tools

In tutorial 024 , I showed an example of creating a custom QML object in C ++ using QQuickPaintedItem , which has a paint() method, and in this paint method, you can draw the necessary objects on the graphics scene using a QPainter class object. Developers who worked extensively with drawing methods from widgets, as well as with customization and delegates in classic widgets, will not see anything fundamentally new when using the paint() method.

But this approach is deprecated, applied to QML, is generally not recommended and is slow, because the rendering is performed by means of the processor, not the graphics card. I was convinced on my personal experience how slow the drawing of a large image on the widget can be.

But a new approach using the updatePaintNode() method, which uses OpenGL tools and accordingly refers to the PC graphical system, is recommended, as well as much more productive than the outdated method.

I suggest repeating the example from tutorial 024 to see the difference in the code and get the following result.


Lesson Agreement

Let's agree that the logic of the timer is completely based on the last lesson 024, so I will concentrate only on the key points of the code, namely, where there will be a difference, to show features compared to the deprecated methods of rendering. Therefore, for an explanation of the logic of this timer, please proceed to the page with a lesson 024 .

Dispose of QQuickPaintedItem

An important, perhaps the key difference is that now we do not inherit from QQuickPaintedItem , but from QQuickItem . Thus, we can no longer use the paint() method, which we no longer need.

clockcircle.h

It was:

class ClockCircle : public QQuickPaintedItem
{
    Q_OBJECT

    /* A lot of code from the last lesson */
public:
    explicit ClockCircle(QQuickItem *parent = 0);

    void paint(QPainter *painter) override; // Override the method in which our object will be rendered

    /* A lot of code from the last lesson */
};

Became:

#include <QSGGeometryNode>

class ClockCircle : public QQuickItem
{
    Q_OBJECT
    /* A lot of code from the last lesson */

public:
    explicit ClockCircle(QQuickItem *parent = 0);

protected:
    virtual QSGNode* updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData* updatePaintNodeData) override;

private:
     /* A lot of code from the last lesson */

    QSGGeometryNode* m_borderActiveNode;
};

Now we inherited from QQuickItem , redefined the updatePaintNode() method, which is responsible for forming the graphic nodes for OpenGL , and declared a pointer to the node of the active part of the timer's rim. If you look at the image above, you will see that the entire timer consists of an outer rim, in which two colors, the active part and the inactive, as well as the inner background of this rim. Time does not count, it is rendered in a QML file through a timer, so do not touch it. But the inactive part of the rim is very necessary for us, since the plus of this method of rendering is that we will change the geometry of only the node that is responsible for the inactive part of the rim, the other two nodes (the inactive part and the inner background will not be recalculated).

clockcircle.cpp

And here the difference will be enormous. I warn you at once, the difficulty and overhead in the number of lines compared with the old method is immediately evident. Drawing through the OpenGL tools entails actually working with the OpenGL API, though wrapped in a wrapper from Qt, but nevertheless, some understanding of working with the OpenGL library can be made if you constantly work with QML.

But before I turn to the methods of rendering, I will note one thing. In the class constructor, you call the following method

setFlag(ItemHasContents);

This means that our QQuickItem has content that needs to be rendered, otherwise the rendering method will not be called at all.

And now let's look at the old implementation:

void ClockCircle::paint(QPainter *painter)
{
    // Object rendering
    QBrush  brush(m_backgroundColor);               // choose background color, ...
    QBrush  brushActive(m_borderActiveColor);       // active color rim, ...
    QBrush  brushNonActive(m_borderNonActiveColor); // not active color of the rim

    painter->setPen(Qt::NoPen);                             // We remove the outline
    painter->setRenderHints(QPainter::Antialiasing, true);  // Turn on anti-aliasing

    painter->setBrush(brushNonActive);                          // Draw the lowest background in the form of a circle
    painter->drawEllipse(boundingRect().adjusted(1,1,-1,-1));   // with a fit for the current dimensions, which
                                                                // will be defined in the QML layer.
                                                                // This will not be the active background of the rim

    // Progress bar will be formed by drawing Pie graphics
    painter->setBrush(brushActive);                         // Draw the active background of the rim, depending on the angle of rotation
    painter->drawPie(boundingRect().adjusted(1,1,-1,-1),    // with a fit to the dimensions in the QML layer
                     90*16,         // Starting point
                     m_angle*16);   // The angle of rotation to which you want to draw an object

    painter->setBrush(brush);       // The main background of the timer, overlapping which on top of the rest
    painter->drawEllipse(boundingRect().adjusted(10,10,-10,-10));   // will be formed a bezel (it's the same progress bar)
}

As you can see, there are not so many lines here, and drawing through the pie graph and the presence of methods for drawing ellipses all the more allow you to not bother with calculating the location of points. And to set colors, you do not need to use the creation of additional objects with allocation of memory in the heap.

But now let's look at the same thing, only with OpenGL.

SGNode* ClockCircle::updatePaintNode(QSGNode* oldNode, QQuickItem::UpdatePaintNodeData* updatePaintNodeData)
{
    Q_UNUSED(updatePaintNodeData)

    // If the node does not exist when updating the node, you must create all objects and attach them to the node
    if (!oldNode)
    {
        // Function for drawing a circle
        auto drawCircle = [this](double radius, QSGGeometry* geometry)
        {
            for (int i = 0; i < 360; ++i)
            {
                double rad = (i - 90) * Deg2Rad;
                geometry->vertexDataAsPoint2D()[i].set(std::cos(rad) * radius + width() / 2, std::sin(rad) * radius + height() / 2);
            }
        };

        // Create an external inactive 360-point rim using vertex geometry
        QSGGeometry* borderNonActiveGeometry = new QSGGeometry(QSGGeometry::defaultAttributes_Point2D(), 360);
        borderNonActiveGeometry->setDrawingMode(GL_POLYGON); // The drawing will be in the form of a polygon, that is, with a fill
        drawCircle(width() / 2, borderNonActiveGeometry); // Setting the coordinates of all points of the rim

        // Color of the inactive portion of the rim
        QSGFlatColorMaterial* borderNonActiveMaterial = new QSGFlatColorMaterial();
        borderNonActiveMaterial->setColor(m_borderNonActiveColor);

        // Create a node to draw through the geometry of vertices
        QSGGeometryNode* borderNonActiveNode = new QSGGeometryNode();
        borderNonActiveNode->setGeometry(borderNonActiveGeometry); // Setting geometry
        borderNonActiveNode->setMaterial(borderNonActiveMaterial); // Setting the Material
        // We set the node as a part for geometry and material,
        // To erase a memory from these objects when a node is destroyed
        borderNonActiveNode->setFlags(QSGNode::OwnsGeometry|QSGNode::OwnsMaterial);

        //-----------------------------------------------------------------------------------------------
        // Creating an object to draw the active part of the rim, at the beginning we do not use any points
        QSGGeometry* borderActiveGeometry = new QSGGeometry(QSGGeometry::defaultAttributes_Point2D(), 0);
        borderActiveGeometry->setDrawingMode(GL_POLYGON); // Also, the drawing will be used as a polygon

        // Color of the active part of the rim
        QSGFlatColorMaterial* borderActiveMaterial = new QSGFlatColorMaterial();
        borderActiveMaterial->setColor(m_borderActiveColor);

        // We need a pointer to this node, since its geometry will have to be constantly changed
        m_borderActiveNode = new QSGGeometryNode();
        m_borderActiveNode->setGeometry(borderActiveGeometry); // Установка геометрии
        m_borderActiveNode->setMaterial(borderActiveMaterial); // Установка материала
        // We set the node as a part for geometry and material,
        // To erase a memory from these objects when a node is destroyed
        m_borderActiveNode->setFlags(QSGNode::OwnsGeometry|QSGNode::OwnsMaterial);
        // We attach the node to the parent
        borderNonActiveNode->appendChildNode(m_borderActiveNode);

        //-----------------------------------------------------------------------------------------------
        // Creation of internal background of the timer, also on 360 points
        QSGGeometry* backgroundGeometry = new QSGGeometry(QSGGeometry::defaultAttributes_Point2D(), 360);
        backgroundGeometry->setDrawingMode(GL_POLYGON); // Draw as a polygon
        drawCircle(width() / 2 - 10, backgroundGeometry); // Set the coordinates of all internal background points

        // Background color
        QSGFlatColorMaterial* backgroundMaterial = new QSGFlatColorMaterial();
        backgroundMaterial->setColor(m_backgroundColor);

        // Create a background node
        QSGGeometryNode* backgroundNode = new QSGGeometryNode();
        backgroundNode->setGeometry(backgroundGeometry); // Setting geometry
        backgroundNode->setMaterial(backgroundMaterial); // Setting the Material
        // We set the node as a part for geometry and material,
        // To erase a memory from these objects when a node is destroyed
        backgroundNode->setFlags(QSGNode::OwnsGeometry|QSGNode::OwnsMaterial);
        // We attach the node to the parent
        borderNonActiveNode->appendChildNode(backgroundNode);

        // Return all the drawn nodes in the original state
        return borderNonActiveNode;
    }
    else
    {
        // If the parent node exists, then everything is initialized and you can draw the active rim
        static const double radius = width() / 2;
        // We get the number of points
        int countPoints = static_cast<int>(angle());
        // We take the geometry from the node of the active part of the rim
        QSGGeometry* geometry = m_borderActiveNode->geometry();
        // We re-allocate memory for points
        geometry->allocate(countPoints + 1, 0);
        // Notify all the drawers of the node geometry change
        m_borderActiveNode->markDirty(QSGNode::DirtyGeometry);
        // draw the center point
        geometry->vertexDataAsPoint2D()[0].set(radius, radius);

        // And also all other points
        for (int i = 1; i < countPoints + 1; ++i)
        {
            double rad = (i - 90) * Deg2Rad;
            geometry->vertexDataAsPoint2D()[i].set(std::cos(rad) * radius + width() / 2, std::sin(rad) * radius + height() / 2);
        }
    }

    // If the node exists, then we return the old node
    return oldNode;
}

Everything looks a little more frightening.

In order to simply draw a circle, you need:

  1. create a node that works with geometric objects, that is, QSGGeometryNode
  2. create a material object, in this case a color fill, that is, QSGFlatColorMaterial
  3. create a geometry object that will be responsible for drawing the object along the vertices, that is, QSGGeometry

The rendering process itself is like this, we have the updatePaintNode method, which for every update request, with the update () method, which calls the redraw event in the Qt events stack. In doing so, it returns a pointer to the node, which contains a set of other nodes, which can also be used for rendering. In our case, this node (it's parent) is the node of the inactive portion of the rim, and the other two are the active part of the rim and the inner background, for which the first node is the parent.

At initial initialization there as an argument there will be nullptr , therefore it is required to create a node and to throw on it other nodes. And with further work, we can simply check the node on nullptr and if it exists, then instead of initializing, do some color or geometry changes. That here also becomes for a node of an active part of color. In this case, when initializing, we return the pointer to the newly created node, and when changing, we return the pointer to the old node.

Attentive and experienced programmers will notice that I'm not setting up a client anywhere (within the framework of the Qt framework, the parent is deleted when removing memory from its children) for the nodes being created, as well as for the objects for geometry and materials. And also nowhere in the class's detructor I delete the created objects, I do not even bring the code of the destructor here.

But everything has a logical explanation.

The fact is that when removing QQuickItem and so it clears the memory for the nodes, because it has ownership rights to them, when the nodes are returned using the updatePaintNode method.

As for materials and geometry, the installation of flags using the following method

backgroundNode->setFlags(QSGNode::OwnsGeometry|QSGNode::OwnsMaterial);

says to the node that now she owns these objects and it would be a good idea to destroy these objects when self-destructing. And when installing new objects and materials, if the node has ownership rights to the old objects, this node also destroys the old objects. So everything is in order and memory leaks should not be.

Setting the vertices of geometry is done through methods that return pointers to vertices in a certain type, and indices point to void in the interior. When you return a vertex, a cast is made into the desired data type. I believe that this is the cost of working with OpenGL.

An important point is also the use of the markDirty() method. This method warns all artists who work with the node that a certain change in the object has occurred and it will be necessary to redraw. Although the example will work without this method, but I have the assumption that in more complex logic without using this method, you can get a number of problems.

Download project

We recommend hosting TIMEWEB
We recommend hosting TIMEWEB
Stable hosting, on which the social network EVILEG is located. For projects on Django we recommend VDS hosting.

Do you like it? Share on social networks!

IT
  • March 11, 2019, 9:54 a.m.

Hi,

Though this blog is a bit old, I am trying this as the only source available to draw my scenegraph based painting. I was so far successful, but I needed to draw a text also as a part of the parent QSgGeometryNode. Is that possible, as there are no convinent classes by default.

How would you recommend.

Regards,
Indrajit

Evgenii Legotckoi
  • March 12, 2019, 6:04 a.m.
  • (edited)

Hello,

In fact, this functionality or is not implemented, or is not documented. I'm not sure. But I think, that it should be implemented in Text QML Type. Because of we can write text in QML. I think you should see sources of this Type.

And I found on GitHub this code .

I think you can implement code from GitHub in your project.

Regards

Comments

Only authorized users can post comments.
Please, Log in or Sign up
e
  • ehot
  • March 31, 2024, 2:29 p.m.

C++ - Тест 003. Условия и циклы

  • Result:78points,
  • Rating points2
B

C++ - Test 002. Constants

  • Result:16points,
  • Rating points-10
B

C++ - Test 001. The first program and data types

  • Result:46points,
  • Rating points-6
Last comments
k
kmssrFeb. 8, 2024, 6:43 p.m.
Qt Linux - Lesson 001. Autorun Qt application under Linux как сделать автозапуск для флэтпака, который не даёт создавать файлы в ~/.config - вот это вопрос ))
Qt WinAPI - Lesson 007. Working with ICMP Ping in Qt Без строки #include <QRegularExpressionValidator> в заголовочном файле не работает валидатор.
EVA
EVADec. 25, 2023, 10:30 a.m.
Boost - static linking in CMake project under Windows Ошибка LNK1104 часто возникает, когда компоновщик не может найти или открыть файл библиотеки. В вашем случае, это файл libboost_locale-vc142-mt-gd-x64-1_74.lib из библиотеки Boost для C+…
J
JonnyJoDec. 25, 2023, 8:38 a.m.
Boost - static linking in CMake project under Windows Сделал всё по-как у вас, но выдаёт ошибку [build] LINK : fatal error LNK1104: не удается открыть файл "libboost_locale-vc142-mt-gd-x64-1_74.lib" Хоть убей, не могу понять в чём дел…
G
GvozdikDec. 18, 2023, 9:01 p.m.
Qt/C++ - Lesson 056. Connecting the Boost library in Qt for MinGW and MSVC compilers Для решения твой проблемы добавь в файл .pro строчку "LIBS += -lws2_32" она решит проблему , лично мне помогло.
Now discuss on the forum
a
a_vlasovApril 14, 2024, 6:41 a.m.
Мобильное приложение на C++Qt и бэкенд к нему на Django Rest Framework Евгений, добрый день! Такой вопрос. Верно ли следующее утверждение: Любое Android-приложение, написанное на Java/Kotlin чисто теоретически (пусть и с большими трудностями) можно написать и на C+…
Павел Дорофеев
Павел ДорофеевApril 14, 2024, 2:35 a.m.
QTableWidget с 2 заголовками Вот тут есть кастомный QTableView с многорядностью проект поддерживается, обращайтесь
f
fastrexApril 4, 2024, 4:47 a.m.
Вернуть старое поведение QComboBox, не менять индекс при resetModel Добрый день! У нас много проектов в которых используется QComboBox, в версии 5.5.1, когда модель испускает сигнал resetModel, currentIndex не менялся. В версии 5.15 при resetModel происходит try…
AC
Alexandru CodreanuJan. 19, 2024, 11:57 a.m.
QML Обнулить значения SpinBox Доброго времени суток, не могу разобраться с обнулением значение SpinBox находящего в делегате. import QtQuickimport QtQuick.ControlsWindow { width: 640 height: 480 visible: tr…

Follow us in social networks