このページでは

マンデルブロ

マンデルブロートの例は、Qt を使用したマルチスレッドプログラミングを実演するものです。ワーカースレッドを使用して、メインスレッドのイベントループをブロックすることなく、負荷の高い計算を実行する方法を示しています。

マンデルブロートの例のスクリーンショット

ここでいう負荷の高い計算とは、おそらく世界で最も有名なフラクタルであるマンデルブロ集合の計算を指します。最近では、XaoSのような高度なプログラムがマンデルブロ集合のリアルタイムズーム機能を提供していますが、標準的なマンデルブロアルゴリズムは、本目的にはちょうど良い程度の処理速度しかありません。

実際の場面では、ここで説明するアプローチは、同期ネットワークI/Oやデータベースアクセスなど、負荷の高い処理が行われている間もユーザーインターフェースの応答性を維持しなければならない、幅広い問題に適用可能です。「Blocking Fortune Client」の例では、TCPクライアントにおいて同じ原理が機能している様子を示しています。

マンデルブロ集合のアプリケーションは、マウスやキーボードを使ったズームやスクロールに対応しています。メインスレッドのイベントループ(ひいてはアプリケーションのユーザーインターフェース)がフリーズするのを防ぐため、フラクタルの計算処理をすべて別のワーカースレッドに委ねています。このスレッドは、フラクタルのレンダリングが完了するとシグナルを発信します。

ワーカースレッドが新しいズーム係数の位置に合わせてフラクタルを再計算している間、メインスレッドは以前にレンダリングされたピクマップを単純に拡大・縮小して、即座にフィードバックを提供します。 その結果は、ワーカースレッドが最終的に提供する画像ほど見栄えは良くありませんが、少なくともアプリケーションの応答性は向上します。以下のスクリーンショットの連作は、元の画像、拡大縮小された画像、および再描画された画像を示しています。

オリジナルのマンデルブロ画像ズーム時に拡大・縮小されるズーム後に再描画

同様に、ユーザーがスクロールすると、以前のピクマップが即座にスクロールされ、ピクマップの端を越えた描画されていない領域が露わになりますが、その間もワーカースレッドによって画像はレンダリングされ続けています。

元の位置未塗装部分を含むスクロールスクロール後に再描画

このアプリケーションは2つのクラスで構成されています:

  • 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() シグナルを発行します。

protectedrun() 関数は、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));
}

コンストラクタでは、restart およびabort 変数をfalse に初期化します。これらの変数は、run() 関数の処理の流れを制御します。

また、一連のRGB色を含むcolormap 配列も初期化します。

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

    wait();
}

デストラクタは、スレッドがアクティブな間、いつでも呼び出される可能性があります。abort をtrue に設定することで、run() にできるだけ早く実行を停止するよう指示します。また、スレッドがスリープ状態にある場合は、QWaitCondition::wakeOne()を呼び出してスレッドを目覚めさせます。(run() を解説する際に見ていくように、スレッドは処理がない場合にスリープ状態になります。)

ここで注意すべき重要な点は、run() が独自のスレッド(ワーカースレッド)で実行されるのに対し、RenderThread のコンストラクタとデストラクタ(およびrender() 関数)は、ワーカースレッドを作成したスレッドによって呼び出されるということです。したがって、run() によっていつでもアクセスされる可能性があるabort およびcondition 変数へのアクセスを保護するために、ミューテックスが必要です。

デストラクタの最後で、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 のサイズを指定します。

この関数は、パラメータをメンバ変数に格納します。スレッドがまだ実行されていない場合は、それを開始します。そうでない場合は、restart をtrue に設定し(これにより、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.start();

            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 を参照)。

ループ内で、restart がtrue に設定されていることが判明した場合(render() による)、直ちにループから抜け出し、制御を速やかに外側のループ(forever ループ)の先頭に戻して、新しいレンダリングパラメータを取得します。 同様に、abort がtrue に設定されていることが判明した場合(RenderThread のデストラクタによって)、関数から直ちに戻り、スレッドを終了させます。

中核となるアルゴリズムについては、このチュートリアルの範囲外です。

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

すべての反復処理が完了したら、restart がtrue でない限り、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ビットのQImageと互換性のあるRGB値に変換するヘルパー関数です。この関数は、コンストラクタから呼び出され、colormap 配列を心地よい色で初期化するために使用されます。

MandelbrotWidget クラスの定義

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 クラスの多くのイベントハンドラを再実装しています。さらに、updatePixmap() スロットを持っており、これをワーカースレッドのrenderedImage() シグナルに接続することで、スレッドから新しいデータを受信するたびに表示を更新します。

プライベート変数には、RenderThread 型のthread と、最後にレンダリングされた画像を格納するpixmap があります。

MandelbrotWidget クラスの実装

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() の呼び出しです。

これは、2つの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);

        QRectF exposed = painter.transform().inverted().mapRect(rect());
        exposed = exposed.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() は、マウスホイールの回転角度を 1 度の 8 分の 1 単位で返します。ほとんどのマウスでは、ホイール 1 ステップが 15 度に相当します。 ホイールの回転数(マウスステップ数)を算出し、それに基づいてズーム倍率を決定します。たとえば、ホイールが正の方向に2ステップ(つまり+30度)回転した場合、ズーム倍率はZoomInFactor の2乗、すなわち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 ` を使っているのか、疑問に思うかもしれません。 なぜ1つの型に統一しないのでしょうか?その理由は、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() と似ていますが、影響を受けるパラメータがcenterX とcenterY である点が異なります。

main() 関数

アプリケーションのマルチスレッド構造は、main() 関数には影響を与えません。この関数は、いつものように単純なものです:

intmain(intargc, char *argv[])
{
    QApplication app(argc,argv);

    QCommandLineParser parser;
    parser.setApplicationDescription(u"Qt マンデルブロの例"_s);
    parser.addHelpOption();
    parser.addVersionOption();
    QCommandLineOption passesOption(u"passes"_s,u"反復回数 (1-8)"_s,u"passes"_s);
    parser.addOption(passesOption);
    parser.process(app);

   if(parser.isSet(passesOption)) {
        const autopassesStr=parser.value(passesOption);
       boolok;
        const intpasses=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();
    returnapp.exec();
}

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

© 2026 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.