В уроці 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