Evgenii Legotckoi
Evgenii LegotckoiҚар. 1, 2017, 2:40 Т.Ж.

QML - 032-сабақ. OpenGL құралдары арқылы C++ тілінен реттелетін QuickItem жасаңыз

В уроке 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

Пікірлер

Тек рұқсаты бар пайдаланушылар ғана пікір қалдыра алады.
Кіріңіз немесе Тіркеліңіз
OI
  • Ora Iro
  • Жел. 24, 2024, 6:38 Т.Ж.

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

  • Нәтиже:40ұпай,
  • Бағалау ұпайлары-8
AD

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

  • Нәтиже:50ұпай,
  • Бағалау ұпайлары-4
m
  • molni99
  • Қаз. 26, 2024, 1:37 Т.Ж.

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

  • Нәтиже:80ұпай,
  • Бағалау ұпайлары4
Соңғы пікірлер
ИМ
Игорь МаксимовҚар. 22, 2024, 11:51 Т.Ж.
Django - Оқулық 017. Теңшелген Django кіру беті Добрый вечер Евгений! Я сделал себе авторизацию аналогичную вашей, все работает, кроме возврата к предидущей странице. Редеректит всегда на главную, хотя в логах сервера вижу запросы на правильн…
Evgenii Legotckoi
Evgenii LegotckoiҚаз. 31, 2024, 2:37 Т.Қ.
Django - Сабақ 064. Python Markdown кеңейтімін қалай жазуға болады Добрый день. Да, можно. Либо через такие же плагины, либо с постобработкой через python библиотеку Beautiful Soup
A
ALO1ZEҚаз. 19, 2024, 8:19 Т.Ж.
Qt Creator көмегімен fb3 файл оқу құралы Подскажите как это запустить? Я не шарю в программировании и кодинге. Скачал и установаил Qt, но куча ошибок выдается и не запустить. А очень надо fb3 переконвертировать в html
ИМ
Игорь МаксимовҚаз. 5, 2024, 7:51 Т.Ж.
Django - Сабақ 064. Python Markdown кеңейтімін қалай жазуға болады Приветствую Евгений! У меня вопрос. Можно ли вставлять свои классы в разметку редактора markdown? Допустим имея стандартную разметку: <ul> <li></li> <li></l…
d
dblas5Шілде 5, 2024, 11:02 Т.Ж.
QML - Сабақ 016. SQLite деректер қоры және онымен QML Qt-та жұмыс істеу Здравствуйте, возникает такая проблема (я новичок): ApplicationWindow неизвестный элемент. (М300) для TextField и Button аналогично. Могу предположить, что из-за более новой верси…
Енді форумда талқылаңыз
Evgenii Legotckoi
Evgenii LegotckoiМаусым 24, 2024, 3:11 Т.Қ.
добавить qlineseries в функции Я тут. Работы оень много. Отправил его в бан.
t
tonypeachey1Қар. 15, 2024, 6:04 Т.Ж.
google domain [url=https://google.com/]domain[/url] domain [http://www.example.com link title]
NSProject
NSProjectМаусым 4, 2022, 3:49 Т.Ж.
Всё ещё разбираюсь с кешем. В следствии прочтения данной статьи. Я принял для себя решение сделать кеширование свойств менеджера модели LikeDislike. И так как установка evileg_core для меня не была возможна, ибо он писался…
9
9AnonimҚаз. 25, 2024, 9:10 Т.Ж.
Машина тьюринга // Начальное состояние 0 0, ,<,1 // Переход в состояние 1 при пустом символе 0,0,>,0 // Остаемся в состоянии 0, двигаясь вправо при встрече 0 0,1,>…

Бізді әлеуметтік желілерде бақылаңыз