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:
- create a node that works with geometric objects, that is, QSGGeometryNode
- create a material object, in this case a color fill, that is, QSGFlatColorMaterial
- 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.
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