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

Bei der rechenintensiven Aufgabe handelt es sich hier um die Mandelbrot-Menge, das wohl berühmteste Fraktal der Welt. Heutzutage bieten zwar ausgefeilte Programme wie XaoS Echtzeit-Zoomfunktionen für die Mandelbrot-Menge, doch der Standard-Mandelbrot-Algorithmus ist für unsere Zwecke gerade langsam genug.
In der Praxis lässt sich der hier beschriebene Ansatz auf eine Vielzahl von Problemen anwenden, darunter synchrone Netzwerk-E/A und Datenbankzugriffe, bei denen die Benutzeroberfläche reaktionsfähig bleiben muss, während eine rechenintensive Operation stattfindet. Das Beispiel „Blocking Fortune Client“ veranschaulicht dasselbe Prinzip in einem TCP-Client.
Die Mandelbrot-Anwendung unterstützt das Zoomen und Scrollen mit der Maus oder der Tastatur. Um ein Einfrieren der Ereignisschleife des Hauptthreads (und damit der Benutzeroberfläche der Anwendung) zu vermeiden, verlagern wir die gesamte Fraktalberechnung in einen separaten Worker-Thread. Der Thread sendet ein Signal aus, sobald er die Darstellung des Fraktals abgeschlossen hat.
Während der Arbeits-Thread das Fraktal neu berechnet, um die neue Position des Zoomfaktors widerzuspiegeln, skaliert der Haupt-Thread lediglich die zuvor gerenderte Pixmap, um sofortiges Feedback zu geben. Das Ergebnis sieht zwar nicht so gut aus wie das, was der Arbeitsthread letztendlich liefert, aber zumindest macht es die Anwendung reaktionsschneller. Die folgende Screenshot-Sequenz zeigt das Originalbild, das skalierte Bild und das neu gerenderte Bild.
![]() | ![]() | ![]() |
Ähnlich verhält es sich beim Scrollen: Die vorherige Pixmap wird sofort mitgescrollt, wodurch noch nicht gezeichnete Bereiche jenseits des Pixmap-Randes sichtbar werden, während das Bild vom Worker-Thread gerendert wird.
![]() | ![]() | ![]() |
Die Anwendung besteht aus zwei Klassen:
RenderThreadist eine Unterklasse von „ QThread “, die die Mandelbrot-Menge darstellt.MandelbrotWidgetist eine Unterklasse von „ QWidget “, die die Mandelbrot-Menge auf dem Bildschirm anzeigt und dem Benutzer das Zoomen und Scrollen ermöglicht.
Falls Sie noch nicht mit der Thread-Unterstützung von Qt vertraut sind, empfehlen wir Ihnen, zunächst die Übersicht „Multithreading in Qt“ zu lesen.
Definition der Klasse „RenderThread“
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 von „ QThread “, sodass sie in einem separaten Thread ausgeführt werden kann. Abgesehen vom Konstruktor und Destruktor ist „ render() “ die einzige öffentliche Funktion. Sobald der Thread die Darstellung eines Bildes abgeschlossen hat, sendet er das Signal „ renderedImage() “ aus.
Die geschützte Funktion ` run() ` wird aus ` QThread` neu implementiert. Sie wird automatisch aufgerufen, wenn der Thread gestartet wird.
Im Abschnitt „ private “ befinden sich ein „ QMutex “, ein „ QWaitCondition “ sowie einige weitere Datenelemente. Der Mutex schützt die anderen Datenelemente.
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 “ mit „ false “. Diese Variablen steuern den Ablauf der Funktion „ run() “.
Außerdem initialisieren wir 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 Zeitpunkt aufgerufen werden, solange der Thread aktiv ist. Wir setzen ` abort ` auf ` true `, um ` run() ` anzuweisen, die Ausführung so bald wie möglich zu beenden. Außerdem rufen wir ` QWaitCondition::wakeOne()` auf, um den Thread zu wecken, falls er sich im Ruhezustand befindet. (Wie wir bei der Betrachtung von ` run()` sehen werden, wird der Thread in den Ruhezustand versetzt, wenn er nichts zu tun hat.)
Wichtig ist hierbei, 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 den Zugriff 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 wurde, 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 ` aufgerufen, wann immer ein neues Bild der Mandelbrot-Menge erzeugt werden soll. Die Parameter ` centerX`, ` centerY` und ` scaleFactor ` legen den zu rendernenden Ausschnitt des Fraktals fest; ` resultSize ` gibt die Größe der resultierenden ` QImage` an.
Die Funktion speichert die Parameter in Membervariablen. Falls der Thread noch nicht läuft, wird er gestartet; andernfalls wird ` restart ` auf ` true ` gesetzt (wodurch ` run() ` angewiesen wird, alle noch nicht abgeschlossenen Berechnungen zu beenden und mit den neuen Parametern neu zu beginnen) und der Thread, der möglicherweise im Ruhezustand ist, wird geweckt.
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 umfangreiche Funktion, daher werden wir sie in Teile zerlegen.
Der Funktionskörper ist eine Endlosschleife, die damit beginnt, die Rendering-Parameter in lokalen Variablen zu speichern. Wie üblich schützen wir den Zugriff auf die Membervariablen mithilfe des Mutex der Klasse. Das Speichern der Membervariablen in lokalen Variablen ermöglicht es uns, die Menge an Code zu minimieren, die durch einen Mutex geschützt werden muss. Dies stellt sicher, dass der Hauptthread niemals zu lange blockieren muss, wenn er auf die Membervariablen 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.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;
}
}Dann folgt der Kern des Algorithmus. Anstatt zu versuchen, ein perfektes Bild der Mandelbrot-Menge zu erzeugen, führen wir mehrere Durchläufe durch und generieren immer präzisere (und rechenintensivere) Näherungen des Fraktals.
Wir erstellen eine hochauflösende Pixmap, indem wir das Gerätepixelverhältnis auf die Zielgröße anwenden (siehe „ Drawing High Resolution Versions of Pixmaps and Images “).
Wenn wir innerhalb der Schleife feststellen, dass restart auf true gesetzt wurde (durch render()), brechen wir die Schleife sofort ab, sodass 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 von RenderThread ): Wir kehren 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 alle Iterationen abgeschlossen haben, rufen wir ` QWaitCondition::wait()` auf, um den Thread in den Ruhezustand zu versetzen, es sei denn, ` restart ` ist ` true`. Es macht keinen Sinn, einen Worker-Thread endlos in einer Schleife laufen zu lassen, solange 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 RGB-Wert umwandelt, der mit 32-Bit- QImagen kompatibel ist. Sie wird vom Konstruktor aufgerufen, um das Array „ colormap “ mit ansprechenden Farben zu initialisieren.
Definition der Klasse „MandelbrotWidget“
Die Klasse „ MandelbrotWidget “ verwendet „ RenderThread “, um die Mandelbrot-Menge auf dem Bildschirm darzustellen. 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 Ereignisbehandler aus „ QWidget “ neu. Darüber hinaus verfügt es über einen „ updatePixmap() “-Slot, den wir mit dem Signal „ renderedImage() “ des Worker-Threads verbinden, um die Anzeige zu aktualisieren, sobald wir neue Daten vom Thread erhalten.
Zu den privaten Variablen gehören „ thread “ vom Typ „ RenderThread “ sowie „ pixmap “, das das zuletzt gerenderte Bild enthält.
Implementierung der Klasse „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;Die Implementierung beginnt mit einigen 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 von ` QObject::connect()`.
Obwohl es wie eine Standard-Signal-Slot-Verbindung zwischen zwei ` QObject`s aussieht, handelt es sich bei der Verbindung effektiv um eine ` queued connection`, da das Signal in einem anderen Thread ausgesendet wird als dem, in dem sich der Empfänger befindet. Diese Verbindungen sind asynchron (d. h. nicht blockierend), was bedeutet, dass der Slot irgendwann nach der Anweisung ` emit ` aufgerufen wird. Zudem wird der Slot in dem Thread aufgerufen, in dem sich der Empfänger befindet. In diesem Fall wird das Signal im Worker-Thread ausgelöst, und der Slot wird im GUI-Thread ausgeführt, sobald die Kontrolle an die Ereignisschleife zurückgegeben wird.
Bei Verbindungen in der Warteschlange muss Qt eine Kopie der an das Signal übergebenen Argumente speichern, damit es diese später an den Slot weitergeben kann. Qt kann Kopien vieler C++- und Qt-Typen erstellen, daher sind für ` QImage` keine weiteren Maßnahmen erforderlich. Würde ein benutzerdefinierter Typ verwendet, wäre ein Aufruf der Template-Funktion ` qRegisterMetaType()` erforderlich, bevor der Typ als Parameter in Verbindungen in der Warteschlange verwendet werden könnte.
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()` füllen wir zunächst den Hintergrund mit Schwarz. Wenn noch nichts zu zeichnen ist (`pixmap ` ist null), zeigen wir auf dem Widget eine Meldung an, in der der Benutzer um etwas Geduld gebeten wird, und kehren sofort aus der Funktion zurück.
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();
}Wenn die Pixmap den richtigen Skalierungsfaktor hat, zeichnen wir die Pixmap direkt auf das Widget.
Andernfalls erstellen wir eine Vorschau-Pixmap, die bis zum Abschluss der Berechnung angezeigt wird, und verschieben das Koordinatensystem entsprechend.
Da wir Transformationen auf den „Painter“ anwenden und eine Überladung von ` QPainter::drawPixmap()` verwenden, die in diesem Fall keine hochauflösenden Pixmaps unterstützt, erstellen wir eine Pixmap mit einem Gerätepixelverhältnis von 1.
Durch die umgekehrte Abbildung des Widget-Rechtecks unter Verwendung der skalierten „painter“-Matrix stellen wir zudem sicher, dass nur die sichtbaren Bereiche der Pixmap gezeichnet werden. Die Aufrufe von ` QPainter::save()` und ` QPainter::restore()` stellen sicher, dass alle anschließend durchgeführten Zeichnungsvorgänge das Standardkoordinatensystem verwenden.
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 Paint-Ereignishandlers zeichnen wir eine Textzeichenfolge 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 die Erzeugung eines neuen Bildes zu starten – mit denselben Parametern für ` centerX`, ` centerY` und ` curScale `, jedoch mit der neuen Widget-Größe.
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 generieren.
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 Tastenereignis-Handler stellt einige Tastaturbelegungen für Benutzer bereit, 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));
}Der Rad-Ereignis-Handler wurde neu implementiert, damit das Mausrad den Zoomfaktor steuert. ` QWheelEvent::angleDelta()` gibt den Winkel der Mausradbewegung in Achtelgrad zurück. Bei den meisten Mäusen entspricht ein Radschritt 15 Grad. Wir ermitteln, wie viele Mausschritte vorliegen, und berechnen daraus den resultierenden Zoomfaktor. Liegen beispielsweise zwei Mausradschritte in positiver Richtung vor (d. h. +30 Grad), ergibt sich als Zoomfaktor die zweite Potenz von ` ZoomInFactor `, also 0,8 * 0,8 = 0,64.
void MandelbrotWidget::mousePressEvent(QMouseEvent *event)
{
if (event->button() == Qt::LeftButton)
lastDragPos = event->position().toPoint();
}Das Zoomen durch Auf- und Zuziehen wurde mit „ QGesture “ implementiert, wie unter „Gesten in Widgets und der 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);
}
#endifWenn 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 bewegt, während die linke Maustaste gedrückt ist, passen wir „ pixmapOffset “ so an, dass die Pixmap an einer verschobenen Position gezeichnet wird, 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. Anschließend rufen wir ` scroll() ` auf, um ein neues Bild für die neue Position zu rendern. (Eine Anpassung von ` pixmapOffset ` reicht nicht aus, da 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 die Darstellung eines Bildes abgeschlossen hat. Zunächst prüfen wir, ob gerade ein Drag-Vorgang läuft, und führen in diesem Fall keine Aktion aus. Im Normalfall speichern wir das Bild in „ pixmap “ und initialisieren einige der anderen Mitglieder neu. Abschließend rufen wir „ QWidget::update()“ auf, um die Anzeige zu aktualisieren.
An dieser Stelle fragen Sie sich vielleicht, warum wir für den Parameter eine ` QImage ` und für das Datenelement eine ` QPixmap ` verwenden. Warum nicht bei einem Typ bleiben? Der Grund dafür ist, dass ` QImage ` die einzige Klasse ist, die direkte Pixelbearbeitung unterstützt, die wir im Arbeitsthread benötigen. Andererseits muss ein Bild, bevor es auf dem Bildschirm gezeichnet werden kann, in eine Pixmap konvertiert werden. Es ist besser, die Konvertierung hier ein für alle Mal durchzuführen, anstatt in ` paintEvent()`.
void MandelbrotWidget::zoom(double zoomFactor)
{
curScale *= zoomFactor;
update();
thread.render(centerX, centerY, curScale, size(), devicePixelRatio());
}In „ zoom() “ berechnen wir „ curScale “ neu. Anschließend rufen wir „ QWidget::update()“ auf, um eine skalierte Pixmap zu zeichnen, und weisen den Worker-Thread an, ein neues Bild zu rendern, das dem neuen Wert von „ curScale “ entspricht.
void MandelbrotWidget::scroll(int deltaX, int deltaY)
{
centerX += deltaX * curScale;
centerY += deltaY * curScale;
update();
thread.render(centerX, centerY, curScale, size(), devicePixelRatio());
}scroll() ähnelt „ zoom() “, mit dem Unterschied, dass die betroffenen Parameter „ centerX “ und „ centerY “ sind.
Die Funktion `main()`
Der Multithread-Charakter der Anwendung hat keine Auswirkungen auf ihre 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-Beispiel"_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();
}© 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.





