Back to Home

Rounded images on Qt Quick Scene Graph

qt · qt quick · qml · qt scene graph

Rounded images on Qt Quick Scene Graph

I have been using Qt in development for more than 6 years, of which the last 3 years to create applications for Android and iOS on Qt Quick. My commitment to this framework is due to two reasons:


  • Qt provides a large package of components, functions, classes, etc., which are enough to develop most applications;
  • If you need to create the missing component, Qt provides several levels of abstraction for this - from simple to encode, to the most productive and functional.

For example, in Qt Quick there is an Image component that places the image in the interface. The component has many parameters: location, scaling method, smoothing, etc., but there is no radius parameter for rounding the image at the corners. At the same time, round images can now be found in almost any modern interface and because of this there is a need to write your Image. With support for all Image parameters and radius. In this article, I will describe several ways to make rounded images.



The first implementation, she is naive


Qt Quick has a library for working with graphic effects QtGraphicalEffects . In fact, each component is a wrapper over shaders and OpenGL. So I suggested that this should work quickly and did something like this:


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
    }
}

Let's see how it works: opacityMaskputs a mask rectangleMaskon the image imageOriginaland displays what happened. Please note that the original image and the rectangle are invisible visible: false. This is necessary to avoid overlap, as opacityMask- a separate component and does not directly affect the display of other elements of the scene.


This is the simplest and slowest implementation possible. Display lags will be immediately visible if you create a long list of images and scroll through it (for example, the contact list as in Telegram). Even greater discomfort will bring the brakes of image resizing. The problem is that all the library components QtGraphicalEffectsheavily load the graphics subsystem, even if the original image and element sizes do not change. The problem can be slightly reduced by using the grubToImage (...) function to create a static round image, but it is better to use another implementation of the image rounding.


Second implementation, Canvas


The next method that came to mind is to draw the corners of the image above the image with the background color using Canvas . In this case, with the same size and radius of the image, Canvas can not be redrawn, but copied for each new element. Due to this optimization, an advantage in rendering speed is achieved, in comparison with the first implementation.


This approach has two minuses. Firstly, any change in size and radius requires a Canvas redraw, which in some cases will reduce performance even lower than in the solution with OpacityMask. And the second - the background under the image should be uniform, otherwise our illusion will open.


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();
        }
    }
}

Third implementation, QPainter


To increase productivity and get rid of the dependence on a uniform background, I created a QML component based on the C ++ class QQuickPaintedItem . This class provides a mechanism for rendering a component through a QPainter . To do this, override the method of the void paint(QPainter *painter)parent class. From the name it’s clear that the method is called to render the component.


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);
}

In the example above, the original image is stretched to the size of the element and is used as a pattern when drawing a rectangle with rounded edges. To simplify the code, hereinafter, image scaling options are not considered: PreserveAspectFitand PreserveAspectFit, but only Stretch.
By default, QPainterdraws on the image, and then copies to the OpenGL buffer. If you draw directly in FBO , then the component rendering will accelerate several times. To do this, call the following two functions in the class constructor:


setRenderTarget(QQuickPaintedItem::FramebufferObject);
setPerformanceHint(QQuickPaintedItem::FastFBOResizing, true);

Final implementation, Qt Quick Scene Graph


Implementation on QQuickPaintedItemworks much faster than the first and second. But even in this case, on smartphones, a rendering delay is noticeable when changing the image size. The fact is that any function of the scaling image is performed at the processor capacities and takes at least 150 ms (measured on the i7 and on the HTC One M8). You can take the scaling to a separate stream and draw the picture as it is ready - this will improve responsiveness (the application will always respond to user actions), but it will not essentially solve the problem - you will see a jerking image when scaling.


Since the bottleneck is the processor, it comes to mind to use the power of the video accelerator. Qt Quick provides the QQuickItem class for this . When inheriting from it, you need to override the method updatePaintNode. The method is called every time the component needs to be drawn.


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;
}

In the example under the spoiler, first we create an object of the QSGGeometryNode class - we return this object to the Qt Quick Scene Graph engine for rendering. Then we indicate the geometry of the object - a rectangle with rounded corners, create a texture from the original image and transfer the texture coordinates (they indicate how the texture is stretched over the geometry). Note: the geometry in the example is set by the method of a fan of triangles . Here is an example of the component:



Conclusion


In this article, I tried to collect different methods for rendering a rounded image in Qt Quick: from the simplest to the most productive. I deliberately missed image loading methods and specifics in creating QML components, because the topic is a separate article with my own pitfalls. However, you can always see the source code of our library, which my friend and I use to create mobile applications: here .

Read Next