QML - Урок 032. Создаём Custom QuickItem из C++ с использованием средств OpenGL

QQuickItem, QQuickPaintedItem, QML, Qt, C++

В уроке 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(). Данный метод опповещает всех отрисощики, которые работают с нодой о том, что произошло некое изменение объекта и необходимо будет произвести перерисовку. Хотя пример будет работать и без этого метода, но имею предположение, что в более сложной логике без использования этого метода можно получить ряд проблем.

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

Комментарии

Комментарии

Только авторизованные пользователи могут оставлять комментарии.
Пожалуйста, Авторизуйтесь или Зарегистрируйтесь
22 февраля 2018 г. 18:58
Oleg_kgd

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

  • Результат 66 баллов
  • Очки рейтинга -1
21 февраля 2018 г. 19:18
sentinel

Qt - Тест 001. Сигналы и слоты

  • Результат 78 баллов
  • Очки рейтинга 2
21 февраля 2018 г. 11:32
barilla

C++ - Тест 006. Перечисления

  • Результат 0 баллов
  • Очки рейтинга -10
Последние комментарии
22 февраля 2018 г. 16:42
soz7557

Qt/C++ - Урок 029. Изображение в базе данных в Qt – Сохранение и Восстановление

Hi, could you please show how to delete file from image Blob?  also if the same image exist in Blob then don't over write..

21 февраля 2018 г. 8:37
EVILEG

Qt/C++ - Урок 027. Полиморфизм в Qt на примере геометрических фигур в QGraphicsScene

Добрый день! 1) Эллипс можно реализовать так void Ellipse::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget){ painter->setPen(QPen(...

20 февраля 2018 г. 22:10
Log159

Qt/C++ - Урок 027. Полиморфизм в Qt на примере геометрических фигур в QGraphicsScene

Здравствуйте! В программировании новичок и есть пара вопросов. Буду очень благодарен за ответ. Не совсем понимаю как: 1) реализовать подобным образом рисование эллипса(конкре...

18 февраля 2018 г. 14:42
EVILEG

QML - Урок 019. Navigation Drawer в Qt Qml Android

Да, теперь представляю, как то работает. Согласен, ваша правка определённо к месту здесь.

Сейчас обсуждают на форуме
21 февраля 2018 г. 22:19
vitaliy_antipov

Проблема с ComboBox

Спасибо за ответы, есть над чем подумать

21 февраля 2018 г. 13:26
sol11

Qtableviev после сортировки

Спасибо, всё заработало :) Единственное вот тут row на id поменял и всё круто :)) if(id == -1){ model->insertRow(model->rowCount(QModelIndex())); map...

20 февраля 2018 г. 13:18
alex_lip

Разбить один qml файл на несколько составляющих

Да спасибо. Просто после необходимости специфичных названий для файла - стараюсь обращать внимание на любую мелочь.

20 февраля 2018 г. 8:13
EVILEG

Передача файлов в django минуя временные папки django и nginx

Тогда я даже и не знаю, прошерстил документацию, но там нет информармации о возможности отключения сохранения временных файлов. Как я понял временные файлы используются, когда тело запро...

18 февраля 2018 г. 12:34
EVILEG

QGraphicsView

Добрый день!QGraphicsView - это виджет, а значит, что в качестве парента для него выступает QWidget, а не QObject.То есть из ошибок, которые сразу бросаются в глаза в этом коде, здесь прису...