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
AD

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

  • Результат:50бали,
  • Рейтинг балів-4
m
  • molni99
  • 26 жовтня 2024 р. 01:37

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

  • Результат:80бали,
  • Рейтинг балів4
m
  • molni99
  • 26 жовтня 2024 р. 01:29

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

  • Результат:20бали,
  • Рейтинг балів-10
Останні коментарі
ИМ
Игорь Максимов22 листопада 2024 р. 11:51
Django - Підручник 017. Налаштуйте сторінку входу до Django Добрый вечер Евгений! Я сделал себе авторизацию аналогичную вашей, все работает, кроме возврата к предидущей странице. Редеректит всегда на главную, хотя в логах сервера вижу запросы на правильн…
Evgenii Legotckoi
Evgenii Legotckoi31 жовтня 2024 р. 14:37
Django - Урок 064. Як написати розширення для Python Markdown Добрый день. Да, можно. Либо через такие же плагины, либо с постобработкой через python библиотеку Beautiful Soup
A
ALO1ZE19 жовтня 2024 р. 08:19
Читалка файлів fb3 на Qt Creator Подскажите как это запустить? Я не шарю в программировании и кодинге. Скачал и установаил Qt, но куча ошибок выдается и не запустить. А очень надо fb3 переконвертировать в html
ИМ
Игорь Максимов05 жовтня 2024 р. 07:51
Django - Урок 064. Як написати розширення для Python Markdown Приветствую Евгений! У меня вопрос. Можно ли вставлять свои классы в разметку редактора markdown? Допустим имея стандартную разметку: <ul> <li></li> <li></l…
d
dblas505 липня 2024 р. 11:02
QML - Урок 016. База даних SQLite та робота з нею в QML Qt Здравствуйте, возникает такая проблема (я новичок): ApplicationWindow неизвестный элемент. (М300) для TextField и Button аналогично. Могу предположить, что из-за более новой верси…
Тепер обговоріть на форумі
Evgenii Legotckoi
Evgenii Legotckoi24 червня 2024 р. 15:11
добавить qlineseries в функции Я тут. Работы оень много. Отправил его в бан.
t
tonypeachey115 листопада 2024 р. 06:04
google domain [url=https://google.com/]domain[/url] domain [http://www.example.com link title]
NSProject
NSProject04 червня 2022 р. 03:49
Всё ещё разбираюсь с кешем. В следствии прочтения данной статьи. Я принял для себя решение сделать кеширование свойств менеджера модели LikeDislike. И так как установка evileg_core для меня не была возможна, ибо он писался…
9
9Anonim25 жовтня 2024 р. 09:10
Машина тьюринга // Начальное состояние 0 0, ,<,1 // Переход в состояние 1 при пустом символе 0,0,>,0 // Остаемся в состоянии 0, двигаясь вправо при встрече 0 0,1,>…

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