만델브로트

만델브로트 예제는 Qt를 사용한 멀티 스레드 프로그래밍을 보여줍니다. 메인 스레드의 이벤트 루프를 차단하지 않고 작업자 스레드를 사용하여 무거운 계산을 수행하는 방법을 보여줍니다.

Screenshot of the Mandelbrot example

여기서 무거운 계산은 세계에서 가장 유명한 프랙탈인 만델브로트 집합입니다. 요즘에는 XaoS와 같은 정교한 프로그램에서 만델브로 집합을 실시간으로 확대할 수 있지만, 표준 만델브로 알고리즘은 우리의 목적에 비해 충분히 느립니다.

실생활에서 여기에 설명된 접근 방식은 동기식 네트워크 I/O 및 데이터베이스 액세스와 같이 일부 무거운 작업이 진행되는 동안 사용자 인터페이스가 응답성을 유지해야 하는 대규모 문제에 적용할 수 있습니다. 블로킹 포춘 클라이언트 예제는 TCP 클라이언트에서 동일한 원리가 작동하는 것을 보여줍니다.

만델브로트 애플리케이션은 마우스나 키보드를 사용한 확대/축소 및 스크롤을 지원합니다. 메인 스레드의 이벤트 루프(그리고 결과적으로 애플리케이션의 사용자 인터페이스)가 멈추는 것을 방지하기 위해 모든 프랙탈 연산을 별도의 작업자 스레드에 넣었습니다. 이 스레드는 프랙탈 렌더링이 완료되면 신호를 보냅니다.

작업자 스레드가 새로운 줌 인자 위치를 반영하기 위해 프랙탈을 재계산하는 동안 메인 스레드는 이전에 렌더링된 픽셀맵의 크기를 조정하여 즉각적인 피드백을 제공합니다. 결과는 작업자 스레드가 최종적으로 제공하는 것만큼 좋아 보이지는 않지만 적어도 애플리케이션의 반응성이 향상됩니다. 아래 스크린샷은 원본 이미지, 크기 조정된 이미지, 다시 렌더링된 이미지를 순서대로 보여줍니다.

마찬가지로 사용자가 스크롤하면 이전 픽셀맵이 즉시 스크롤되어 픽셀맵 가장자리 너머에 페인트되지 않은 영역이 표시되고 작업자 스레드에 의해 이미지가 렌더링되는 동안 이미지가 표시됩니다.

애플리케이션은 두 개의 클래스로 구성됩니다:

  • RenderThread 는 만델브로트 세트를 렌더링하는 QThread 서브클래스입니다.
  • MandelbrotWidget 는 만델브로트 집합을 화면에 표시하고 사용자가 확대/축소 및 스크롤할 수 있도록 하는 QWidget 서브클래스입니다.

Qt의 스레드 지원에 익숙하지 않다면 Qt의 스레드 지원 개요부터 읽어보는 것이 좋습니다.

RenderThread 클래스 정의

RenderThread 클래스의 정의부터 시작하겠습니다:

class RenderThread : public QThread
{
    Q_OBJECT

public:
    RenderThread(QObject *parent = nullptr);
    ~RenderThread();

    void render(double centerX, double centerY, double scaleFactor, QSize resultSize,
                double devicePixelRatio);

    static void setNumPasses(int n) { numPasses = n; }

    static QString infoKey() { return QStringLiteral("info"); }

signals:
    void renderedImage(const QImage &image, double scaleFactor);

protected:
    void run() override;

private:
    static uint rgbFromWaveLength(double wave);

    QMutex mutex;
    QWaitCondition condition;
    double centerX;
    double centerY;
    double scaleFactor;
    double devicePixelRatio;
    QSize resultSize;
    static int numPasses;
    bool restart = false;
    bool abort = false;

    static constexpr int ColormapSize = 512;
    uint colormap[ColormapSize];
};

이 클래스는 QThread 을 상속하여 별도의 스레드에서 실행할 수 있는 기능을 얻습니다. 생성자와 소멸자를 제외하면 render() 이 유일한 공용 함수입니다. 스레드는 이미지 렌더링이 완료될 때마다 renderedImage() 신호를 보냅니다.

보호된 run() 함수는 QThread 에서 다시 구현되며 스레드가 시작될 때 자동으로 호출됩니다.

private 섹션에는 QMutex, QWaitCondition, 그리고 몇 가지 다른 데이터 멤버가 있습니다. 뮤텍스는 다른 데이터 멤버를 보호합니다.

