使用 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。
下面这两个例子体现了这种机制的用武之地:
实现 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),不能够自动识别。