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

QQuickItem, QQuickPaintedItem, QML, Qt, C++

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.
Support the author Donate
IT

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

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
May 25, 2019, 4:20 p.m.
Андрей Янкович

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

  • Result:93points,
  • Rating points8
m
May 19, 2019, 1:49 a.m.
mahhaki

Qt - Test 001. Signals and slots

  • Result:78points,
  • Rating points2
S
May 17, 2019, 1:14 p.m.
SunBro

Qt - Test 001. Signals and slots

  • Result:42points,
  • Rating points-8
Last comments
May 21, 2019, 8:10 p.m.
Дмитрий

Приветствую! Я думаю дойдёт и до этого, но пока изучать его у меня нет желания.
May 20, 2019, 7:20 p.m.
Евгений Легоцкой

Добрый день! Вы не думали разместить репозиторий проекта на GitHub?
P.
May 18, 2019, 2:03 p.m.
PELMYACH .

Спасибо большое! Вскоре буду разбираться!
May 18, 2019, 9:13 a.m.
Евгений Легоцкой

Добрый день! Отнимать значение общего счётчика можно в деструкторе класса кнопки QDynamicButton::~QDynamicButton(){ ResID--;} При этом я бы ещё переустанавливал значения вс...
P.
May 14, 2019, 10:33 p.m.
PELMYACH .

Здравствуйте!А не подскажите, как можно при удалении какой либо кнопки, у щётчика отнять значение?Дабы например четвёртой кнопке соответствовал ID 4, а не 5 скажем
Now discuss on the forum
May 24, 2019, 6:48 a.m.
Евгений Легоцкой

Если там будут только перечисления внутри namespace, то жа, достаточно будет заголовочного файла
May 24, 2019, 6:28 a.m.
Андрей Янкович

работает любой http сервер, и можно использовать обсалютно любой портпример <RemoteRepositories> <Repository> <Url>http://178.124.160.6:3030/A/B&l...;
May 23, 2019, 2:40 p.m.
Михаиллл

Попробовал сделать этот запрос по http и получил json файл. request.setUrl(QUrl("https://jsonplaceholder.typicode.com/todos/1")); Как Вы думаете, почему https не работает и как это и...
May 23, 2019, 10:42 a.m.
Михаиллл

Спасибо, помогло.
May 23, 2019, 6:31 a.m.
Евгений Легоцкой

Для задач и граф-то не нужен. Достаточно будет таблицы в локальной базе данных SQLite, в которой указывается задача, время и т.д. В этом разделе есть примеры по работа с базой д...

For registered users on the site there is a minimum amount of advertising

EVILEG
About
Services
Join us
© EVILEG 2015-2019
Recommend hosting TIMEWEB