異步繪製Mandelbrot分形

示例說明

Mandelbrot示例演示了使用Qt進行多線程編程。它展示瞭如何使用工作線程執行大量計算而不阻塞主線程的事件循環。Mandelbrot應用程序支持使用鼠標或鍵盤進行縮放和滾動。爲了避免凍結主線程的事件循環(應用程序的用戶界面),我們將所有分形計算放在單獨的工作線程中。該線程在繪製分形完成時發出信號。

在工作線程重新計算分形以反映新的縮放因子位置的時候,主線程簡單地縮放先前呈現的像素映射以提供即時反饋。結果看起來不如工作線程最終提供的結果好,但至少它使應用程序更有響應性。下面的屏幕截圖序列顯示原始圖像、縮放圖像和重新繪製的圖像。
在這裏插入圖片描述
類似地,當用戶滾動時,會立即滾動上一個像素映射,當時會顯示像素映射邊緣之外的未繪製區域,圖像則再由工作線程來呈現。
在這裏插入圖片描述

代碼解析

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() override;
    
    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()信號。
函數從QThread重新實現受保護的run()函數。當線程啓動時,會自動調用它。
在私有部分中,我們有一個QMutex、一個QWaitCondu和一些其他的數據成員。互斥保護其他數據成員。

RenderThread類實現

    #include <QtWidgets>
    #include <cmath>    
    //在構造函數中,我們將restart和abort變量初始化爲false。這些變量控制run()函數的流。
    //還初始化了包含一系列RGB顏色的colormap數組。
    RenderThread::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));
    }
    
    //當線程處於活動狀態時,可以隨時調用析構函數。我們將ABORT設置爲true,以告訴run()儘快停止運行。
    //調用QWaitCondu:wakeOne()來喚醒線程(如果線程處於休眠狀態)。(將在查看run()時看到,當線程沒有什麼事做時,線程就會進入休眠狀態。)
    //這裏要注意的是,run()是在它自己的線程(工作線程)中執行的,而RenderThread構造函數和析構函數(以及Render()函數)則由創建工作線程的線程調用。
    //因此,我們需要一個互斥來保護對中止和條件變量的訪問,這些變量可以在任何時候被run()訪問。
    //在析構函數的末尾,我們調用QThread:WAIT(),直到run()退出,然後調用基類析構函數。
    RenderThread::~RenderThread()
    {
        mutex.lock();
        abort = true;
        condition.wakeOne();
        mutex.unlock();
    
        wait();
    }
    
    //每當需要生成Mandelbrot集的新圖形時,它都會調用Render()函數。
    //CentX、CentY和ScaleFactor參數指定要呈現的分形部分;ResultSize指定生成的QImage的大小。
    //如果線程尚未運行,則啓動它;否則,它將重新啓動設置爲true(告訴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();
        }
    }

    //函數體是一個無限循環
    //使用類的互斥對象保護對成員變量的訪問
    //將成員變量存儲在局部變量中可以使需要受互斥保護的代碼的數量最小化。這確保主線程在需要訪問RenderThread的成員變量(例如Render())時不會阻塞太久。
    void RenderThread::run()
    {
        forever {
            mutex.lock();
            QSize resultSize = this->resultSize;
            double scaleFactor = this->scaleFactor;
            double centerX = this->centerX;
            double centerY = this->centerY;
            mutex.unlock();
    
            //算法的核心, 核心算法超出了本教程的範圍不做研究
            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;
                }
            }
    
            //完成了所有的迭代,我們就調用QWaitCondition::wait(),通過調用使線程進入休眠狀態,除非重restart爲true。
            //在沒有事情可做的情況下,讓工作線程無限期地循環是沒有用的。
            mutex.lock();
            if (!restart)
                condition.wait(&mutex);
            restart = false;
            mutex.unlock();
        }
    }
   
    //rgbFromWaveLength()函數是一個輔助函數,它將波長轉換爲與32位QImage兼容的RGB值。
    //它是從構造函數中調用來初始化顏色圖數組的。
    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 = 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));
    }

MandelbrotWidget 類定義

class MandelbrotWidget : public QWidget
{
    Q_OBJECT

public:
    MandelbrotWidget(QWidget *parent = 0);

protected:
    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;

private slots:
    void updatePixmap(const QImage &image, double scaleFactor);
    void zoom(double zoomFactor);

private:
    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()信號,以便每當從線程接收到新數據時更新顯示。

MandelbrotWidget 類實現

#include <QPainter>
#include <QKeyEvent>

#include <math.h>

#include "mandelbrotwidget.h"



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

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

}

//首先用黑色填充背景。如果我們還沒有什麼可繪製的(pixmap爲NULL)
//我們會在widget上打印一條消息,要求用戶耐心等待並立即從函數返回。
//如果pixmap具有正確的縮放因子,則將pixmap直接繪製到widget上。
//否則,在繪製pixmap之前,我們對座標系統進行縮放和轉換。
//通過使用scaled painter matrix反向映射小部件的矩形,確保只繪製像素映射的暴露區域。
//對QPainter:Save()和QPainter:restore()的調用確保以後執行的任何繪製都使用標準座標系
//在畫圖事件處理程序的末尾,我們在分形的頂部畫一個文本字符串和一個半透明的矩形。
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..."));
    }

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

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


//每當用戶調整小部件的大小時,我們就調用Render()來生成一個新的映像
//具有相同的centX、centY和curScale參數,但是具有新的widget大小。
void MandelbrotWidget::resizeEvent(QResizeEvent * /* event */)
{
    thread.render(centerX, centerY, curScale, size());
}

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

#ifndef QT_NO_WHEELEVENT
void MandelbrotWidget::wheelEvent(QWheelEvent *event)
{
    int numDegrees = event->delta() / 8;
    double numSteps = numDegrees / 15.0f;
    zoom(pow(ZoomInFactor, numSteps));
}
#endif

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

void MandelbrotWidget::mouseMoveEvent(QMouseEvent *event)
{
    if (event->buttons() & Qt::LeftButton) {
        pixmapOffset += event->pos() - lastDragPos;
        lastDragPos = event->pos();
        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);
    }
}
//當工作線程完成圖像呈現時,會調用updatePixmap()槽。
//首先檢查拖動是否有效,在這種情況下什麼也不做,直接return。
//在正常情況下,將圖像存儲在像素映射中,並重新初始化其他一些成員。
//最後,我們調用QWidget:update()刷新顯示。
void MandelbrotWidget::updatePixmap(const QImage &image, double scaleFactor)
{
    if (!lastDragPos.isNull())
        return;

    pixmap = QPixmap::fromImage(image);
    pixmapOffset = QPoint();
    lastDragPos = QPoint();
    pixmapScale = scaleFactor;
    update();
}

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

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

使用方式

#include "mandelbrotwidget.h"

#include <QApplication>

int main(int argc, char *argv[])
{
    QApplication app(argc, argv);
    MandelbrotWidget widget;
    widget.show();
    return app.exec();
}

運行效果

在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章