画像の拡大縮小

画像を非同期にダウンロードして拡大縮小する方法を示します。

この例では、QFutureQPromiseQFutureWatcher クラスを使用して、UI をブロックすることなく、ネットワークから画像のコレクションをダウンロードし、スケーリングする方法を示します。

このアプリケーションは、以下のステップで構成されています:

  1. ユーザーが指定したURLのリストから画像をダウンロードする。
  2. 画像を拡大縮小する。
  3. 拡大縮小された画像をグリッドレイアウトで表示する。

ダウンロードから始めましょう:

QFuture<QByteArray> Images::download(const QList<QUrl> &urls)
{

download() メソッドはURLのリストを受け取り、QFuture を返します。QFuture は、ダウンロードした画像ごとに受け取ったバイト配列データを格納します。QFuture 内にデータを格納するために、QPromise オブジェクトを作成し、ダウンロードの開始を示すために開始したことを報告します:

    QSharedPointer<QPromise<QByteArray>> promise(new QPromise<QByteArray>());
    promise->start();
    ...
    return promise->future();
}

約束に関連付けられた未来が呼び出し元に返される。

詳細は後述するが、promiseオブジェクトはQSharedPointer

QNetworkAccessManager 、ネットワークリクエストを送信し、各URLのデータをダウンロードする:

for (const auto &url : urls) {
    QSharedPointer<QNetworkReply> reply(qnam.get(QNetworkRequest(url)));
    replies.push_back(reply);

ここからが面白いところだ:

    QtFuture::connect(reply.get(), &QNetworkReply::finished).then([=] {
        if (promise->isCanceled()) {
            if (!promise->future().isFinished())
                promise->finish();
            return;
        }

        if (reply->error() != QNetworkReply::NoError) {
            if (!promise->future().isFinished())
                throw reply->error();
        }
        promise->addResult(reply->readAll());

        // Report finished on the last download
        if (promise->future().resultCount() == urls.size())
            promise->finish();
    }).onFailed([promise] (QNetworkReply::NetworkError error) {
        promise->setException(std::make_exception_ptr(error));
        promise->finish();
    }).onFailed([promise] {
        const auto ex = std::make_exception_ptr(
                    std::runtime_error("Unknown error occurred while downloading."));
        promise->setException(ex);
        promise->finish();
    });
}
    ...

QObject::connect() メソッドを使用してQNetworkReply のシグナルに接続する代わりに、QtFuture::connect() を使用します。QObject::connect() と似たような動作をしますが、QFuture オブジェクトを返します。このオブジェクトは、QNetworkReply::finished() シグナルが発信されるとすぐに利用可能になります。これにより、この例で行われているように、継続や失敗ハンドラをアタッチすることができる。

.then() によってアタッチされた継続では、ユーザがダウンロードのキャンセルを要求したかどうかをチェックする。もしそうなら、リクエストの処理を中止する。QPromise::finish() メソッドを呼び出すことで、処理が終了したことをユーザーに通知します。ネットワーク・リクエストがエラーで終了した場合は、例外をスローします。この例外は、.onFailed() メソッドを使用してアタッチされた失敗ハンドラで処理されます。最初のハンドラはネットワーク・エラーを捕捉し、2番目のハンドラは実行中にスローされた他のすべての例外を捕捉します。どちらのハンドラも例外をpromiseオブジェクト内に保存し(download() メソッドの呼び出し側で処理される)、計算が終了したことを報告する。また、簡単のために、エラーが発生した場合、保留中のダウンロードをすべて中断することに注意すること。

リクエストがキャンセルされず、エラーも発生していない場合、ネットワーク・リプライからデータを読み取り、promiseオブジェクトの結果リストに追加します:

    ...
    promise->addResult(reply->readAll());

    // Report finished on the last download
    if (promise->future().resultCount() == urls.size())
        promise->finish();
    ...

promiseオブジェクトに格納されている結果の数が、ダウンロードされるurlの数と等しい場合、処理するリクエストはもうないので、promiseが終了したことも報告します。

QSharedPointerプロミス・オブジェクトは、各ネットワーク・リプライに接続されたハンドラ ー間で共有されるため、複数の場所で同時にプロミス・オブジェクトをコ ピーして使用する必要があります。そのため、QSharedPointer を使用します。

download() メソッドはImages::process メソッドから呼び出されます。このメソッドは、ユーザーが"Add URLs "ボタンを押すと呼び出される:

    ...
    connect(addUrlsButton, &QPushButton::clicked, this, &Images::process);
    ...

前回のダウンロードの残り物を消去した後、ユーザーがダウンロードする画像のURLを指定できるようにダイアログを作成します。指定されたURL数に基づいて、画像が表示されるレイアウトを初期化し、ダウンロードを開始します。download() メソッドによって返されたフューチャーは保存されるので、ユーザは必要に応じてダウンロードをキャンセルすることができます:

