Пример "Mandelbrot"Файлы:
Пример "Mandelbrot" показывает, как использовать рабочий поток для тяжелых вычислений, не блокируя при этом цикл обработки событий основного потока. Тяжелые вычисления в данном случае - множество Мандельброта, пожалуй, самый известный фрактал в мире. В наши дни, когда существуют такие сложные программы, как XaoS, обеспечивающие увеличение множества Мандельброта в реальном времени, стандартный алгоритм Мандельброта как раз настолько медленный, что подходит для наших целей. В действительности, описанный здесь подход применим к большому набору проблем, включая синхронный сетевой ввод/вывод и доступ к базам данных, когда пользовательский интерфейс должен оставаться быстро реагирующим во время выполнения некоторых тяжелых операций. Пример network/blockingfortuneclient показывает тот же принцип при работе TCP-клиента. Приложение "Mandelbrot" поддерживает масштабирование и прокрутку с использованием мыши и клавиатуры. Чтобы избежать замораживания цикла обработки событий основного потока (и, как следствие, пользовательского интерфейса приложения), мы помещаем все вычисления фракталов в отдельный рабочий поток. Поток испускает сигнал, когда он заканчивает рисовать фрактал. В то время как рабочий поток пересчитывает фрактал для отражения нового коэффициента увеличения, основной поток просто масштабирует предыдущее изображение для обеспечения немедленной обратной связи. Результат не выглядит настолько же хорошим, как то, что рабочий поток обеспечит в конечном итоге, но по крайней мере, это делает приложение более отзывчивым. На последовательности скриншотов ниже показаны исходное изображение, отмасштабированное изображение и заново сформированное изображение. Аналогично, когда пользователь использует прокрутку, предыдущее изображение прокручивается сразу, открывая неотрисованные районы за пределами отрисованного изображения, в то время как изображение формируется рабочим потоком. Приложение состоит из двух классов:
Если вы еще не знакомы с поддержкой потоков в Qt, мы рекомендуем вам начать с прочтения обзора Поддержка потоков в Qt. Определение класса RenderThreadМы начнем с определения класса RenderThread: class RenderThread : public QThread { Q_OBJECT public: RenderThread(QObject *parent = 0); ~RenderThread(); void render(double centerX, double centerY, double scaleFactor, QSize resultSize); signals: void renderedImage(const QImage &image, double scaleFactor); protected: void run(); private: uint rgbFromWaveLength(double wave); QMutex mutex; QWaitCondition condition; double centerX; double centerY; double scaleFactor; QSize resultSize; bool restart; bool abort; enum { ColormapSize = 512 }; uint colormap[ColormapSize]; }; Класс унаследован от QThread, так что он получает возможность работать в отдельном потоке. Помимо конструктора и деструктора, render () является единственной публичной функцией. Всякий раз, когда поток заканчивает формировать изображение, он испускает сигнал renderedImage(). Защищенная функция run() переопределена от QThread. Она автоматически вызывается, когда запускается поток. В секции private у нас есть QMutex, QWaitCondition и несколько других членов данных. Мьютекс защищает другие члены данных. Реализация класса RenderThreadRenderThread::RenderThread(QObject *parent) : QThread(parent) { restart = false; abort = false; for (int i = 0; i < ColormapSize; ++i) colormap[i] = rgbFromWaveLength(380.0 + (i * 400.0 / ColormapSize)); } В конструкторе мы инициализируем переменные restart и abort значением false. Эти переменные управляют течением функции run(). Мы также инициализируем массив colormap, который содержит ряд RGB-цветов. RenderThread::~RenderThread() { mutex.lock(); abort = true; condition.wakeOne(); mutex.unlock(); wait(); } Деструктор может быть вызван в любой момент, пока активен поток. Мы устанавливаем abort в значение true, чтобы сказать run(), что надо как можно скорее прекратить работу. Мы также вызываем QWaitCondition::wakeOne(), чтобы разбудить поток, если он спит. (Как мы увидим, когда будем рассматривать run(), поток переводится в сон, когда ему нечего делать.) Здесь важно отметить, что run() выполняется в собственном потоке (рабочий поток), в то время как конструктор и деструктор RenderThread (а также функция render()) вызываются в потоке, который был создан рабочим потоком. Поэтому мы должны использовать мьютекс для защиты доступа к переменным abort и condition, которые могут быть в любое время доступны из run(). В конце деструктора мы вызываем QThread::wait() для ожидания до тех пор, пока run() не завершится, перед тем, как будет вызван деструктор класса. void RenderThread::render(double centerX, double centerY, double scaleFactor, QSize resultSize) { QMutexLocker locker(&mutex); this->centerX = centerX; this->centerY = centerY; this->scaleFactor = scaleFactor; this->resultSize = resultSize; if (!isRunning()) { start(LowPriority); } else { restart = true; condition.wakeOne(); } } Функция render() вызывается из MandelbrotWidget всякий раз, когда ему необходимо создать новое изображение множества Мандельброта. Параметры centerX, centerY и scaleFactor определяют часть фрактала для формирования; resultSize определяет размер получаемого QImage. Функция сохраняет параметры в переменных-членах. Если поток не запущен, она запускает его; в противном случае она устанавливает restart в true (сообщает run(), что необходимо остановить любое незаконченное вычисление и начать все заново с новыми параметрами) и пробуждает поток, который мог бы спать. void RenderThread::run() { forever { mutex.lock(); QSize resultSize = this->resultSize; double scaleFactor = this->scaleFactor; double centerX = this->centerX; double centerY = this->centerY; mutex.unlock(); run() довольно большая функция, поэтому мы разбили ее на части. Тело функции - бесконечный цикл, который начинается с сохранения параметров формирования в локальных переменных. Как обычно, мы защищаем доступ к переменным-членам, используя мьютекс класса. Хранение переменных-членов в локальных переменных позволяет минимизировать объем кода, который должен быть защищен мьютексом. Это гарантирует, что основной поток кода никогда не будет блокироваться слишком долго, когда ему необходим доступ к переменным-членам RenderThread (например, в render()). forever - ключевое слово, подобное псевдоключевому слову foreach в Qt. int halfWidth = resultSize.width() / 2; int halfHeight = resultSize.height() / 2; QImage image(resultSize, QImage::Format_RGB32); const int NumPasses = 8; int pass = 0; while (pass < NumPasses) { const int MaxIterations = (1 << (2 * pass + 6)) + 32; const int Limit = 4; bool allBlack = true; for (int y = -halfHeight; y < halfHeight; ++y) { if (restart) break; if (abort) return; uint *scanLine = reinterpret_cast<uint *>(image.scanLine(y + halfHeight)); double ay = centerY + (y * scaleFactor); for (int x = -halfWidth; x < halfWidth; ++x) { double ax = centerX + (x * scaleFactor); double a1 = ax; double b1 = ay; int numIterations = 0; do { ++numIterations; double a2 = (a1 * a1) - (b1 * b1) + ax; 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) emit renderedImage(image, scaleFactor); ++pass; } } Затем идет ядро алгоритма. Вместо того чтобы создать идеальное отображение множества Мандельброта, мы делаем несколько проходов и создаём все более и более точные (и вычислительно дорогие) приближения фрактала. Если мы обнаруживаем в цикле, что restart была установлена в true (в render()), то мы сразу выходим из цикла, так, чтобы управление сразу вернулось к самому началу внешнего цикла (цикл forever), и мы получили новые параметры формирования. Аналогично, если мы обнаруживаем, что abort была установлена в true (в деструкторе RenderThread), мы сразу возвращаемся из функции, прерывая поток. Основной алгоритм выходит за рамки данного руководства. mutex.lock(); if (!restart) condition.wait(&mutex); restart = false; mutex.unlock(); } } Как только мы заканчиваем все итерации, мы вызываем QWaitCondition::wait(), чтобы перевести поток в состояние ожидания, если restart не true. Не имеет смысла сохранять цикл рабочего потока в течении неопределенного времени, пока все равно нечего делать. uint RenderThread::rgbFromWaveLength(double wave) { double r = 0.0; double g = 0.0; double b = 0.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 = pow(r * s, 0.8); g = pow(g * s, 0.8); b = pow(b * s, 0.8); return qRgb(int(r * 255), int(g * 255), int(b * 255)); } Функция rgbFromWaveLength() - вспомогательная, она преобразует длины волн в RGB-значение, совместимое с 32-битным QImage. Она вызывается из конструктора для инициализации массива colormap приятными цветами. Определение класса MandelbrotWidgetКласс MandelbrotWidget использует RenderThread для рисования множества Мандельброта на экране. Вот определение класса: class MandelbrotWidget : public QWidget { Q_OBJECT public: MandelbrotWidget(QWidget *parent = 0); protected: void paintEvent(QPaintEvent *event); void resizeEvent(QResizeEvent *event); void keyPressEvent(QKeyEvent *event); void wheelEvent(QWheelEvent *event); void mousePressEvent(QMouseEvent *event); void mouseMoveEvent(QMouseEvent *event); void mouseReleaseEvent(QMouseEvent *event); private slots: void updatePixmap(const QImage &image, double scaleFactor); private: void zoom(double zoomFactor); void scroll(int deltaX, int deltaY); RenderThread thread; QPixmap pixmap; QPoint pixmapOffset; QPoint lastDragPos; double centerX; double centerY; double pixmapScale; double curScale; }; Виджет переопределяет многие обработчики событий QWidget. Кроме того, он имеет слот updatePixmap(), который мы подключим к сигналу renderedImage() рабочего потока для обновления экрана при получении новых данных из потока. Среди закрытых переменных у нас есть thread типа RenderThread и pixmap, который содержит последнее сформированное изображение. Реализация класса MandelbrotWidgetconst double DefaultCenterX = -0.637011f; const double DefaultCenterY = -0.0395159f; const double DefaultScale = 0.00403897f; const double ZoomInFactor = 0.8f; const double ZoomOutFactor = 1 / ZoomInFactor; const int ScrollStep = 20; Реализация начинается с нескольких констант, которые будут необходимы нам позже. MandelbrotWidget::MandelbrotWidget(QWidget *parent) : QWidget(parent) { centerX = DefaultCenterX; centerY = DefaultCenterY; pixmapScale = DefaultScale; curScale = DefaultScale; qRegisterMetaType<QImage>("QImage"); connect(&thread, SIGNAL(renderedImage(QImage,double)), this, SLOT(updatePixmap(QImage,double))); setWindowTitle(tr("Mandelbrot")); #ifndef QT_NO_CURSOR setCursor(Qt::CrossCursor); #endif resize(550, 400); } Интересная часть конструктора - вызовы qRegisterMetaType() и QObject::connect(). Давайте начнем с вызова connect(). Хотя это выглядит как стандартное соединение сигнал-слот между двумя QObject, это не так, поскольку сигнал испускается в потоке, отличном от того, где живет получатель, поэтому фактически это соединение через очередь. Эти соединения являются асинхронными (т.е. неблокирующими), а это означает, что слот будет вызван через некоторое время после оператора emit. Более того, слот будет вызван в потоке, в котором живет приёмник. В данном случае сигнал испускается в рабочем потоке, а слот будет выполнен в потоке ГПИ, когда управление будет возвращено в цикл обработки событий. При использовании соединений через очередь Qt должен сохранять копию аргументов, которые были переданы с сигналом, чтобы их можно было позже передать в слот. Qt знает, как получить копии многих типов C++ и Qt, но QImage не входит в их число. Поэтому мы должны вызвать шаблонную функцию qRegisterMetaType() до того, как использовать QImage в качестве параметра в соединениях через очередь. 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, tr("Rendering initial image, please wait...")); return; } В paintEvent() мы начинаем с заполнения фона черным цветом. Если у нас еще ничего не нарисовано (pixmap равен null), мы выводим сообщение на виджет с просьбой об ожидании и сразу возвращаемся из функции. if (curScale == pixmapScale) { painter.drawPixmap(pixmapOffset, pixmap); } else { double scaleFactor = pixmapScale / curScale; int newWidth = int(pixmap.width() * scaleFactor); int newHeight = int(pixmap.height() * scaleFactor); int newX = pixmapOffset.x() + (pixmap.width() - newWidth) / 2; int newY = pixmapOffset.y() + (pixmap.height() - newHeight) / 2; painter.save(); painter.translate(newX, newY); painter.scale(scaleFactor, scaleFactor); QRectF exposed = painter.matrix().inverted().mapRect(rect()).adjusted(-1, -1, 1, 1); painter.drawPixmap(exposed, pixmap, exposed); painter.restore(); } Если изображение имеет верный масштабный коэффициент, то мы отображаем его прямо на виджете. Иначе мы масштабируем и переводим систему координат перед отображением изображения. При обратном преобразовании прямоугольника виджета, используя масштабируемую матрицу рисовальщика, мы также уверены, что только видимые участки изображения будут нарисованы. Вызовы QPainter::save() и QPainter::restore() дают уверенность, что любое последующее рисование использует стандартную систему координат. QString text = tr("Use mouse wheel or the '+' and '-' keys to zoom. " "Press and hold left mouse button to scroll."); QFontMetrics metrics = painter.fontMetrics(); int textWidth = metrics.width(text); painter.setPen(Qt::NoPen); painter.setBrush(QColor(0, 0, 0, 127)); painter.drawRect((width() - textWidth) / 2 - 5, 0, textWidth + 10, metrics.lineSpacing() + 5); painter.setPen(Qt::white); painter.drawText((width() - textWidth) / 2, metrics.leading() + metrics.ascent(), text); } В конце обработчика события рисования мы отображаем текстовую строку и полупрозрачный прямоугольник в верхней части фрактала. void MandelbrotWidget::resizeEvent(QResizeEvent * /* event */) { thread.render(centerX, centerY, curScale, size()); } Каждый раз, когда пользователь изменяет размеры виджета, мы вызываем render(), чтобы начать создание нового изображения с теми же параметрами centerX, centerY и curScale, но с новым размером виджета. Обратите внимание, что мы полагаемся на автоматический вызов resizeEvent() самим Qt при отображении виджета в первый раз, для первичной генерации изображения. 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; default: QWidget::keyPressEvent(event); } } Обработчик событий нажатия клавиш обеспечивает несколько клавиатурных привязок для пользователей, у которых нет мыши. Функции zoom() и scroll() будут описаны далее. void MandelbrotWidget::wheelEvent(QWheelEvent *event) { int numDegrees = event->delta() / 8; double numSteps = numDegrees / 15.0f; zoom(pow(ZoomInFactor, numSteps)); } Обработчик событий колеса прокрутки переопределен, чтобы реализовать изменение масштаба по колесу мыши. QWheelEvent::delta() возвращает угол поворота колеса мыши в восьмых долях градуса. Для большинства мышей один шаг прокрутки колеса составляет 15 градусов. Мы узнаём, на сколько шагов прокрутили колесо мыши, и в результате определяем коэффициент масштабирования. Например, если было два шага прокрутки колеса мыши в положительном направлении (т.е. +30 градусов), то коэффициент масштабирования становится равным ZoomInFactor во второй степени, т.е. 0.8 * 0.8 = 0.64. void MandelbrotWidget::mousePressEvent(QMouseEvent *event) { if (event->button() == Qt::LeftButton) lastDragPos = event->pos(); } Когда пользователь нажимает левую кнопку мыши, мы сохраняем позицию указателя мыши в lastDragPos. void MandelbrotWidget::mouseMoveEvent(QMouseEvent *event) { if (event->buttons() & Qt::LeftButton) { pixmapOffset += event->pos() - lastDragPos; lastDragPos = event->pos(); update(); } } Когда пользователь перемещает мышь с нажатой левой кнопкой мыши, мы корректируем pixmapOffset для рисования изображения в новой позиции и вызываем QWidget::update() для принудительной перерисовки. void MandelbrotWidget::mouseReleaseEvent(QMouseEvent *event) { if (event->button() == Qt::LeftButton) { pixmapOffset += event->pos() - lastDragPos; lastDragPos = QPoint(); int deltaX = (width() - pixmap.width()) / 2 - pixmapOffset.x(); int deltaY = (height() - pixmap.height()) / 2 - pixmapOffset.y(); scroll(deltaX, deltaY); } } Когда левая кнопка мыши отпускается, мы обновляем pixmapOffset точно так же, как при перемещении, и сбрасываем lastDragPos на значение по умолчанию. Затем мы вызываем scroll() для формирования нового изображения в новой позиции. (Корректировки pixmapOffset недостаточно, поскольку отображенные при перетаскивании мышью области отображаются в черном цвете.) void MandelbrotWidget::updatePixmap(const QImage &image, double scaleFactor) { if (!lastDragPos.isNull()) return; pixmap = QPixmap::fromImage(image); pixmapOffset = QPoint(); lastDragPos = QPoint(); pixmapScale = scaleFactor; update(); } Слот updatePixmap() выполняется, когда рабочий поток завершает формирование изображения. Мы начинаем с проверки того, не находимся ли мы в режиме перетаскивания, и если да, то ничего не делаем в этом случае. В нормальном случае мы сохраняем изображение в pixmap и повторно инициализируем некоторые другие члены. В завершении мы вызываем QWidget::update() для обновления экрана. Тут вы можете задаться вопросом, почему мы используем QImage для параметра, а QPixmap - как член данных. Почему бы не придерживаться одного типа? Причина в том, что QImage - это единственный класс, который поддерживает прямое манипулирование пикселями, которые нам нужны в рабочем потоке. С другой стороны, прежде чем изображение может быть отображено, оно должно быть преобразовано в растровое изображение. Лучше сделать преобразование однократно здесь, а не в paintEvent(). void MandelbrotWidget::zoom(double zoomFactor) { curScale *= zoomFactor; update(); thread.render(centerX, centerY, curScale, size()); } В zoom() мы пересчитываем curScale. Затем вызываем QWidget::update() для отображения отмасштабированного растрового изображения и просим рабочий поток сформировать новое изображение, соответствующее новому значению curScale. void MandelbrotWidget::scroll(int deltaX, int deltaY) { centerX += deltaX * curScale; centerY += deltaY * curScale; update(); thread.render(centerX, centerY, curScale, size()); } scroll() похожа на zoom(), за исключением того что затрагиваются параметры centerX и centerY. Функция main()Многопоточный характер приложения не влияет на функцию main(), которая так же проста, как и обычно: int main(int argc, char *argv[]) { QApplication app(argc, argv); MandelbrotWidget widget; widget.show(); return app.exec(); } |
Попытка перевода Qt документации. Если есть желание присоединиться, или если есть замечания или пожелания, то заходите на форум: Перевод Qt документации на русский язык... Люди внесшие вклад в перевод: Команда переводчиков |