Evgenii Legotckoi
1 ноября 2017 г. 12:40

QML - Урок 032. Создаём Custom QuickItem из C++ с использованием средств OpenGL

В уроке 024 я показал пример создания кастомного объекта QML в С++ с помощью QQuickPaintedItem , который имеет метод paint() , а в этом методе paint можно отрисовывать как на графической сцене необходимые объекты с помощью объекта класса QPainter . Разработчики, которые активно работали с методами отрисовки у виджетов, а также с кастомизацией и делегатами в классических виджетах, не увидят ничего принципиально нового при использовании метода paint().

Но данный подход является устаревшим, применительно к QML, в целом не рекомендуется и является медленным, поскольку отрисовка осуществляется средствами процессора, а не графической карты. Я на личном опыте убедился, насколько медленным может быть отрисовка перемещения большого изображения на виджете.

А вот новый подход с использованием метода updatePaintNode(), который использует средства OpenGL и соответственно обращается к графической системе ПК, является рекомендуемым, а также значительно более производительным, чем устаревший метод.

Предлагаю повторить пример из урока 024, чтобы увидеть разницу в коде и получить следующий результат.


Соглашение о уроке

Условимся на том, что логика работы таймера полностью основана на прошлом уроке 024, поэтому я сконцентрируюсь только на ключевых моментах кода, а именно там, где будет отличие, чтобы показать особенности по сравнению с устаревшим методов отрисовки. Поэтому за разъяснениями логики работы данного таймера проследуйте, пожалуйста, на страницу с уроком 024 .

Избавляемся от QQuickPaintedItem

Важным, пожалуй ключевым отличием будет то, что теперь мы наследуемся не от QQuickPaintedItem , а от QQuickItem . Таким образом, мы уже не сможем использовать метод paint() , который нам теперь больше не нужен.

clockcircle.h

Было:

  1. class ClockCircle : public QQuickPaintedItem
  2. {
  3. Q_OBJECT
  4.  
  5. /* Много всякого кода из прошлого урока */
  6. public:
  7. explicit ClockCircle(QQuickItem *parent = 0);
  8.  
  9. void paint(QPainter *painter) override; // Переопределяем метод, в котором будет отрисовываться наш объект
  10.  
  11. /* Много всякого кода из прошлого урока */
  12. };

Стало:

  1. #include <QSGGeometryNode>
  2.  
  3. class ClockCircle : public QQuickItem
  4. {
  5. Q_OBJECT
  6. /* Много кода из предыдущего урока */
  7.  
  8. public:
  9. explicit ClockCircle(QQuickItem *parent = 0);
  10.  
  11. protected:
  12. virtual QSGNode* updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData* updatePaintNodeData) override;
  13.  
  14. private:
  15. /* Много кода из предыдущего урока */
  16.  
  17. QSGGeometryNode* m_borderActiveNode;
  18. };

Теперь мы наследовались от QQuickItem, переопределили метод updatePaintNode() , который отвечает за формирования графических нод для OpenGL, и объявили указатель на ноду активной части ободка таймера. Если Вы посмотрите на изображение выше, то увидете, что весь таймер состоит из внешнего обода, в котором два цвета, активная часть и неактивная, а также внутренний фон этого обода. Время не считаем, оно отрисовывается в QML файле через таймер, поэтому его не трогаем. А вот нода неактивной части обода нам очень понадобится, поскольку плюс данного метода отрисовки в том, что  мы будем изменять геометрию только той ноды, которая отвечает за неактивную часть обода, две других ноды (неактивная часть и внутренний фон не будут подвергаться перерасчёту).

clockcircle.cpp

А вот тут различие будет колоссальным. Предупреждаю сразу, сложность и оверхед по количеству строчек по сравнению со старым методом бросается в глаза сразу. Отрисовка через средства OpenGL влечёт за собой фактически работу с API OpenGL, хоть и завёрнутую в обёртку от Qt, но тем не менее, некоторое понимание работы с библиотекой OpenGL можно будет вынести, если постоянно работать с QML.

