Я использую 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-компонентов, потому что тема отдельной статьи со своими подводными камнями. Впрочем, вы всегда можете посмотреть исходный код нашей библиотеки, которую мы с другом используем для создания мобильных приложений: тут.
Комментарии (7)
Antervis
23.01.2017 14:49попробуйте сделать через ShaderEffect
QtRoS
23.01.2017 22:52Для большой области (весь экран приложения) не очень быстро работало по моему опыту. С QSG, к сожалению, не сравнивал.
elderorb
23.01.2017 15:34я когда-то делал как-то так:
RoundImage.qml:
import QtQuick 2.0 Item { property alias border: borderRectangle.border property alias source: image.source property alias fillMode: image.fillMode Image { id: image anchors.fill: parent anchors.margins: borderRectangle.border.width } Rectangle { id: mask anchors.fill: image radius: width / 2 } TransparencyMask { anchors.fill: image source: image maskSource: mask } Rectangle { id: borderRectangle anchors.fill: parent radius: width / 2 color: "transparent" } }
TransparencyMask.qml:
import QtQuick 2.0 Item { property alias source: sourceImage.sourceItem property alias maskSource: sourceMask.sourceItem anchors.fill: parent ShaderEffectSource { id: sourceMask smooth: true hideSource: true } ShaderEffectSource { id: sourceImage hideSource: true } ShaderEffect { id: maskEffect anchors.fill: parent property variant source: sourceImage property variant mask: sourceMask fragmentShader: { " varying highp vec2 qt_TexCoord0; uniform lowp sampler2D source; uniform lowp sampler2D mask; void main() { highp vec4 maskColor = texture2D(mask, vec2(qt_TexCoord0.x, qt_TexCoord0.y)); highp vec4 sourceColor = texture2D(source, vec2(qt_TexCoord0.x, qt_TexCoord0.y)); if(maskColor.a > 0.0) gl_FragColor = sourceColor; else gl_FragColor = maskColor; } " } } }
evolit
aleusessentia
Здесь `clip: true` излишний. И в случае `fillMode: Image.PreserveAspectFit` нужно подстраивать размеры `rectangleMask` с помощью `imageOriginal.paintedWidth` и `imageOriginal.paintedHeight`. Я упомянул, что не рассматриваю в статье этот случай масштабирования изображения.
aleusessentia
И ещё у Item нет свойства radius.
cyberbobs
А ещё clip работает только по прямоугольным границам Item и вызывает перерисовку через дополнительный фрейм-буфер. Короче, не взлетит. Что предполагал автор комментария — останется тайной.