Mandelbrot
El ejemplo Mandelbrot demuestra la programación multihilo usando Qt. Muestra cómo utilizar un hilo de trabajo para realizar cálculos pesados sin bloquear el bucle de eventos del hilo principal.

El cálculo pesado aquí es el conjunto de Mandelbrot, probablemente el fractal más famoso del mundo. Hoy en día, mientras que sofisticados programas, como XaoS, proporcionan zoom en tiempo real en el conjunto de Mandelbrot, el algoritmo estándar de Mandelbrot es lo suficientemente lento para nuestros propósitos.
En la vida real, el enfoque descrito aquí es aplicable a un amplio conjunto de problemas, como la E/S síncrona de red y el acceso a bases de datos, en los que la interfaz de usuario debe seguir respondiendo mientras se realiza alguna operación pesada. El ejemplo del Cliente de Fortuna Bloqueante muestra el mismo principio en funcionamiento en un cliente TCP.
La aplicación Mandelbrot permite hacer zoom y desplazarse utilizando el ratón o el teclado. Para evitar congelar el bucle de eventos del hilo principal (y, como consecuencia, la interfaz de usuario de la aplicación), ponemos todo el cálculo fractal en un hilo trabajador separado. El hilo emite una señal cuando termina de renderizar el fractal.
Durante el tiempo en el que el subproceso trabajador vuelve a calcular el fractal para reflejar la nueva posición del factor de zoom, el subproceso principal simplemente escala el mapa de píxeles previamente renderizado para proporcionar una respuesta inmediata. El resultado no es tan bueno como el que acaba proporcionando el subproceso trabajador, pero al menos hace que la aplicación responda mejor. La siguiente secuencia de capturas de pantalla muestra la imagen original, la imagen escalada y la imagen renderizada.
![]() | ![]() | ![]() |
Del mismo modo, cuando el usuario se desplaza, el pixmap anterior se desplaza inmediatamente, revelando áreas sin pintar más allá del borde del pixmap, mientras que la imagen es renderizada por el hilo trabajador.
![]() | ![]() | ![]() |
La aplicación consta de dos clases:
RenderThreades una subclase de QThread que renderiza el conjunto Mandelbrot.MandelbrotWidgetes una subclase de QWidget que muestra el conjunto de Mandelbrot en pantalla y permite al usuario hacer zoom y desplazarse.
Si aún no estás familiarizado con el soporte de hilos de Qt, te recomendamos que empieces por leer la descripción general de Multi-threading en Qt.
Definición de la clase RenderThread
Comenzaremos con la definición de la clase 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 clase hereda QThread de forma que adquiere la capacidad de ejecutarse en un hilo separado. Aparte del constructor y destructor, render() es la única función pública. Cada vez que el hilo termina de renderizar una imagen, emite la señal renderedImage().
La función protegida run() se reimplementa a partir de QThread. Se llama automáticamente cuando se inicia el hilo.
En la sección private, tenemos un QMutex, un QWaitCondition, y algunos otros miembros de datos. El mutex protege el otro miembro de datos.
Implementación de la Clase RenderThread
RenderThread::RenderThread(QObject *parent) : QThread(parent) { for (int i = 0; i < ColormapSize; ++i) colormap[i] = rgbFromWaveLength(380.0 + (i * 400.0 / ColormapSize)); }
En el constructor, inicializamos las variables restart y abort a false. Estas variables controlan el flujo de la función run().
También inicializamos el array colormap, que contiene una serie de colores RGB.
RenderThread::~RenderThread() { mutex.lock(); abort = true; condition.wakeOne(); mutex.unlock(); wait(); }
El destructor puede ser llamado en cualquier momento mientras el hilo esté activo. Establecemos abort a true para decirle a run() que deje de ejecutarse tan pronto como sea posible. También llamamos a QWaitCondition::wakeOne() para despertar el hilo si está durmiendo. (Como veremos cuando revisemos run(), el hilo se pone a dormir cuando no tiene nada que hacer).
Lo importante a notar aquí es que run() es ejecutado en su propio hilo (el hilo trabajador), mientras que el constructor y destructor de RenderThread (así como la función render() ) son llamados por el hilo que creó el hilo trabajador. Por lo tanto, necesitamos un mutex para proteger los accesos a las variables abort y condition, que podrían ser accedidas en cualquier momento por run().
Al final del destructor, llamamos a QThread::wait() para esperar hasta que run() haya salido antes de que el destructor de la clase base sea invocado.
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 función render() es llamada por MandelbrotWidget cada vez que necesita generar una nueva imagen del conjunto de Mandelbrot. Los parámetros centerX, centerY, y scaleFactor especifican la porción del fractal a renderizar; resultSize especifica el tamaño del QImage resultante.
La función almacena los parámetros en variables miembro. Si el hilo aún no está en marcha, lo inicia; de lo contrario, establece restart en true (indicando a run() que detenga cualquier cálculo inacabado y comience de nuevo con los nuevos parámetros) y despierta el hilo, que podría estar durmiendo.
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() es una función bastante grande, así que la dividiremos en partes.
El cuerpo de la función es un bucle infinito que comienza almacenando los parámetros de renderizado en variables locales. Como de costumbre, protegemos los accesos a las variables miembro usando el mutex de la clase. Almacenar las variables miembro en variables locales nos permite minimizar la cantidad de código que necesita ser protegido por un mutex. Esto asegura que el hilo principal nunca tendrá que bloquearse durante demasiado tiempo cuando necesite acceder a las variables miembro de RenderThread(por ejemplo, en render()).
La palabra clave forever es una pseudopalabra clave de 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; } }
A continuación viene el núcleo del algoritmo. En lugar de intentar crear una imagen perfecta del conjunto de Mandelbrot, hacemos múltiples pasadas y generamos aproximaciones cada vez más precisas (y costosas computacionalmente) del fractal.
Creamos un mapa de píxeles de alta resolución aplicando la proporción de píxeles del dispositivo al tamaño objetivo (véase Drawing High Resolution Versions of Pixmaps and Images).
Si descubrimos dentro del bucle que restart ha sido ajustado a true (por render()), salimos del bucle inmediatamente, de forma que el control vuelve rápidamente a la parte superior del bucle exterior (el bucle forever ) y obtenemos los nuevos parámetros de renderizado. Del mismo modo, si descubrimos que abort se ha establecido en true (por el destructor RenderThread ), volvemos de la función inmediatamente, terminando el hilo.
El algoritmo central está más allá del alcance de este tutorial.
mutex.lock(); if (!restart) condition.wait(&mutex); restart = false; mutex.unlock(); } }
Una vez que hemos terminado con todas las iteraciones, llamamos a QWaitCondition::wait() para poner el hilo a dormir, a menos que restart sea true. No tiene sentido mantener un hilo de trabajo en bucle indefinidamente mientras no hay nada que hacer.
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 función rgbFromWaveLength() es una función de ayuda que convierte una longitud de onda en un valor RGB compatible con QImages de 32 bits. Se llama desde el constructor para inicializar la matriz colormap con colores agradables.
Definición de la clase MandelbrotWidget
La clase MandelbrotWidget utiliza RenderThread para dibujar el conjunto de Mandelbrot en la pantalla. Aquí está la definición de la clase:
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; };
El widget reimplementa muchos manejadores de eventos de QWidget. Además, tiene una ranura updatePixmap() que conectaremos a la señal renderedImage() del hilo trabajador para actualizar la pantalla cada vez que recibamos nuevos datos del hilo.
Entre las variables privadas, tenemos thread de tipo RenderThread y pixmap, que contiene la última imagen renderizada.
Implementación de la clase 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;
La implementación comienza con unas cuantas constantes que necesitaremos más adelante.
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 parte interesante del constructor es la llamada a QObject::connect().
Aunque parece una conexión de ranura de señal estándar entre dos QObjects, debido a que la señal se emite en un hilo diferente al que vive el receptor, la conexión es efectivamente un queued connection. Estas conexiones son asíncronas (es decir, no bloqueantes), lo que significa que la ranura será llamada en algún momento después de la sentencia emit. Es más, la ranura será invocada en el hilo en el que vive el receptor. En este caso, la señal se emite en el hilo worker, y el slot se ejecuta en el hilo GUI cuando el control vuelve al bucle de eventos.
Con conexiones en cola, Qt debe almacenar una copia de los argumentos que se pasaron a la señal para poder pasarlos al slot más tarde. Qt sabe cómo tomar una copia de muchos tipos C++ y Qt, por lo que no es necesaria ninguna acción adicional para QImage. Si se utilizara un tipo personalizado, sería necesaria una llamada a la función de plantilla qRegisterMetaType() antes de poder utilizar el tipo como parámetro en las conexiones en cola.
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; }
En paintEvent(), empezamos rellenando el fondo con negro. Si todavía no tenemos nada que pintar (pixmap es null), mostramos un mensaje en el widget pidiendo al usuario que sea paciente y que vuelva de la función inmediatamente.
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 el pixmap tiene el factor de escala correcto, dibujamos el pixmap directamente en el widget.
En caso contrario, creamos un pixmap de previsualización que se mostrará hasta que finalice el cálculo y trasladamos el sistema de coordenadas en consecuencia.
Como vamos a usar transformaciones en el pintor y usamos una sobrecarga de QPainter::drawPixmap() que no soporta pixmaps de alta resolución en ese caso, creamos un pixmap con ratio de pixel de dispositivo 1.
Al invertir el mapeado del rectángulo del widget usando la matriz escalada del pintor, también nos aseguramos de que sólo se dibujan las áreas expuestas del pixmap. Las llamadas a QPainter::save() y QPainter::restore() aseguran que cualquier pintura realizada posteriormente utilice el sistema de coordenadas estándar.
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); }
Al final del manejador del evento paint, dibujamos una cadena de texto y un rectángulo semitransparente sobre el fractal.
void MandelbrotWidget::resizeEvent(QResizeEvent * /* event */) { thread.render(centerX, centerY, curScale, size(), devicePixelRatio()); }
Cada vez que el usuario cambia el tamaño del widget, llamamos a render() para empezar a generar una nueva imagen, con los mismos parámetros centerX, centerY, y curScale pero con el nuevo tamaño del widget.
Observa que confiamos en que resizeEvent() sea llamado automáticamente por Qt cuando el widget se muestra por primera vez para generar la imagen inicial.
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); } }
El manejador de eventos de pulsación de tecla proporciona algunos enlaces de teclado para el beneficio de los usuarios que no tienen ratón. Las funciones zoom() y scroll() se tratarán más adelante.
void MandelbrotWidget::wheelEvent(QWheelEvent *event) { const int numDegrees = event->angleDelta().y() / 8; const double numSteps = numDegrees / double(15); zoom(pow(ZoomInFactor, numSteps)); }
El manejador de eventos de la rueda se reimplementa para hacer que la rueda del ratón controle el nivel de zoom. QWheelEvent::angleDelta() devuelve el ángulo del movimiento de la rueda del ratón, en octavos de grado. Para la mayoría de los ratones, un paso de rueda corresponde a 15 grados. Averiguamos cuántos pasos de ratón tenemos y determinamos el factor de zoom resultante. Por ejemplo, si tenemos dos pasos de rueda en sentido positivo (es decir, +30 grados), el factor de zoom pasa a ser ZoomInFactor a la segunda potencia, es decir, 0,8 * 0,8 = 0,64.
void MandelbrotWidget::mousePressEvent(QMouseEvent *event) { if (event->button() == Qt::LeftButton) lastDragPos = event->position().toPoint(); }
La función Pellizcar para hacer zoom se ha implementado con QGesture, como se indica en Gestos en widgets y vista gráfica.
#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
Cuando el usuario pulsa el botón izquierdo del ratón, almacenamos la posición del puntero del ratón en lastDragPos.
void MandelbrotWidget::mouseMoveEvent(QMouseEvent *event) { if (event->buttons() & Qt::LeftButton) { pixmapOffset += event->position().toPoint() - lastDragPos; lastDragPos = event->position().toPoint(); update(); } }
Cuando el usuario mueve el puntero del ratón mientras el botón izquierdo del ratón está pulsado, ajustamos pixmapOffset para pintar el pixmap en una posición desplazada y llamamos a QWidget::update() para forzar un repintado.
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); } }
Cuando se suelta el botón izquierdo del ratón, actualizamos pixmapOffset igual que hicimos al mover el ratón y reseteamos lastDragPos a un valor por defecto. Luego, llamamos a scroll() para renderizar una nueva imagen para la nueva posición. (Ajustar pixmapOffset no es suficiente porque las áreas reveladas al arrastrar el pixmap se dibujan en negro).
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 ranura updatePixmap() se invoca cuando el hilo trabajador ha terminado de renderizar una imagen. Empezamos comprobando si hay un arrastre y no hacemos nada en ese caso. En el caso normal, almacenamos la imagen en pixmap y reinicializamos algunos de los otros miembros. Al final, llamamos a QWidget::update() para refrescar la pantalla.
Llegados a este punto, puede que te preguntes por qué utilizamos un QImage para el parámetro y un QPixmap para el miembro de datos. ¿Por qué no nos ceñimos a un solo tipo? La razón es que QImage es la única clase que soporta la manipulación directa de píxeles, que necesitamos en el hilo trabajador. Por otro lado, antes de que una imagen pueda ser dibujada en pantalla, debe ser convertida en un mapa de píxeles. Es mejor hacer la conversión de una vez por todas aquí, en lugar de en paintEvent().
void MandelbrotWidget::zoom(double zoomFactor) { curScale *= zoomFactor; update(); thread.render(centerX, centerY, curScale, size(), devicePixelRatio()); }
En zoom(), volvemos a calcular curScale. Luego llamamos a QWidget::update() para dibujar un pixmap escalado, y pedimos al hilo trabajador que renderice una nueva imagen correspondiente al nuevo valor de curScale.
void MandelbrotWidget::scroll(int deltaX, int deltaY) { centerX += deltaX * curScale; centerY += deltaY * curScale; update(); thread.render(centerX, centerY, curScale, size(), devicePixelRatio()); }
scroll() es similar a zoom(), excepto que los parámetros afectados son centerX y centerY.
La función main()
La naturaleza multihilo de la aplicación no tiene ningún impacto en su función main(), que es tan simple como de costumbre:
int main(int argc, char *argv[]) { QApplication app(argc, argv); QCommandLineParser parser; parser.setApplicationDescription(u"Ejemplo Qt Mandelbrot"_s); parser.addHelpOption(); parser.addVersionOption(); QCommandLineOption passesOption(u"passes"_s, u"Número de pasadas (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.





