Evgenii Legotckoi
01 листопада 2017 р. 12:40

QML - Підручник 032. Створіть користувацький QuickItem з C++ за допомогою інструментів OpenGL

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

Все виглядає дещо страшніше.

Для того, щоб просто змалювати коло, необхідно:

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

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

При первинній ініціалізації там як аргумент буде nullptr , тому потрібно створити ноду і накидати на неї інші ноди. А при подальшій роботі ми можемо просто перевіряти ноду на nullptr і якщо вона існує, то замість ініфіалізації робити деякі зміни кольору або геометрії. Що тут робиться для ноди активної частини кольору. При цьому при ініціалізації повертаємо вказівник на новостворену ноду, а при зміні повертаємо вже вказівник на стару ноду.

Уважні та досвідчені програмісти помітять, що я тут ніде не встановлюю парента (в рамках фреймворку Qt парент підчищається при видаленні пам'яті від своїх дітей) для нод, що створюються, а також парента для об'єктів геометрії та матеріалів. А також ніде у детрукторі класу не видаляю створені об'єкти, я навіть не наводжу код деструктора тут.

Але всьому є логічне пояснення.

Справа в тому, що при видаленні QQuickItem і так підчищає пам'ять за нодами, оскільки має права володіння на них, коли ноди повертаються методом updatePaintNode .

А щодо матеріалів та геометрії, то встановлення прапорів за допомогою наступного методу

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

Коментарі

Only authorized users can post comments.
Please, Log in or Sign up