Способы создания пользовательских компонентов в QML

от автора

Введение

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

Также одним из условий будет, чтобы внешний вид полученных элементов можно было редактировать в QtCreator.

Все исходники в репозитории.

Критерии сравнения

Сравнивать разные способы будем по следующим критериям:

  • Сложность реализации (моя субъективная оценка)

  • Использование в QtCreator в режиме Design

  • Производительность в приложении

Окружение

Для примеров буду использовать следующий софт: ОС Ubuntu 24.04, Qt 6.8.2, QtCreator 16.0.0.

Сборка и установка модуля QML с «Circles»

Чтобы использовать все варианты рисовки в QtCreator, нужно собрать QML модуль.

В репозитории он расположен в директории src/circles.

Для работы модуля в QtCreator достаточно скопировать файлы из директории сборки в директорию с QML модулями в директории с окружением Qt. Пример пути до директории установки — «~/Qt/6.8.2/gcc_64/qml/».

Для копирования только нужных файлов я добавил в CMake модуля секцию install.

CMakeLists.txt QML модуля Circles
cmake_minimum_required(VERSION 3.16)  project(circles VERSION 0.1 LANGUAGES CXX)  set(CMAKE_CXX_STANDARD_REQUIRED ON)  find_package(Qt6 REQUIRED COMPONENTS Quick Core5Compat ShaderTools)  qt_standard_project_setup(REQUIRES 6.5)  qt6_add_qml_module(Circles     URI Circles     VERSION 1.0     QML_FILES         Circle1.qml         Circle2.qml         Circle3.qml         Circle4.qml         Circle5.qml     RESOURCES         circle_fill.png         circle_border.png     SOURCES         circle6.h         circle6.cpp         circle7.h         circle7.cpp )  qt6_add_shaders(Circles "Circles"     PREFIX         "/qt/qml/Circles"     FILES         "circle5.frag" )  target_link_libraries(Circles     PRIVATE Qt6::Quick Qt6::Core5Compat )  install(     FILES         ${CMAKE_BINARY_DIR}/qmldir         ${CMAKE_BINARY_DIR}/Circles.qmltypes         ${CMAKE_BINARY_DIR}/Circle1.qml         ${CMAKE_BINARY_DIR}/Circle2.qml         ${CMAKE_BINARY_DIR}/Circle3.qml         ${CMAKE_BINARY_DIR}/Circle4.qml         ${CMAKE_BINARY_DIR}/Circle5.qml         ${CMAKE_BINARY_DIR}/circle_border.png         ${CMAKE_BINARY_DIR}/circle_fill.png         ${CMAKE_BINARY_DIR}/${CMAKE_SHARED_LIBRARY_PREFIX}Circles${CMAKE_SHARED_LIBRARY_SUFFIX}         ${CMAKE_BINARY_DIR}/${CMAKE_SHARED_LIBRARY_PREFIX}Circlesplugin${CMAKE_SHARED_LIBRARY_SUFFIX}     DESTINATION         ${CMAKE_PREFIX_PATH}/qml/Circles ) 

Сборку и установку модуля можно произвести так:

Открыть проект в QtCreator, произвести настройку проекта в Release, далее из директории теневой сборки (build/Desktop_Qt_6_8_2-Release) выполнить команды:

Сборку и установку модуля можно произвести так: открыть проект в QtCreator, произвести настройку проекта в Release, далее из директории теневой сборки (build/Desktop_Qt_6_8_2-Release) выполнить команды:

# переход в директорию теневой сборки cd build/Desktop_Qt_6_8_2-Release/ # сборка cmake --build . # копирование файлов в директорию Qt/6.8.2/gcc_64/qml, чтобы использовать свой модель в QtCreator cmake --install .

Важный момент!

Чтобы QtCreator смог загрузить модуль, нужно собирать модуль компилятором таким же как у самого QtCreator (важно какой это компилятор и его разрядность).

Это можно проверить в окне About Qt Creator.

Убедитесь, что собираете QML модуль соответствующей версией компилятора

Убедитесь, что собираете QML модуль соответствующей версией компилятора

Под Linux это обычно GCC x86_64, а под Windows MSVC 32bit.

Если под Windows используется MINGW, то собрать модуль для QtCreator им не получится.

Способы рисования

Тестовый пример

Создадим пользовательский элемент — «Circle«.

Он должен вписываться в границы элемента, и иметь следующие настройки:

  • цвет заливки

  • цвет окружности (границы)

  • толщину окружности

Окно QtCreator в режиме Design и свойства объекта типа Circle

Окно QtCreator в режиме Design и свойства объекта типа Circle

Способ №1

Использовать другие компоненты QML как дочерние элементы.

Самый простой и часто используемый в примерах компонент Qt QuickRectangle.

Он легко превращается в круг, если в его свойство radius задать значение побольше.