void Images::process()
{
    // Clean previous state
    replies.clear();
    addUrlsButton->setEnabled(false);

    if (downloadDialog->exec() == QDialog::Accepted) {

        const auto urls = downloadDialog->getUrls();
        if (urls.empty())
            return;

        cancelButton->setEnabled(true);

        initLayout(urls.size());

        downloadFuture = download(urls);
        statusBar->showMessage(tr("Downloading..."));
    ...

次に、スケーリングステップを処理するために継続をアタッチします。これについては後で説明します:

downloadFuture
        .then([this](auto) {
            cancelButton->setEnabled(false);
            updateStatus(tr("Scaling..."));
            scalingWatcher.setFuture(QtConcurrent::run(Images::scaled,
                                                       downloadFuture.results()));
        })
    ...

その後、onCanceled ()とonFailed ()ハンドラをアタッチします:

        .onCanceled([this] {
            updateStatus(tr("Download has been canceled."));
        })
        .onFailed([this](QNetworkReply::NetworkError error) {
            updateStatus(tr("Download finished with error: %1").arg(error));
            // Abort all pending requests
            abortDownload();
        })
        .onFailed([this](const std::exception &ex) {
            updateStatus(tr(ex.what()));
        })
    ...

.onCanceled()メソッドでアタッチされたハンドラは、ユーザーが"キャンセル "ボタンを押した場合に呼び出されます:

    ...
    connect(cancelButton, &QPushButton::clicked, this, &Images::cancel);
    ...

cancel() メソッドは、保留中のリクエストをすべて中止します:

void Images::cancel()
{
    statusBar->showMessage(tr("Canceling..."));

    downloadFuture.cancel();
    abortDownload();
}

.onFailed()メソッド経由でアタッチされたハンドラは、前のステップの間にエラーが発生した場合に呼び出されます。例えば、ダウンロードステップ中にネットワークエラーがプロミス内に保存された場合、QNetworkReply::NetworkError を引数とするハンドラに伝搬されます。

downloadFuture がキャンセルされず、エラーも報告されなかった場合、スケーリングの続行が実行される。

スケーリングは計算量が多く、メインスレッドをブロックしたくないので、QtConcurrent::run() を使用して、スケーリングステップを新しいスレッドで起動します。

scalingWatcher.setFuture(QtConcurrent::run(Images::scaled,
                                           downloadFuture.results()));

スケーリングは別のスレッドで実行されるため、スケーリング処理中にユーザーがアプリケーションの終了を決定する可能性があります。このような状況を優雅に処理するために、QtConcurrent::run()が返すQFutureQFutureWatcher インスタンスに渡します。

ウォッチャーのQFutureWatcher::finished シグナルはImages::scaleFinished スロットに接続されています:

    connect(&scalingWatcher, &QFutureWatcher<QList<QImage>>::finished,
            this, &Images::scaleFinished);

このスロットは、スケーリングされた画像をUIに表示したり、スケーリング中に起こりうるエラーを処理したりします:

void Images::scaleFinished()
{
    const OptionalImages result = scalingWatcher.result();
    if (result.has_value()) {
        const auto scaled = result.value();
        showImages(scaled);
        updateStatus(tr("Finished"));
    } else {
        updateStatus(tr("Failed to extract image data."));
    }
    addUrlsButton->setEnabled(true);
}

エラー報告は、Images::scaled() メソッドからオプショナルを返すことで実装されます:

Images::OptionalImages Images::scaled(const QList<QByteArray> &data)
{
    QList<QImage> scaled;
    for (const auto &imgData : data) {
        QImage image;
        image.loadFromData(imgData);
        if (image.isNull())
            return std::nullopt;

        scaled.push_back(image.scaled(100, 100, Qt::KeepAspectRatio));
    }

    return scaled;
}

ここでのImages::OptionalImages 型は、単にstd::optional の typedef です:

using OptionalImages = std::optional<QList<QImage>>;

注: .onFailed ()ハンドラを使用して、非同期スケーリング操作によるエラーを処理することはできません。なぜなら、ハンドラはUIスレッドのImages オブジェクトのコンテキストで実行される必要があるからです。なぜなら、ハンドラはUIスレッドのImages オブジェクトのコンテキストで実行される必要があるからです。非同期計算が行われている間にユーザーがアプリケーションを閉じると、 オブジェクトは破壊され、継続からそのメンバにアクセスするとクラッシュにつながります。QFutureWatcher とそのシグナルを使用することで、この問題を回避することができます。なぜなら、シグナルはQFutureWatcher が破棄されるときに切断されるので、関連するスロットが破棄されたコンテキストで実行されることはないからです。

残りのコードは簡単です。詳細はサンプル・プロジェクトを参照してください。

サンプルを実行する

Qt Creator からサンプルを実行するには、Welcome モードを開き、Examples からサンプルを選択します。詳細は、Building and Running an Example を参照してください。

サンプルプロジェクト @ code.qt.io

©2024 The Qt Company Ltd. 本書に含まれるドキュメントの著作権は、それぞれの所有者に帰属します。 ここで提供されるドキュメントは、Free Software Foundation が発行したGNU Free Documentation License version 1.3に基づいてライセンスされています。 Qtおよびそれぞれのロゴは、フィンランドおよびその他の国におけるThe Qt Company Ltd.の 商標です。その他すべての商標は、それぞれの所有者に帰属します。