QML Image 通过 QQuickAsyncImageProvider 异步加载图片

相关:QML Image 通过 QQuickImageProvider 加载图片-CSDN博客

前言

根据 QQuickImageProvider 的文档说明,该类其实是支持在独立线程中异步加载的,但是 Async 类提供了专用于异步加载的接口,操作起来也不复杂,对于大文件或者网络文件,都可以采用异步方式加载图片。

操作流程

继续 QQuickAsyncImageProvider 只需要实现一个虚函数,但是返回的 QQuickImageResponse 需要我们继承重写部分接口

class MyAsyncImageProvider : public QObject, public QQuickAsyncImageProvider
{
    Q_OBJECT
public:
    explicit MyAsyncImageProvider(QObject *parent = nullptr);

    // 通过该接口给 QML Image 提供数据
    // 文档注释:此方法可能由多个线程调用,因此请确保此方法的实现是可重入的
    QQuickImageResponse *requestImageResponse(const QString &id, const QSize &requestedSize) override;
};

若 Image url 为 "image://Provider/imageTag/0",那么 id 就是 "imageTag/0"。注意这个 id  是 url 编码之后的,如果你有一些会被 url 转义的字符记得转回来。 

class MyAsyncImageResponse : public QQuickImageResponse
{
    Q_OBJECT
public:
    MyAsyncImageResponse(const QString &filepath, const QSize &requestedSize);
    ~MyAsyncImageResponse();

    // Factory 提供接口从 QML 加载自定义纹理
    QQuickTextureFactory *textureFactory() const override;
    // 加载失败可以给报错提示
    QString errorString() const override;

public slots:
    // Image 没有引用该图了,还没返回就需要取消
    void cancel() override;
};

textureFactory 返回最终结果,可以直接由 QImage 构造:

QQuickTextureFactory::textureFactoryForImage(mImage);

此时还有一个问题,怎么异步加载呢,通过在接口内部打印线程 ID,都是在 QQuickPixmapReader 这个线程。

最后在 github 一个叫 mixxx 的项目上找到了解答,MyAsyncImageResponse 多继承 QQuickImageResponse 和 QRunnable,利用 QRunnable 的 run 接口去加载图像。

实现代码

github 链接:https://github.com/gongjianbo/MyTestCode/tree/master/Qml/TestQml_20240118_AsyncImageProvider

#pragma once
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 


// 参考:https://github.com/mixxxdj/mixxx.git 项目 src/qml/asyncimageprovider.h
class MyAsyncImageResponse : public QQuickImageResponse, public QRunnable
{
    Q_OBJECT
public:
    MyAsyncImageResponse(const QString &filepath, const QSize &requestedSize);
    ~MyAsyncImageResponse();

    // QRunnable 线程中加载图片
    void run() override;
    // Factory 提供接口从 QML 加载自定义纹理
    QQuickTextureFactory *textureFactory() const override;
    // 加载失败可以给报错提示
    QString errorString() const override;

public slots:
    // Image 没有引用该图了,还没返回就需要取消
    void cancel() override;

private:
    QString mFilepath;
    QSize mRequestedSize;
    QImage mImage;
    std::atomic_bool mRunning{true};
};

// 如果需要信号槽的话可以继承 QObject
class MyAsyncImageProvider : public QObject, public QQuickAsyncImageProvider
{
    Q_OBJECT
public:
    explicit MyAsyncImageProvider(QObject *parent = nullptr);

    // 通过该接口给 QML Image 提供数据
    // 文档注释:此方法可能由多个线程调用,因此请确保此方法的实现是可重入的
    QQuickImageResponse *requestImageResponse(const QString &id, const QSize &requestedSize) override;

signals:
    // 测试刷新
    void imageChanged(const QString &imageTag);

private:
    // 加载图片的线程池
    QThreadPool pool;
    // 测试定时刷新
    QTimer *timer;
};

#include "MyAsyncImageProvider.h"
#include 
#include 
#include 

MyAsyncImageResponse::MyAsyncImageResponse(const QString &filepath, const QSize &requestedSize)
    : mFilepath(filepath)
    , mRequestedSize(requestedSize)
{
    setAutoDelete(false);
    qDebug()<<__FUNCTION__<mFilepath);
        if (!reader.canRead())
            break;
        mImage = reader.read();
        // 只缩小,不放大
        bool do_scale = mRequestedSize.width() < mImage.size().width() &&
                        mRequestedSize.height() < mImage.size().height();
        if (!mImage.isNull() && mRequestedSize.isValid() && do_scale) {
            // 如果用 QML 中的缩放设置,就不在这里进行缩放了
            // Qt::FastTransformation
            mImage = mImage.scaled(mRequestedSize, Qt::KeepAspectRatio, Qt::SmoothTransformation);
        }
    }
    while(false);
    // 在父类构造函数关联,发出之后才会进入下一步
    emit finished();
}

QQuickTextureFactory *MyAsyncImageResponse::textureFactory() const
{
    return QQuickTextureFactory::textureFactoryForImage(mImage);
}

QString MyAsyncImageResponse::errorString() const
{
    if (!mRunning && mImage.isNull()) {
        return "Image Load Error";
    }
    return "";
}

void MyAsyncImageResponse::cancel()
{
    // 这里设置 run 的退出条件
    qDebug()<<__FUNCTION__;
}

MyAsyncImageProvider::MyAsyncImageProvider(QObject *parent)
    : QObject{parent}
    , timer{new QTimer(this)}
{
    pool.setMaxThreadCount(8);

    // 测试刷新
    connect(timer, &QTimer::timeout, this, [this](){
        emit imageChanged("imageTag");
    });
    timer->start(20);
}

QQuickImageResponse *MyAsyncImageProvider::requestImageResponse(const QString &id, const QSize &requestedSize)
{
    // 指定 Image 的 sourceSize 会作为 requestedSize 参数,默认是 QSize(-1, -1)
    // 若 url 为 "image://Provider/imageTag/0",那么 id 就是 "imageTag/0"
    // 注意 url 的编码问题,如果有特殊符号之类的不能直接从中截取 QML 中设置的路径
    // id 到实际路径的转换根据实际需求来,此处取固定值
    Q_UNUSED(id)
    QString path = ":/flower.jpg";
    auto response = new MyAsyncImageResponse(path, requestedSize);
    pool.start(response);
    return response;
}
import QtQuick 2.15
import QtQuick.Window 2.15

Window {
    width: 640
    height: 480
    visible: true
    title: qsTr("Async Image Provider")

    Image {
        id: image
        anchors.centerIn: parent
        asynchronous: true
        cache: false
        // image://provider_id/image_id
        source: "image://Provider/imageTag/0"
        // 指定 Image 的 sourceSize 会作为 requestedSize 参数,默认是 QSize(-1, -1)
        // sourceSize: Qt.size(120, 120)
    }
    // 加一个 Shader 是为了防止刷新时闪烁,Qt 6.5 之后也可以用 Image 的 layer.live
    // https://bugreports.qt.io/browse/QTBUG-66713
    ShaderEffectSource {
        id: texture
        anchors.fill: image
        sourceItem: image
        live: image.status === Image.Ready
        hideSource: true
    }

    property int updateNum: 0
    Connections {
        target: provider
        function onImageChanged(imageTag) {
            // Image 的 load 接口不是 public 的,所以只能切换 url 来重新加载数据
            // 异步加载时间更长,刷新会闪烁
            updateNum++
            image.source = "image://Provider/" + imageTag + "/" + updateNum
        }
    }
}

你可能感兴趣的:(QML,三言两语,QML,Image)