Mandelbrot

Das Mandelbrot-Beispiel demonstriert die Multithread-Programmierung mit Qt. Es zeigt, wie man einen Worker-Thread verwendet, um schwere Berechnungen durchzuführen, ohne die Ereignisschleife des Hauptthreads zu blockieren.

Screenshot of the Mandelbrot example

Die schwere Berechnung ist hier die Mandelbrot-Menge, das wohl berühmteste Fraktal der Welt. Heutzutage bieten zwar ausgefeilte Programme wie XaoS die Möglichkeit, die Mandelbrot-Menge in Echtzeit zu vergrößern, aber der Standard-Mandelbrot-Algorithmus ist für unsere Zwecke gerade langsam genug.

In der Praxis ist der hier beschriebene Ansatz auf eine Vielzahl von Problemen anwendbar, darunter synchrone Netzwerk-E/A und Datenbankzugriffe, bei denen die Benutzeroberfläche reaktionsschnell bleiben muss, während eine schwere Operation stattfindet. Das Beispiel des Blocking Fortune Client zeigt das gleiche Prinzip in einem TCP-Client.

Die Mandelbrot-Anwendung unterstützt das Zoomen und Scrollen mit der Maus oder der Tastatur. Um zu vermeiden, dass die Ereignisschleife des Hauptthreads (und damit die Benutzeroberfläche der Anwendung) einfriert, haben wir die gesamte Fraktalberechnung in einen separaten Worker-Thread ausgelagert. Der Thread gibt ein Signal aus, wenn er mit dem Rendering des Fraktals fertig ist.

In der Zeit, in der der Worker-Thread das Fraktal neu berechnet, um die neue Position des Zoomfaktors widerzuspiegeln, skaliert der Haupt-Thread einfach die zuvor gerenderte Pixmap, um ein unmittelbares Feedback zu geben. Das Ergebnis sieht zwar nicht so gut aus wie das, was der Worker-Thread am Ende liefert, aber zumindest ist die Anwendung dadurch reaktionsschneller. Die nachstehende Screenshot-Sequenz zeigt das Originalbild, das skalierte Bild und das neu gerenderte Bild.

Wenn der Benutzer scrollt, wird die vorherige Pixmap sofort gescrollt, so dass die nicht gemalten Bereiche jenseits des Pixmap-Randes sichtbar werden, während das Bild vom Worker-Thread gerendert wird.

Die Anwendung besteht aus zwei Klassen:

  • RenderThread QThread ist eine Unterklasse, die die Mandelbrot-Menge rendert.
  • MandelbrotWidget ist eine QWidget Unterklasse, die die Mandelbrot-Menge auf dem Bildschirm anzeigt und den Benutzer zoomen und scrollen lässt.

Wenn Sie noch nicht mit der Thread-Unterstützung von Qt vertraut sind, empfehlen wir Ihnen, zunächst den Überblick über die Thread-Unterstützung in Qt zu lesen.

Definition der RenderThread-Klasse

Wir beginnen mit der Definition der Klasse 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];
};

Die Klasse erbt QThread, so dass sie die Fähigkeit erhält, in einem eigenen Thread zu laufen. Abgesehen vom Konstruktor und Destruktor ist render() die einzige öffentliche Funktion. Immer wenn der Thread mit dem Rendern eines Bildes fertig ist, sendet er das Signal renderedImage().

Die geschützte Funktion run() ist eine Neuimplementierung von QThread. Sie wird automatisch aufgerufen, wenn der Thread gestartet wird.

Im Abschnitt private gibt es ein QMutex, ein QWaitCondition und einige andere Datenelemente. Der Mutex schützt die anderen Datenmitglieder.

Implementierung der Klasse RenderThread

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

Im Konstruktor initialisieren wir die Variablen restart und abort auf false. Diese Variablen steuern den Ablauf der Funktion run().

Wir initialisieren auch das Array colormap, das eine Reihe von RGB-Farben enthält.

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

    wait();
}