Circle1.qml
import QtQuick  Item {     id: root      // Устанавливаем ширину и высоту корневого элемента.     width: 100     height: 100      // Определяем пользовательские свойства:     // color - цвет заливки круга (по умолчанию "silver").     // borderColor - цвет границы круга (по умолчанию "black").     // borderWidth - толщина границы круга (по умолчанию 5).     property color color: "silver"     property color borderColor: "black"     property real borderWidth: 5      Rectangle {         id: view          // Радиус круга равен минимальному значению из ширины и высоты корневого элемента.         // Это гарантирует, что круг будет вписан в прямоугольник корневого элемента.         radius: Math.min(root.width, root.height)          // Ширина и высота прямоугольника равны радиусу, чтобы он был идеально круглым.         width: view.radius         height: view.radius          // Центрируем круг внутри корневого элемента.         // Вычисляем смещение по осям X и Y так, чтобы круг находился точно по центру.         x: (root.width - view.radius) / 2         y: (root.height - view.radius) / 2          // Устанавливаем цвет заливки круга, используя пользовательское свойство color.         color: root.color          // Определяем свойства границы круга:         // width - толщина границы, берется из пользовательского свойства borderWidth.         // color - цвет границы, берется из пользовательского свойства borderColor.         border {             width: root.borderWidth             color: root.borderColor         }     } } 

Сложность использования: Легко

Способ №2

Использовать Canvas.

В отличии от предыдущего способа, этот скорее императивный, чем декларативный.

Context2D API реализует стандарт W3C Canvas 2D Context API.

Circle2.qml
import QtQuick  Item {     id: root      // Устанавливаем ширину и высоту корневого элемента.     width: 100     height: 100      // Определяем пользовательские свойства:     // color - цвет заливки круга (по умолчанию "silver").     // borderColor - цвет границы круга (по умолчанию "black").     // borderWidth - толщина границы круга (по умолчанию 5).     property color color: "silver"     property color borderColor: "black"     property real borderWidth: 5      // Обработчики изменений свойств:     // При изменении ширины или высоты корневого элемента вызываем метод requestPaint(),     // чтобы перерисовать содержимое Canvas.     onWidthChanged: view.requestPaint()     onHeightChanged: view.requestPaint()      // При изменении пользовательских свойств (цвета, толщины границы, цвета границы)     // также вызываем requestPaint() для обновления отображения.     onColorChanged: view.requestPaint()     onBorderWidthChanged: view.requestPaint()     onBorderColorChanged: view.requestPaint()      // Вызываем requestPaint() при завершении инициализации компонента,     // чтобы гарантировать первоначальную отрисовку.     Component.onCompleted: view.requestPaint()      // Элемент Canvas используется для рисования круга.     Canvas {         id: view          // Ширина и высота Canvas равны размерам корневого элемента.         width: root.width         height: root.height          // Вычисляем координаты центра Canvas.         property real centerX: width / 2         property real centerY: height / 2          // Радиус круга вычисляется как минимальное значение из половины ширины и высоты,         // с учетом отступа для границы (root.borderWidth / 2).         property real radius: Math.min(view.centerX, view.centerY) - root.borderWidth / 2          // Метод onPaint вызывается каждый раз, когда требуется перерисовать содержимое Canvas.         onPaint: {             // Получаем контекст 2D-графики для рисования.             var ctx = getContext("2d");              // Начинаем новый путь для рисования.             ctx.beginPath();              // Рисуем окружность с центром в (centerX, centerY) и радиусом radius.             ctx.arc(view.centerX, view.centerY, view.radius, 0, 2 * Math.PI, false);              // Задаем цвет заливки круга и выполняем заливку.             ctx.fillStyle = root.color;             ctx.fill();              // Задаем толщину линии границы и цвет границы, затем рисуем границу.             ctx.lineWidth = root.borderWidth;             ctx.strokeStyle = root.borderColor;             ctx.stroke();         }     } } 

Сложность использования: Средне

Способ №3

Использовать спрайты.

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

Менять толщину окружности при использовании такого способа не получится.

Есть пара моментов для Qt6.

  1. В Qt6 нет нативной реализации ColorOverlay и нужно подключать этот компонент через import Qt5Compat.GraphicalEffects. Для этого нужно установить Qt5 Compatibility Module.

  2. В Qt6 для CMake появилась удобная функция qt6_add_qml_module. С её помощью можно сформировать файлы для QML модуля. Для добавления самих спрайтов можно использовать список ресурсов RESOURCES.

Qt5 Compatibility Module в списке устанавливаемых компонентов

Qt5 Compatibility Module в списке устанавливаемых компонентов
Circle3.qml
import QtQuick import Qt5Compat.GraphicalEffects  Item {     id: root      // Устанавливаем ширину и высоту корневого элемента.     width: 100     height: 100      // Определяем пользовательские свойства:     // color - цвет заливки круга (по умолчанию "silver").     // borderColor - цвет границы круга (по умолчанию "black").     property color color: "silver"     property color borderColor: "black"      // Используем изображение для создания формы внутренней части круга.     Image {         id: image_fill          // Заполняем родительский элемент (корневой Item).         anchors.fill: parent          // Указываем путь к изображению, которое представляет форму внутренней части круга.         source: "circle_fill.png"          // Режим отображения изображения: сохраняем пропорции и масштабируем,         // чтобы изображение вписывалось в доступную область.         fillMode: Image.PreserveAspectFit     }      // Применяем эффект ColorOverlay для изменения цвета внутренней части круга.     ColorOverlay {         // Заполняем область изображения image_fill.         anchors.fill: image_fill          // Указываем источником изображение image_fill.         source: image_fill          // Устанавливаем цвет заливки, используя пользовательское свойство color.         color: root.color     }      // Используем изображение для создания формы границы круга.     Image {         id: image_border          // Заполняем родительский элемент (корневой Item).         anchors.fill: parent          // Указываем путь к изображению, которое представляет форму границы круга.         source: "circle_border.png"          // Режим отображения изображения: сохраняем пропорции и масштабируем,         // чтобы изображение вписывалось в доступную область.         fillMode: Image.PreserveAspectFit     }      // Применяем эффект ColorOverlay для изменения цвета границы круга.     ColorOverlay {         // Заполняем область изображения image_border.         anchors.fill: image_border          // Указываем источником изображение image_border.         source: image_border          // Устанавливаем цвет границы, используя пользовательское свойство borderColor.         color: root.borderColor     } } 

Сложность использования: Средне

Способ №4

Использовать Qt Quick Shapes.

Вот некоторая информация из официальной документации.

Qt Quick Shapes рисует фигуры из треугольников и выводит их на GPU. Поэтому изменение контрольных точек элементов приведет к повторной триангуляции затронутых кривых, что приведет к некоторому снижению производительности. Кроме того, кривые сглаживаются перед визуализацией, поэтому применение очень высокого масштаба к фигуре может привести к появлению артефактов там, где видно, что кривые представлены последовательностью более мелких прямых линий.

Circle4.qml
import QtQuick import QtQuick.Shapes  Item {     id: root      // Устанавливаем ширину и высоту корневого элемента.     width: 100     height: 100      // Определяем пользовательские свойства:     // color - цвет заливки круга (по умолчанию "silver").     // borderColor - цвет границы круга (по умолчанию "black").     // borderWidth - толщина границы круга (по умолчанию 5).     property color color: "silver"     property color borderColor: "black"     property real borderWidth: 5      // Используем элемент Shape для рисования круга.     Shape {         // Заполняем родительский элемент (корневой Item).         anchors.fill: parent          // Элемент ShapePath используется для определения пути рисования.         ShapePath {             id: circlePath              // Устанавливаем цвет заливки круга, используя пользовательское свойство color.             fillColor: root.color              // Устанавливаем цвет границы круга, используя пользовательское свойство borderColor.             strokeColor: root.borderColor              // Устанавливаем толщину границы круга, используя пользовательское свойство borderWidth.             strokeWidth: root.borderWidth              // Элемент PathAngleArc используется для рисования дуги или окружности.             PathAngleArc {                 // Координаты центра окружности вычисляются как половина ширины и высоты корневого элемента.                 centerX: root.width / 2                 centerY: root.height / 2                  // Радиусы по осям X и Y вычисляются как половина ширины и высоты корневого элемента.                 // Это гарантирует, что круг будет идеально вписан в доступную область.                 radiusX: root.width / 2                 radiusY: root.height / 2                  // Угол начала дуги (0 градусов).                 startAngle: 0                  // Угол разворота дуги (360 градусов), чтобы нарисовать полную окружность.                 sweepAngle: 360             }         }     } } 

Сложность использования: Средне

Способ №5

Использование ShaderEffect.

Для рисования круга можно не использовать вершинный шейдер и обойтись только лишь фрагментным.

Для добавления кода шейдера в проект в Qt6 можно использовать CMake функцию qt6_add_shaders.

При использовании функции qt6_add_shaders инструмент Qt Shader Baker будет автоматически вызван системой сборки, и полученные файлы .qsb будут неявно добавлены в систему ресурсов.

circle5.frag
#version 440 // Указываем версию GLSL (OpenGL Shading Language), которую используем.  // Входные текстурные координаты (qt_TexCoord0) передаются из вершинного шейдера. // Они используются для определения положения текселя (текстурного пикселя). layout(location = 0) in vec2 qt_TexCoord0;  // Выходной цвет фрагмента (пикселя). Это значение будет записано в буфер кадра. layout(location = 0) out vec4 fragColor;  // Служебные вещи, которые нужно обязательно указать // Блок uniform содержит данные, которые могут быть установлены извне (например, из OpenGL или Qt). // std140 указывает на способ выравнивания данных в памяти. layout(std140, binding = 0) uniform buf {     mat4 qt_Matrix;       // Матрица преобразования (не используется в данном коде).     vec4 color;           // Цвет внутренней части круга.     vec4 borderColor;     // Цвет границы (окружности).     float borderWidth;    // Ширина границы в нормализованных координатах. };  // Основная функция шейдера, которая выполняется для каждого фрагмента. void main() {      // Нормализуем координаты от (0, 0) до (1, 1) и центрируем их     vec2 uv = qt_TexCoord0.st - vec2(0.5, 0.5);     // Переносим текстурные координаты так, чтобы центр круга находился в точке (0.5, 0.5).     // Это упрощает дальнейшие вычисления расстояния.      // Вычисляем расстояние от центра до текущего пикселя     float dist = length(uv);     // Функция length вычисляет евклидово расстояние от центра (0.5, 0.5) до текущей точки (uv.x, uv.y).      // Рисуем круг     if (dist > 0.5) {         // Если расстояние больше 0.5 (радиус круга), то текущий фрагмент находится за пределами круга.         // Устанавливаем прозрачный цвет (RGBA = 0, 0, 0, 0).         fragColor = vec4(0.0); // Прозрачный фон      } else if (dist > 0.5 - borderWidth) {         // Если расстояние находится в диапазоне между 0.5 и 0.5 - borderWidth,         // то текущий фрагмент находится на границе круга.         // Устанавливаем цвет границы (borderColor).         fragColor = borderColor; // Используем цвет окружности      } else {         // Если расстояние меньше или равно 0.5 - borderWidth,         // то текущий фрагмент находится внутри круга.         // Устанавливаем цвет внутренней части круга (color).         fragColor = color; // Используем цвет круга     } } 

Circle5.qml
import QtQuick  Item {     id: root      // Устанавливаем ширину и высоту корневого элемента.     width: 100     height: 100      // Определяем пользовательские свойства для настройки внешнего вида круга:     // color - цвет заливки круга (по умолчанию "silver").     // borderColor - цвет границы круга (по умолчанию "black").     // borderWidth - толщина границы круга (по умолчанию 5).     property color color: "silver"        // Цвет заливки круга.     property color borderColor: "black"   // Цвет границы круга.     property real borderWidth: 5          // Толщина границы круга.      // Используем ShaderEffect для рисования круга с помощью шейдера.     ShaderEffect {         id: circleEffect          // Центрируем ShaderEffect внутри корневого элемента.         anchors.centerIn: root          // Устанавливаем ширину и высоту ShaderEffect равными минимальному значению         // из ширины и высоты корневого элемента, чтобы круг был идеально круглым.         width: Math.min(root.width, root.height)         height: width          // Включаем смешивание цветов (blending), чтобы граница круга корректно         // накладывалась на фон.         blending: true          // Определяем свойства, которые будут передаваться в шейдер:         // color - цвет заливки круга, берется из корневого элемента.         // borderWidth - толщина границы круга, нормализованная относительно ширины.         // borderColor - цвет границы круга, берется из корневого элемента.         property color color: root.color         property color borderColor: root.borderColor         property real borderWidth: root.borderWidth / width  // Нормализуем толщину границы.          // Указываем путь к файлу фрагментного шейдера, который будет использоваться         // для рисования круга. Файл должен быть предварительно скомпилирован в формат QSB.         fragmentShader: "circle5.frag.qsb"     } } 

Сложность использования: Сложно

Способ №6

Реализовать новый тип от класса QQuickPaintedItem.

Пример из официальной документации

QPainter — проверенный временем инструмент. Существует очень давно (в Qt3 уже был). Знакомый для многих функционал, который можно использовать и в контексте QML.

circle6.h
#ifndef CIRCLE6_H #define CIRCLE6_H  #include <QObject> #include <QQmlEngine> #include <QQuickPaintedItem>  // Объявление класса Circle6, который наследуется от QQuickPaintedItem. // Это позволяет использовать QPainter для рисования содержимого элемента. class Circle6 : public QQuickPaintedItem {     // Макрос, необходимый для использования сигналов и слотов в классе.     Q_OBJECT      // Регистрация типа как доступного для QML.     QML_ELEMENT      // Объявление пользовательских свойств, которые будут доступны в QML:     // color - цвет заливки круга.     // borderColor - цвет границы круга.     // borderWidth - толщина границы круга.     Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY colorChanged)     Q_PROPERTY(QColor borderColor READ borderColor WRITE setBorderColor NOTIFY borderColorChanged)     Q_PROPERTY(qreal borderWidth READ borderWidth WRITE setBorderWidth NOTIFY borderWidthChanged)  public:     // Конструктор класса. Принимает указатель на родительский элемент.     explicit Circle6(QQuickItem *parent = nullptr);      // Возвращает текущий цвет заливки круга.     QColor color() const;      // Устанавливает новый цвет заливки круга.     void setColor(const QColor &color);      // Возвращает текущий цвет границы круга.     QColor borderColor() const;      // Устанавливает новый цвет границы круга.     void setBorderColor(const QColor &borderColor);      // Возвращает текущую толщину границы круга.     qreal borderWidth() const;      // Устанавливает новую толщину границы круга.     void setBorderWidth(qreal borderWidth);      // Переопределение метода paint для рисования содержимого элемента.     // Этот метод вызывается автоматически при необходимости перерисовки элемента.     void paint(QPainter *painter) override;  signals:     // Сигналы, которые уведомляют об изменении свойств:     // colorChanged - изменился цвет заливки.     // borderColorChanged - изменился цвет границы.     // borderWidthChanged - изменилась толщина границы.     void colorChanged(const QColor &);     void borderColorChanged(const QColor &);     void borderWidthChanged(qreal);  private:     // Приватные поля для хранения значений свойств:     QColor m_color;        // Цвет заливки круга.     QColor m_borderColor;  // Цвет границы круга.     qreal m_borderWidth;   // Толщина границы круга. };  #endif // CIRCLE6_H 

circle6.cpp
#include "circle6.h"  #include <QBrush> #include <QPainter>  // Конструктор класса Circle6. Вызывается при создании объекта. Circle6::Circle6(QQuickItem *parent):     QQuickPaintedItem{ parent } // Инициализация базового класса QQuickPaintedItem {     // Установка начального размера круга (100x100 пикселей).     setSize(QSizeF{ 100, 100 });      // Установка начального цвета заливки круга (серый).     setColor(QColor{ "silver" });      // Установка начального цвета границы круга (черный).     setBorderColor(QColor{ "black" });      // Установка начальной толщины границы круга (5 пикселей).     setBorderWidth(5); }  // Метод для получения текущего цвета заливки круга. QColor Circle6::color() const {     return m_color; // Возвращаем значение приватного поля m_color. }  // Метод для установки нового цвета заливки круга. void Circle6::setColor(const QColor &color) {     m_color = color; // Обновляем значение приватного поля m_color.     emit colorChanged(color); // Генерируем сигнал об изменении цвета заливки.     update(); // Запрашиваем перерисовку элемента. }  // Метод для получения текущего цвета границы круга. QColor Circle6::borderColor() const {     return m_borderColor; // Возвращаем значение приватного поля m_borderColor. }  // Метод для установки нового цвета границы круга. void Circle6::setBorderColor(const QColor &borderColor) {     m_borderColor = borderColor; // Обновляем значение приватного поля m_borderColor.     emit borderColorChanged(borderColor); // Генерируем сигнал об изменении цвета границы.     update(); // Запрашиваем перерисовку элемента. }  // Метод для получения текущей толщины границы круга. qreal Circle6::borderWidth() const {     return m_borderWidth; // Возвращаем значение приватного поля m_borderWidth. }  // Метод для установки новой толщины границы круга. void Circle6::setBorderWidth(qreal borderWidth) {     m_borderWidth = borderWidth; // Обновляем значение приватного поля m_borderWidth.     emit borderWidthChanged(borderWidth); // Генерируем сигнал об изменении толщины границы.     update(); // Запрашиваем перерисовку элемента. }  // Метод для отрисовки круга. void Circle6::paint(QPainter *painter) {     // Устанавливаем кисть (brush) для заливки круга заданным цветом.     painter->setBrush(m_color);      // Устанавливаем перо (pen) для рисования границы круга с заданным цветом и толщиной.     painter->setPen(QPen{ m_borderColor, m_borderWidth });      // Получаем текущий размер элемента.     QSizeF itemSize = size();      // Рисуем эллипс (круг), учитывая толщину границы.     // Координаты и размеры эллипса рассчитываются так, чтобы граница не выходила за пределы элемента.     painter->drawEllipse(         m_borderWidth, // Начальная координата X (с учетом толщины границы).         m_borderWidth, // Начальная координата Y (с учетом толщины границы).         itemSize.width() - m_borderWidth * 2, // Ширина эллипса (учитываем границу с обеих сторон).         itemSize.height() - m_borderWidth * 2 // Высота эллипса (учитываем границу с обеих сторон).     ); } 

Сложность использования: Средне

Способ №7

Реализовать новый тип от класса QQuickItem.

Пример из официальной документации

Используется в базовых объектах Qt Quick (Исходники Rectangle)

Более производительный по сравнению с QPainter, но при этом более сложный.
Используется класс QSGNode.

circle7.h
#ifndef CIRCLE7_H #define CIRCLE7_H  #include <QObject> #include <QQuickItem> #include <QSGGeometryNode>  // Объявление класса Circle7, который наследуется от QQuickItem. // Этот класс использует QSGNode для рендеринга круга. class Circle7 : public QQuickItem {      // Макрос, необходимый для использования сигналов и слотов в классе.     Q_OBJECT      // Регистрация типа как доступного для QML.     QML_ELEMENT      // Объявление пользовательских свойств, которые будут доступны в QML:     // color - цвет заливки круга.     // borderColor - цвет границы круга.     // borderWidth - толщина границы круга.     Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY colorChanged)     Q_PROPERTY(QColor borderColor READ borderColor WRITE setBorderColor NOTIFY borderColorChanged)     Q_PROPERTY(qreal borderWidth READ borderWidth WRITE setBorderWidth NOTIFY borderWidthChanged)  public:     // Конструктор класса. Принимает указатель на родительский элемент.     explicit Circle7(QQuickItem *parent = nullptr);      // Возвращает текущий цвет заливки круга.     QColor color() const;      // Устанавливает новый цвет заливки круга.     void setColor(const QColor &color);      // Возвращает текущий цвет границы круга.     QColor borderColor() const;      // Устанавливает новый цвет границы круга.     void setBorderColor(const QColor &borderColor);      // Возвращает текущую толщину границы круга.     qreal borderWidth() const;      // Устанавливает новую толщину границы круга.     void setBorderWidth(qreal borderWidth);  protected:     // Переопределение метода updatePaintNode для управления узлами QSGNode.     // Этот метод вызывается автоматически при необходимости обновления графического представления элемента.     QSGNode *updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *) override;  signals:     // Сигналы, которые уведомляют об изменении свойств:     // colorChanged - изменился цвет заливки.     // borderColorChanged - изменился цвет границы.     // borderWidthChanged - изменилась толщина границы.     void colorChanged(const QColor &);     void borderColorChanged(const QColor &);     void borderWidthChanged(qreal);  private:     // Приватные поля для хранения значений свойств:     QColor m_color;        // Цвет заливки круга.     QColor m_borderColor;  // Цвет границы круга.     qreal m_borderWidth;   // Толщина границы круга.      // Вспомогательный метод для обновления геометрии круга.     // Принимает узел геометрии, цвет и радиус круга.     void updateCircleGeometry(QSGGeometryNode *node, const QColor &color, float radius); };  #endif // CIRCLE7_H 

circle7.cpp
#include "circle7.h"  #include <QSGFlatColorMaterial> #include <GL/gl.h>  // Количество вершин для аппроксимации круга (чем больше, тем более плавный круг). const int numVertices = 32;  // Статические таблицы для предвычисленных значений sin и cos. // Эти таблицы позволяют избежать повторного вычисления тригонометрических функций в реальном времени. static float precomputedCos[numVertices]; static float precomputedSin[numVertices];  // Структура для инициализации статических таблиц при запуске программы. struct StaticInitializer {     StaticInitializer() {         // Заполняем таблицы значениями sin и cos для углов, равномерно распределенных по окружности.         for (int i = 0; i < numVertices; ++i) {             float angle = 2 * M_PI * i / numVertices; // Угол в радианах для текущей вершины.             precomputedCos[i] = cos(angle); // Вычисляем косинус угла.             precomputedSin[i] = sin(angle); // Вычисляем синус угла.         }     } };  // Создаем объект инициализации, который выполнится при запуске программы. // Это гарантирует, что таблицы будут заполнены до использования. static StaticInitializer staticInitializer;  // Конструктор класса Circle7. Вызывается при создании объекта. Circle7::Circle7(QQuickItem *parent):     QQuickItem{ parent } // Инициализация базового класса QQuickItem. {     // Устанавливаем начальный размер круга (100x100 пикселей).     setSize(QSizeF{ 100, 100 });      // Устанавливаем начальный цвет заливки круга (светло-серый).     setColor(QColor{ "silver" });      // Устанавливаем начальный цвет границы круга (черный).     setBorderColor(QColor{ "black" });      // Устанавливаем начальную толщину границы круга (5 пикселей).     setBorderWidth(5);      // Включаем флаг, указывающий, что элемент имеет собственное содержимое для отрисовки.     setFlag(ItemHasContents, true); }  // Метод для получения текущего цвета заливки круга. QColor Circle7::color() const {     return m_color; // Возвращаем значение приватного поля m_color. }  // Метод для установки нового цвета заливки круга. void Circle7::setColor(const QColor &color) {     m_color = color; // Обновляем значение приватного поля m_color.     emit colorChanged(color); // Генерируем сигнал об изменении цвета заливки.     update(); // Запрашиваем перерисовку элемента. }  // Метод для получения текущего цвета границы круга. QColor Circle7::borderColor() const {     return m_borderColor; // Возвращаем значение приватного поля m_borderColor. }  // Метод для установки нового цвета границы круга. void Circle7::setBorderColor(const QColor &borderColor) {     m_borderColor = borderColor; // Обновляем значение приватного поля m_borderColor.     emit borderColorChanged(borderColor); // Генерируем сигнал об изменении цвета границы.     update(); // Запрашиваем перерисовку элемента. }  // Метод для получения текущей толщины границы круга. qreal Circle7::borderWidth() const {     return m_borderWidth; // Возвращаем значение приватного поля m_borderWidth. }  // Метод для установки новой толщины границы круга. void Circle7::setBorderWidth(qreal borderWidth) {     m_borderWidth = borderWidth; // Обновляем значение приватного поля m_borderWidth.     emit borderWidthChanged(borderWidth); // Генерируем сигнал об изменении толщины границы.     update(); // Запрашиваем перерисовку элемента. }  // Метод для обновления графического узла (node), который используется для отрисовки. QSGNode *Circle7::updatePaintNode(QSGNode *oldNode, UpdatePaintNodeData *) {     auto rootNode = oldNode;      // Если корневой узел не существует, создаем его и добавляем дочерние узлы.     if (rootNode == nullptr) {         rootNode = new QSGNode; // Создаем корневой узел.          // Создаем два дочерних узла: один для внешней границы, другой для внутреннего круга.         auto outerNode = new QSGGeometryNode;         auto innerNode = new QSGGeometryNode;          // Добавляем дочерние узлы в корневой узел.         rootNode->appendChildNode(outerNode);         rootNode->appendChildNode(innerNode);     }      // Получаем ссылки на дочерние узлы (внешний и внутренний круги).     auto outerNode = static_cast<QSGGeometryNode *>(rootNode->childAtIndex(0));     auto innerNode = static_cast<QSGGeometryNode *>(rootNode->childAtIndex(1));      // Вычисляем радиус круга как половину минимальной стороны элемента.     const auto radius = qMin(width(), height()) / 2;      // Обновляем геометрию для внешнего круга (границы).     updateCircleGeometry(outerNode, m_borderColor, radius);      // Обновляем геометрию для внутреннего круга (основная заливка).     updateCircleGeometry(innerNode, m_color, radius - m_borderWidth);      return rootNode; // Возвращаем обновленный корневой узел. }  // Метод для обновления геометрии круга. void Circle7::updateCircleGeometry(QSGGeometryNode *node, const QColor &color, float radius) {     // Создаем новый объект геометрии для треугольников.     QSGGeometry *geometry = new QSGGeometry(QSGGeometry::defaultAttributes_Point2D(), numVertices * 3);      // Устанавливаем режим рисования как треугольники (GL_TRIANGLES).     geometry->setDrawingMode(GL_TRIANGLES);      // Привязываем геометрию к узлу и указываем, что узел владеет этой геометрией.     node->setGeometry(geometry);     node->setFlag(QSGNode::OwnsGeometry);      // Создаем материал для заливки цветом.     QSGFlatColorMaterial *material = new QSGFlatColorMaterial;     material->setColor(color); // Устанавливаем цвет материала.      // Привязываем материал к узлу и указываем, что узел владеет этим материалом.     node->setMaterial(material);     node->setFlag(QSGNode::OwnsMaterial);      // Получаем доступ к массиву вершин геометрии.     QSGGeometry::Point2D *vertices = geometry->vertexDataAsPoint2D();      // Вычисляем центр круга.     const QPointF center(width() / 2, height() / 2);      // Заполняем массив вершин для аппроксимации круга треугольниками.     for (int i = 0; i < numVertices; ++i) {         // Используем предвычисленные значения cos и sin из таблиц.         float cosAngle1 = precomputedCos[i];         float sinAngle1 = precomputedSin[i];         float cosAngle2 = precomputedCos[(i + 1) % numVertices];         float sinAngle2 = precomputedSin[(i + 1) % numVertices];          // Центр круга (первая вершина треугольника).         vertices[i * 3].set(center.x(), center.y());          // Первая точка на окружности (вторая вершина треугольника).         vertices[i * 3 + 1].set(center.x() + radius * cosAngle1,                                 center.y() + radius * sinAngle1);          // Вторая точка на окружности (третья вершина треугольника).         vertices[i * 3 + 2].set(center.x() + radius * cosAngle2,                                 center.y() + radius * sinAngle2);     }      // Помечаем геометрию как измененную для обновления.     node->markDirty(QSGNode::DirtyGeometry); } 

Сложность использования: Сложно

Использование в QtCreator в режиме Design

Всё 7 способов можно использовать в QtCreator в режиме Design.

Как было описано выше — для использования в Design нужно собрать и установить модуль Circles.

QtCreator в режиме Design cо сценой из 1024 кругов Circle1

QtCreator в режиме Design cо сценой из 1024 кругов Circle1

Сложности возникли только со способом №3 — использование спрайтов. Если во всех остальных способах получалось открыть сцену с 1024 кругами, то в случае с типом Circle3 QtCreator смог открыть только сцену с 400 кругами.

Результаты изменения производительности в приложении

Для сравнения производительности разных способов попробуем посчитать среднее число кадров в секунду на сложной сцене из кругов.

Для этих целей в репозитории есть приложение Benchmark (src/benchmark).

Оно работает следующим образом:

  • Принимает на вход параметры теста (какой из 7 компонентов использовать для рисования сцены, число кругов, длительность теста в секундах).

  • Создаёт сцену и ждёт указанное число секунд считая сколько кадров было нарисовано.

  • После истечения таймера делит общее число кадров на время теста и выводит в stdout.

В Benchmark будем генерировать разное число кругов разными способами. Чтобы дать нагрузку сделаем круги движущимся в произвольных направлениях.

Главный QML файл приложения Benchmark — Main.qml
import QtQuick import QtQuick.Window  import Circles  Window {     id: root      visible: true // Делаем окно видимым.     width: 1920 // Устанавливаем ширину окна.     height: 1080 // Устанавливаем высоту окна.     title: "Circles on Benchmark" // Заголовок окна.      // Контейнер для двигающихся объектов.     Item {         anchors.fill: parent // Растягиваем контейнер на всё окно.          // Repeater создаёт несколько экземпляров компонента.         Repeater {             model: $dataExchange && $dataExchange.objectCount // Количество объектов (передано из C++).              // Компонент, который будет двигаться.             // Используем Loader для динамической загрузки компонентов.             Loader {                 id: circleLoader                  // Определяем, какой компонент загружать, в зависимости от $objectType.                 sourceComponent: {                     switch ($dataExchange && $dataExchange.objectType) {                     case 1: return circle1Component; // Circle1                     case 2: return circle2Component; // Circle2                     case 3: return circle3Component; // Circle3                     case 4: return circle4Component; // Circle4                     case 5: return circle5Component; // Circle5                     case 6: return circle6Component; // Circle6                     case 7: return circle7Component; // Circle7                     default: return circle1Component; // По умолчанию Circle1.                     }                 }                  // Начальные координаты (случайные).                 x: Math.random() * (root.width - circleLoader.width)                 y: Math.random() * (root.height - circleLoader.height)                  // Скорость перемещения (случайная).                 property real velocityX: (Math.random() - 0.5) * 10 // Скорость по X.                 property real velocityY: (Math.random() - 0.5) * 10 // Скорость по Y.                  // Таймер для обновления позиции.                 Timer {                     id: updateTimer                     interval: 10 // Интервал обновления (в миллисекундах).                     running: true // Таймер запущен.                     repeat: true // Таймер повторяется.                      onTriggered: {                         // Обновляем позицию объекта.                         circleLoader.x += circleLoader.velocityX;                         circleLoader.y += circleLoader.velocityY;                          // Проверяем столкновение с границами окна.                         if (circleLoader.x <= 0 || circleLoader.x + circleLoader.width >= root.width) {                             circleLoader.velocityX *= -1; // Меняем направление по X.                         }                         if (circleLoader.y <= 0 || circleLoader.y + circleLoader.height >= root.height) {                             circleLoader.velocityY *= -1; // Меняем направление по Y.                         }                     }                 }             }         }     }      // Текстовая информация о типе объектов, их количестве и общем количестве кадров.     Text {         id: infoText         anchors.left: root.left // Привязываем текст к левому краю окна.         anchors.top: root.top // Привязываем текст к верхнему краю окна.         font.pointSize: 20 // Размер шрифта.          text: "Type: " + ($dataExchange && $dataExchange.objectType) + // Тип объектов.               "   Count: " + ($dataExchange && $dataExchange.objectCount) + // Количество объектов.               "   Total frame count: " + ($dataExchange && $dataExchange.totalFrameCount) // Общее количество кадров.     }      // Обработчик события переключения кадров.     onFrameSwapped: {         if ($dataExchange) {             $dataExchange.totalFrameCount++; // Увеличиваем счетчик кадров.         }     }      // Компоненты для каждого типа Circle.     Component {         id: circle1Component         Circle1 {             width: 50 // Ширина объекта.             height: 50 // Высота объекта.             color: Qt.rgba(Math.random(), Math.random(), Math.random(), 1) // Случайный цвет.         }     }      Component {         id: circle2Component         Circle2 {             width: 50             height: 50             color: Qt.rgba(Math.random(), Math.random(), Math.random(), 1)         }     }      Component {         id: circle3Component         Circle3 {             width: 50             height: 50             color: Qt.rgba(Math.random(), Math.random(), Math.random(), 1)         }     }      Component {         id: circle4Component         Circle4 {             width: 50             height: 50             color: Qt.rgba(Math.random(), Math.random(), Math.random(), 1)         }     }      Component {         id: circle5Component         Circle5 {             width: 50             height: 50             color: Qt.rgba(Math.random(), Math.random(), Math.random(), 1)         }     }      Component {         id: circle6Component         Circle6 {             width: 50             height: 50             color: Qt.rgba(Math.random(), Math.random(), Math.random(), 1)         }     }      Component {         id: circle7Component         Circle7 {             width: 50             height: 50             color: Qt.rgba(Math.random(), Math.random(), Math.random(), 1)         }     } } 

Для подсчёта общего числа кадров воспользуемся сигналом frameSwapped() компонента Window.

Через флаги Benchmark будем также передавать число секунд, которое длиться тест.

Для корректного отсчёта времени, таймер нужно запускать в отдельном потоке, иначе, когда сцена начнёт тормозить — это повлияет на работу таймера и мы получим искажённый результат.

Класс на C++ для обратного отсчёта времени работы программы
#ifndef COUNTDOWN_THREAD_H #define COUNTDOWN_THREAD_H  #include <QObject> #include <QThread> #include <QTimer>  // Класс CountdownThread представляет поток, который запускает таймер на заданное количество секунд. class CountdownThread : public QThread {     Q_OBJECT // Макрос для поддержки сигналов и слотов.  public:     // Конструктор класса. Принимает длительность таймера в секундах.     explicit CountdownThread(int durationSeconds):         m_durationSeconds{ durationSeconds } // Инициализация приватного поля m_durationSeconds.     {}  signals:     // Сигнал, который будет отправлен после истечения времени таймера.     void triggered();  protected:     // Переопределенный метод run(), который выполняется при запуске потока.     void run() override {         // Создаем объект QTimer для управления временем.         QTimer timer;          // Подключаем сигнал timeout() таймера к лямбда-функции.         connect(&timer, &QTimer::timeout, this, [this]() {             emit triggered(); // Генерируем сигнал triggered() при истечении времени.         });          // Запускаем таймер на заданное количество миллисекунд.         // Переводим секунды в миллисекунды, умножая на 1000.         timer.start(m_durationSeconds * 1000);          // Запускаем цикл обработки событий для текущего потока.         // Это необходимо для того, чтобы таймер мог работать внутри потока.         exec();     }  private:     int m_durationSeconds; // Приватное поле для хранения длительности таймера в секундах. };  #endif // COUNTDOWN_THREAD_H 

Имея общее число кадров и продолжительность теста вычислим среднее значение кадров в секунду и выведем его в stdout.

Работа приложения Benchmark

Работа приложения Benchmark

Постепенно увеличивая число элементов на сцене сравним среднее число кадров для разных способов.

Из полученных данных построим графики, где по горизонтали — число элементов, а по вертикали — среднее число кадров.

Среднее число FPS в зависимости от числа объектов на сцене для всех 7 способов

Среднее число FPS в зависимости от числа объектов на сцене для всех 7 способов

Итак — первые 3 места:

  1. Стандартные компоненты QtQuick.

  2. Qt Quick Shapes.

  3. Canvas.

Выводы

Готовые компоненты, предоставляемые Qt Quick, такие как Rectangle, Text и другие, являются наиболее производительными и удобными для использования. Эти компоненты просты в настройке и интеграции, что делает их идеальным выбором для большинства задач разработки интерфейса.

Если требуется отрисовка сложной геометрии (например, кривые, многоугольники или произвольные фигуры), можно рассмотреть использование модуля Qt Quick Shapes или элемента Canvas. Qt Quick Shapes предоставляет высокоуровневый API для отрисовки фигур, таких как эллипсы, пути и полилинии. Этот модуль обеспечивает хорошую производительность. Canvas подходит для рисования произвольных фигур с использованием API, похожего на HTML5 Canvas. Выбор между Shapes и Canvas зависит от требований к производительности и сложности геометрии.

Спрайты (например, изображения, загружаемые через Image) могут быть удобны для статических элементов, но их использование для динамических объектов или анимаций часто приводит к снижению производительности. Спрайты требуют дополнительных ресурсов для загрузки и отрисовки. Они менее гибкие в сравнении с векторными элементами. Рекомендуется минимизировать использование спрайтов и заменять их векторными элементами там, где это возможно.

Использование низкоуровневых механизмов, таких как QQuickItem с управлением графическими узлами (QSGNode), является мощным инструментом для создания высокопроизводительных пользовательских элементов. Однако такие подходы требуют глубокого понимания внутренней архитектуры Qt Quick. Разработка с использованием QSGNode может быть трудоемкой и подверженной ошибкам.

Выбор подхода зависит от конкретной задачи и требований к производительности. Для большинства приложений достаточно готовых компонентов Qt Quick и модуля Shapes. Спрайты и сложные низкоуровневые механизмы следует использовать только в тех случаях, когда другие подходы не обеспечивают нужного результата. Понимание деталей реализации и особенностей работы помогает принимать обоснованные решения и создавать эффективные приложения.


ссылка на оригинал статьи https://habr.com/ru/articles/894728/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *