В уроке 024 я показал пример создания кастомного объекта QML в С++ с помощью QQuickPaintedItem , который имеет метод paint() , а в этом методе paint можно отрисовывать как на графической сцене необходимые объекты с помощью объекта класса QPainter . Разработчики, которые активно работали с методами отрисовки у виджетов, а также с кастомизацией и делегатами в классических виджетах, не увидят ничего принципиально нового при использовании метода paint().
Но данный подход является устаревшим, применительно к QML, в целом не рекомендуется и является медленным, поскольку отрисовка осуществляется средствами процессора, а не графической карты. Я на личном опыте убедился, насколько медленным может быть отрисовка перемещения большого изображения на виджете.
А вот новый подход с использованием метода updatePaintNode(), который использует средства OpenGL и соответственно обращается к графической системе ПК, является рекомендуемым, а также значительно более производительным, чем устаревший метод.
Предлагаю повторить пример из урока 024, чтобы увидеть разницу в коде и получить следующий результат.
Соглашение о уроке
Условимся на том, что логика работы таймера полностью основана на прошлом уроке 024, поэтому я сконцентрируюсь только на ключевых моментах кода, а именно там, где будет отличие, чтобы показать особенности по сравнению с устаревшим методов отрисовки. Поэтому за разъяснениями логики работы данного таймера проследуйте, пожалуйста, на страницу с уроком 024 .
Избавляемся от QQuickPaintedItem
Важным, пожалуй ключевым отличием будет то, что теперь мы наследуемся не от QQuickPaintedItem , а от QQuickItem . Таким образом, мы уже не сможем использовать метод paint() , который нам теперь больше не нужен.
clockcircle.h
Было:
class ClockCircle : public QQuickPaintedItem { Q_OBJECT /* Много всякого кода из прошлого урока */ public: explicit ClockCircle(QQuickItem *parent = 0); void paint(QPainter *painter) override; // Переопределяем метод, в котором будет отрисовываться наш объект /* Много всякого кода из прошлого урока */ };
Стало:
#include <QSGGeometryNode> class ClockCircle : public QQuickItem { Q_OBJECT /* Много кода из предыдущего урока */ public: explicit ClockCircle(QQuickItem *parent = 0); protected: virtual QSGNode* updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData* updatePaintNodeData) override; private: /* Много кода из предыдущего урока */ QSGGeometryNode* m_borderActiveNode; };
Теперь мы наследовались от QQuickItem, переопределили метод updatePaintNode() , который отвечает за формирования графических нод для OpenGL, и объявили указатель на ноду активной части ободка таймера. Если Вы посмотрите на изображение выше, то увидете, что весь таймер состоит из внешнего обода, в котором два цвета, активная часть и неактивная, а также внутренний фон этого обода. Время не считаем, оно отрисовывается в QML файле через таймер, поэтому его не трогаем. А вот нода неактивной части обода нам очень понадобится, поскольку плюс данного метода отрисовки в том, что мы будем изменять геометрию только той ноды, которая отвечает за неактивную часть обода, две других ноды (неактивная часть и внутренний фон не будут подвергаться перерасчёту).
clockcircle.cpp
А вот тут различие будет колоссальным. Предупреждаю сразу, сложность и оверхед по количеству строчек по сравнению со старым методом бросается в глаза сразу. Отрисовка через средства OpenGL влечёт за собой фактически работу с API OpenGL, хоть и завёрнутую в обёртку от Qt, но тем не менее, некоторое понимание работы с библиотекой OpenGL можно будет вынести, если постоянно работать с QML.
Но прежде чем переходить к рассмотрению методов отрисовки, отмечу один момент. В конструкторе класса необходимо вызвать следующий метод
setFlag(ItemHasContents);
Это означает, что наш QQuickItem имеет контент, который необходимо отрисовывать, в противном случае метод отрисовки не будет вызываться вообще.
А теперь посмотрим на старую реализацию:
void ClockCircle::paint(QPainter *painter) { // Отрисовка объекта QBrush brush(m_backgroundColor); // выбираем цвет фона, ... QBrush brushActive(m_borderActiveColor); // активный цвет ободка, ... QBrush brushNonActive(m_borderNonActiveColor); // не активный цвет ободка painter->setPen(Qt::NoPen); // Убираем абрис painter->setRenderHints(QPainter::Antialiasing, true); // Включаем сглаживание painter->setBrush(brushNonActive); // Отрисовываем самый нижний фон в виде круга painter->drawEllipse(boundingRect().adjusted(1,1,-1,-1)); // с подгонкой под текущие размеры, которые // будут определяться в QML-слое. // Это будет не активный фон ободка // Прогресс бар будет формироваться с помощью отрисовки Pie графика painter->setBrush(brushActive); // Отрисовываем активный фон ободка в зависимости от угла поворота painter->drawPie(boundingRect().adjusted(1,1,-1,-1), // с подгонкой под размеры в QML слое 90*16, // Стартовая точка m_angle*16); // угол поворота, до которого нужно отрисовать объект painter->setBrush(brush); // основной фон таймера, перекрытием которого поверх остальных painter->drawEllipse(boundingRect().adjusted(10,10,-10,-10)); // будет сформирован ободок (он же прогресс бар) }
Как видите, здесь не так много строчек, а отрисовка через pie график и наличие методов отрисовки эллипсов тем более позволяет не заморачиваться по расчёту местоположения точек. Да и для установки цветов не понадобится использовать создание дополнительных объектов с выделением памяти в куче.
А вот теперь посмотрим на тоже самое, только средствами OpenGL.
SGNode* ClockCircle::updatePaintNode(QSGNode* oldNode, QQuickItem::UpdatePaintNodeData* updatePaintNodeData) { Q_UNUSED(updatePaintNodeData) // Если при обновлении нода не существует, то необходимо создать все объекты и прикрепить их к ноде if (!oldNode) { // Функция для отрисовки круга 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); } }; // Создаём внешний неактивный обод по 360 точкам с помощью геометрии вершин QSGGeometry* borderNonActiveGeometry = new QSGGeometry(QSGGeometry::defaultAttributes_Point2D(), 360); borderNonActiveGeometry->setDrawingMode(GL_POLYGON); // Отрисовка будет в виде полигона, то есть с заливкой drawCircle(width() / 2, borderNonActiveGeometry); // Установка координат всех точек обода // Цвет неактивной части обода QSGFlatColorMaterial* borderNonActiveMaterial = new QSGFlatColorMaterial(); borderNonActiveMaterial->setColor(m_borderNonActiveColor); // Создаём ноду для отрисовки через геометрию вершин QSGGeometryNode* borderNonActiveNode = new QSGGeometryNode(); borderNonActiveNode->setGeometry(borderNonActiveGeometry); // Установка геометрии borderNonActiveNode->setMaterial(borderNonActiveMaterial); // Установка материала // Устанавливаем ноду в качестве парента для геометрии и материала, // Чтобы при уничтожении нода очистила память от этих объектов borderNonActiveNode->setFlags(QSGNode::OwnsGeometry|QSGNode::OwnsMaterial); //----------------------------------------------------------------------------------------------- // Создание объекта для отрисовки активной части обода, по началу не используем ни одной точки QSGGeometry* borderActiveGeometry = new QSGGeometry(QSGGeometry::defaultAttributes_Point2D(), 0); borderActiveGeometry->setDrawingMode(GL_POLYGON); // Также отрисовка будет в качестве полигона // Цвет активной части обода QSGFlatColorMaterial* borderActiveMaterial = new QSGFlatColorMaterial(); borderActiveMaterial->setColor(m_borderActiveColor); // Нам потребуется указатель на эту ноду, поскольку именно её геометрию придётся постоянно менять m_borderActiveNode = new QSGGeometryNode(); m_borderActiveNode->setGeometry(borderActiveGeometry); // Установка геометрии m_borderActiveNode->setMaterial(borderActiveMaterial); // Установка материала // Устанавливаем ноду в качестве парента для геометрии и материала, // Чтобы при уничтожении нода очистила память от этих объектов m_borderActiveNode->setFlags(QSGNode::OwnsGeometry|QSGNode::OwnsMaterial); // Прикрепляем ноду к родительской borderNonActiveNode->appendChildNode(m_borderActiveNode); //----------------------------------------------------------------------------------------------- // Создание внутреннего фона таймера, также по 360 точкам QSGGeometry* backgroundGeometry = new QSGGeometry(QSGGeometry::defaultAttributes_Point2D(), 360); backgroundGeometry->setDrawingMode(GL_POLYGON); // Отрисовываем как полигон drawCircle(width() / 2 - 10, backgroundGeometry); // Установка координат всех точек внутреннего фона // Цвет внутреннего фона QSGFlatColorMaterial* backgroundMaterial = new QSGFlatColorMaterial(); backgroundMaterial->setColor(m_backgroundColor); // Создаём ноду фона QSGGeometryNode* backgroundNode = new QSGGeometryNode(); backgroundNode->setGeometry(backgroundGeometry); // Установка геометрии backgroundNode->setMaterial(backgroundMaterial); // Установка материала // Устанавливаем ноду в качестве парента для геометрии и материала, // Чтобы при уничтожении нода очистила память от этих объектов backgroundNode->setFlags(QSGNode::OwnsGeometry|QSGNode::OwnsMaterial); // Прикрепляем ноду к родительской borderNonActiveNode->appendChildNode(backgroundNode); // Возвращаем все отрисованные ноды в первоначальном состоянии return borderNonActiveNode; } else { // Если родительская нода существует, значит всё инициализовано и можно отрисовывать активный обод static const double radius = width() / 2; // Получаем количество точек int countPoints = static_cast<int>(angle()); // Берём геометрию из ноды активной части обода QSGGeometry* geometry = m_borderActiveNode->geometry(); // Перевыделяем память под точки geometry->allocate(countPoints + 1, 0); // Уведомляем все отрисовщики об изменении геометрии ноды m_borderActiveNode->markDirty(QSGNode::DirtyGeometry); // отрисовываем центральную точку geometry->vertexDataAsPoint2D()[0].set(radius, radius); // А также все остальные точки 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); } } // Если нода существовала, то возвращаем старую ноду return oldNode; }
Всё выглядит несколько страшнее.
Для того, чтобы просто отрисовать круг, необходимо:
- создать ноду, работающую с геометрическими объектами, то есть QSGGeometryNode
- создать объект материала, в данном случае заливка цветом, то есть QSGFlatColorMaterial
- создать объект геометрии, который будет отвечать за отрисовку объекта по вершинам, то есть QSGGeometry
Сам процесс отрисовки таков, у нас есть метод updatePaintNode , который при каждом запросе на обновление, с помощью метода update(), который вызывает событие перерисовки в стеке событий Qt. При этом он возвращает указатель на ноду, которая содержит комплект других нод, которые также могут использовать для отрисовки. В нашем случае этой нодой (она родительская) выступает нода неактивной части обода, а две других - это активная часть обода и внутренний фон, для которых первая нода родительская.
При первичной инициализации там в качестве аргумента будет nullptr , поэтому потребуется создать ноду и накидать на неё другие ноды. А при дальнейшей работе мы можем просто проверять ноду на nullptr и если она существует, то вместо инифиализации делать некоторые изменения цвета или геометрии. Что здесь и делается для ноды активной части цвета. При этом при инициализации возвращаем указатель на вновь созданную ноду, а при изменении возвращаем уже указатель на старую ноду.
Внимательные и опытные программисты заметят, что я здесь нигде не устанавливаю парента (в рамках фреймворка Qt парент подчищается при удалении память от своих детей) для создаваемых нод, а также парента для объектов геометрии и материалов. А также нигде в детрукторе класса не удаляю созданные объекты, я даже не привожу код деструктора здесь.
Но всему есть логичное объяснение.
Дело в том, что при удалении QQuickItem и так подчищает память за нодами, поскольку имеет права владения на них, когда ноды возвращаются методом updatePaintNode .
А что касается материалов и геометрии, то установка флагов с помощью следующего метода
backgroundNode->setFlags(QSGNode::OwnsGeometry|QSGNode::OwnsMaterial);
говорит ноде, что теперь она владеет этими объектами и при самоуничтожении неплохо было бы уничтожить и эти объекты. А при установке новых объектов и материалов, если нода имеет права владения на старые объекты, эта нода также уничтожает старые объекты. Так что всё в порядке и утечек памяти не должно быть.
Установка вершин геометрии производится через методы, которые возвращают указатели на вершины в определённом типе, а во внутренностях храняться указатели на void . При возвращении вершины делается каст в нужный тип данных. Полагаю, что это издержки работы с OpenGL.
Важным моментом также является использование метода markDirty(). Данный метод опповещает всех отрисощики, которые работают с нодой о том, что произошло некое изменение объекта и необходимо будет произвести перерисовку. Хотя пример будет работать и без этого метода, но имею предположение, что в более сложной логике без использования этого метода можно получить ряд проблем.
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