Sur cette page

Mandelbrot

L'exemple Mandelbrot illustre la programmation multithread à l'aide de Qt. Il montre comment utiliser un fil d'exécution pour effectuer des calculs lourds sans bloquer la boucle d'événements du fil principal.

Capture d'écran de l'exemple de Mandelbrot

Le calcul lourd ici est l'ensemble de Mandelbrot, probablement la fractale la plus célèbre au monde. De nos jours, bien que des programmes sophistiqués, tels que XaoS, permettent de zoomer en temps réel sur l'ensemble de Mandelbrot, l'algorithme standard de Mandelbrot est juste assez lent pour nos besoins.

Dans la réalité, l'approche décrite ici est applicable à un grand nombre de problèmes, notamment les E/S réseau synchrones et l'accès aux bases de données, où l'interface utilisateur doit rester réactive pendant qu'une opération lourde est en cours. L'exemple du client Fortune bloquant montre le même principe à l'œuvre dans un client TCP.

L'application Mandelbrot permet d'effectuer des zooms et des défilements à l'aide de la souris ou du clavier. Pour éviter de geler la boucle d'événements du fil principal (et, par conséquent, l'interface utilisateur de l'application), nous plaçons tous les calculs fractals dans un fil d'exécution distinct. Ce thread émet un signal lorsqu'il a terminé le rendu de la fractale.

Pendant que le thread de travail recalcule la fractale pour refléter la nouvelle position du facteur de zoom, le thread principal met simplement à l'échelle la pixmap précédemment rendue pour fournir un retour d'information immédiat. Le résultat n'est pas aussi bon que ce que le fil d'exécution finit par fournir, mais au moins il rend l'application plus réactive. La séquence de captures d'écran ci-dessous montre l'image originale, l'image mise à l'échelle et l'image recalculée.

De même, lorsque l'utilisateur fait défiler l'écran, la pixmap précédente est immédiatement affichée, révélant les zones non peintes au-delà du bord de la pixmap, tandis que l'image est rendue par le fil d'exécution.

L'application se compose de deux classes :

  • RenderThread une sous-classe QThread qui rend l'ensemble de Mandelbrot.
  • MandelbrotWidget est une sous-classe de QWidget qui affiche l'ensemble de Mandelbrot à l'écran et permet à l'utilisateur de zoomer et de faire défiler l'image.

Si vous n'êtes pas encore familiarisé avec la prise en charge des threads par Qt, nous vous recommandons de commencer par lire l'aperçu du multithreading dans Qt.

Définition de la classe RenderThread

Nous commencerons par la définition de la classe 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];
};

La classe hérite de QThread, ce qui lui permet de s'exécuter dans un thread séparé. En dehors du constructeur et du destructeur, render() est la seule fonction publique. Chaque fois que le thread a terminé le rendu d'une image, il émet le signal renderedImage().

La fonction protégée run() est réimplémentée à partir de QThread. Elle est automatiquement appelée lorsque le thread est démarré.

Dans la section private, nous avons un QMutex, un QWaitCondition, et quelques autres membres de données. Le mutex protège l'autre membre de données.

Mise en œuvre de la classe RenderThread

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

Dans le constructeur, nous initialisons les variables restart et abort à false. Ces variables contrôlent le flux de la fonction run().

Nous initialisons également le tableau colormap, qui contient une série de couleurs RVB.

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

    wait();
}

Le destructeur peut être appelé à n'importe quel moment pendant que le thread est actif. Nous attribuons la valeur abort à true pour indiquer à run() qu'il doit cesser de fonctionner dès que possible. Nous appelons également QWaitCondition::wakeOne() pour réveiller le thread s'il est endormi. (Comme nous le verrons lorsque nous examinerons run(), le thread est mis en sommeil lorsqu'il n'a rien à faire).

La chose importante à noter ici est que run() est exécuté dans son propre thread (le worker thread), alors que le constructeur et le destructeur de RenderThread (ainsi que la fonction render() ) sont appelés par le thread qui a créé le worker thread. Par conséquent, nous avons besoin d'un mutex pour protéger les accès aux variables abort et condition, qui peuvent être accédées à tout moment par run().

À la fin du destructeur, nous appelons QThread::wait() pour attendre que run() soit sorti avant d'invoquer le destructeur de la classe de base.

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();
    }
}

La fonction render() est appelée par MandelbrotWidget chaque fois qu'elle doit générer une nouvelle image de l'ensemble de Mandelbrot. Les paramètres centerX, centerY et scaleFactor spécifient la partie de la fractale à rendre ; resultSize spécifie la taille de l'image QImage.