Der Destruktor kann zu jedem beliebigen Zeitpunkt aufgerufen werden, solange der Thread aktiv ist. Wir setzen abort auf true, um run() mitzuteilen, dass die Ausführung so bald wie möglich beendet werden soll. Wir rufen auch QWaitCondition::wakeOne() auf, um den Thread aufzuwecken, wenn er schläft. (Wie wir sehen werden, wenn wir run() durchgehen, wird der Thread in den Schlaf versetzt, wenn er nichts zu tun hat.)

Wichtig ist hier, dass run() in einem eigenen Thread (dem Worker-Thread) ausgeführt wird, während der Konstruktor und der Destruktor von RenderThread (sowie die Funktion render() ) von dem Thread aufgerufen werden, der den Worker-Thread erstellt hat. Daher benötigen wir einen Mutex, um die Zugriffe auf die Variablen abort und condition zu schützen, auf die run() jederzeit zugreifen könnte.

Am Ende des Destruktors rufen wir QThread::wait() auf, um zu warten, bis run() beendet ist, bevor der Destruktor der Basisklasse aufgerufen wird.

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

Die Funktion render() wird von MandelbrotWidget immer dann aufgerufen, wenn ein neues Bild der Mandelbrotmenge erzeugt werden muss. Die Parameter centerX, centerY und scaleFactor geben den Teil des Fraktals an, der gerendert werden soll; resultSize gibt die Größe des resultierenden QImage an.

Die Funktion speichert die Parameter in Mitgliedsvariablen. Wenn der Thread noch nicht läuft, startet sie ihn; andernfalls setzt sie restart auf true (was run() anweist, alle noch nicht abgeschlossenen Berechnungen zu stoppen und mit den neuen Parametern neu zu beginnen) und weckt den Thread auf, der möglicherweise schläft.

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() ist eine ziemlich große Funktion, also werden wir sie in Teile aufteilen.

Der Hauptteil der Funktion ist eine Endlosschleife, die mit der Speicherung der Rendering-Parameter in lokalen Variablen beginnt. Wie üblich schützen wir die Zugriffe auf die Mitgliedsvariablen mit dem Mutex der Klasse. Durch die Speicherung der Mitgliedsvariablen in lokalen Variablen können wir den Umfang des Codes, der durch einen Mutex geschützt werden muss, minimieren. Dadurch wird sichergestellt, dass der Hauptthread nie zu lange blockiert wird, wenn er auf die Mitgliedsvariablen von RenderThread zugreifen muss (z.B. in render()).

Das Schlüsselwort forever ist ein Qt-Pseudo-Schlüsselwort.

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

Dann kommt der Kern des Algorithmus. Anstatt zu versuchen, ein perfektes Bild der Mandelbrot-Menge zu erzeugen, führen wir mehrere Durchgänge durch und generieren immer präzisere (und rechenintensivere) Annäherungen an das Fraktal.

Wir erstellen eine hochauflösende Pixmap, indem wir das Pixelverhältnis des Geräts auf die Zielgröße anwenden (siehe Drawing High Resolution Versions of Pixmaps and Images).

Wenn wir innerhalb der Schleife feststellen, dass restart auf true (durch render()) gesetzt wurde, brechen wir die Schleife sofort ab, so dass die Steuerung schnell an den Anfang der äußeren Schleife (die forever -Schleife) zurückkehrt und wir die neuen Rendering-Parameter abrufen. Ähnlich verhält es sich, wenn wir feststellen, dass abort auf true gesetzt wurde (durch den Destruktor RenderThread ), kehren wir sofort aus der Funktion zurück und beenden den Thread.

Der Kernalgorithmus würde den Rahmen dieses Tutorials sprengen.

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

Sobald wir mit allen Iterationen fertig sind, rufen wir QWaitCondition::wait() auf, um den Thread in den Ruhezustand zu versetzen, es sei denn, restart ist true. Es hat keinen Sinn, einen Worker-Thread in einer Endlosschleife laufen zu lassen, wenn es nichts zu tun gibt.

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