Но прежде чем переходить к рассмотрению методов отрисовки, отмечу один момент. В конструкторе класса необходимо вызвать следующий метод

  1. setFlag(ItemHasContents);

Это означает, что наш QQuickItem имеет контент, который необходимо отрисовывать, в противном случае метод отрисовки не будет вызываться вообще.

А теперь посмотрим на старую реализацию:

  1. void ClockCircle::paint(QPainter *painter)
  2. {
  3. // Отрисовка объекта
  4. QBrush brush(m_backgroundColor); // выбираем цвет фона, ...
  5. QBrush brushActive(m_borderActiveColor); // активный цвет ободка, ...
  6. QBrush brushNonActive(m_borderNonActiveColor); // не активный цвет ободка
  7.  
  8. painter->setPen(Qt::NoPen); // Убираем абрис
  9. painter->setRenderHints(QPainter::Antialiasing, true); // Включаем сглаживание
  10.  
  11. painter->setBrush(brushNonActive); // Отрисовываем самый нижний фон в виде круга
  12. painter->drawEllipse(boundingRect().adjusted(1,1,-1,-1)); // с подгонкой под текущие размеры, которые
  13. // будут определяться в QML-слое.
  14. // Это будет не активный фон ободка
  15.  
  16. // Прогресс бар будет формироваться с помощью отрисовки Pie графика
  17. painter->setBrush(brushActive); // Отрисовываем активный фон ободка в зависимости от угла поворота
  18. painter->drawPie(boundingRect().adjusted(1,1,-1,-1), // с подгонкой под размеры в QML слое
  19. 90*16, // Стартовая точка
  20. m_angle*16); // угол поворота, до которого нужно отрисовать объект
  21.  
  22. painter->setBrush(brush); // основной фон таймера, перекрытием которого поверх остальных
  23. painter->drawEllipse(boundingRect().adjusted(10,10,-10,-10)); // будет сформирован ободок (он же прогресс бар)
  24. }

Как видите, здесь не так много строчек, а отрисовка через pie график и наличие методов отрисовки эллипсов тем более позволяет не заморачиваться по расчёту местоположения точек. Да и для установки цветов не понадобится использовать создание дополнительных объектов с выделением памяти в куче.

