Sur cette page

Mandelbrot

L'exemple Mandelbrot illustre la programmation multithread avec Qt. Il montre comment utiliser un thread de travail pour effectuer des calculs intensifs sans bloquer la boucle d'événements du thread principal.

Capture d'écran de l'exemple de Mandelbrot

Le calcul intensif en question est ici l'ensemble de Mandelbrot, sans doute la fractale la plus célèbre au monde. De nos jours, alors 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 pratique, l’approche décrite ici s’applique à un large éventail 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 intensive est en cours. L’exemple « Blocking Fortune Client » illustre le même principe dans un client TCP.

L’application Mandelbrot prend en charge le zoom et le défilement à l’aide de la souris ou du clavier. Pour éviter de bloquer la boucle d’événements du thread principal (et, par conséquent, l’interface utilisateur de l’application), nous plaçons tous les calculs fractals dans un thread de travail 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 se contente de redimensionner la carte de pixels précédemment rendue afin de fournir un retour immédiat. Le résultat n’est pas aussi beau que celui fourni à terme par le thread de travail, mais cela rend au moins l’application plus réactive. La séquence de captures d’écran ci-dessous montre l’image d’origine, l’image redimensionnée et l’image réaffichée.

Image originale de MandelbrotMise à l'échelle lors du zoomRéaffiché après le zoom

De même, lorsque l’utilisateur fait défiler l’écran, l’image pixmap précédente défile immédiatement, révélant les zones non dessinées au-delà du bord de l’image pixmap, tandis que l’image est rendue par le thread de travail.

Position initialeDéroulé avec une zone non peinteRaffichage actualisé après le défilement

L’application se compose de deux classes :

  • RenderThread est une sous-classe de ` QThread ` qui affiche 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 la présentation du multithreading dans Qt.

Définition de la classe RenderThread

Commençons 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 afin de pouvoir s’exécuter dans un thread distinct. Outre le constructeur et le destructeur, render() est la seule fonction publique. Dès 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 au démarrage du thread.

Dans la section private, on trouve un QMutex, un QWaitCondition et quelques autres membres de données. Le mutex protège les autres membres de données.

Implémentation 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 déroulement 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é à tout moment tant que le thread est actif. Nous définissons abort sur true pour indiquer à run() de cesser son exécution dès que possible. Nous appelons également QWaitCondition::wakeOne() pour réveiller le thread s’il est en veille. (Comme nous le verrons lors de l’étude de run(), le thread est mis en veille lorsqu’il n’a rien à faire.)

Il est important de noter ici que run() s’exécute dans son propre thread (le thread de travail), tandis que le constructeur et le destructeur de RenderThread (ainsi que la fonction render() ) sont appelés par le thread qui a créé le thread de travail. Par conséquent, nous avons besoin d’un mutex pour protéger les accès aux variables abort et condition, auxquelles run() pourrait accéder à tout moment.

À la fin du destructeur, nous appelons ` QThread::wait()` pour attendre que ` run() ` se soit terminé avant que le destructeur de la classe de base ne soit invoqué.

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 que celui-ci a besoin de 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 à afficher ; resultSize spécifie la taille de l’ QImage résultant.

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 lance ; sinon, elle définit restart sur true (ce qui indique à run() d’arrêter tout calcul inachevé et de recommencer avec les nouveaux paramètres) et réveille le thread, qui est peut-être en veille.

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() C’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 à l’aide du mutex de la classe. Le stockage des variables membres dans des variables locales nous permet de réduire au minimum la quantité de code devant être protégée par un mutex. Cela garantit que le thread principal n’aura jamais à se bloquer trop longtemps lorsqu’il devra 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 passes 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 de haute résolution en appliquant le rapport de pixels de l’appareil à la taille cible (voir Drawing High Resolution Versions of Pixmaps and Images).

Si, à l’intérieur de la boucle, nous constatons que restart a été défini sur true (par render()), nous sortons immédiatement de la boucle, de sorte que le contrôle revienne rapidement tout en haut de la boucle externe (la boucle forever ) et que nous récupérions les nouveaux paramètres de rendu. De même, si nous constatons que ` abort ` a été défini à ` true ` (par le destructeur de ` RenderThread `), nous quittons immédiatement la fonction, ce qui met fin au thread.

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, sauf si ` restart ` est égal à ` true`. Il est inutile de laisser un thread de travail tourner en boucle indéfiniment alors 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 les ` QImage` de 32 bits. Elle est appelée depuis 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 la classe QWidget. De plus, il dispose d’un slot updatePixmap() que nous allons connecter au signal renderedImage() du thread de travail afin de mettre à jour l’affichage chaque fois que nous recevons de nouvelles données de ce thread.