Die Funktion rgbFromWaveLength() ist eine Hilfsfunktion, die eine Wellenlänge in einen mit 32-Bit kompatiblen RGB-Wert umwandelt QImages. Sie wird vom Konstruktor aufgerufen, um das Array colormap mit angenehmen Farben zu initialisieren.

MandelbrotWidget-Klassendefinition

Die Klasse MandelbrotWidget verwendet RenderThread, um die Mandelbrotmenge auf dem Bildschirm zu zeichnen. Hier ist die Klassendefinition:

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

Das Widget implementiert viele Ereignishandler von QWidget neu. Darüber hinaus verfügt es über einen updatePixmap() Slot, den wir mit dem renderedImage() Signal des Worker-Threads verbinden, um die Anzeige zu aktualisieren, sobald wir neue Daten von diesem Thread erhalten.

Unter den privaten Variablen befinden sich thread vom Typ RenderThread und pixmap, die das zuletzt gerenderte Bild enthalten.

Implementierung der MandelbrotWidget-Klasse

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;

Die Implementierung beginnt mit ein paar Konstanten, die wir später benötigen werden.

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
}

Der interessante Teil des Konstruktors ist der Aufruf QObject::connect().

Obwohl er wie eine Standard-Signal-Slot-Verbindung zwischen zwei QObjectaussieht, ist die Verbindung effektiv eine queued connection, da das Signal in einem anderen Thread ausgesendet wird, als der Empfänger lebt. Diese Verbindungen sind asynchron (d. h. nicht blockierend), was bedeutet, dass der Slot irgendwann nach der Anweisung emit aufgerufen wird. Darüber hinaus wird der Slot in dem Thread aufgerufen, in dem sich der Empfänger befindet. In diesem Fall wird das Signal im Worker-Thread ausgegeben, und der Slot wird im GUI-Thread ausgeführt, wenn die Kontrolle zur Ereignisschleife zurückkehrt.

Bei Warteschlangenverbindungen muss Qt eine Kopie der Argumente, die an das Signal übergeben wurden, speichern, um sie später an den Slot weitergeben zu können. Qt weiß, wie man eine Kopie vieler C++- und Qt-Typen ablegt, daher ist für QImage keine weitere Aktion erforderlich. Bei Verwendung eines benutzerdefinierten Typs wäre ein Aufruf der Vorlagenfunktion qRegisterMetaType() erforderlich, bevor der Typ als Parameter in Warteschlangenverbindungen verwendet werden kann.

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

In paintEvent() beginnen wir damit, den Hintergrund mit Schwarz zu füllen. Wenn es noch nichts zu malen gibt (pixmap ist null), wird eine Meldung auf dem Widget angezeigt, in der der Benutzer aufgefordert wird, sich zu gedulden und die Funktion sofort zu verlassen.

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

Wenn die Pixmap den richtigen Skalierungsfaktor hat, zeichnen wir die Pixmap direkt auf das Widget.

Andernfalls erstellen wir eine Vorschau-Pixmap, die angezeigt wird, bis die Berechnung abgeschlossen ist, und verschieben das Koordinatensystem entsprechend.

Da wir Transformationen auf den Painter anwenden und eine Überladung von QPainter::drawPixmap() verwenden werden, die in diesem Fall keine hochauflösenden Pixmaps unterstützt, erstellen wir eine Pixmap mit dem Gerätepixelverhältnis 1.

Indem wir das Rechteck des Widgets mithilfe der skalierten Malermatrix umkehren, stellen wir außerdem sicher, dass nur die belichteten Bereiche der Pixmap gezeichnet werden. Die Aufrufe von QPainter::save() und QPainter::restore() stellen sicher, dass beim anschließenden Malen das Standardkoordinatensystem verwendet wird.

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

}

Am Ende des Mal-Ereignis-Handlers zeichnen wir eine Textzeichenkette und ein halbtransparentes Rechteck über das Fraktal.

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

