Evgenii Legotckoi
Evgenii Legotckoi1 ноября 2017 г. 2: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

Было:

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 г. 9: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 г. 6: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

Комментарии

Только авторизованные пользователи могут публиковать комментарии.
Пожалуйста, авторизуйтесь или зарегистрируйтесь
e
  • ehot
  • 1 апреля 2024 г. 0:29

C++ - Тест 003. Условия и циклы

  • Результат:78баллов,
  • Очки рейтинга2
B

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

  • Результат:16баллов,
  • Очки рейтинга-10
B

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

  • Результат:46баллов,
  • Очки рейтинга-6
Последние комментарии
k
kmssr9 февраля 2024 г. 5:43
Qt Linux - Урок 001. Автозапуск Qt приложения под Linux как сделать автозапуск для флэтпака, который не даёт создавать файлы в ~/.config - вот это вопрос ))
АК
Анатолий Кононенко5 февраля 2024 г. 12:50
Qt WinAPI - Урок 007. Работаем с ICMP Ping в Qt Без строки #include <QRegularExpressionValidator> в заголовочном файле не работает валидатор.
EVA
EVA25 декабря 2023 г. 21:30
Boost - статическая линковка в CMake проекте под Windows Ошибка LNK1104 часто возникает, когда компоновщик не может найти или открыть файл библиотеки. В вашем случае, это файл libboost_locale-vc142-mt-gd-x64-1_74.lib из библиотеки Boost для C+…
J
JonnyJo25 декабря 2023 г. 19:38
Boost - статическая линковка в CMake проекте под Windows Сделал всё по-как у вас, но выдаёт ошибку [build] LINK : fatal error LNK1104: не удается открыть файл "libboost_locale-vc142-mt-gd-x64-1_74.lib" Хоть убей, не могу понять в чём дел…
G
Gvozdik19 декабря 2023 г. 8:01
Qt/C++ - Урок 056. Подключение библиотеки Boost в Qt для компиляторов MinGW и MSVC Для решения твой проблемы добавь в файл .pro строчку "LIBS += -lws2_32" она решит проблему , лично мне помогло.
Сейчас обсуждают на форуме
a
a_vlasov14 апреля 2024 г. 16:41
Мобильное приложение на C++Qt и бэкенд к нему на Django Rest Framework Евгений, добрый день! Такой вопрос. Верно ли следующее утверждение: Любое Android-приложение, написанное на Java/Kotlin чисто теоретически (пусть и с большими трудностями) можно написать и на C+…
Павел Дорофеев
Павел Дорофеев14 апреля 2024 г. 12:35
QTableWidget с 2 заголовками Вот тут есть кастомный QTableView с многорядностью проект поддерживается, обращайтесь
f
fastrex4 апреля 2024 г. 14:47
Вернуть старое поведение QComboBox, не менять индекс при resetModel Добрый день! У нас много проектов в которых используется QComboBox, в версии 5.5.1, когда модель испускает сигнал resetModel, currentIndex не менялся. В версии 5.15 при resetModel происходит try…
P
Pisych27 февраля 2023 г. 15:04
Как получить в массив значения из связанной модели? Спасибо, разобрался:))
AC
Alexandru Codreanu19 января 2024 г. 22:57
QML Обнулить значения SpinBox Доброго времени суток, не могу разобраться с обнулением значение SpinBox находящего в делегате. import QtQuickimport QtQuick.ControlsWindow { width: 640 height: 480 visible: tr…

Следите за нами в социальных сетях