RenderThread 클래스 구현

RenderThread::RenderThread(QObject *parent)
    : QThread(parent)
{
    for (int i = 0; i < ColormapSize; ++i)
        colormap[i] = rgbFromWaveLength(380.0 + (i * 400.0 / ColormapSize));
}

생성자에서 restartabort 변수를 false 로 초기화합니다. 이 변수는 run() 함수의 흐름을 제어합니다.

또한 일련의 RGB 색상이 포함된 colormap 배열을 초기화합니다.

RenderThread::~RenderThread()
{
    mutex.lock();
    abort = true;
    condition.wakeOne();
    mutex.unlock();

    wait();
}

소멸자는 스레드가 활성화되어 있는 동안 언제든지 호출할 수 있습니다. aborttrue 으로 설정하여 run() 에 가능한 한 빨리 실행을 중지하도록 지시합니다. 또한 QWaitCondition::wakeOne()를 호출하여 스레드가 잠자고 있는 경우 깨우기도 합니다. ( run() 을 검토할 때 보겠지만 스레드는 할 일이 없을 때 절전 모드로 전환됩니다.)

여기서 주목해야 할 중요한 점은 run() 은 자체 스레드(작업자 스레드)에서 실행되는 반면 RenderThread 생성자와 소멸자( render() 함수)는 작업자 스레드를 생성한 스레드에서 호출된다는 것입니다. 따라서 run() 에서 언제든지 액세스할 수 있는 abortcondition 변수에 대한 액세스를 보호하기 위해 뮤텍스가 필요합니다.

소멸자 끝에서 QThread::wait()를 호출하여 run() 이 종료될 때까지 기다렸다가 베이스 클래스 소멸자를 호출합니다.

void RenderThread::render(double centerX, double centerY, double scaleFactor,
                          QSize resultSize, double devicePixelRatio)
{
    QMutexLocker locker(&mutex);

    this->centerX = centerX;
    this->centerY = centerY;
    this->scaleFactor = scaleFactor;
    this->devicePixelRatio = devicePixelRatio;
    this->resultSize = resultSize;

    if (!isRunning()) {
        start(LowPriority);
    } else {
        restart = true;
        condition.wakeOne();
    }
}

render() 함수는 만델브로 집합의 새 이미지를 생성해야 할 때마다 MandelbrotWidget 에서 호출됩니다. centerX , centerY, scaleFactor 매개변수는 렌더링할 프랙탈의 부분을 지정하고, resultSize 은 결과물인 QImage 의 크기를 지정합니다.

이 함수는 매개변수를 멤버 변수에 저장합니다. 스레드가 아직 실행 중이 아니라면 스레드를 시작하고, 그렇지 않으면 restarttrue 으로 설정하고( run() 에 완료되지 않은 계산을 중지하고 새 매개변수로 다시 시작하라고 알려줌) 잠자고 있을 수 있는 스레드를 깨웁니다.

