이미지 크기 조정

이미지를 비동기적으로 다운로드하고 크기를 조정하는 방법을 보여줍니다.

이 예는 QFuture, QPromise, QFutureWatcher 클래스를 사용하여 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();
}

약속과 관련된 미래가 호출자에게 반환됩니다.

아직 자세히 설명하지 않고 프로미스 객체가 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() 메서드를 사용하여 첨부된 오류 처리기에서 처리됩니다. 두 개의 실패 처리기가 있는데, 첫 번째 처리기는 네트워크 오류를 캡처하고 두 번째 처리기는 실행 중에 발생한 다른 모든 예외를 캡처합니다. 두 핸들러 모두 예외를 프로미스 객체 내부에 저장하고( download() 메서드의 호출자가 처리할 수 있도록) 계산이 완료되었음을 보고합니다. 또한 간단하게 설명하기 위해 오류가 발생하면 보류 중인 모든 다운로드를 중단합니다.

요청이 취소되지 않았고 오류가 발생하지 않은 경우, 네트워크 응답에서 데이터를 읽어 프라미스 오브젝트의 결과 목록에 추가합니다:

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

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

프라미스 객체에 저장된 결과의 수가 다운로드할 url의 수와 같으면 더 이상 처리할 요청이 없으므로 프라미스가 완료된 것으로 보고합니다.

앞서 언급했듯이 프로미스를 QSharedPointer 안에 감쌌습니다. 프로미스 객체는 각 네트워크 응답에 연결된 핸들러 간에 공유되므로 여러 위치에서 동시에 프로미스 객체를 복사하여 사용해야 합니다. 따라서 QSharedPointer 가 사용됩니다.

download() 메서드는 Images::process 메서드에서 호출됩니다. 사용자가 "URL 추가" 버튼을 누르면 호출됩니다:

    ...
    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 에 대한 타입 정의입니다:

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

참고: 처리기는 UI 스레드의 Images 객체 컨텍스트에서 실행되어야 하므로 .onFailed() 처리기를 사용하여 비동기 스케일링 작업의 오류를 처리할 수 없습니다. 비동기 계산이 완료되는 동안 사용자가 애플리케이션을 닫으면 Images 객체가 소멸되고 그 이후부터 해당 객체의 멤버에 액세스하면 충돌이 발생합니다. QFutureWatcher 와 그 신호를 사용하면 QFutureWatcher 이 파괴될 때 신호 연결이 끊어지므로 관련 슬롯이 파괴된 컨텍스트에서 실행되지 않으므로 문제를 피할 수 있습니다.

나머지 코드는 간단하며 자세한 내용은 예제 프로젝트에서 확인할 수 있습니다.

예제 실행하기

에서 예제를 실행하려면 Qt Creator에서 Welcome 모드를 열고 Examples 에서 예제를 선택합니다. 자세한 내용은 예제 빌드 및 실행하기를 참조하세요.

예제 프로젝트 @ code.qt.io

© 2025 The Qt Company Ltd. Documentation contributions included herein are the copyrights of their respective owners. The documentation provided herein is licensed under the terms of the GNU Free Documentation License version 1.3 as published by the Free Software Foundation. Qt and respective logos are trademarks of The Qt Company Ltd. in Finland and/or other countries worldwide. All other trademarks are property of their respective owners.