实现 QQuickImageProvider 的若干问题的思路

使用 QQuickImageProvider

在 Qml 中使用 QQuickImageProvider 的方式:

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

使用 image:// 开头的 url,实际上调用了自定义的 ImageProvider。

下面这两个例子体现了这种机制的用武之地:

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

在 Qt 中实现变色的图标(tintColor)

实现 QQuickImageProvider

实现 QQuickImageProvider 一个关键步骤是选择工作模式。QQuickImageProvider 可以是下面三种模式:

  • 同步模式
  • 独立线程模式(ForceAsynchronousImageLoading)
  • 纯异步模式(ImageResponse)

因为我们需要先从网络上获取原始图片,同步模式会阻塞 UI 线程执行,所以显然不适合。

那么独立线程模式是否可行呢?因为它是在独立的线程处理网络下载,应该说理论上是可以的,但是有些麻烦。 

注意到 QNetworkAccessManager 是基于信号槽工作的,并且是多线程异步的,所以需要一个 Qt 事件线程来处理其信号。但是 QQuickImageProvider 的独立线程并没有事件循环,所以需要借助其他线程(比如 UI 线程)来处理信号,然后将结果同步到 QQuickImageProvider 的线程中(借助 QCondition)。

这样,不仅实现复杂,并且独立线程没有并发处理能力,所以不建议使用独立线程模式。

剩下的就是纯异步模式了,接下来我们就看看如何实现。

实现 QQuickAsyncImageProvider

Qt 提供了 QQuickAsyncImageProvider 帮助实现纯异步模式。通过查看源代码,发现 QQuickAsyncImageProvider 只是简单的设置了默认模式:

QQuickAsyncImageProvider::QQuickAsyncImageProvider()
 : QQuickImageProvider(ImageResponse, ForceAsynchronousImageLoading)
 , d(nullptr) // just as a placeholder in case we need it for the future
{
    Q_UNUSED(d);
}
QQuickAsyncImageProvider::~QQuickAsyncImageProvider()
{
}

这里默认设置了 ForceAsynchronousImageLoading(独立线程模式),也就是对 ImageProvier 的请求是在独立线程(不是 UI 线程)发出的,但是我们希望最好有个 Qt 事件线程来启动网络请求。

能不能不要这个 Flag 呢?我们可以通过复写(override)Flags flags() const 方法,去除这个 flag,但是实测并不能工作。进一步查看源代码,发现这种方式是设计时就默认了的,所以还需要费些周章将启动网络的任务 post 到 UI 线程执行。

static QQuickPixmapData* createPixmapDataSync(QQuickPixmap *declarativePixmap, QQmlEngine *engine, const QUrl &url, const QSize &requestSize, const QQuickImageProviderOptions &providerOptions, bool *ok)
{
    ...
            case QQuickImageProvider::ImageResponse:
            {
                // Fall through, ImageResponse providers never get here
                Q_ASSERT(imageType != QQuickImageProvider::ImageResponse && "Sync call to ImageResponse provider");
            }
    ...
}

实现 QQuickAsyncImageProvider,主要的工作就是实现下面的方法,准确的说是实现 QQuickImageResponse 派生类:

QQuickImageResponse * MyImageProvider::requestImageResponse(const QString &id, const QSize &requestedSize)
{
    return new MyImageResponse(network_, id, requestedSize);
}

实现 QQuickImageResponse

在 MyImageResponse,需要完成网络图片的下载,对图片的整形,其大概的实现框架是这样的:

class MyImageResponse : public QQuickImageResponse
{
public:
    MyImageResponse(QNetworkAccessManager * network, QString const & id, QSize const &requestedSize);

public:
    QQuickTextureFactory *textureFactory() const override { return QQuickTextureFactory::textureFactoryForImage(image_); }
    QString errorString() const override { return errorString_; }

public slots:
    void cancel() override {
        if (reply_)
            reply_->abort();
    }

private:
    void parseSettings(QString const & id);

    void setImage(QImage image);

private:
    QSize requestedSize_;
    QUrl realUrl_;
    qreal borderWidth_ = 0.;
    QColor borderColor_;
    qreal cornerRadius_ = 0.;
    QVector<qreal> cornerRadii_;

    QImage image_;
    QString errorString_;
    QNetworkReply * reply_;
};

实现 textureFactory 想要返回一个 QQuickTextureFactory 对象,Texture 是图形硬件中的二维纹理,看起来不太好实现,但是 QQuickTextureFactory 提供了从 QImage 构造 TextureFactory 的辅助方法,这就很简单了。注意,不要缓存 QQuickTextureFactory 对象,而是让 Qml 自己去管理这些对象。

我们要实现用圆角矩形去剪切图片,所以需要有 cornerRadius、borderWith 这些配置变量,它们来源于外部(requestImageResponse 方法的参数)输入的 id。

从网络下载图片

通常网络下载文件的代码是这样的:

QNetworkRequest request(realUrl_);
reply_ = network->get(request);
QObject::connect(reply_, &QNetworkReply::finished, [this] () {
    ....
});

在  QQuickAsyncImageProvider 中,这段代码不能生效,因为信号-槽的跨线程调用,依赖事件分发,普通的线程并没有事件分发。

因此,需要将这段代码 post 到带有事件分发的线程中,其实就是 post 一个 QEvent 对象,像下面这样的:

template<typename F>
struct AsyncEvent : QEvent
{
    AsyncEvent(F f) : QEvent(None), f_(f) {}
    ~AsyncEvent() { f_(); }
private:
    F f_;
};

 这个 AsyncEvent post 给哪个 QObject 并不重要,我们主要利用事件的析构方法来执行某一段代码。我们 post 给 network (QNetworkManager)对象,事件不会有什么代码处理,经过分派后就会被删除,从而调用了析构方法。

QCoreApplication::postEvent(network, new AsyncEvent([this, network] {
   ...
});

然后下载图片数据需要转换为 QImage 对象,我们通过 QImageReader 来实现:

if (reply_->error() == QNetworkReply::NoError) {
    QImageReader reader(reply_, nullptr);
    QImage image = reader.read();
    if (image.isNull()) {
        errorString_ = reader.errorString();
    } else {
        setImage(image);
    }
}

注意,直接通过 QImage::load(data, format) 方法解析图片不能取得期望的效果,可能是需要调用者指定数据格式(format),不能够自动识别。

从另一个 ImageProvider 获取图片

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Fighting Horse

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值