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
AD

C ++ - Test 004. Pointers, Arrays and Loops

  • Result:50points,
  • Rating points-4
m

C ++ - Test 004. Pointers, Arrays and Loops

  • Result:80points,
  • Rating points4
m

C ++ - Test 004. Pointers, Arrays and Loops

  • Result:20points,
  • Rating points-10
Last comments
ИМ
Игорь МаксимовNov. 22, 2024, 10:51 p.m.
Django - Tutorial 017. Customize the login page to Django Добрый вечер Евгений! Я сделал себе авторизацию аналогичную вашей, все работает, кроме возврата к предидущей странице. Редеректит всегда на главную, хотя в логах сервера вижу запросы на правильн…
Evgenii Legotckoi
Evgenii LegotckoiNov. 1, 2024, 12:37 a.m.
Django - Lesson 064. How to write a Python Markdown extension Добрый день. Да, можно. Либо через такие же плагины, либо с постобработкой через python библиотеку Beautiful Soup
A
ALO1ZEOct. 19, 2024, 6:19 p.m.
Fb3 file reader on Qt Creator Подскажите как это запустить? Я не шарю в программировании и кодинге. Скачал и установаил Qt, но куча ошибок выдается и не запустить. А очень надо fb3 переконвертировать в html
ИМ
Игорь МаксимовOct. 5, 2024, 5:51 p.m.
Django - Lesson 064. How to write a Python Markdown extension Приветствую Евгений! У меня вопрос. Можно ли вставлять свои классы в разметку редактора markdown? Допустим имея стандартную разметку: <ul> <li></li> <li></l…
d
dblas5July 5, 2024, 9:02 p.m.
QML - Lesson 016. SQLite database and the working with it in QML Qt Здравствуйте, возникает такая проблема (я новичок): ApplicationWindow неизвестный элемент. (М300) для TextField и Button аналогично. Могу предположить, что из-за более новой верси…
Now discuss on the forum
Evgenii Legotckoi
Evgenii LegotckoiJune 25, 2024, 1:11 a.m.
добавить qlineseries в функции Я тут. Работы оень много. Отправил его в бан.
t
tonypeachey1Nov. 15, 2024, 5:04 p.m.
google domain [url=https://google.com/]domain[/url] domain [http://www.example.com link title]
NSProject
NSProjectJune 4, 2022, 1:49 p.m.
Всё ещё разбираюсь с кешем. В следствии прочтения данной статьи. Я принял для себя решение сделать кеширование свойств менеджера модели LikeDislike. И так как установка evileg_core для меня не была возможна, ибо он писался…
9
9AnonimOct. 25, 2024, 7:10 p.m.
Машина тьюринга // Начальное состояние 0 0, ,<,1 // Переход в состояние 1 при пустом символе 0,0,>,0 // Остаемся в состоянии 0, двигаясь вправо при встрече 0 0,1,>…

Follow us in social networks