Immer wenn der Benutzer die Größe des Widgets ändert, rufen wir render() auf, um ein neues Bild zu erzeugen, mit denselben Parametern centerX, centerY und curScale, aber mit der neuen Größe des Widgets.

Beachten Sie, dass wir uns darauf verlassen, dass resizeEvent() automatisch von Qt aufgerufen wird, wenn das Widget zum ersten Mal angezeigt wird, um das Anfangsbild zu erzeugen.

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

Der Event-Handler für das Drücken einer Taste bietet ein paar Tastaturbindungen für Benutzer, die keine Maus haben. Die Funktionen zoom() und scroll() werden später behandelt.

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

Die Rad-Ereignishandhabung wurde neu implementiert, um das Mausrad zur Steuerung der Zoomstufe zu verwenden. QWheelEvent::angleDelta() gibt den Winkel der Mausbewegung des Rads in Achtelgraden zurück. Bei den meisten Mäusen entspricht ein Mausradschritt 15 Grad. Wir finden heraus, wie viele Mausschritte wir haben, und bestimmen den daraus resultierenden Zoomfaktor. Wenn wir z. B. zwei Radschritte in positiver Richtung haben (d. h. +30 Grad), wird der Zoomfaktor ZoomInFactor zur zweiten Potenz, d. h. 0,8 * 0,8 = 0,64.

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

Pinch to zoom wurde mit QGesture implementiert, wie in Gesten in Widgets und Grafikansicht beschrieben.

#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

Wenn der Benutzer die linke Maustaste drückt, speichern wir die Position des Mauszeigers in lastDragPos.

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

Wenn der Benutzer den Mauszeiger bei gedrückter linker Maustaste bewegt, passen wir pixmapOffset an, um die Pixmap an einer verschobenen Position zu zeichnen, und rufen QWidget::update() auf, um ein erneutes Zeichnen zu erzwingen.

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

Wenn die linke Maustaste losgelassen wird, aktualisieren wir pixmapOffset genau wie bei einer Mausbewegung und setzen lastDragPos auf einen Standardwert zurück. Dann rufen wir scroll() auf, um ein neues Bild für die neue Position zu rendern. (Die Anpassung von pixmapOffset reicht nicht aus, da die Bereiche, die beim Ziehen der Pixmap sichtbar werden, schwarz gezeichnet werden).

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

Der Slot updatePixmap() wird aufgerufen, wenn der Worker-Thread das Rendern eines Bildes beendet hat. Zunächst wird geprüft, ob ein Drag in Kraft ist, und in diesem Fall wird nichts unternommen. Im Normalfall speichern wir das Bild in pixmap und reinitialisieren einige der anderen Mitglieder. Am Ende rufen wir QWidget::update() auf, um die Anzeige zu aktualisieren.

An dieser Stelle werden Sie sich vielleicht fragen, warum wir ein QImage für den Parameter und ein QPixmap für das Datenelement verwenden. Warum bleiben wir nicht bei einem Typ? Der Grund ist, dass QImage die einzige Klasse ist, die eine direkte Pixelmanipulation unterstützt, die wir im Worker-Thread benötigen. Andererseits muss ein Bild, bevor es auf dem Bildschirm gezeichnet werden kann, in eine Pixmap umgewandelt werden. Es ist besser, diese Umwandlung ein für alle Mal hier vorzunehmen, als in paintEvent().

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

In zoom() berechnen wir curScale neu. Dann rufen wir QWidget::update() auf, um eine skalierte Pixmap zu zeichnen, und bitten den Worker-Thread, ein neues Bild zu rendern, das dem neuen Wert curScale entspricht.

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

scroll() ist ähnlich wie zoom(), mit dem Unterschied, dass die betroffenen Parameter centerX und centerY sind.

Die Funktion main()

Die Multithreading-Natur der Anwendung hat keinen Einfluss auf die Funktion main(), die so einfach wie immer ist:

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"Anzahl der Durchläufe (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(); }

Beispielprojekt @ 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.