void RenderThread::run()
{
    QElapsedTimer timer;
    forever {
        mutex.lock();
        const double devicePixelRatio = this->devicePixelRatio;
        const QSize resultSize = this->resultSize * devicePixelRatio;
        const double requestedScaleFactor = this->scaleFactor;
        const double scaleFactor = requestedScaleFactor / devicePixelRatio;
        const double centerX = this->centerX;
        const double centerY = this->centerY;
        mutex.unlock();

run() 는 꽤 큰 함수이므로 여러 부분으로 나눠서 설명하겠습니다.

함수 본문은 렌더링 매개변수를 로컬 변수에 저장하는 것으로 시작하는 무한 루프입니다. 평소와 마찬가지로 클래스의 뮤텍스를 사용하여 멤버 변수에 대한 액세스를 보호합니다. 멤버 변수를 로컬 변수에 저장하면 뮤텍스로 보호해야 하는 코드의 양을 최소화할 수 있습니다. 이렇게 하면 메인 스레드가 RenderThread 의 멤버 변수에 액세스해야 할 때(예: render() 에서) 너무 오래 차단할 필요가 없습니다.

forever 키워드는 Qt 의사 키워드입니다.

        const int halfWidth = resultSize.width() / 2;
        const int halfHeight = resultSize.height() / 2;
        QImage image(resultSize, QImage::Format_RGB32);
        image.setDevicePixelRatio(devicePixelRatio);

        int pass = 0;
        while (pass < numPasses) {
            const int MaxIterations = (1 << (2 * pass + 6)) + 32;
            constexpr int Limit = 4;
            bool allBlack = true;

            timer.restart();

            for (int y = -halfHeight; y < halfHeight; ++y) {
                if (restart)
                    break;
                if (abort)
                    return;

                auto scanLine =
                        reinterpret_cast<uint *>(image.scanLine(y + halfHeight));
                const double ay = centerY + (y * scaleFactor);

                for (int x = -halfWidth; x < halfWidth; ++x) {
                    const double ax = centerX + (x * scaleFactor);
                    double a1 = ax;
                    double b1 = ay;
                    int numIterations = 0;

                    do {
                        ++numIterations;
                        const double a2 = (a1 * a1) - (b1 * b1) + ax;
                        const double b2 = (2 * a1 * b1) + ay;
                        if ((a2 * a2) + (b2 * b2) > Limit)
                            break;

                        ++numIterations;
                        a1 = (a2 * a2) - (b2 * b2) + ax;
                        b1 = (2 * a2 * b2) + ay;
                        if ((a1 * a1) + (b1 * b1) > Limit)
                            break;
                    } while (numIterations < MaxIterations);

                    if (numIterations < MaxIterations) {
                        *scanLine++ = colormap[numIterations % ColormapSize];
                        allBlack = false;
                    } else {
                        *scanLine++ = qRgb(0, 0, 0);
                    }
                }
            }

            if (allBlack && pass == 0) {
                pass = 4;
            } else {
                if (!restart) {
                    QString message;
                    QTextStream str(&message);
                    str << " Pass " << (pass + 1) << '/' << numPasses
                        << ", max iterations: " << MaxIterations << ", time: ";
                    const auto elapsed = timer.elapsed();
                    if (elapsed > 2000)
                        str << (elapsed / 1000) << 's';
                    else
                        str << elapsed << "ms";
                    image.setText(infoKey(), message);

                    emit renderedImage(image, requestedScaleFactor);
                }
                ++pass;
            }
        }

이제 알고리즘의 핵심이 나옵니다. 완벽한 만델브로 세트 이미지를 만드는 대신, 여러 번의 패스를 통해 프랙탈의 점점 더 정밀한(그리고 계산 비용이 많이 드는) 근사치를 생성합니다.

목표 크기에 디바이스 픽셀 비율을 적용하여 고해상도 픽셀맵을 생성합니다( Drawing High Resolution Versions of Pixmaps and Images 참조 ).

루프 내부에서 restarttrue 로 설정된 것을 발견하면( render() 참조) 즉시 루프에서 벗어나 제어가 외부 루프( forever 루프)의 맨 위로 빠르게 돌아가서 새 렌더링 매개변수를 가져옵니다. 마찬가지로 aborttrue ( RenderThread 소멸자에 의해)로 설정된 것을 발견하면 즉시 함수에서 반환하여 스레드를 종료합니다.

핵심 알고리즘은 이 튜토리얼의 범위를 벗어납니다.

        mutex.lock();
        if (!restart)
            condition.wait(&mutex);
        restart = false;
        mutex.unlock();
    }
}

모든 반복이 끝나면 restarttrue 이 아닌 한 QWaitCondition::wait()를 호출하여 스레드를 절전 모드로 전환합니다. 할 일이 없는데 워커 스레드를 무한정 반복하는 것은 아무런 소용이 없습니다.

uint RenderThread::rgbFromWaveLength(double wave)
{
    double r = 0;
    double g = 0;
    double b = 0;

    if (wave >= 380.0 && wave <= 440.0) {
        r = -1.0 * (wave - 440.0) / (440.0 - 380.0);
        b = 1.0;
    } else if (wave >= 440.0 && wave <= 490.0) {
        g = (wave - 440.0) / (490.0 - 440.0);
        b = 1.0;
    } else if (wave >= 490.0 && wave <= 510.0) {
        g = 1.0;
        b = -1.0 * (wave - 510.0) / (510.0 - 490.0);
    } else if (wave >= 510.0 && wave <= 580.0) {
        r = (wave - 510.0) / (580.0 - 510.0);
        g = 1.0;
    } else if (wave >= 580.0 && wave <= 645.0) {
        r = 1.0;
        g = -1.0 * (wave - 645.0) / (645.0 - 580.0);
    } else if (wave >= 645.0 && wave <= 780.0) {
        r = 1.0;
    }

    double s = 1.0;
    if (wave > 700.0)
        s = 0.3 + 0.7 * (780.0 - wave) / (780.0 - 700.0);
    else if (wave <  420.0)
        s = 0.3 + 0.7 * (wave - 380.0) / (420.0 - 380.0);

    r = std::pow(r * s, 0.8);
    g = std::pow(g * s, 0.8);
    b = std::pow(b * s, 0.8);
    return qRgb(int(r * 255), int(g * 255), int(b * 255));
}