Parmi les variables privées, on trouve 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 par la suite.

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 qu’elle ressemble à une connexion signal-slot standard entre deux objets ` QObject`, comme le signal est émis dans un thread différent de celui où réside le récepteur, cette connexion est en réalité une connexion de type ` 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 où réside le récepteur. Ici, le signal est émis dans le thread de travail, et le slot est exécuté dans le thread de l’interface graphique lorsque le contrôle revient à la boucle d’événements.

Avec les connexions mises en file d’attente, Qt doit stocker une copie des arguments qui ont été passés au signal afin de pouvoir les transmettre au slot ultérieurement. Qt sait comment créer une copie de nombreux types C++ et Qt ; aucune action supplémentaire n’est donc 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 la fonction ` paintEvent()`, nous commençons par remplir l’arrière-plan de noir. Si nous n’avons encore rien à dessiner (`pixmap ` est nul), nous affichons un message sur le widget demandant à l’utilisateur de patienter, puis nous quittons immédiatement la fonction.

    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 le pixmap possède le bon facteur d’échelle, on le dessine directement sur le widget.

Sinon, nous créons une image d’aperçu qui s’affichera jusqu’à la fin du calcul et nous translatons le système de coordonnées en conséquence.

Comme nous allons appliquer des transformations au peintre et utiliser une surcharge de la fonction ` QPainter::drawPixmap()` qui, dans ce cas, ne prend pas en charge les pixmaps haute résolution, nous créons un pixmap avec un rapport de pixels de périphérique égal à 1.

En effectuant un mappage inverse du rectangle du widget à l’aide de la matrice de peinture mise à l’échelle, nous nous assurons également que seules les zones visibles de l’image sont dessinées. Les appels à ` QPainter::save()` et ` QPainter::restore()` garantissent que tout dessin effectué 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 dessin, nous dessinons une chaîne de texte et un rectangle semi-transparent par-dessus 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 lancer la génération d’une nouvelle image, avec les mêmes paramètres ` centerX`, ` centerY` et ` curScale `, mais en tenant compte de la nouvelle taille du widget.

Notez que nous comptons sur le fait que la méthode resizeEvent() soit automatiquement appelée par Qt lors de la première affichage du widget pour 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énements de frappe propose quelques raccourcis clavier à l’intention des utilisateurs qui ne disposent pas de souris. Les fonctions zoom() et scroll() seront abordées plus loin.

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énements de molette est réimplémenté pour permettre à la molette de la souris de contrôler le niveau de zoom. La fonction ` QWheelEvent::angleDelta()` renvoie l’angle de rotation de la molette, en huitièmes de degré. Pour la plupart des souris, un pas de molette correspond à 15 degrés. Nous déterminons le nombre de pas de la souris et calculons le facteur de zoom correspondant. Par exemple, si nous avons deux pas de molette dans le sens positif (c'est-à-dire +30 degrés), le facteur de zoom est égal à ZoomInFactor élevé à la puissance deux, soit 0,8 * 0,8 = 0,64.

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

Le zoom par pincement a été implémenté à l’aide de la fonction ` QGesture `, comme décrit 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 tout en maintenant le bouton gauche enfoncé, nous ajustons ` pixmapOffset ` pour dessiner la carte de pixels à une position décalée et appelons ` QWidget::update()` pour forcer un rafraîchissement de l’affichage.

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 afficher une nouvelle image correspondant à la nouvelle position. (Le simple ajustement de ` pixmapOffset ` ne suffit pas, car les zones révélées lors du glissement de l’image sont affiché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();
}

La méthode ` updatePixmap() ` est appelée lorsque le thread de travail a terminé le rendu d'une image. Nous commençons par vérifier si un glissement est en cours et, dans ce cas, nous ne faisons rien. Dans le cas normal, nous stockons l'image dans ` pixmap ` et réinitialisons certains des autres membres. À la fin, nous appelons ` QWidget::update()` pour actualiser l'affichage.

À ce stade, vous vous demandez peut-être pourquoi nous utilisons une ` QImage ` pour le paramètre et une ` 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 prend en charge la manipulation directe des pixels, dont nous avons besoin dans le thread de travail. D'autre part, avant qu’une image puisse être dessinée à l’écran, elle doit être convertie en pixmap. Il vaut mieux effectuer cette conversion une bonne 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 un pixmap mis à l'échelle, puis nous demandons au thread de travail de générer 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(), à l’exception des paramètres concernés, qui sont centerX et centerY.

La fonction `main()`

La nature multithread de l’application n’a aucune incidence sur sa fonction ` main() `, qui reste aussi simple que d’habitude :

int main(int argc, char *argv[])
{
    QApplication app(argc, argv);

    QCommandLineParser parser;
    parser.setApplicationDescription(u"Exemple Qt Mandelbrot"_s);
    parser.addHelpOption();
    parser.addVersionOption();
    QCommandLineOption passesOption(u"passes"_s, u"Nombre de passages (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 sur 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.