[Из песочницы] Закруглённые изображения на Qt Quick Scene Graph
Я использую Qt в разработке уже более 6 лет, из них последние 3 года для создания приложений под Android и iOS на Qt Quick. Моя приверженность этому framework’у обусловлена двумя причинами:
- Qt предоставляется большой пакет компонентов, функций, классов и т.п., которых хватает для разработки большинства приложений;
- Если нужно создать недостающий компонент, Qt предоставляет несколько уровней абстракции для этого — от простой для кодирования, до наиболее производительной и функциональной.
К примеру, в Qt Quick есть компонент Image, который размещает изображение в интерфейсе. Компонент имеет множество параметров: расположение, способ масштабирования, сглаживание и др, но нет параметра radius для скругления изображения по углам. В то же время круглые изображения сейчас можно встретить, практически, в любом современном интерфейсе и из-за этого возникла потребность написать свой Image. С поддержкой всех параметров Image и радиусом. В этой статье я опишу несколько способов сделать закруглённые изображения.
Первая реализация, она же наивная
В Qt Quick есть библиотека для работы с графическими эффектами QtGraphicalEffects. По сути каждый компонент — обёртка над шейдерами и OpenGL. Поэтому я предположил, что это должно работать быстро и сделал нечто вроде этого:
import QtQuick 2.0
import QtGraphicalEffects 1.0
Item {
property alias source: imageOriginal.source
property alias radius: mask.radius
Image {
id: imageOriginal
anchors.fill: parent
visible: false
}
Rectangle {
id: rectangleMask
anchors.fill: parent
radius: 0.5*height
visible: false
}
OpacityMask {
id: opacityMask
anchors.fill: imageOriginal
source: imageOriginal
maskSource: rectangleMask
}
}
Давайте разберём, как это работает: opacityMask
накладывает маску rectangleMask
на изображение imageOriginal
и отображает что получилось. Прошу заметить, что изначальное изображение и прямоугольник невидимы visible: false
. Это нужно, чтобы избежать наложения, т.к. opacityMask
— отдельный компонент и напрямую не влияет на отображение других элементов сцены.
Это самая простая и самая медленная реализация из всех возможных. Лаги отображения будут сразу видны, если создать длинный список изображений и пролистать его (к примеру список контактов как в Telegram). Ещё больший дискомфорт доставят тормоза изменения размеров изображения. Проблема в том, что все компоненты библиотеки QtGraphicalEffects
сильно нагружают графическую подсистему, даже если исходное изображение и размеры элемента не меняются. Проблему можно слегка уменьшить, воспользовавшись функцией grubToImage (…) для создания статического круглого изображения, но лучше воспользоваться другой реализацией закругления изображения.
Вторая реализация, Canvas
Следующий способ, который пришёл в голову — это нарисовать над изображением углы цветом фона с помощью Canvas. В таком случае, при неизменных размерах и радиусе изображения Canvas можно не перерисовывать, а копировать для каждого нового элемента. За счёт этой оптимизации достигается преимущество в скорости рендеринга, в сравнении с первой реализацией.
У этого подхода два минуса. Во-первых, любое изменение размеров и радиуса требует перерисовки Canvas’а, что в некоторых случаях уменьшит производительность даже ниже чем в решении с OpacityMask. И второе — фон под изображением должен быть однородным, иначе раскроется наша иллюзия.
import QtQuick 2.0
import QtGraphicalEffects 1.0
Item {
property alias source: imageOriginal.source
property real radius: 20
property color backgroundColor: "white"
Image {
id: imageOriginal
anchors.fill: parent
visible: false
}
Canvas {
id: roundedCorners
anchors.fill: parent
onPaint: {
var ctx = getContext("2d");
ctx.reset();
ctx.fillStyle = backgroundColor;
ctx.beginPath();
ctx.moveTo(0, radius)
ctx.lineTo(0, 0);
ctx.lineTo(radius, 0);
ctx.arc(radius, radius, radius, 3/2*Math.PI, Math.PI, true);
ctx.closePath();
ctx.fill();
ctx.beginPath();
ctx.moveTo(width, radius)
ctx.lineTo(width, 0);
ctx.lineTo(width-radius, 0);
ctx.arc(width-radius, radius, radius, 3/2*Math.PI, 2*Math.PI, false);
ctx.closePath();
ctx.fill();
ctx.beginPath();
ctx.moveTo(0, height-radius)
ctx.lineTo(0, height);
ctx.lineTo(radius, height);
ctx.arc(radius, height-radius, radius, 0.5*Math.PI, Math.PI, false);
ctx.closePath();
ctx.fill();
ctx.beginPath();
ctx.moveTo(width-radius, height)
ctx.lineTo(width, height);
ctx.lineTo(width, height-radius);
ctx.arc(width-radius, height-radius, radius, 0, 0.5*Math.PI, false);
ctx.closePath();
ctx.fill();
}
}
}
Третья реализация, QPainter
Чтобы увеличить производительность и избавится от зависимости от однородного фона, я создал QML-компонент на основе C++ класса QQuickPaintedItem. Этот класс предоставляет механизм отрисовки компонента через QPainter. Для этого нужно переопределить метод void paint(QPainter *painter)
родительского класса. Из названия понятно, что метод вызывается для отрисовки компонента.
void ImageRounded::paint(QPainter *painter)
{
QPen pen;
pen.setStyle(Qt::NoPen);
painter->setPen(pen);
QImage *image = new QImage("image.png");
// Указываем изображение в качестве паттерна
QBrush brush(image);
// Растягиваем изображение
qreal wi = static_cast(image.width());
qreal hi = static_cast(image.height());
qreal sw = wi / width();
qreal sh = hi / height();
brush.setTransform(QTransform().scale(1/sw, 1/sh));
painter->setBrush(brush);
// Рисуем прямоугольник с закруглёнными краями
qreal radius = 10
painter->drawRoundedRect(QRectF(0, 0, width(), height()), radius, radius);
}
В примере выше исходное изображение растягивается до размеров элемента и используется в качестве паттерна при отрисовки прямоугольника с закруглёнными краями. Для упрощения кода, здесь и далее не рассматривается варианты масштабирования изображений: PreserveAspectFit
и PreserveAspectFit
, а только Stretch
.
По умолчанию, QPainter
рисует на изображении, а потом копирует в буфер OpenGL. Если рисовать напрямую в FBO, то ренденринг компонента ускорится в несколько раз. Для этого нужно вызвать две следующие функции в конструкторе класса:
setRenderTarget(QQuickPaintedItem::FramebufferObject);
setPerformanceHint(QQuickPaintedItem::FastFBOResizing, true);
Финальная реализация, Qt Quick Scene Graph
Реализация на QQuickPaintedItem
работает гораздо быстрее первой и второй. Но даже в этом случае на смартфонах заметна задержка рендеринга при изменении размера изображения. Дело в том, что любая функция масштабирующая изображение производится на мощностях процессора и занимает не менее 150 мс (замерял на i7 и на HTC One M8). Можно вынести масштабирование в отдельный поток и отрисовывать картинку по готовности — это улучшит отзывчивость (приложение будет всегда реагировать на действия пользователя), но проблему по сути не решит — видно будет дёрганье изображения при масштабировании.
Раз узкое место — это процессор, на ум приходит использовать мощности видеоускорителя. В Qt Quick для этого предусмотрен класс QQuickItem. При наследовании от него нужно переопределить метод updatePaintNode
. Метод вызывается каждый раз, когда компонент нужно отрисовать.
QSGNode* ImageRounded::updatePaintNode(QSGNode *oldNode, QQuickItem::UpdatePaintNodeData *)
{
if (_status != Ready) {
return nullptr;
}
QSGGeometryNode *node;
if (!oldNode) {
node = new QSGGeometryNode();
// Создаём объект для геометрии
QSGGeometry *geometry = new QSGGeometry(QSGGeometry::defaultAttributes_TexturedPoint2D(), _segmentCount);
geometry->setDrawingMode(QSGGeometry::DrawTriangleFan);
setGeometry(geometry);
node->setFlag(QSGNode::OwnsGeometry);
node->setFlag(QSGNode::OwnsOpaqueMaterial);
// Задаём текстуру и материал
auto image = new QImage("image.png");
auto texture = qApp->view()->createTextureFromImage(image);
auto material = new QSGOpaqueTextureMaterial;
material->setTexture(texture);
material->setFiltering(QSGTexture::Linear);
material->setMipmapFiltering(QSGTexture::Linear);
setMaterial(material);
node->markDirty(QSGNode::DirtyGeometry | QSGNode::DirtyMaterial);
} else {
node = oldNode;
node->markDirty(QSGNode::DirtyGeometry);
}
// Определяем геометрию и точки привязки текстуры
QSGGeometry::TexturedPoint2D *vertices = node->geometry()->vertexDataAsTexturedPoint2D();
const int count = 20; // Количество точек на закруглённый угол
const int segmentCount = 4*count + 3; // Общее количество точек
Coefficients cf = {0, 0, width(), height()
,0, 0, 1/width(), 1/height()};
const float ox = 0.5f*cf.w + cf.x;
const float oy = 0.5f*cf.h + cf.y;
const float lx = 0.5f*cf.w + cf.x;
const float ly = cf.y;
const float ax = 0 + cf.x;
const float ay = 0 + cf.y;
const float bx = 0 + cf.x;
const float by = cf.h + cf.y;
const float cx = cf.w + cf.x;
const float cy = cf.h + cf.y;
const float dx = cf.w + cf.x;
const float dy = 0 + cf.y;
const float r = 2*_radius <= cf.w && 2*_radius <= cf.h
? _radius
: 2*_radius <= cf.w
? 0.5f*cf.w
: 0.5f*cf.h;
vertices[0].set(ox, oy, ox*cf.tw+cf.tx, oy*cf.th+cf.ty);
vertices[1].set(lx, ly, lx*cf.tw+cf.tx, ly*cf.th+cf.ty);
// Левый верхний угол
int start = 2;
for (int i=0; i < count; ++i) {
double angle = M_PI_2 * static_cast(i) / static_cast(count-1);
float x = ax + r*(1 - qFastSin(angle));
float y = ay + r*(1 - qFastCos(angle));
vertices[start+i].set (x, y, x*cf.tw+cf.tx, y*cf.th+cf.ty);
}
// Левый нижний угол
start += count;
for (int i=0; i < count; ++i) {
double angle = M_PI_2 * static_cast(i) / static_cast(count-1);
float x = bx + r*(1 - qFastCos(angle));
float y = by + r*(-1 + qFastSin(angle));
vertices[start+i].set (x, y, x*cf.tw+cf.tx, y*cf.th+cf.ty);
}
// Правый нижний угол
start += count;
for (int i=0; i < count; ++i) {
double angle = M_PI_2 * static_cast(i) / static_cast(count-1);
float x = cx + r*(-1 + qFastSin(angle));
float y = cy + r*(-1 + qFastCos(angle));
vertices[start+i].set (x, y, x*cf.tw+cf.tx, y*cf.th+cf.ty);
}
// Правый верхний угол
start += count;
for (int i=0; i < count; ++i) {
double angle = M_PI_2 * static_cast(i) / static_cast(count-1);
float x = dx + r*(-1 + qFastCos(angle));
float y = dy + r*(1 - qFastSin(angle));
vertices[start+i].set (x, y, x*cf.tw+cf.tx, y*cf.th+cf.ty);
}
vertices[segmentCount-1].set(lx, ly, lx*cf.tw+cf.tx, ly*cf.th+cf.ty);
return node;
}
В примере под спойлером, сначала создаём объект класса QSGGeometryNode — этот объект мы возвращаем в движок Qt Quick Scene Graph для рендеринга. Затем указываем геометрию объекта — прямоугольник с закруглёнными углами, создаём текстуру из оригинального изображения и передаём текстурные координаты (они указывают как текстура натягивается на геометрию). Примечание: геометрия в примере задаётся методом веера треугольников. Вот пример работы компонента:
Заключение
В этой статье я постарался собрать разные методы отрисовки закругленного изображения в Qt Quick: от наиболее простого до наиболее производительного. Я сознательно упустил методы загрузки изображения и конкретику в создании QML-компонентов, потому что тема отдельной статьи со своими подводными камнями. Впрочем, вы всегда можете посмотреть исходный код нашей библиотеки, которую мы с другом используем для создания мобильных приложений: https://github.com/SiberianProgrammers/sp_qt_libs.