rgbFromWaveLength() 함수는 파장을 32비트 QImages와 호환되는 RGB 값으로 변환하는 헬퍼 함수로, 생성자에서 호출되어 colormap 배열을 보기 좋은 색상으로 초기화합니다.

만델브로트 위젯 클래스 정의

MandelbrotWidget 클래스는 RenderThread 을 사용하여 화면에 만델브로트 세트를 그립니다. 다음은 클래스 정의입니다:

class MandelbrotWidget : public QWidget
{
    Q_DECLARE_TR_FUNCTIONS(MandelbrotWidget)

public:
    MandelbrotWidget(QWidget *parent = nullptr);

protected:
    QSize sizeHint() const override { return {1024, 768}; };
    void paintEvent(QPaintEvent *event) override;
    void resizeEvent(QResizeEvent *event) override;
    void keyPressEvent(QKeyEvent *event) override;
#if QT_CONFIG(wheelevent)
    void wheelEvent(QWheelEvent *event) override;
#endif
    void mousePressEvent(QMouseEvent *event) override;
    void mouseMoveEvent(QMouseEvent *event) override;
    void mouseReleaseEvent(QMouseEvent *event) override;
#ifndef QT_NO_GESTURES
    bool event(QEvent *event) override;
#endif

private:
    void updatePixmap(const QImage &image, double scaleFactor);
    void zoom(double zoomFactor);
    void scroll(int deltaX, int deltaY);
#ifndef QT_NO_GESTURES
    bool gestureEvent(QGestureEvent *event);
#endif

    RenderThread thread;
    QPixmap pixmap;
    QPoint pixmapOffset;
    QPoint lastDragPos;
    QString help;
    QString info;
    double centerX;
    double centerY;
    double pixmapScale;
    double curScale;
};

위젯은 QWidget 의 많은 이벤트 핸들러를 재구현합니다. 또한 작업자 스레드의 renderedImage() 신호에 연결하여 스레드에서 새 데이터를 수신할 때마다 디스플레이를 업데이트하는 updatePixmap() 슬롯이 있습니다.

개인 변수 중에는 RenderThread 유형의 thread 와 마지막으로 렌더링된 이미지를 포함하는 pixmap 가 있습니다.

만델브로트 위젯 클래스 구현

constexpr double DefaultCenterX = -0.637011;
constexpr double DefaultCenterY = -0.0395159;
constexpr double DefaultScale = 0.00403897;

constexpr double ZoomInFactor = 0.8;
constexpr double ZoomOutFactor = 1 / ZoomInFactor;
constexpr int ScrollStep = 20;

구현은 나중에 필요한 몇 가지 상수로 시작합니다.

MandelbrotWidget::MandelbrotWidget(QWidget *parent) :
    QWidget(parent),
    centerX(DefaultCenterX),
    centerY(DefaultCenterY),
    pixmapScale(DefaultScale),
    curScale(DefaultScale)
{
    help = tr("Zoom with mouse wheel, +/- keys or pinch.  Scroll with arrow keys or by dragging.");
    connect(&thread, &RenderThread::renderedImage,
            this, &MandelbrotWidget::updatePixmap);

    setWindowTitle(tr("Mandelbrot"));
#if QT_CONFIG(cursor)
    setCursor(Qt::CrossCursor);
#endif
}

생성자에서 흥미로운 부분은 QObject::connect() 호출입니다.

두 개의 QObject사이의 표준 신호 슬롯 연결처럼 보이지만, 신호가 수신자가 있는 스레드가 아닌 다른 스레드에서 방출되므로 사실상 queued connection 연결입니다. 이러한 연결은 비동기식(즉, 비블록킹)이므로 emit 문 이후 어느 시점에 슬롯이 호출됩니다. 또한 슬롯은 수신자가 있는 스레드에서 호출됩니다. 여기서 신호는 워커 스레드에서 방출되고, 제어가 이벤트 루프로 돌아갈 때 슬롯은 GUI 스레드에서 실행됩니다.

