Qt/QML 实现图片圆角剪切效果

在很多 UI 设计中,需要将图片按照一定的方式整形。比如下面的 VIP 图片就是用一个圆形剪切原始图片,形成的效果。

Qt/QML 实现图片圆角剪切效果_第1张图片

其实它的原始图片是这样的:

Qt/QML 实现图片圆角剪切效果_第2张图片

要在 QML 中实现这样的效果,可以使用 OpacityMask(QtGraphicalEffects 1.0)。但我们知道 QtGraphicalEffects 依赖图形硬件支持,在某些(比如嵌入式)平台,并没有相应的硬件支持,也就是说,这样的方案存在兼容性问题。

下面我们介绍一种完全基于 CPU 的方案,不依赖图形处理硬件,在嵌入式平台也能够工作。

给 Qml Image 增加 cornerRadius

不过我们只打算在 Qml 中支持,如果你使用 QWidget,也可以参考我们的思路自己动手实现一个。

先看一下最终给使用者的控件:

Image {
    property url originSource
    property real cornerRadius: 15
    source: {
        return "image://Effect/clip/" + encodeURIComponent(originSource)
                + "?size=" + width + "x" + height
                + "&cornerRadius=" + cornerRadius
    }
}

使用者只需要设置 originSource 和 cornerRadius 就可以了。

实现 QQuickImageProvider

上面的关键是 "image://" 开头的 url,是通过 ImageProvider 实现的。

如何实现 QQuickImageProvider,在我的另一篇文章中有进过,所以不再重复了。

用任意形状剪切图片

在 QQuickImageProvider 中,拿到原始图片 image 后,接下来就是变魔法的环节了。

我们的方案是基于 QPainter 的一个特殊机制——混合模式。

利用 QPainter 的混合模式,可以实现很多酷炫的效果。今天我们只做一个简单的——圆角剪切,它用到了 SoureIn 混合模式。

所谓 Source,就是我们的原始图片,对应的 Dest 是 QPainter 作用的画布上已有的像素颜色。SoureIn 混合中,起作用的是 Source 的颜色值和 Dest 的 alpha 值,两者相乘,就是混合结果。

因此,我们要剪切 Source 的一部分,保留另一部分,只要在 Dest 上将需要保留的区域的 alpha 设置为 255(100%),其他设置为 0,然后再  SoureIn 混合一下就可以了。

QImage output(requestedSize_, QImage::Format_ARGB32_Premultiplied);
output.fill(0);
QPainter painter(&output);
QRectF rect({0, 0}, requestedSize_);
painter.setRenderHint(QPainter::Antialiasing);
painter.setPen(Qt::NoPen);
painter.setBrush(Qt::black);
painter.drawRoundedRect(rect, cornerRadius_, cornerRadius_);
painter.setRenderHint(QPainter::Antialiasing, false);
painter.setCompositionMode(QPainter::CompositionMode_SourceIn);
painter.drawImage(rect, image, QRectF({0, 0}, image.size()));

画圆角的时候,需要打开抗锯齿(Antialiasing);而在图片混合时,则需要关闭 Antialiasing,因为两者不能同时工作。

另一个圆角剪切方案

我们也可以手动修改图片的像素,实现上面的混合效果。因为圆角的尺寸相对比较小,要修改的像素并不太多,所以效率上并不逊色于前一个方案,而且兼容性更好(有些平台不支持 QPainter 的混合模式)。

首先创建圆角 mask 图片,与前一个方案类似,但是尺寸上只需要 radius 的 两倍多一点就可以了。

qreal n = (ceil(radius) + 1) * 2;
mask = QImage({int(n), int(n)}, QImage::Format::Format_Alpha8);
mask.fill(0);
QPainter painter(&mask);
painter.setPen(Qt::NoPen);
painter.setBrush(Qt::white);
painter.setRenderHint(QPainter::Antialiasing);
painter.drawRoundedRect(QRectF{0, 0, n, n}, radius, radius);
painter.end();

 然后手动在四个角上,混合 mask 图片和原始图片的像素:

for (int corner = 0; corner < 4; ++corner) {
    auto size = mask.bytesPerLine() / 2;
    int offset1 = corner < 2 ? 0 : output.height() - size;
    int offset2 = corner < 2 ? 0 : mask.height() - size;
    int delta1 = 0, delta2 = 0;
    int d1 = 4, d2 = 1;
    if (corner == 1 || corner == 2) {
        delta1 = (output.width() - 1) * 4;
        delta2 = mask.width() - 1;
        d1 = -4; d2 = -1;
    }
    for (int i = 0; i < size; ++i) {
        uchar * bits1 = output.scanLine(offset1 + i) + delta1;
        uchar const * bits2 = mask.constScanLine(offset2 + i) + delta2;
        for ( ; ; ) {
            uchar alpha = *bits2;
            if (alpha == 255) break;
            bits1[0] = bits1[0] * alpha / 255;
            bits1[1] = bits1[1] * alpha / 255;
            bits1[2] = bits1[2] * alpha / 255;
            bits1[3] = bits1[3] * alpha / 255;
            bits1 += d1;
            bits2 += d2;
        }
    }
}

你可能感兴趣的:(Qt/QML,qt,QML)