Закруглені зображення на 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
real property 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
. Метод викликається кожного разу, коли компонент потрібно промалювати.
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.5 f*cf.w + cf.x;
const float oy = 0.5 f*cf.h + cf.y;
const float lx = 0.5 f*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.5 f*cf.w
: 0.5 f*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-компонентів, тому що тема окремої статті зі своїми підводними каменями. Втім, ви завжди можете подивитися вихідний код нашої бібліотеки, яку ми з другом використовуємо для створення мобільних додатків: тут.
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.