큐 연결의 경우, Qt는 나중에 슬롯에 전달할 수 있도록 신호에 전달된 인자의 복사본을 저장해야 합니다. Qt는 많은 C++ 및 Qt 타입의 복사본을 가져오는 방법을 알고 있으므로 QImage 에 대해서는 추가 조치가 필요하지 않습니다. 사용자 정의 타입을 사용하는 경우, 템플릿 함수 qRegisterMetaType()를 호출해야 해당 타입을 큐 연결에서 파라미터로 사용할 수 있습니다.

void MandelbrotWidget::paintEvent(QPaintEvent * /* event */)
{
    QPainter painter(this);
    painter.fillRect(rect(), Qt::black);

    if (pixmap.isNull()) {
        painter.setPen(Qt::white);
        painter.drawText(rect(), Qt::AlignCenter|Qt::TextWordWrap,
                         tr("Rendering initial image, please wait..."));
        return;
    }

paintEvent()에서는 배경을 검정색으로 채우는 것으로 시작합니다. 아직 칠할 것이 없으면(pixmap 은 null) 위젯에 사용자에게 잠시만 기다려 달라는 메시지를 표시하고 함수에서 즉시 반환합니다.

    if (qFuzzyCompare(curScale, pixmapScale)) {
        painter.drawPixmap(pixmapOffset, pixmap);
    } else {
        const auto previewPixmap = qFuzzyCompare(pixmap.devicePixelRatio(), qreal(1))
            ? pixmap
            : pixmap.scaled(pixmap.deviceIndependentSize().toSize(), Qt::KeepAspectRatio,
                            Qt::SmoothTransformation);
        const double scaleFactor = pixmapScale / curScale;
        const int newWidth = int(previewPixmap.width() * scaleFactor);
        const int newHeight = int(previewPixmap.height() * scaleFactor);
        const int newX = pixmapOffset.x() + (previewPixmap.width() - newWidth) / 2;
        const int newY = pixmapOffset.y() + (previewPixmap.height() - newHeight) / 2;

        painter.save();
        painter.translate(newX, newY);
        painter.scale(scaleFactor, scaleFactor);

        const QRectF exposed = painter.transform().inverted().mapRect(rect())
                                       .adjusted(-1, -1, 1, 1);
        painter.drawPixmap(exposed, previewPixmap, exposed);
        painter.restore();
    }

픽셀맵의 배율이 올바른 경우 픽셀맵을 위젯에 직접 그립니다.

그렇지 않으면 계산이 완료될 때까지 표시할 미리보기 픽셀맵을 생성하고 그에 따라 좌표계를 변환합니다.

이 경우 고해상도 픽셀맵을 지원하지 않는 QPainter::drawPixmap()의 오버로드를 사용하고 페인터에서 변환을 사용할 예정이므로 디바이스 픽셀 비율이 1인 픽셀맵을 만듭니다.

스케일링된 페인터 매트릭스를 사용하여 위젯의 사각형을 리버스 매핑함으로써 픽셀맵의 노출된 영역만 그려지도록 합니다. QPainter::save () 및 QPainter::restore() 호출은 이후 수행되는 모든 페인팅이 표준 좌표계를 사용하도록 합니다.

    const QFontMetrics metrics = painter.fontMetrics();
    if (!info.isEmpty()){
        const int infoWidth = metrics.horizontalAdvance(info);
        const int infoHeight = (infoWidth/width() + 1) * (metrics.height() + 5);

        painter.setPen(Qt::NoPen);
        painter.setBrush(QColor(0, 0, 0, 127));
        painter.drawRect((width() - infoWidth) / 2 - 5, 0, infoWidth + 10, infoHeight);

        painter.setPen(Qt::white);
        painter.drawText(rect(), Qt::AlignHCenter|Qt::AlignTop|Qt::TextWordWrap, info);
    }

    const int helpWidth = metrics.horizontalAdvance(help);
    const int helpHeight = (helpWidth/width() + 1) * (metrics.height() + 5);

    painter.setPen(Qt::NoPen);
    painter.setBrush(QColor(0, 0, 0, 127));
    painter.drawRect((width() - helpWidth) / 2 - 5, height()-helpHeight, helpWidth + 10,
                     helpHeight);

    painter.setPen(Qt::white);
    painter.drawText(rect(), Qt::AlignHCenter|Qt::AlignBottom|Qt::TextWordWrap, help);

}

