Evgenii Legotckoi
Evgenii Legotckoi01 листопада 2017 р. 02: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(). Даний метод сповіщає всіх малюнків, які працюють з нодою про те, що відбулася певна зміна об'єкта і необхідно буде зробити перемальовку. Хоча приклад працюватиме і без цього методу, але маю припущення, що у складнішій логіці без використання цього методу можна отримати низку проблем.

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

Рекомендуємо хостинг TIMEWEB
Рекомендуємо хостинг TIMEWEB
Стабільний хостинг, на якому розміщується соціальна мережа EVILEG. Для проектів на Django радимо VDS хостинг.

Вам це подобається? Поділіться в соціальних мережах!

IT
  • 11 березня 2019 р. 09: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 р. 06: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
d
  • dsfs
  • 26 квітня 2024 р. 14:56

C++ - Тест 004. Указатели, Массивы и Циклы

  • Результат:80бали,
  • Рейтинг балів4
d
  • dsfs
  • 26 квітня 2024 р. 14:45

C++ - Тест 002. Константы

  • Результат:50бали,
  • Рейтинг балів-4
d
  • dsfs
  • 26 квітня 2024 р. 14:35

C++ - Тест 001. Первая программа и типы данных

  • Результат:73бали,
  • Рейтинг балів1
Останні коментарі
k
kmssr09 лютого 2024 р. 05:43
Qt Linux - Урок 001. Автозапуск програми Qt під Linux как сделать автозапуск для флэтпака, который не даёт создавать файлы в ~/.config - вот это вопрос ))
АК
Анатолий Кононенко05 лютого 2024 р. 12:50
Qt WinAPI - Урок 007. Робота з ICMP Ping в Qt Без строки #include <QRegularExpressionValidator> в заголовочном файле не работает валидатор.
EVA
EVA25 грудня 2023 р. 21:30
Boost - статичне зв&#39;язування в проекті CMake під Windows Ошибка LNK1104 часто возникает, когда компоновщик не может найти или открыть файл библиотеки. В вашем случае, это файл libboost_locale-vc142-mt-gd-x64-1_74.lib из библиотеки Boost для C+…
J
JonnyJo25 грудня 2023 р. 19:38
Boost - статичне зв&#39;язування в проекті CMake під Windows Сделал всё по-как у вас, но выдаёт ошибку [build] LINK : fatal error LNK1104: не удается открыть файл "libboost_locale-vc142-mt-gd-x64-1_74.lib" Хоть убей, не могу понять в чём дел…
G
Gvozdik19 грудня 2023 р. 08:01
Qt/C++ - Урок 056. Підключення бібліотеки Boost в Qt для компіляторів MinGW і MSVC Для решения твой проблемы добавь в файл .pro строчку "LIBS += -lws2_32" она решит проблему , лично мне помогло.
Тепер обговоріть на форумі
PS
Peter Son04 травня 2024 р. 03:57
Best Indian Food Restaurant In Cincinnati OH Ready to embark on a gastronomic journey like no other? Join us at App india restaurant and discover why we're renowned as the Best Indian Food Restaurant In Cincinnati OH . Whether y…
Evgenii Legotckoi
Evgenii Legotckoi03 травня 2024 р. 00:07
Мобильное приложение на C++Qt и бэкенд к нему на Django Rest Framework Добрый день. По моему мнению - да, но то, что будет касаться вызовов к функционалу Андроида, может создать огромные трудности.
IscanderChe
IscanderChe30 квітня 2024 р. 14:22
Во Flask рендер шаблона не передаётся в браузер Доброе утро! Имеется вот такой шаблон: <!doctype html><html> <head> <title>{{ title }}</title> <link rel="stylesheet" href="{{ url_…
G
Gar22 квітня 2024 р. 15:46
Clipboard Как скопировать окно целиком в clipb?
Павел Дорофеев
Павел Дорофеев14 квітня 2024 р. 12:35
QTableWidget с 2 заголовками Вот тут есть кастомный QTableView с многорядностью проект поддерживается, обращайтесь

Слідкуйте за нами в соціальних мережах