La fonction stocke les paramètres dans des variables membres. Si le thread n'est pas déjà en cours d'exécution, elle le démarre ; sinon, elle affecte la valeur restart à true (indiquant à run() d'arrêter tout calcul inachevé et de recommencer avec les nouveaux paramètres) et réveille le thread, qui peut être en train de dormir.

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() est une fonction assez volumineuse, nous allons donc la décomposer en plusieurs parties.

Le corps de la fonction est une boucle infinie qui commence par stocker les paramètres de rendu dans des variables locales. Comme d'habitude, nous protégeons les accès aux variables membres en utilisant le mutex de la classe. Le stockage des variables membres dans des variables locales nous permet de minimiser la quantité de code qui doit être protégée par un mutex. Cela garantit que le thread principal n'aura jamais à bloquer trop longtemps lorsqu'il doit accéder aux variables membres de RenderThread(par exemple, dans render()).

Le mot-clé forever est un pseudo-mot-clé 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;
            }
        }

Vient ensuite le cœur de l'algorithme. Au lieu d'essayer de créer une image parfaite de l'ensemble de Mandelbrot, nous effectuons plusieurs passages et générons des approximations de plus en plus précises (et coûteuses en calcul) de la fractale.

Nous créons une image haute résolution en appliquant le ratio de pixels de l'appareil à la taille cible (voir Drawing High Resolution Versions of Pixmaps and Images).

Si nous découvrons à l'intérieur de la boucle que restart a été réglé sur true (par render()), nous sortons immédiatement de la boucle, de sorte que le contrôle revient rapidement au sommet de la boucle extérieure (la boucle forever ) et nous récupérons les nouveaux paramètres de rendu. De même, si nous découvrons que abort a été remplacé par true (par le destructeur RenderThread ), nous quittons immédiatement la fonction, ce qui met fin au fil de discussion.

L'algorithme de base dépasse le cadre de ce tutoriel.

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

Une fois toutes les itérations terminées, nous appelons QWaitCondition::wait() pour mettre le thread en veille, à moins que restart ne soit true. Il n'est pas utile de laisser un fil d'exécution tourner en boucle indéfiniment tant qu'il n'y a rien à faire.

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));
}

La fonction rgbFromWaveLength() est une fonction d'aide qui convertit une longueur d'onde en une valeur RVB compatible avec QImages 32 bits. Elle est appelée par le constructeur pour initialiser le tableau colormap avec des couleurs agréables.

Définition de la classe MandelbrotWidget

La classe MandelbrotWidget utilise RenderThread pour dessiner l'ensemble de Mandelbrot à l'écran. Voici la définition de la classe :

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;
};

Le widget réimplémente de nombreux gestionnaires d'événements de QWidget. De plus, il possède un slot updatePixmap() que nous connecterons au signal renderedImage() du thread de travail pour mettre à jour l'affichage chaque fois que nous recevons de nouvelles données du thread.

Parmi les variables privées, nous avons thread de type RenderThread et pixmap, qui contient la dernière image rendue.

Implémentation de la classe 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;

L'implémentation commence par quelques constantes dont nous aurons besoin plus tard.

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
}

La partie intéressante du constructeur est l'appel à QObject::connect().

Bien que cela ressemble à une connexion standard entre deux QObjects, parce que le signal est émis dans un thread différent de celui dans lequel vit le récepteur, la connexion est en fait une queued connection. Ces connexions sont asynchrones (c'est-à-dire non bloquantes), ce qui signifie que le slot sera appelé à un moment donné après l'instruction emit. De plus, le slot sera invoqué dans le thread dans lequel vit le récepteur. Ici, le signal est émis dans le thread du travailleur, et le slot est exécuté dans le thread de l'interface graphique lorsque le contrôle revient à la boucle d'événement.

Avec les connexions en file d'attente, Qt doit stocker une copie des arguments qui ont été passés au signal afin de pouvoir les passer au slot plus tard. Qt sait comment prendre une copie de nombreux types C++ et Qt, donc aucune autre action n'est nécessaire pour QImage. Si un type personnalisé était utilisé, un appel à la fonction modèle qRegisterMetaType() serait nécessaire avant que le type puisse être utilisé comme paramètre dans les connexions en file d'attente.

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;
    }

Dans paintEvent(), nous commençons par remplir l'arrière-plan de noir. Si nous n'avons encore rien à peindre (pixmap est null), nous affichons un message sur le widget demandant à l'utilisateur d'être patient et de quitter la fonction immédiatement.

    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();
    }

Si la pixmap a le bon facteur d'échelle, nous la dessinons directement sur le widget.

Dans le cas contraire, nous créons une pixmap de prévisualisation qui sera affichée jusqu'à la fin du calcul et nous traduisons le système de coordonnées en conséquence.