А вот теперь посмотрим на тоже самое, только средствами OpenGL.

  1. SGNode* ClockCircle::updatePaintNode(QSGNode* oldNode, QQuickItem::UpdatePaintNodeData* updatePaintNodeData)
  2. {
  3. Q_UNUSED(updatePaintNodeData)
  4.  
  5. // Если при обновлении нода не существует, то необходимо создать все объекты и прикрепить их к ноде
  6. if (!oldNode)
  7. {
  8. // Функция для отрисовки круга
  9. auto drawCircle = [this](double radius, QSGGeometry* geometry)
  10. {
  11. for (int i = 0; i < 360; ++i)
  12. {
  13. double rad = (i - 90) * Deg2Rad;
  14. geometry->vertexDataAsPoint2D()[i].set(std::cos(rad) * radius + width() / 2, std::sin(rad) * radius + height() / 2);
  15. }
  16. };
  17.  
  18. // Создаём внешний неактивный обод по 360 точкам с помощью геометрии вершин
  19. QSGGeometry* borderNonActiveGeometry = new QSGGeometry(QSGGeometry::defaultAttributes_Point2D(), 360);
  20. borderNonActiveGeometry->setDrawingMode(GL_POLYGON); // Отрисовка будет в виде полигона, то есть с заливкой
  21. drawCircle(width() / 2, borderNonActiveGeometry); // Установка координат всех точек обода
  22.  
  23. // Цвет неактивной части обода
  24. QSGFlatColorMaterial* borderNonActiveMaterial = new QSGFlatColorMaterial();
  25. borderNonActiveMaterial->setColor(m_borderNonActiveColor);
  26.  
  27. // Создаём ноду для отрисовки через геометрию вершин
  28. QSGGeometryNode* borderNonActiveNode = new QSGGeometryNode();
  29. borderNonActiveNode->setGeometry(borderNonActiveGeometry); // Установка геометрии
  30. borderNonActiveNode->setMaterial(borderNonActiveMaterial); // Установка материала
  31. // Устанавливаем ноду в качестве парента для геометрии и материала,
  32. // Чтобы при уничтожении нода очистила память от этих объектов
  33. borderNonActiveNode->setFlags(QSGNode::OwnsGeometry|QSGNode::OwnsMaterial);
  34.  
  35. //-----------------------------------------------------------------------------------------------
  36. // Создание объекта для отрисовки активной части обода, по началу не используем ни одной точки
  37. QSGGeometry* borderActiveGeometry = new QSGGeometry(QSGGeometry::defaultAttributes_Point2D(), 0);
  38. borderActiveGeometry->setDrawingMode(GL_POLYGON); // Также отрисовка будет в качестве полигона
  39.  
  40. // Цвет активной части обода
  41. QSGFlatColorMaterial* borderActiveMaterial = new QSGFlatColorMaterial();
  42. borderActiveMaterial->setColor(m_borderActiveColor);
  43.  
  44. // Нам потребуется указатель на эту ноду, поскольку именно её геометрию придётся постоянно менять
  45. m_borderActiveNode = new QSGGeometryNode();
  46. m_borderActiveNode->setGeometry(borderActiveGeometry); // Установка геометрии
  47. m_borderActiveNode->setMaterial(borderActiveMaterial); // Установка материала
  48. // Устанавливаем ноду в качестве парента для геометрии и материала,
  49. // Чтобы при уничтожении нода очистила память от этих объектов
  50. m_borderActiveNode->setFlags(QSGNode::OwnsGeometry|QSGNode::OwnsMaterial);
  51. // Прикрепляем ноду к родительской
  52. borderNonActiveNode->appendChildNode(m_borderActiveNode);
  53.  
  54. //-----------------------------------------------------------------------------------------------
  55. // Создание внутреннего фона таймера, также по 360 точкам
  56. QSGGeometry* backgroundGeometry = new QSGGeometry(QSGGeometry::defaultAttributes_Point2D(), 360);
  57. backgroundGeometry->setDrawingMode(GL_POLYGON); // Отрисовываем как полигон
  58. drawCircle(width() / 2 - 10, backgroundGeometry); // Установка координат всех точек внутреннего фона
  59.  
  60. // Цвет внутреннего фона
  61. QSGFlatColorMaterial* backgroundMaterial = new QSGFlatColorMaterial();
  62. backgroundMaterial->setColor(m_backgroundColor);
  63.  
  64. // Создаём ноду фона
  65. QSGGeometryNode* backgroundNode = new QSGGeometryNode();
  66. backgroundNode->setGeometry(backgroundGeometry); // Установка геометрии
  67. backgroundNode->setMaterial(backgroundMaterial); // Установка материала
  68. // Устанавливаем ноду в качестве парента для геометрии и материала,
  69. // Чтобы при уничтожении нода очистила память от этих объектов
  70. backgroundNode->setFlags(QSGNode::OwnsGeometry|QSGNode::OwnsMaterial);
  71. // Прикрепляем ноду к родительской
  72. borderNonActiveNode->appendChildNode(backgroundNode);
  73.  
  74. // Возвращаем все отрисованные ноды в первоначальном состоянии
  75. return borderNonActiveNode;
  76. }
  77. else
  78. {
  79. // Если родительская нода существует, значит всё инициализовано и можно отрисовывать активный обод
  80. static const double radius = width() / 2;
  81. // Получаем количество точек
  82. int countPoints = static_cast<int>(angle());
  83. // Берём геометрию из ноды активной части обода
  84. QSGGeometry* geometry = m_borderActiveNode->geometry();
  85. // Перевыделяем память под точки
  86. geometry->allocate(countPoints + 1, 0);
  87. // Уведомляем все отрисовщики об изменении геометрии ноды
  88. m_borderActiveNode->markDirty(QSGNode::DirtyGeometry);
  89. // отрисовываем центральную точку
  90. geometry->vertexDataAsPoint2D()[0].set(radius, radius);
  91.  
  92. // А также все остальные точки
  93. for (int i = 1; i < countPoints + 1; ++i)
  94. {
  95. double rad = (i - 90) * Deg2Rad;
  96. geometry->vertexDataAsPoint2D()[i].set(std::cos(rad) * radius + width() / 2, std::sin(rad) * radius + height() / 2);
  97. }
  98. }
  99.  
  100. // Если нода существовала, то возвращаем старую ноду
  101. return oldNode;
  102. }

