Реклама

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

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

Реклама

Комментарии

Комментарии

Только авторизованные пользователи могут оставлять комментарии.
Пожалуйста, Авторизуйтесь или Зарегистрируйтесь

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

  • Результат 50 баллов
  • Очки рейтинга -4

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

  • Результат 80 баллов
  • Очки рейтинга 4

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

  • Результат 6 баллов
  • Очки рейтинга -10
Последние комментарии

QML - Урок 002. Custom Button in QML Android

Нашел http://doc.qt.io/Qt-5/qtquickcontrols2-customize.html#customizing-button

QML - Урок 002. Custom Button in QML Android

А как кастомайзить Button если использовать QtQuick.Controls 2.0 ? В этом случае пишет Cannot assign to non-existent property "style"

  • EVILEG
  • 15 ноября 2017 г. 13:56

Qt/C++ - Урок 072. Пример векторного редактора на Qt

You need add common QWidget to form, after that you need right-click on this QWidget in the form and select "Promote to..." in Context Menu. After that You will see dialog. In dialog choose Ba...

  • cordsac
  • 15 ноября 2017 г. 3:33

Qt/C++ - Урок 072. Пример векторного редактора на Qt

Sir,In this form design how did you add verectanglesettings.ui,vepolylinesettings.ui UI's to this mainwindow.ui ? have any QT tool to add so. this image shows what I meaning.

Сейчас обсуждают на форуме
  • Docent
  • 21 ноября 2017 г. 19:39

Видео на сцене QGraphicsScene, как правильно сделать?

Ситуация такая: имеется область в памяти в которую пишутся кадры с камеры. Каким наиболее оптимальным образом отобразить их на сцене. У меня это реализовано таким образом, но подозреваю что мо...

Многопоточность. Ошибки при обращении к переменной

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

как отключить событие при открытии формы?

как обойти проблему: см. комментарий кода - может кто подскажет. TableView { id: table model: tableModel anchors.fill: parent focus: true Component.onComple...

  • EVILEG
  • 21 ноября 2017 г. 14:40

QGraphicsItem конструктор с параметрами

За что именно отвечают x_c , y_c ? Это положение объекта на графической сцене? Тогда лучше не в методе paint их применять, а через метод setPo...

  • EVILEG
  • 21 ноября 2017 г. 1:36

QGraphicsItem

Я так понимаю, вы каким-то образом передаёте указатель на отрисовываемый текст, то есть на QString. Но лучше добавить переменную в объявление класса, которая будет отвечать за текст, и п...