Puisque nous allons utiliser des transformations sur le peintre et utiliser une surcharge de QPainter::drawPixmap() qui ne supporte pas les pixmaps haute résolution dans ce cas, nous créons un pixmap avec un ratio de pixel de périphérique 1.

En inversant le rectangle du widget à l'aide de la matrice du peintre mise à l'échelle, nous nous assurons également que seules les zones exposées de la pixmap sont dessinées. Les appels à QPainter::save() et QPainter::restore() garantissent que toute peinture effectuée par la suite utilise le système de coordonnées standard.

    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);

}

À la fin du gestionnaire d'événement de peinture, nous dessinons une chaîne de texte et un rectangle semi-transparent au-dessus de la fractale.

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

Chaque fois que l'utilisateur redimensionne le widget, nous appelons render() pour commencer à générer une nouvelle image, avec les mêmes paramètres centerX, centerY, et curScale mais avec la nouvelle taille du widget.

Notez que nous comptons sur l'appel automatique de resizeEvent() par Qt Widgets lorsque le widget est affiché pour la première fois afin de générer l'image initiale.

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);
    }
}

Le gestionnaire d'événement de pression de touche fournit quelques liaisons clavier pour les utilisateurs qui n'ont pas de souris. Les fonctions zoom() et scroll() seront abordées ultérieurement.

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

Le gestionnaire d'événement de la roue est réimplémenté pour que la roue de la souris contrôle le niveau de zoom. QWheelEvent::angleDelta() renvoie l'angle du mouvement de la molette de la souris, en huitièmes de degré. Pour la plupart des souris, un pas de souris correspond à 15 degrés. Nous déterminons le nombre de pas de souris que nous avons et le facteur de zoom qui en résulte. Par exemple, si nous avons deux pas de souris dans la direction positive (c'est-à-dire +30 degrés), le facteur de zoom devient ZoomInFactor à la deuxième puissance, c'est-à-dire 0,8 * 0,8 = 0,64.

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

La fonction "pincer pour zoomer" a été mise en œuvre à l'adresse QGesture, comme indiqué dans la section " Gestes dans les widgets et la vue graphique".

#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

Lorsque l'utilisateur appuie sur le bouton gauche de la souris, nous enregistrons la position du pointeur de la souris dans lastDragPos.

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

Lorsque l'utilisateur déplace le pointeur de la souris alors que le bouton gauche de la souris est enfoncé, nous ajustons pixmapOffset pour peindre la pixmap à une position décalée et appelons QWidget::update() pour forcer une nouvelle peinture.

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);
    }
}

Lorsque le bouton gauche de la souris est relâché, nous mettons à jour pixmapOffset comme nous l'avons fait lors d'un déplacement de la souris et nous réinitialisons lastDragPos à sa valeur par défaut. Ensuite, nous appelons scroll() pour rendre une nouvelle image pour la nouvelle position. (L'ajustement de pixmapOffset n'est pas suffisant car les zones révélées par le déplacement de la pixmap sont dessinées en noir).

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();
}

Le slot updatePixmap() est invoqué lorsque le thread de travail a terminé le rendu d'une image. Nous commençons par vérifier si un drag est en cours et ne faisons rien dans ce cas. Dans le cas normal, nous stockons l'image dans pixmap et réinitialisons certains des autres membres. À la fin, nous appelons QWidget::update() pour rafraîchir l'affichage.

À ce stade, vous vous demandez peut-être pourquoi nous utilisons un QImage pour le paramètre et un QPixmap pour le membre de données. Pourquoi ne pas s'en tenir à un seul type ? La raison est que QImage est la seule classe qui supporte la manipulation directe des pixels, ce dont nous avons besoin dans le fil d'exécution. D'autre part, avant qu'une image puisse être dessinée à l'écran, elle doit être convertie en pixmap. Il est préférable d'effectuer cette conversion une fois pour toutes ici, plutôt que dans paintEvent().

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

Dans zoom(), nous recalculons curScale. Ensuite, nous appelons QWidget::update() pour dessiner une pixmap à l'échelle, et nous demandons au thread de travail de rendre une nouvelle image correspondant à la nouvelle valeur de curScale.

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

scroll() est similaire à zoom(), sauf que les paramètres concernés sont centerX et centerY.

La fonction main()

La nature multithread de l'application n'a aucun impact sur la fonction main(), qui est aussi simple que d'habitude :

int main(int argc, char *argv[]) { QApplication app(argc, argv) ; QCommandLineParser parser ; parser.setApplicationDescription(u"Qt Mandelbrot Example"_s) ; parser.addHelpOption() ; parser.addVersionOption() ; QCommandLineOption passesOption(u"passes"_s, u"Nombre de passes (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() ; }

Exemple de projet @ 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.