페인트 이벤트 핸들러의 마지막에는 프랙탈 위에 텍스트 문자열과 반투명 직사각형을 그립니다.

void MandelbrotWidget::resizeEvent(QResizeEvent * /* event */)
{
    thread.render(centerX, centerY, curScale, size(), devicePixelRatio());
}

사용자가 위젯의 크기를 조정할 때마다 render() 를 호출하여 동일한 centerX, centerY, curScale 매개 변수를 사용하지만 새 위젯 크기를 사용하여 새 이미지 생성을 시작합니다.

위젯이 처음 표시될 때 Qt가 resizeEvent() 를 자동으로 호출하여 초기 이미지를 생성하는 것에 주목하세요.

void MandelbrotWidget::keyPressEvent(QKeyEvent *event)
{
    switch (event->key()) {
    case Qt::Key_Plus:
        zoom(ZoomInFactor);
        break;
    case Qt::Key_Minus:
        zoom(ZoomOutFactor);
        break;
    case Qt::Key_Left:
        scroll(-ScrollStep, 0);
        break;
    case Qt::Key_Right:
        scroll(+ScrollStep, 0);
        break;
    case Qt::Key_Down:
        scroll(0, -ScrollStep);
        break;
    case Qt::Key_Up:
        scroll(0, +ScrollStep);
        break;
    case Qt::Key_Q:
        close();
        break;
    default:
        QWidget::keyPressEvent(event);
    }
}

키 누르기 이벤트 핸들러는 마우스가 없는 사용자를 위해 몇 가지 키보드 바인딩을 제공합니다. zoom()scroll() 함수는 나중에 다룰 예정입니다.

void MandelbrotWidget::wheelEvent(QWheelEvent *event)
{
    const int numDegrees = event->angleDelta().y() / 8;
    const double numSteps = numDegrees / double(15);
    zoom(pow(ZoomInFactor, numSteps));
}

휠 이벤트 핸들러는 마우스 휠로 줌 레벨을 제어할 수 있도록 재구현되었습니다. QWheelEvent::angleDelta()는 휠 마우스 움직임의 각도를 8도 단위로 반환합니다. 대부분의 마우스에서 한 휠 단계는 15도에 해당합니다. 마우스 단계가 몇 개인지 알아내어 결과 줌 배율을 결정합니다. 예를 들어 양수 방향(즉, +30도)으로 두 개의 휠 스텝이 있는 경우 줌 배율은 ZoomInFactor 의 두 번째 거듭제곱, 즉 0.8 * 0.8 = 0.64가 됩니다.

void MandelbrotWidget::mousePressEvent(QMouseEvent *event)
{
    if (event->button() == Qt::LeftButton)
        lastDragPos = event->position().toPoint();
}

핀치 투 줌은 위젯 및 그래픽 보기의 제스처에 설명된 대로 QGesture 로 구현되었습니다.

#ifndef QT_NO_GESTURES
bool MandelbrotWidget::gestureEvent(QGestureEvent *event)
{
    if (auto *pinch = static_cast<QPinchGesture *>(event->gesture(Qt::PinchGesture))) {
        if (pinch->changeFlags().testFlag(QPinchGesture::ScaleFactorChanged))
            zoom(1.0 / pinch->scaleFactor());
        return true;
    }
    return false;
}

bool MandelbrotWidget::event(QEvent *event)
{
    if (event->type() == QEvent::Gesture)
        return gestureEvent(static_cast<QGestureEvent*>(event));
    return QWidget::event(event);
}
#endif

사용자가 마우스 왼쪽 버튼을 누르면 마우스 포인터 위치가 lastDragPos 에 저장됩니다.

void MandelbrotWidget::mouseMoveEvent(QMouseEvent *event)
{
    if (event->buttons() & Qt::LeftButton) {
        pixmapOffset += event->position().toPoint() - lastDragPos;
        lastDragPos = event->position().toPoint();
        update();
    }
}

사용자가 마우스 왼쪽 버튼을 누른 상태에서 마우스 포인터를 이동하면 pixmapOffset 을 조정하여 이동된 위치에 픽셀맵을 페인트하고 QWidget::update()을 호출하여 강제로 다시 페인트합니다.

