Закруглённые изображения на Qt Quick Scene Graph

в 10:28, , рубрики: QML, qt, qt quick, qt scene graph, разработка мобильных приложений

Я использую Qt в разработке уже более 6 лет, из них последние 3 года для создания приложений под Android и iOS на Qt Quick. Моя приверженность этому framework'у обусловлена двумя причинами:

  • Qt предоставляется большой пакет компонентов, функций, классов и т.п., которых хватает для разработки большинства приложений;
  • Если нужно создать недостающий компонент, Qt предоставляет несколько уровней абстракции для этого — от простой для кодирования, до наиболее производительной и функциональной.

К примеру, в Qt Quick есть компонент Image, который размещает изображение в интерфейсе. Компонент имеет множество параметров: расположение, способ масштабирования, сглаживание и др, но нет параметра radius для скругления изображения по углам. В то же время круглые изображения сейчас можно встретить, практически, в любом современном интерфейсе и из-за этого возникла потребность написать свой Image. С поддержкой всех параметров Image и радиусом. В этой статье я опишу несколько способов сделать закруглённые изображения.

Telegram

Первая реализация, она же наивная

В 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. Метод вызывается каждый раз, когда компонент нужно отрисовать.

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-компонентов, потому что тема отдельной статьи со своими подводными камнями. Впрочем, вы всегда можете посмотреть исходный код нашей библиотеки, которую мы с другом используем для создания мобильных приложений: https://github.com/SiberianProgrammers/sp_qt_libs.

Автор: aleusessentia

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js