Всё выглядит несколько страшнее.

Для того, чтобы просто отрисовать круг, необходимо:

  1. создать ноду, работающую с геометрическими объектами, то есть QSGGeometryNode
  2. создать объект материала, в данном случае заливка цветом, то есть QSGFlatColorMaterial
  3. создать объект геометрии, который будет отвечать за отрисовку объекта по вершинам, то есть QSGGeometry

Сам процесс отрисовки таков, у нас есть метод updatePaintNode , который при каждом запросе на обновление, с помощью метода update(), который вызывает событие перерисовки в стеке событий Qt. При этом он возвращает указатель на ноду, которая содержит комплект других нод, которые также могут использовать для отрисовки. В нашем случае этой нодой (она родительская) выступает нода неактивной части обода, а две других - это активная часть обода и внутренний фон, для которых первая нода родительская.

При первичной инициализации там в качестве аргумента будет nullptr , поэтому потребуется создать ноду и накидать на неё другие ноды. А при дальнейшей работе мы можем просто проверять ноду на nullptr и если она существует, то вместо инифиализации делать некоторые изменения цвета или геометрии. Что здесь и делается для ноды активной части цвета. При этом при инициализации возвращаем указатель на вновь созданную ноду, а при изменении возвращаем уже указатель на старую ноду.

Внимательные и опытные программисты заметят, что я здесь нигде не устанавливаю парента (в рамках фреймворка Qt парент подчищается при удалении память от своих детей) для создаваемых нод, а также парента для объектов геометрии и материалов. А также нигде в детрукторе класса не удаляю созданные объекты, я даже не привожу код деструктора здесь.

Но всему есть логичное объяснение.

Дело в том, что при удалении QQuickItem и так подчищает память за нодами, поскольку имеет права владения на них, когда ноды возвращаются методом updatePaintNode .

А что касается материалов и геометрии, то установка флагов с помощью следующего метода

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

говорит ноде, что теперь она владеет этими объектами и при самоуничтожении неплохо было бы уничтожить и эти объекты. А при установке новых объектов и материалов, если нода имеет права владения на старые объекты, эта нода также уничтожает старые объекты. Так что всё в порядке и утечек памяти не должно быть.

Установка вершин геометрии производится через методы, которые возвращают указатели на вершины в определённом типе, а во внутренностях храняться указатели на void . При возвращении вершины делается каст в нужный тип данных. Полагаю, что это издержки работы с OpenGL.

Важным моментом также является использование метода markDirty(). Данный метод опповещает всех отрисощики, которые работают с нодой о том, что произошло некое изменение объекта и необходимо будет произвести перерисовку. Хотя пример будет работать и без этого метода, но имею предположение, что в более сложной логике без использования этого метода можно получить ряд проблем.

Скачать проект

Вам это нравится? Поделитесь в социальных сетях!

IT
  • 11 марта 2019 г. 19:54

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
  • 12 марта 2019 г. 16:04
  • (ред.)

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

Комментарии

Только авторизованные пользователи могут публиковать комментарии.
Пожалуйста, авторизуйтесь или зарегистрируйтесь