Qt/c++ — урок 089. кнопки с абсолютным позиционированием внутри qgraphicsview
Содержание:
- Анимации
- Как попробовать Qt Quick 3D?
- Что насчет инструментов?
- Эпилог
- widget.cpp
- Будущие релизы
- mypoint.cpp
- figure.cpp
- Преамбула
- mygraphicview.cpp
- moveitem.h
- Система плагинов и архитектура приложения
- Численный метод против аналитического
- widget.cpp
- Конвертация проектов Qt 3D Studio
- Как насчет Qt 3D?
- Каков план продвижения вперед?
- QGraphicsView
- Муки выбора
Анимации
Анимации в Qt Quick 3D используют ту же систему анимации, что и Qt Quick. Вы можете привязать любое свойство к аниматору, и оно будет анимировано и обновлено, как ожидалось. Используя модуль
QtQuickTimeline
, также можно использовать анимации на основе ключевых кадров.
Как и компонентная модель, это еще один важный шаг в сокращении разрыва между 2D и 3D-сценами, поскольку здесь не используются отдельные, потенциально конфликтующие системы анимации.
В настоящее время нет поддержки фальсифицированных анимаций, но это планируется в будущем.
Как попробовать Qt Quick 3D?
Намерение состоит в том, чтобы выпустить Qt Quick 3D в качестве технического предварительного просмотра вместе с выпуском Qt 5.14. Между тем, его можно будет использовать уже сейчас, против Qt 5.12 и выше.
Что насчет инструментов?
Цель состоит в том, чтобы через Qt Design Studio можно было сделать все необходимое для настройки 3D-сцены. Это означает, что вы можете визуально разметить сцену, импортировать 3D asset’ы, такие как сетки, материалы и текстуры, и преобразовать эти asset’ы в эффективные форматы времени выполнения, используемые движком.
Демонстрация ранней интеграции Qt Design Studio для Qt Quick 3D.
Эпилог
Что касается рисования, то на этом все. А виджету следует добавить функциональности. В моем случае было добавлено булево поле «использовать ли состояние»», еще одно булево поле, определяющее состояние «Вкл» или «Выкл» и цвета по умолчанию для этих состояний, а также открытые геттеры и сеттеры для всего этого. Эти поля используются в функции paintEvent() для выбора цвета, передаваемого drawLed() в виде параметра. В результате можно отключить использование состояний и задавать «лампочке» любой цвет, а можно включить состояния и зажигать или гасить лампочку по событиям. Особенно удобно сделать сеттер состояния открытым слотом и соединить его с сигналом, который надо отслеживать.
Использование mousePressEvent демонстрирует, что виджет можно сделать не только индикатором, но и кнопкой, делая ее нажатой, отпущенной, гнутой, скрученной, раскрашенной и какой хотите еще по событиям наведения, нажатия и отпускания.
Но это уже не принципиально. Целью было показать, где можно взять образцы для подражания при прорисовке собственных виджетов и как эту прорисовку несложно реализовать без использования картинок растровых или векторных, в ресурсах или файлах.
widget.cpp
In this file are configured objects QGraphicsView,
QGraphicsScene
, and the triangle object is created and installed on the graphic scene.
#include "widget.h" #include "ui_widget.h" Widget::Widget(QWidget *parent) : QWidget(parent), ui(new Ui::Widget) { ui->setupUi(this); this->resize(600,600); // Set sizes of widget this->setFixedSize(600,600); // Fix sizes of widget scene = new QGraphicsScene(); // Init graphic scene triangle = new Triangle(); // Init Triangle ui->graphicsView->setScene(scene); // Set graphics scene into graphicsView ui->graphicsView->setRenderHint(QPainter::Antialiasing); ui->graphicsView->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); ui->graphicsView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); scene->setSceneRect(-250,-250,500,500); scene->addLine(-250,0,250,0,QPen(Qt::black)); // Add horizontal line via center scene->addLine(0,-250,0,250,QPen(Qt::black)); // Add vertical line via center scene->addItem(triangle); triangle->setPos(0,0); } Widget::~Widget() { delete ui; }
Будущие релизы
- Qt 5.9 — текущий релиз (long term supported — 3 года)
- Qt 5.9.2 — скоро
- Qt 5.10 — скоро бета, релиз в ноябре
- Qt 5.11 — май 2018
- Qt 5.12 — ноябрь 2018 (возможный кандидат следующего long term supported)
Плановая работа
- Улучшение производительности
- Распознавание речи (цифровой помощник?)
- AR/VR
- Инструменты IDE (рефакторинг, workflow)
C++14 & 17
Мы используем C++11 в той мере, в которой позволяет VS2013. Мы бы хотели использовать:
- if constexpr
- Initializers в if/switch
- Structured bindings
- Overaligned data
Уже используем , , , . Добавили QStringView.
C++20
Мог бы стать очень интересным для Qt: концепты, модули, рефлексия (может быть даже удалось бы избавиться от moc когда-нибудь в будущем).
mypoint.cpp
Самое главное, на что рекомендую обратить внимание в данном классе, так это на то, что мы переопределяем метод для вызова события нажатия на графический объект правой кнопкой мыши. И вызываем в данном методе СИГНАЛ.
#include "mypoint.h" MyPoint::MyPoint(QObject *parent) : QObject(parent), QGraphicsItem() { } MyPoint::~MyPoint() { } QRectF MyPoint::boundingRect() const { /* возвращаем координаты расположения точки * по ним будет определяться нажатие точки * */ return QRectF(0,0,100,100); } void MyPoint::paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget) { // Устанавливаем кисть в QPainter и отрисовываем круг, то есть ТОЧКУ painter->setBrush(Qt::black); painter->drawEllipse(QRectF(0, 0, 100, 100)); Q_UNUSED(option); Q_UNUSED(widget); } /* Переопределив метод перехвата события нажатия кнопки мыши, * добавляем посылку СИГНАЛА от объекта * */ void MyPoint::mousePressEvent(QGraphicsSceneMouseEvent *event) { emit signal1(); // Вызываем родительскую функцию события нажатия кнопки мыши QGraphicsItem::mousePressEvent(event); }
figure.cpp
#include "figure.h" #include <QPainter> Figure::Figure(QPointF point, QObject *parent) : QObject(parent), QGraphicsItem() { // Устанавливаем стартовую координату для отрисовки фигуры this->setStartPoint(mapFromScene(point)); this->setEndPoint(mapFromScene(point)); /* Подключаем сигнал изменения координат к слоту запуска обновления содержимого объекта * Сигнал и слот присутствуют в базовом классе * */ connect(this, &Figure::pointChanged, this, &Figure::updateRomb); } Figure::~Figure() { } QRectF Figure::boundingRect() const { /* Возвращаем область, в которой лежит фигура. * Обновляемая область зависит от стартовой точки отрисовки и от конечной точки * */ return QRectF((endPoint().x() > startPoint().x() ? startPoint().x() : endPoint().x()) - 5, (endPoint().y() > startPoint().y() ? startPoint().y() : endPoint().y()) - 5, qAbs(endPoint().x() - startPoint().x()) + 10, qAbs(endPoint().y() - startPoint().y()) + 10); } void Figure::updateRomb() { // Вызываем обновление области, в которой лежит фигура this->update((endPoint().x() > startPoint().x() ? startPoint().x() : endPoint().x()) - 5, (endPoint().y() > startPoint().y() ? startPoint().y() : endPoint().y()) - 5, qAbs(endPoint().x() - startPoint().x()) + 10, qAbs(endPoint().y() - startPoint().y()) + 10); } void Figure::setStartPoint(const QPointF point) { m_startPoint = mapFromScene(point); emit pointChanged(); } void Figure::setEndPoint(const QPointF point) { m_endPoint = mapFromScene(point); emit pointChanged(); } QPointF Figure::startPoint() const { return m_startPoint; } QPointF Figure::endPoint() const { return m_endPoint; }
Преамбула
Началось все с того, что понадобилась однажды индикация одноразрядных признаков. Некоторое приложение получает по некоторому порту некоторые данные, пакет надо разобрать и отобразить на экране. Хорошо бы при этом как-то имитировать привычную приборную лицевую панель. Для отображения цифровых данных Qt предлагает «из коробки» класс QLCDNumber, похожий на знакомые семисегментные индикаторы, а вот одиночных лампочек что-то не видно.
Использование флажков (они же check boxes) и переключателей (они же radio buttons) для этих целей плохо, и вот список причин:
- Это неправильно семантически. Кнопки — они и есть кнопки, и предназначены для ввода пользователем, а не для показа ему чего-либо.
- Отсюда вытекает второе: пользователь так и норовит тыкнуть в такие кнопки. Если при этом обновление информации не особенно быстрое, индикация будет врать, а пользователь — сообщать о неправильной работе программы, мерзко хихикая.
- Если заблокировать кнопку для нажатия (setEnabled(false)), то она становится некрасиво серой. Помнится, в Delphi, в районе версии 6, был такой финт ушами: можно было положить флажок на панель и отключить доступность панели, а не флажка, тогда флажок не был ни серым, ни активным. Тут такой фокус не проходит.
- Кнопки имеют фокус ввода. Соответственно, если в окне есть элементы ввода, и пользователь гуляет по ним с помощью клавиши «Tab», ему придется погулять и по элементам вывода, это неудобно и некрасиво.
- В конце концов, такие кнопки просто неэстетично смотрятся, особенно рядом с семисегментниками.
Вывод: надо рисовать лампочку самому.
mygraphicview.cpp
Для перерисовки объектов в
QGraphicsScene
эти самые объекты необходимо будет удалять, поэтому для удобства работы элементы этих объектов буду сгруппированы, а также будет написан метод для удаления всех элементов группы. Это удобно в том случае, если Вам необходимо перерисовать только один объект из нескольких, который состоит из ряда элементов.
#include "mygraphicview.h" MyGraphicView::MyGraphicView(QWidget *parent) : QGraphicsView(parent) { /* Немного поднастроим отображение виджета и его содержимого */ this->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); // Отключим скроллбар по горизонтали this->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); // Отключим скроллбар по вертикали this->setAlignment(Qt::AlignCenter); // Делаем привязку содержимого к центру this->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); // Растягиваем содержимое по виджету /* Также зададим минимальные размеры виджета * */ this->setMinimumHeight(100); this->setMinimumWidth(100); scene = new QGraphicsScene(); // Инициализируем сцену для отрисовки this->setScene(scene); // Устанавливаем сцену в виджет group_1 = new QGraphicsItemGroup(); // Инициализируем первую группу элементов group_2 = new QGraphicsItemGroup(); // Инициализируем вторую группу элементов scene->addItem(group_1); // Добавляем первую группу в сцену scene->addItem(group_2); // Добавляем вторую группу в сцену timer = new QTimer(); // Инициализируем Таймер timer->setSingleShot(true); // Подключаем СЛОТ для отрисовки к таймеру connect(timer, SIGNAL(timeout()), this, SLOT(slotAlarmTimer())); timer->start(50); // Стартуем таймер на 50 миллисекунд } MyGraphicView::~MyGraphicView() { } void MyGraphicView::slotAlarmTimer() { /* Удаляем все элементы со сцены, * если они есть перед новой отрисовкой * */ this->deleteItemsFromGroup(group_1); this->deleteItemsFromGroup(group_2); int width = this->width(); // определяем ширину нашего виджета int height = this->height(); // определяем высоту нашего виджета /* Устанавливаем размер сцены по размеру виджета * Первая координата - это левый верхний угол, * а Вторая - это правый нижний угол * */ scene->setSceneRect(0,0,width,height); /* Приступаем к отрисовке произвольной картинки * */ QPen penBlack(Qt::black); // Задаём чёрную кисть QPen penRed(Qt::red); // Задаём красную кисть /* Нарисуем черный прямоугольник * */ group_1->addToGroup(scene->addLine(20,20, width - 20, 20, penBlack)); group_1->addToGroup(scene->addLine(width - 20, 20, width - 20, height -20, penBlack)); group_1->addToGroup(scene->addLine(width - 20, height -20, 20, height -20, penBlack)); group_1->addToGroup(scene->addLine(20, height -20, 20, 20, penBlack)); /* Нарисуем красный квадрат * */ int sideOfSquare = (height > width) ? (width - 60) : (height - 60); int centerOfWidget_X = width/2; int centerOfWidget_Y = height/2; group_2->addToGroup(scene->addLine(centerOfWidget_X - (sideOfSquare/2), centerOfWidget_Y - (sideOfSquare/2), centerOfWidget_X + (sideOfSquare/2), centerOfWidget_Y - (sideOfSquare/2), penRed)); group_2->addToGroup(scene->addLine(centerOfWidget_X + (sideOfSquare/2), centerOfWidget_Y - (sideOfSquare/2), centerOfWidget_X + (sideOfSquare/2), centerOfWidget_Y + (sideOfSquare/2), penRed)); group_2->addToGroup(scene->addLine(centerOfWidget_X + (sideOfSquare/2), centerOfWidget_Y + (sideOfSquare/2), centerOfWidget_X - (sideOfSquare/2), centerOfWidget_Y + (sideOfSquare/2), penRed)); group_2->addToGroup(scene->addLine(centerOfWidget_X - (sideOfSquare/2), centerOfWidget_Y + (sideOfSquare/2), centerOfWidget_X - (sideOfSquare/2), centerOfWidget_Y - (sideOfSquare/2), penRed)); } /* Этим методом перехватываем событие изменения размера виджет * */ void MyGraphicView::resizeEvent(QResizeEvent *event) { timer->start(50); // Как только событие произошло стартуем таймер для отрисовки QGraphicsView::resizeEvent(event); // Запускаем событие родителького класса } /* Метод для удаления всех элементов из группы * */ void MyGraphicView::deleteItemsFromGroup(QGraphicsItemGroup *group) { /* Перебираем все элементы сцены, и если они принадлежат группе, * переданной в метод, то удаляем их * */ foreach( QGraphicsItem *item, scene->items(group->boundingRect())) { if(item->group() == group ) { delete item; } } }
moveitem.h
Для осуществления красивого перетаскивания графических объектов Нам понадобится использовать функции
mouseMoveEvent
,
mousePressEvent
и
mouseReleaseEvent
. В функции
mouseMoveEvent
будет производиться непосредственное перетаскивание графического объекта, а в двух других будет производиться смена внешнего вида курсора мыши, которые будет сигнализировать о том, что мы берём и отпускаем графический объект.
#ifndef MOVEITEM_H #define MOVEITEM_H #include <QObject> #include <QGraphicsItem> #include <QPainter> #include <QGraphicsSceneMouseEvent> #include <QDebug> #include <QCursor> class MoveItem : public QObject, public QGraphicsItem { Q_OBJECT public: explicit MoveItem(QObject *parent = 0); ~MoveItem(); signals: private: QRectF boundingRect() const; void paint(QPainter *painter, const QStyleOptionGraphicsItem *option, QWidget *widget); void mouseMoveEvent(QGraphicsSceneMouseEvent *event); void mousePressEvent(QGraphicsSceneMouseEvent *event); void mouseReleaseEvent(QGraphicsSceneMouseEvent *event); public slots: }; #endif // MOVEITEM_H
Система плагинов и архитектура приложения
1.1 Использование плагинов Qt
Наши плагины будут уметь выполнять 2 операции — возвращать свое имя (отображается в выпадающем списке методов интегрирования) и выполнять численное интегрирование. В результате интегрирования, плагин формирует 2 дополнительных вектора с координатами точек, используемых для визуализации.
Скомпилированные плагины помещаются в подкаталог plugins, в котором осуществляется их поиск после запуска приложения.
#include "mainform.h" #include <QApplication> int main(int argc, char *argv[]) { QApplication app(argc, argv); MainForm form; form.loadPlugins(); form.show(); app.exec(); }
Каждый загруженный плагин представляет собой объект с заданным интерфейсом, который должен быть заранее зарегистрирован макросом Q_DECLARE_INTERFACE.
#ifndef PLUGININTERFACE_H # define PLUGININTERFACE_H # include <functional> # include <QString> # include <QtPlugin> class PluginInterface { public: virtual ~PluginInterface() { } virtual QString text() const = 0; //!< возвращает текст для идентификации плагина virtual double calc(const std::function<double(double)>& f, const double a, const double b, const int n, QVector<double> &x, QVector<double> &y) = 0; //!< \brief функция интегрирования //!< интегрирует f на интервале //!< делит интервал на n частей //!< возвращает данные для визуализации }; Q_DECLARE_INTERFACE(PluginInterface, "com.pro-prof.PluginInterface") #endif // PLUGININTERFACE_H
Вторым аргументом Q_DECLARE_INTERFACE принимает строку, которая должна идентифицировать плагин. Макрос объявляет несколько шаблонных функций, в том числе qobject_cast, используемую для проверки соответствия загружаемого плагина нашему интерфейсу.
void MainForm::loadPlugins() { QDir dir(QApplication::applicationDirPath()); if (false == dir.cd("../plugins")) return; foreach (QString fileName, dir.entryList(QDir::Files)) { QPluginLoader loader(dir.absoluteFilePath(fileName)); QObject *plugin = loader.instance(); if (nullptr == plugin) continue; PluginInterface *pI = qobject_cast<PluginInterface*>(plugin); if (nullptr == pI) continue; m_ui->methods->addItem(pI->text(), (unsigned int) pI); } }
Для загрузки плагинов используется объект QPluginLoader, возвращающий указатель на загруженный плагин. В нашем примере после щелчка по кнопке «Считать» должен вызваться метод объекта того плагина, который пользователь выбрал в выпадающем списке. Хранить где-то список загруженных плагинов — было бы правильное и хорошее решение. Однако, в примере используется грязный трюк — адрес объекта плагина помещается в качестве «пользовательских данных» элемента выпадающего списка (второй аргумент метода addItem).
void MainForm::on_run() { // ... PluginInterface *pI = (PluginInterface*)m_ui->methods->currentData().toUInt(); // ... r = pI->calc(integfun, a, b, n, xp, yp); // ... }
В слоте-обработчике кнопки «Считать» адрес объекта плагина восстанавливается из данных, хранимых элементом выпадающего списка, а затем, используется как обычно.
Теперь мы умеем взаимодействовать с плагинами Qt — загружать их и обращаться к методам. Разберемся с созданием собственных плагинов.
1.2 Разработка своих плагинов Qt
Для плагина нет необходимости описывать точку входа (функцию main), однако, необходимо задать некоторые опции проекта и использовать пару макросов.
TEMPLATE = lib CONFIG += plugin
#ifndef LEFT_RECT_PLUGIN_H # define LEFT_RECT_PLUGIN_H # include "../app/plugininterface.h" # include <QObject> class Left_rect_plugin : public QObject, public PluginInterface { Q_OBJECT Q_PLUGIN_METADATA(IID "com.pro-prof.Left_rect_plugin") Q_INTERFACES(PluginInterface) public: explicit Left_rect_plugin(QObject *parent = 0); virtual QString text() const; virtual double calc(const std::function<double(double)>& f, const double a, const double b, const int n, QVector<double> &x, QVector<double> &y); }; #endif // LEFT_RECT_PLUGIN_H
Плагин использует множественное наследование — он является объектом Qt, но реализует заданный нами интерфейс. Макрос Q_INTERFACES определяет функцию qt_metacast, позволяющую системе связывать плагин с именем интерфейса. Макрос Q_PLUGIN_METADATA позволяет задавать файл с метаданными плагина (необязательный второй параметр, в нашем примере не используется) и настраивает экспорт плагина (выполняет работу устаревшего макроса Q_EXPORT_PLUGIN2).
Численный метод против аналитического
В компьютерной анимации есть всего несколько общих принципов, используемых практически во всех ситуациях. Сегодня мы поработаем с одним из методов анимации: расчёт по неким правилам движения (например, при симуляции законов физики). Мы разберём разницу между численным и аналитическим методом расчёта величие.
Численный метод: реализуем отталкивание от стенок
Чтобы шар отталкивался от стенок, мы будем проверять, что (левая, правая, верхняя, нижняя) граница шарика столкнулась с соответствующей границей окна. При столкновении мы будем менять скорость по одной из координат на обратную (симулируя отражение).
Сначала подготовимся: добавим структуру RectF, в которой будем хранить границы области, в которой двигается шарик. Создайте заголовочный файл “RectF.h” и перенесите в него реализацию структуры:
Теперь изменим определение класса PoolTableScene: добавим поля для хранения скорости шарика (которая теперь будет изменяться динамически), размера шарика (неизменного), границ сцены (неизменных).
Все величины будем инициализировать в конструкторе.
Чтобы передать размеры сцены при конструировании, изменим конструктор RasterWindow и выделим константы, хранящие размер окна:
Метод “PoolTableScene::update” станет сложнее: после пересчёта координат, хранимых в , мы должны проверить два случая
- левая граница шарика левее левой границы поля
- правая граница шарика правее правой границы поля
Наконец, мы изменим константы скорости шарика, чтобы быстрее увидеть эффект отталкивания:
Аналогичные проверки потребуются для верхней и нижней границ. Добавьте их, и будьте внимательны при замене переменных. Запустите программу и проверьте: шарик должен отталкиваться от всех 4 стенок
Данный метод расчёта является численным (или итеративным): новая позиция шарика вычисляется на основе только предыдущей позиции и скорости, без учёта начальных значений величин.
Аналитический метод: реализуем волновое движение
При использовании аналитического метода позиция шарика будет функцией от начальных значений величин и времени, прошедшего с начального момента. Изменим код так, чтобы вместо прямолинейного движения с отталкиванием от стенок шар двигался по синусоиде.
Удалите весь код из “PoolTableScene::update”. Затем удалите поле “m_ballSpeed”. Теперь добавим в PoolTableScene возможность учёта времени, прошедшего с начального момента.
- добавьте классу поле
- в начале метода “update” добавьте инструкцию
Внешний вид синусоиды показан ниже. Рисунок подсказывает простой способ анимации движения по синусоиде: возьмём время за t, тогда в каждый момент времени:
- , где
-
, где
- ampY — амплитуда (максимальное отклонение по оси Y)
- period — период, через который повторяется один шаг движения
Важный навык программиста — умение запрограммировать формулу, выраженную математически. Сейчас мы прокачаем этот навык!
Введём новые константы, параметризующие движение шарика, и уберём константу :
Добавим в конструктор PoolTableScene инициализацию “m_ballPosition”:
Используя универсальное конструирование и перегруженные операторы класса Vector2f, мы можем описать обновление координат одним выражением:
Соберите и запустите программу. Шарик должен двигаться с невысокой скоростью по синусоиде:
Задание cg2.2: волновое движение с отталкиванием
Задание: совместите отталкивание от стенок и волновое движение так, чтобы шарик двигался по синусоиде и отталкивался от левой и правой стенок.
Подсказка: реализовать симуляцию движения в данных условиях можно тремя способами — число аналитическим методом (используя деление по модулю), чисто численным (используя производные) и смешанным численно-аналитическим методом.
widget.cpp
В данном файле осуществляем косметическую настройку приложения — это касается конструктора класса. И производим создание графических объектов, которые будем перетаскивать на графической сцене. При этом объекты при создании располагаются в произвольном порядке.
#include "widget.h" #include "ui_widget.h" /* Функция для получения рандомного числа * в диапазоне от минимального до максимального * */ static int randomBetween(int low, int high) { return (qrand() % ((high + 1) - low) + low); } Widget::Widget(QWidget *parent) : QWidget(parent), ui(new Ui::Widget) { ui->setupUi(this); // Косметическая подготовка приложения this->resize(640,640); // Устанавливаем размеры окна приложения this->setFixedSize(640,640); scene = new QGraphicsScene(this); // Инициализируем графическую сцену scene->setItemIndexMethod(QGraphicsScene::NoIndex); // настраиваем индексацию элементов ui->graphicsView->resize(600,600); // Устанавливаем размер graphicsView ui->graphicsView->setScene(scene); // Устанавливаем графическую сцену в graphicsView ui->graphicsView->setRenderHint(QPainter::Antialiasing); // Настраиваем рендер ui->graphicsView->setCacheMode(QGraphicsView::CacheBackground); // Кэш фона ui->graphicsView->setViewportUpdateMode(QGraphicsView::BoundingRectViewportUpdate); scene->setSceneRect(0,0,500,500); // Устанавливаем размер сцены } Widget::~Widget() { delete ui; } void Widget::on_pushButton_clicked() { MoveItem *item = new MoveItem(); // Создаём графический элемента item->setPos(randomBetween(30, 470), // Устанавливаем случайную позицию элемента randomBetween(30,470)); scene->addItem(item); // Добавляем элемент на графическую сцену }
Конвертация проектов Qt 3D Studio
В дополнение к возможности генерировать компоненты 3D QML из инструментов создания 3D-asset’ов, также был создан плагин для инструмента импорта asset’ов для преобразования существующих проектов Qt 3D Studio. Если вы ранее использовали Qt 3D Studio, вы будете знать, что он генерирует проекты в формате XML для определения сцены. Если вы передадите инструменту balsam проект UIP или UIA, сгенерированный Qt 3D Studio, он также сгенерирует проект Qt Quick 3D на основе этого
Однако обратите внимание, что, поскольку среда выполнения, используемая Qt 3D Studio, отличается от Qt Quick 3D, не все будет преобразовано. Тем не менее, он должен дать хорошее приближенное значение или отправную точку для преобразования существующего проекта
Qt Company надеется продолжить улучшение поддержки этого пути, чтобы сгладить переход для существующих пользователей Qt 3D Studio.
Пример приложения Qt 3D Studio, портированного с помощью инструмента импорта Qt Quick 3D (это еще не идеально).
Как насчет Qt 3D?
Первый вопрос, который Qt Company ожидает получить, это почему бы просто не использовать Qt 3D? Этот вопрос, который они изучали последние пару лет.
Одно предположение состоит в том, что можно просто собрать весь Qt Quick поверх Qt 3D, если смешать 2D и 3D. Qt Company собирались и начали делать это с выпуском 2.3 Qt 3D Studio. Мощный API Qt 3D предоставил хорошую абстракцию для реализации движка рендеринга, чтобы воссоздать поведение, ожидаемое Qt Quick и Qt 3D Studio. Однако, архитектура Qt 3D затрудняет получение необходимой производительности на встроенном оборудовании начального уровня. Qt 3D также имеет определенные издержки из-за собственной ограниченной среды выполнения, а также из-за того, что является еще одним уровнем абстракции между Qt Quick и графическим оборудованием. В своей нынешней форме Qt 3D не идеален для дальнейшего развития, если они хотят достичь полностью унифицированной графической истории, обеспечивая при этом постоянную хорошую поддержку для широкого спектра платформ и устройств, начиная от низкого до высокого класса.
В то же время уже был движок рендеринга в Qt 3D Studio, который делал именно то, что нужно, и являлся хорошей основой для создания дополнительных функциональных возможностей. Это связано с недостатком, заключающимся в том, что у них больше нет мощных API, которые идут с Qt 3D, но на практике, когда вы начинаете создавать среду выполнения поверх Qt 3D, вы уже в конечном итоге принимаете решения о том, как все должно работать, что в любом случае приводит к ограниченной возможности настроить framegraph (граф кадра). В конце концов, наиболее практичным решением было использовать существующий движок рендеринга Qt 3D Studio в качестве базы и опираться на него.
Каков план продвижения вперед?
Этот релиз — только предварительный просмотр того, что должно быть. Планируется предоставить Qt Quick 3D, как полностью поддерживаемый модуль вместе с Qt 5.15 LTS. Тем временем Qt Company работает над дальнейшей разработкой Qt Quick 3D в качестве релиза Tech Preview с Qt 5.14.
Для серии Qt 5 они ограничены в том, насколько глубоко они могут комбинировать 2D и 3D из-за обещаний двоичной совместимости. С выпуском Qt 6 Qt Company планирует еще более глубокую интеграцию Qt Quick 3D в Qt Quick, чтобы обеспечить еще более плавную работу.
Цель в том, что они хотят быть максимально эффективными при смешивании 2D и 3D контента, без дополнительных затрат для пользователей, которые вообще не используют 3D контент. Они не будут делать ничего радикального, например, заставлять все приложения Qt Quick проходить через новый рендер, только те, которые смешивают 2D и 3D.
В Qt 6 также будет использоваться аппаратный интерфейс рендеринга Qt для рендеринга сцен Qt Quick (включая 3D), что должно устранить многие из текущих проблем, с которыми Qt Company сталкивается сегодня при развертывании приложений OpenGL (с использованием DirectX в Windows, Metal в macOS и т. д.).
Qt Company также желает, чтобы конечные пользователи могли использовать C ++ Rendering API, который создали более обобщённо, без Qt Quick. Код теперь, как у частного API, но они ждут времени Qt 6 (и портирования RHI), прежде чем делать обещания совместимости, которые идут с общедоступными API.
QGraphicsView
I’ll do at least one separate post on graphicsview alone, so I’ll just comment quickly on the difference between using with items vs ‘s. QGraphicsView with its scene populated with items is in many ways very similar to the widgets and their repaint handling. With the addition of layouts and the line is even more blurry. So which solution should you pick? More and more often, we’re seeing that people choose to create their UI’s in graphics view rather than creating them using traditional widgets.
Compared to widgets, items in a graphics view are very cheap. If we consider the photo gallery again, then using a separate item for each of the items in the view may (I say may) be reasonable. A widget is repainted through its . A is repainted through its function. The good thing with the items function is that there is no as the painter is already properly set up for rendering. Another good thing is that the painter has less guaranteed state than the in the widget case. There may be a transformation and some clip, but no guarantees about fonts, pens or brushes. This makes the setup a bit cheaper.
Another huge improvement over widgets is that items are not clipped by default. They have a bounding rectangle and there is a contract between the subclass implementer and the scene that the item does not paint outside. If we compare this to the system clip we need to set for widgets, then again there is less work to be done for the items. If the item violates this there will be rendering artifacts, but for graphicsview this has proven an acceptable compromise.
Most UI elements are rather simple. A button, for instance, can be composed of a background image and a short text. In QPainter terms that is one call to and one call to . The less time spent between painter calls the better the performance. The less state changes between painter calls, the better the performance. Looking back at how much happens between these calls for a button, you quickly realize that the traditional widgets are quite heavy. If widgets are going to survive the test of time, then they need to behave more like ‘s.
Муки выбора
Сначала поискал готовые решения. В ту далекую пору, когда использовал Delphi, можно было найти просто гигантское количество готовых компонентов, как от серьезных фирм, так и любительского изготовления. В Qt с этим напряженка. У QWT есть кое-какие элементы, но не то. Любительщины вообще не видел. Наверное, если грамотно рыть на Github`е, то можно что-то найти, но я, пожалуй, быстрее сам сделаю.
Первое, что напрашивалось из самодельного — использовать два файла-картинки с изображениями включенной и выключенной лампочки. Плохо:
- Надо найти хорошие картинки (или нарисовать, но художник я никакой);
- Принципиальный вопрос: тырить нехорошо, даже картинки, даже валяющиеся под ногами;
- Надо их хранить где-то. В файлах совсем плохо: случайно сотрется — и нету кнопок. В ресурсах получше, но тоже не хочется, если можно обойтись;
- Масштабируемость никакая;
- Настраиваемость (цвета, например) достигается только добавлением файлов. То есть, ресурсоемко и негибко.
Второе, что вытекает из первого — вместо картинок использовать векторные изображения. Тем более, что Qt умеет рендерить SVG. Тут уже чуть проще с поиском собственно изображения: в сети много уроков по векторной графике, можно найти что-то более-менее подходящее и адаптировать под свои нужды. Но остается вопрос по хранению и настраиваемости, да и рендеринг не бесплатен по ресурсам. Копейки, конечно, но все же…
И третье вытекает из второго: можно же воспользоваться принципами векторной графики при самостоятельной прорисовке изображения! Файл векторной картинки в текстовом виде указывает, что и как рисовать. Я могу кодом указать то же самое, используя векторные туториалы. Благо, у объекта QPainter имеются в наличии необходимые инструменты: перо, кисть, градиент и рисование примитивов, даже заливка текстурой. Да, инструменты далеко не все: нет масок, режимов наложения, но совсем уж фотореалистичности не требуется.
Поискал немного примеры в сети. Взял первый попавшийся урок: «Рисуем кнопку в графическом редакторе Inkscape» с сайта «Рисовать легко». Кнопка из этого урока гораздо больше похожа на лампочку, чем на кнопку, что меня вполне устраивает. Делаю заготовку: вместо Inkscape — проект в Qt.