Я использую 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<qreal>(image.width()); qreal hi = static_cast<qreal>(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<double>(i) / static_cast<double>(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<double>(i) / static_cast<double>(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<double>(i) / static_cast<double>(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<double>(i) / static_cast<double>(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-компонентов, потому что тема отдельной статьи со своими подводными камнями. Впрочем, вы всегда можете посмотреть исходный код нашей библиотеки, которую мы с другом используем для создания мобильных приложений: тут.