void MandelbrotWidget::mouseReleaseEvent(QMouseEvent *event)
{
    if (event->button() == Qt::LeftButton) {
        pixmapOffset += event->position().toPoint() - lastDragPos;
        lastDragPos = QPoint();

        const auto pixmapSize = pixmap.deviceIndependentSize().toSize();
        const int deltaX = (width() - pixmapSize.width()) / 2 - pixmapOffset.x();
        const int deltaY = (height() - pixmapSize.height()) / 2 - pixmapOffset.y();
        scroll(deltaX, deltaY);
    }
}

마우스 왼쪽 버튼에서 손을 떼면 마우스 이동 시와 마찬가지로 pixmapOffset 을 업데이트하고 lastDragPos 을 기본값으로 재설정합니다. 그런 다음 scroll() 을 호출하여 새 위치에 대한 새 이미지를 렌더링합니다. (픽셀맵을 드래그할 때 드러나는 영역이 검은색으로 그려지므로 pixmapOffset 조정만으로는 충분하지 않습니다.)

void MandelbrotWidget::updatePixmap(const QImage &image, double scaleFactor)
{
    if (!lastDragPos.isNull())
        return;

    info = image.text(RenderThread::infoKey());

    pixmap = QPixmap::fromImage(image);
    pixmapOffset = QPoint();
    lastDragPos = QPoint();
    pixmapScale = scaleFactor;
    update();
}

updatePixmap() 슬롯은 작업자 스레드가 이미지 렌더링을 완료하면 호출됩니다. 드래그가 유효한지 확인하는 것으로 시작하여 드래그가 유효하지 않은 경우에는 아무것도 하지 않습니다. 일반적인 경우에는 이미지를 pixmap 에 저장하고 다른 멤버 중 일부를 다시 초기화합니다. 마지막에는 QWidget::update()를 호출하여 디스플레이를 새로 고칩니다.

이 시점에서 왜 매개변수에 QImage 를 사용하고 데이터 멤버에 QPixmap 를 사용하는지 궁금하실 것입니다. 왜 한 가지 유형을 고수하지 않을까요? 그 이유는 QImage 이 워커 스레드에서 필요한 직접 픽셀 조작을 지원하는 유일한 클래스이기 때문입니다. 반면에 이미지를 화면에 그리려면 먼저 이미지를 픽셀맵으로 변환해야 합니다. 변환은 paintEvent() 에서 하는 것보다 여기서 한 번에 수행하는 것이 좋습니다.

void MandelbrotWidget::zoom(double zoomFactor)
{
    curScale *= zoomFactor;
    update();
    thread.render(centerX, centerY, curScale, size(), devicePixelRatio());
}

zoom() 에서 curScale 을 다시 계산합니다. 그런 다음 QWidget::update()를 호출하여 스케일링된 픽셀맵을 그리고 작업자 스레드에 새로운 curScale 값에 해당하는 새 이미지를 렌더링하도록 요청합니다.

void MandelbrotWidget::scroll(int deltaX, int deltaY)
{
    centerX += deltaX * curScale;
    centerY += deltaY * curScale;
    update();
    thread.render(centerX, centerY, curScale, size(), devicePixelRatio());
}

scroll()zoom() 와 유사하지만, 영향을 받는 매개변수가 centerXcenterY 라는 점이 다릅니다.

main() 함수

애플리케이션의 멀티스레드 특성은 평소와 같이 간단한 main() 함수에 영향을 미치지 않습니다:

int main(int argc, char *argv[]) { {.  QApplication app(argc, argv);    QCommandLineParser 파서; 파서.setApplicationDescription(u"Qt Mandelbrot Example"_s); 파서.addHelpOption(); 파서.addVersionOption();    QCommandLineOption passesOption(u"passes"_s, u"통과 횟수 (1-8)"_s, u"passes"_s); parser.addOption(passesOption); parser.process(app); if (parser.isSet(passesOption)) { const auto passesStr = parser.value(passesOption); bool ok; const int passes = passesStr.toInt(&ok); if (!ok || passes < 1 || passes > 8) {            qWarning() << "Invalid value:" << passesStr;
           return-1; } RenderThread::setNumPasses(passes); } MandelbrotWidget widget; widget.grabGesture(Qt::PinchGesture); widget.show(); return app.exec(); }

예제 프로젝트 @ 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.