Qt例子学习笔记 - Examples/Qt-6.2.0/corelib/threads/mandelbrot

71 篇文章 4 订阅

//这里的繁重计算是 Mandelbrot 集,它可能是世界上最著名的分形。
//如今,虽然 XaoS 等复杂程序在 Mandelbrot 集合中提供实时缩放
//但标准 Mandelbrot 算法对于我们的目的来说已经足够慢了
//在现实生活中,这里描述的方法适用于大量问题
//包括同步网络 I/O 和数据库访问
//其中用户界面必须在一些繁重的操作发生时保持响应
//Blocking Fortune Client 示例展示了在 TCP 客户端中工作的相同原理。

//Mandelbrot 应用程序支持使用鼠标或键盘进行缩放和滚动。
//为了避免冻结主线程的事件循环(以及应用程序的用户界面)
//我们将所有分形计算放在一个单独的工作线程中
//分形渲染完成后,线程会发出一个信号。
//在工作线程重新计算分形以反映新的缩放因子位置期间
//主线程简单地缩放先前渲染的像素图以提供即时反馈。
//果看起来不如工作线程最终提供的效果好
//但至少它使应用程序响应更快

//该应用程序由两个类组成:
//RenderThread 是一个 QThread 子类,用于渲染 Mandelbrot 集。
//MandelbrotWidget 是一个 QWidget 子类,它在屏幕上显示 Mandelbrot 集并让用户缩放和滚动。
//如果您还不熟悉 Qt 的线程支持
//我们建议您首先阅读 Qt 中的线程支持概述。
//我们将从 RenderThread 类的定义开始:

    class RenderThread : public QThread
    {
        Q_OBJECT
    public:
        RenderThread(QObject *parent = nullptr);
        ~RenderThread();

        void render(double centerX,double centerY,double scoleFactor,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;

        enum{ ColormapSize = 512};
        uint colormap[ColormapSize];
    }

//该类继承了 QThread,因此它获得了在单独线程中运行的能力。
//除了构造函数和析构函数
//render() 是唯一的公共函数
//每当线程完成渲染图像时,它就会发出 renderImage() 信号。
//受保护的 run() 函数是从 QThread 重新实现的。
//它在线程启动时自动调用。
//在私有部分,我们有一个 QMutex、一个 QWaitCondition 和一些其他数据成员。
//互斥锁保护另一个数据成员。

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

    //在构造函数中,我们将 restart 和 abort 变量初始化为 false。
    //这些变量控制 run() 函数的流程。
    //我们还初始化了包含一系列 RGB 颜色的颜色图数组。
    RenderThread::~RenderThread()
    {
        mutex.lock();
        abort = true;
        condition.wakeOne();
        mutex.unlock();
        wait();
    }    

//当线程处于活动状态时,
//可以随时调用析构函数
//我们将 abort 设置为 true 以告诉 run() 尽快停止运行。
//我们还调用 QWaitCondition::wakeOne() 来唤醒正在休眠的线程
//(正如我们在回顾 run() 时将看到的,线程在无事可做时进入睡眠状态。)
//这里要注意的重要一点是 run() 在它自己的线程(工作线程)中执行
//而 RenderThread 构造函数和析构函数(以及 render() 函数)
//由创建工作线程的线程调用
//因此,我们需要一个互斥锁来保护对 abort 和条件变量的访问
//这些变量可能随时被 run() 访问。
//在析构函数的末尾,我们调用 QThread::wait()
//等待直到 run() 退出,然后才会调用基类析构函数。

    void RenderThread::render(double centerX,double centerY,double scaleFactor,
                              QSize resultSie,double devicePixelRatio)
    {
        QMutexLocker locker(&mutex);

        this->centerX = centerX;
        this->centerY = centerY;
        this->scaleFactor = scaleFactor;
        this->resultSize = resultSize;

        if(!isRunning())
        {
            start(LowPriority);
        }
        else
        {
            restart = true;
            condition.wakeOne();
        }
    }

//每当需要生成 Mandelbrot 集的新图像时
//就会由 MandelbrotWidget 调用 render() 函数。
//centerX、centerY 和 scaleFactor 参数指定要渲染的分形部分
//resultSize 指定生成的 QImage 的大小。
//该函数将参数存储在成员变量中。
//如果线程尚未运行,则启动它
//否则,它将重新启动设置为 true(告诉 run()
//停止任何未完成的计算并使用新参数重新开始)
//并唤醒可能正在休眠的线程。

    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 = requestedSacleFactor / devicePixelRatio;
            const double centerX = this->centerX;
            const double centerY = this->centerY;
            mutex.unlock();
        }
    }

//run() 是一个相当大的函数,所以我们将把它分解成几个部分。
//函数体是一个无限循环,首先将渲染参数存储在局部变量中。
//像往常一样,我们使用类的互斥锁保护对成员变量的访问。
//将成员变量存储在局部变量中可以让我们最大限度地减少
//需要由互斥锁保护的代码量。
//这确保了主线程在需要访问
//RenderThread 的成员变量
//(例如,在 render() 中)时永远不会阻塞太长时间。
//与 foreach 一样,forever 关键字是 Qt 伪关键字。

    int halfWidth = resultSize.width() / 2;
    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;
        const 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 > 200)
                    str<<(elapsed / 1000)<<'s';
                else
                    str<<elapsed<<"ms";
                image.setText(infoKey(),message);

                emit renderedImage(image,requestedScaleFactor);
            }
            ++pass;
        }
    }

//然后是算法的核心。
//我们没有尝试创建完美的 Mandelbrot 集图像,
//而是进行多次传递并生成越来越精确(且计算成本高)的分形近似值。
//我们通过将设备像素比应用于目标尺寸来创建高分辨率像素图
//如果我们在循环内部发现 restart 已设置为 true
//(通过 render())
//我们立即跳出循环,以便控制快速返回到外循环(永远循环)的最顶部
//我们获取 新的渲染参数。
//类似地,如果我们发现 abort 已设置为 true(由 RenderThread 析构函数)
//我们立即从函数返回,终止线程。

//核心算法超出了本教程的范围。

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

//一旦我们完成了所有的迭代,
//我们调用 QWaitCondition::wait() 来让线程进入睡眠状态
//除非重新启动为真
//在无事可做的情况下保持工作线程无限循环是没有用的

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

//rgbFromWaveLength() 函数是一个辅助函数,
//可将波长转换为与 32 位 QImages 兼容的 RGB 值。
//从构造函数调用它以使用令人愉悦的颜色初始化颜色图数组。

//MandelbrotWidget 类定义
//MandelbrotWidget 类使用 RenderThread 在屏幕上绘制 Mandelbrot 集。
//这是类定义:

    class MandelbrotWidget : public QWidget
    {
        Q_OBJECT
    public:
        MandelbrotWidget(QWidget *parent =nullptr);
    protected:
        void paintEvent(QPaintEvent *event) override;
        void resizeEvent(QPaintEvent *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;
        QString help;
        QString info;
        double centerX;
        double centerY;
        double pixmapScale;
        double curScale;
    };

    //该小部件从 QWidget 重新实现了许多事件处理程序。
    //此外,它有一个 updatePixmap() 槽,我们将连接到工作线程的
    //renderImage() 信号以在我们从线程接收新数据时更新显示。

//在私有变量中,我们有 RenderThread 和 pixmap 类型的线程,它包含最后渲染的图像。
//MandelbrotWidget Class Implementation

    const double DefaultCenterX = -0.637011;
    const double DefaultCenterY = -0.0395159;
    const double DefaultScale = 0.00403897;

    const double ZoomInFactor = 0.8;
    const double ZoomOutFactor = 1 / ZoomInFactor;
    const int ScrollStep = 20;

//实现从一些我们稍后需要的常量开始。

    MandelbrotWidget::MandelbrotWidget(QWidget *parent):
    QWidget(parent),
    centerX(DefaultCenterX),
    centerY(DefaultCenterY),
    curScale(DefaultScale)
    {
        help = tr("Use mouse wheel or the '+' and '-' keys to zoom"
                   "Press and hold left mouse button to scroll");
        connect(&thread,&RenderThread::renderedImage,
                this,&MandelbrotWidget::updatePixmap);

        setWindowTitle(tr("Mandelbrot"));
        #if QT_CONFIG(cursor)
            setCursor(Qt::CrossCursor);
        #endif
    }
    

//构造函数的有趣部分是 QObject::connect() 调用。
//尽管它看起来像两个 QObject 之间的标准信号槽连接
//由于信号是在与接收器所在的线程不同的线程中发出的
//因此该连接实际上是一个排队连接。
//这些连接是异步的(即非阻塞的)
//这意味着将在执行后的某个时刻调用该插槽
//更重要的是,该插槽将在接收者所在的线程中被调用
//在这里,信号在工作线程中发出
//当控制权返回到事件循环时,槽在 GUI 线程中执行。
//对于排队连接,Qt 必须存储传递给信号的参数的副本
//以便稍后将它们传递到插槽。
//Qt 知道如何复制许多 C++ 和 Qt 类型
//因此,QImage 不需要进一步的操作。
//如果使用自定义类型,则需要调用模板函数 qRegisterMetaType(),
//然后才能将该类型用作排队连接中的参数。

    void MandelbrotWidget::paintEvent(QPaintEvent *)
    {
        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 为空)
//我们会在小部件上显示一条消息,要求用户耐心等待并立即从函数中返回。

    if(qFuzzyCompare(curScale,pixmapScale))
    {
        painter.drawPixmap(pixmapOffset,pixmap);
    }
    else
    {
        auto previewPixmap = qFuzzyCompare(pixmap.devicePixelRatio(),qreal(1))
            ?pixmap
            :pixmap.scaled(pixmap.size() / pixmap.devicePixelRatio(),Qt::KeepAspectRatio,
             Qt::SmoothTransformation);
            
        double scaleFactor = pixmapScale / curScale;
        int newWidth = int(previewPixmap.width() * scaleFactor);
        int newHeight = int(previewPixmap.height() * scaleFactor);
        int newX = pixmapOffset.x() + (previewPixmap.width() - newWidth) / 2;
        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()).adjusted(-1, -1, 1, 1);
        painter.drawPixmap(exposed,previewPixmap,exposed);
        painter.restore(); 
    }

//如果像素图具有正确的比例因子,我们将直接将像素图绘制到小部件上。
//否则,我们创建一个预览像素图,直到计算完成并相应地转换坐标系。
//由于我们将在画家上使用转换并在这种情况下使用不支持高分
//辨率像素图的 QPainter::drawPixmap() 的重载,
//因此我们创建了一个设备像素比为 1 的像素图。
//通过使用缩放的画家矩阵反向映射小部件的矩形
//我们还确保仅绘制像素图的暴露区域。
//对 QPainter::save() 和 QPainter::restore() 的调用确保之后执行的任何绘画都使用标准坐标系。

    QString text = help;
    if(!info.isEmpty())
        text += ' '+info;

    QFontMetrix metrix = painter.fontMetrics();
    int textWidth = metrics.horizontalAdvance(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,matrics.leading() + matrics.ascent(),text);

//在绘制事件处理程序的末尾,
//我们在分形的顶部绘制一个文本字符串和一个半透明矩形。

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

//每当用户调整小部件的大小时
//我们调用 render() 开始生成新图像
//具有相同的 centerX、centerY 和 curScale 参数,但具有新的小部件尺寸。
//请注意,当小部件第一次显示时
//我们依赖于 Qt 自动调用 resizeEvent() 来生成初始图像。

    void MandelbrotWidget::keyPressEvent(QKeyEvent *event)
    {
        switch(event->key())
        {
            case Qt::Key_Plush:
                zoom(ZoomInFactor);
                break;
            case Qt::Key_Minus:
                zoom(ZoomOutFactor);
                break;
            case Qt::Key_Left:
                scroll(~ScrollStep,0);
                break;
            case Qt::Key_Right:
                scroll(+ScrollStop,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);
        }
    }

//按键事件处理程序为没有鼠标的用户提供了一些键盘绑定
//zoom() 和 scroll() 函数将在后面介绍。

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

//重新实现滚轮事件处理程序以使鼠标滚轮控制缩放级别。
//QWheelEvent::angleDelta() 返回滚轮鼠标移动的角度
//以八分之一度为单位。
//对于大多数鼠标,一个轮步对应 15 度。
//我们找出我们有多少鼠标步骤并确定结果缩放因子。
//例如,如果我们在正方向(即 +30 度)有两个轮步
//,则缩放系数变为 ZoomInFactor 的二次幂,即 0.8 * 0.8 = 0.64。

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

//当用户按下鼠标左键时,我们将鼠标指针位置存储在 lastDragPos 中。

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

//当用户在按下鼠标左键的同时移动鼠标指针时
//我们调整 pixmapOffset 以在移动的位置绘制像素图并调用
//QWidget::update() 强制重新绘制。

    void MandelbrotWidget::mouseReleaseEvent(QMouseEvent *event)
    {
        if(event->button() ==  Qt::LeftButton)
        {
            pixmapOffset += event->position().toPoint() - lastDragPos;
            lastDragPos = QPoint();

            const auto pixmapSize = pixmap.size() / pixmap.devicePixelRatio();
            int deltaX = (width() - pixmapSize.width()) / 2 - pixmapOffset.x();
            int deltaY = (height() - pixmapSize.height()) / 2-  pixmapOffset.y();
            scroll(deltaX,deltaY);
        }
    }

//当释放鼠标左键时,我们更新 pixmapOffset 就像我们在鼠标移
//动时所做的一样,并将 lastDragPos 重置为默认值。
//然后,我们调用 scroll() 为新位置渲染新图像。
//(调整像素图偏移是不够的,因为拖动像素图时显示的区域是黑色的。)

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

//当工作线程完成渲染图像时调用 updatePixmap() 槽。
//我们首先检查拖动是否有效,在这种情况下什么也不做。
//在正常情况下,我们将图像存储在 pixmap 中并重新初始化其他一些成员。
//最后,我们调用 QWidget::update() 来刷新显示。
//此时,您可能想知道为什么我们使用 QImage 作为参数
//而使用 QPixmap 作为数据成员。
//为什么不坚持一种类型? 原因是 QImage 是唯一支持直接像素操作的类,
//这是我们在工作线程中需要的。
//另一方面,在屏幕上绘制图像之前,必须将其转换为像素图。
//最好在此处一劳永逸地进行转换,而不是在paintEvent() 中进行转换。

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

//在 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(),devicePixelRatio());
    }

//scroll() 与zoom() 类似,只是受影响的参数是centerX 和centerY。
//The main() Function
//应用程序的多线程特性对其 main() 函数没有影响,它像往常一样简单:

    int main(int argc,char *argv[])
    {
        QApplication app(argc,argv);

        QCommandLineParser parser;
        parser.setApplicationDescription("Qt Mandelbrot Example");
        parser.addHelpOption();
        parser.addVersionOption();
        QCommandLineOption passesOption("passes","Number of passes (1-8)","passes");
        parser.addOption(passesOption);
        parser.process(app);

        if(parser.isSer(passesOption))
        {
            const auto passesStr = parser.value(passesOption);
            bool ok;
            const auto passes = passesStr.toInt(&ok);
            if(!ok || passes < 1 || passes > 8)
            {
                qWarning()<<"Invalid value:"<<passesStr;
                return -1;
            }
            RenderThread::setNumPasses(passes);
        }
        MandelbrotWidget widget;
        const auto geometry = widget.screen()->availableGeometry();
        widget.resize((2*geometry.size())/3);
        const auto pos = (geometry.size() - width.size()) / 2;
        widget.move(geometry.topLeft() + QPoint(pos.width(),pos.height()));

        widget.show();
        return app.exec();
    }

main.cpp

#include "mandelbrotwidget.h"

#include <QApplication>

#include <QScreen>

#include <QCommandLineParser>
#include <QCommandLineOption>
#include <QDebug>
#include <QRect>

//! [0]
int main(int argc, char *argv[])
{
    QApplication app(argc, argv);
    //QCommandLineParser 类提供了一种处理命令行选项的方法
    QCommandLineParser parser;
    //void QCommandLineParser::setApplicationDescription(const QString &description)
    //设置 helpText() 显示的应用程序描述。
    parser.setApplicationDescription("Qt Mandelbrot Example");
    //QCommandLineOption QCommandLineParser::addHelpOption()
    //添加帮助选项(Windows 上的 -h、--help 和 -?)以及选项 --help-all 
    //以在输出中包含特定于 Qt 的选项。
    //这些选项由 QCommandLineParser 自动处理。
    //记得使用 setApplicationDescription 来设置应用描述
    //使用该选项时会显示。
    ```cpp
        int main(int argc,char *argv[])
        {
            QCoreApplication app(argc,argv);
            QCoreApplication::setApplicationName("my-copy-program");
            QCoreApplication::setApplicationVersion("1.0");

            QCommandLineParser parser;
            parser.setApplicationDescription("Text Helper");
            parser.addHelperOption();
            parser.addVersionOption();
            parser.addPositionArgument("source",QCoreApplication::translate("main","source file to copy"));
            parser.addPositionArgument("destination",QCoreApplication::translate("main","Destination directory"));

            //A boolean option with a single name (-p)
            QCommandLineOption forceOption(QStringList()<<"f"<<"force",
                            QCoreApplication::translate("name","Override existing files"));
            parser.addOption(forceOption);

            //An option with a value
            QCommandLineOption targetDirectoryOption(QStringList()<<"t"<<"target-directory",
                                QCoreApplication::translate("main","Copy all source files into <directory>"),
                                QCoreApplication::translate("main","directory"));
            parser.addOption(targetDirectoryOption);                  
            
            //Process the actual command line arguments given by the  user
            parser.process(app);

            const QStringList args = parser.positionalArguments();
            //source is args.at(0) destination is args.at()

            bool showProcess = parser.isSet(showProcessOption);
            bool force = parser.isSet(forceOption);
            QString targetDir = parser.value(targetDirectoryOption);

        }
    ```
    parser.addHelpOption();
    parser.addVersionOption();
    QCommandLineOption passesOption("passes", "Number of passes (1-8)", "passes");
    //bool QCommandLineParser::addOption(const QCommandLineOption &option)
    //添加选项选项以在解析时查找。
    //如果添加选项成功,则返回 true; 否则返回false。
    //如果选项没有附加名称,或者选项的名称与之前添加的选项名
    //称冲突,则添加选项将失败
    parser.addOption(passesOption);
    //void QCommandLineParser::process(const QStringList &arguments)
    //处理命令行参数。
    //除了解析选项(如 parse()),这个函数还处理内置选项和处理错误。
    //内置选项是 --version 如果 addVersionOption 被调用
    //并且 --help / --help-all 如果 addHelpOption 被调用。
    //当调用这些选项之一时,或发生错误时(例如传递了未知选项)
    //当前进程将使用 exit() 函数停止。
    parser.process(app);
    //bool QCommandLineParser::isSet(const QString &name) const
    //检查选项名称是否已传递给应用程序。
    //如果设置了选项名称,则返回 true,否则返回 false。
    //提供的名称可以是使用 addOption() 添加的任何选项的任何长名称或短名称
    //所有选项名称都被视为等效的。
    //如果名称无法识别或该选项不存在,则返回 false。
    ```cpp
        bool verbose = parser.isSet("verbose");
    ```

    if (parser.isSet(passesOption)) {
        //QString QCommandLineParser::value(const QString &optionName) const
        //返回为给定选项名称 optionName 找到的选项值
        //如果未找到则返回空字符串。
        //提供的名称可以是使用 addOption() 添加的任何选项的任何长名称或短名称
        //所有选项名称都被视为等效的
        //如果无法识别名称或该选项不存在,则返回空字符串。
        //对于解析器找到的选项
        //返回为该选项找到的最后一个值。
        //如果未在命令行上指定该选项,则返回默认值。
        //如果选项不带值,则返回空字符串。
        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;
    //关于每英寸逻辑点数与物理点数的说明:
    //物理 DPI 基于可用的实际物理像素大小
    //对于打印预览和其他需要知道屏幕显示内容的确切物理尺寸的情况很有用。
    //每英寸逻辑点数用于将字体和用户界面元素从磅大小转换为像素大小
    //并且可能与每英寸物理点数不同。
    //每英寸的逻辑点数有时是用户可在桌面环境的设置面板中设置的,
    //以让用户全局控制不同应用程序中的 UI 和字体大小。
    //[read-only] availableGeometry : const QRect
    //此属性以像素为单位保存屏幕的可用几何图形
    //可用几何图形是不包括窗口管理器保留区域(例如任务栏和系统菜单)的几何图形。
    //请注意,在 X11 上,如果窗口管理器设置了 _NET_WORKAREA 原子,
    //这将仅在具有一台监视器的系统上返回真正的可用几何图形。
    //在所有其他情况下,这等于 geometry()。 这是 X11 窗口管理器规范中的一个限制。
    const auto geometry = widget.screen()->availableGeometry();
    //size : QSize
    //此属性保存小部件的大小,不包括任何窗口框架
    //如果小部件在调整大小时可见,它会立即收到调整大小事件 (resizeEvent())。
    //如果小部件当前不可见,则保证在显示之前收到一个事件。
    //如果大小超出由 minimumSize() 和 maximumSize() 定义的范围,则会调整大小。
    //默认情况下,此属性包含一个取决于用户平台和屏幕几何形状的值
    //警告:在 resizeEvent() 中调用 resize() 或 setGeometry() 会导致无限递归。
    //注意:将大小设置为 QSize(0, 0) 将导致小部件不会出现在屏幕上。 这也适用于窗户。
    widget.resize((2 * geometry.size()) / 3);
    const auto pos = (geometry.size() - widget.size()) / 2;
    widget.move(geometry.topLeft() + QPoint(pos.width(), pos.height()));

    widget.show();
    return app.exec();
}
//! [0]

renderthread.h

#ifndef RENDERTHREAD_H
#define RENDERTHREAD_H

#include <QMutex>
#include <QSize>
#include <QThread>
#include <QWaitCondition>

QT_BEGIN_NAMESPACE
class QImage;
QT_END_NAMESPACE

//QThread 对象管理程序中的一个控制线程
//QThreads 在 run() 中开始执行。
//默认情况下,run() 通过调用 exec() 启动事件循环,
//并在线程内运行 Qt 事件循环。
//您可以通过使用 QObject::moveToThread() 将工作对象移动到线程来使用它们。
/*
    class Worker : public QObject
    {
        Q_OBJECT
    public slots:
        void doWork(const QString &parameter)
        {
            QString result;
            //here is the expensive or blocking operation
            emit resultReady(result);
        }
    signals:
        void resultReady(const QString &result);
    };
    
    class Controller : public QObject
    {
        Q_OBJECT
        QThread workThread;
    public:
        Controller()
        {
            Worker *worker = new Worker;
            worker->moveToThread(&workerThread);
            connect(&workerThread, &QThread::finished,worker,&QObject::deleteLater);
            connect(this,&Controller::operator,worker,&Worker::doWork);
            connect(worker,&Worker::resultReady,this,&Controller::handleResults);
            workerThread.start();
        }
        ~Constroller()
        {
            workerThread.quit();
            workerThread.wait();
        }
    public slots:
        void handleResults(const QString&);
    signals:
        void operate(const QString &);
    };

*/
//Worker 插槽内的代码将在单独的线程中执行。
//但是,您可以自由地将 Worker 的插槽连接到来自任何对象
//任何线程中的任何信号。
//由于一种称为排队连接的机制,跨不同线程连接信号和槽是安全的。
//使代码在单独的线程中运行的另一种方法是继承 QThread 并重新实现
// run()。 例如:
/*
    class WorkerThread : public QThread
    {
        Q_OBJECT
        void run() override
        {
            QString result;
            // ... here is the expensive or blocking operation ...
            emit resultReady(result);
        }
    signals:
        void resultReady(const QString &s);
    };

    void MyObject::startWorkInAThread()
    {
        WorkerThread *workerThread = new WorkerThread(this);
        connect(workerThread,&WorkerThread::resultReady, this, &MyObject::handleResults);
        connect(workerThread,&WorkerThread::finished,workerThread,&Object::deleteLater);
        workerThread->start();
    }
*/
//在该示例中,线程将在 run 函数返回后退出。
//除非您调用 exec(),否则线程中不会运行任何事件循环。
//重要的是要记住 QThread 实例存在于实例化它的旧线程中
//而不是调用 run() 的新线程中。
//这意味着所有 QThread 的排队槽和调用的方法都将在旧线程中执行。
//因此,希望在新线程中调用槽的开发人员必须使用工作对象方法;
//不应直接在子类 QThread 中实现新插槽。
//注意:跨不同线程与对象交互时必须小心。
//作为一般规则,除非文档另有说明,否则只能从创建 QThread 
//对象本身的线程(例如 setPriority())调用函数

//管理线程
//QThread 会在线程开始() 和完成() 时通过信号通知您,
//或者您可以使用 is Finished() 和 is Running() 来查询线程的状态。
//您可以通过调用 exit() 或 quit() 来停止线程。
//在极端情况下,您可能希望强制终止()正在执行的线程。
//然而,这样做是危险的,也是令人沮丧的
//请阅读 terminate() 和 setTerminationEnabled() 的文档以获取详细信息。
//从Qt 4.8 开始,可以通过将finished() 信号连接到QObject::deleteLater() 来释放刚刚结束的线程中的对象。
//使用 wait() 阻塞调用线程,直到另一个线程完成执行(或直到指定的时间过去)。
//QThread 还提供静态的、平台独立的睡眠函数
//eep()、msleep() 和 usleep() 分别允许完整的秒、毫秒和微秒分辨率。 这些函数在 Qt 5.0 中公开。
//注意:通常来说,wait() 和 sleep() 函数应该是不必要的
//因为 Qt 是一个事件驱动的框架。
//而不是wait(),考虑监听finished() 信号。
//考虑使用 QTimer,而不是 sleep() 函数。
//静态函数 currentThreadId() 和 currentThread() 返回当前正在执行的线程的标识符。
//前者返回线程的平台特定 ID; 后者返回一个 QThread 指针。
//要选择您的线程的名称(例如,由 Linux 上的命令 ps -L 标识)
//您可以在启动线程之前调用 setObjectName()
//如果您不调用 setObjectName()
//则为您的线程指定的名称将是您的线程对象的运行时类型的类名
//(例如,在 Mandelbrot 示例中为“RenderThread”,因为这是 QThread 子类)
//请注意,目前这不适用于 Windows 上的发布版本。
//QThread::QThread(QObject *parent = nullptr)
//构造一个新的 QThread 来管理一个新的线程。
//父级拥有 QThread 的所有权。
//在调用 start() 之前,线程不会开始执行。
//[slot] void QThread::exit(int returnCode = 0)
//告诉线程的事件循环以返回码退出。
//调用此函数后,线程离开事件循环并从对 QEventLoop::exec() 的调用返回。
//QEventLoop::exec() 函数返回 returnCode。
//按照惯例,returnCode 为 0 表示成功,任何非零值表示错误。
//请注意,与同名的 C 库函数不同
//该函数确实返回给调用者——停止的是事件处理。
//在再次调用 QThread::exec() 之前,
//将不再在该线程中启动 QEventLoops。
//如果 QThread::exec() 中的 eventloop 没有运行,
//那么下一次对 QThread::exec() 的调用也将立即返回。

//[private signal] void QThread::finished()
//该信号在相关线程完成执行之前发出。
//当这个信号发出时,事件循环已经停止运行。
//除了延迟删除事件外,线程中将不再处理更多事件。
//该信号可以连接到 QObject::deleteLater(),以释放该线程中的对象。
//注意:如果关联的线程使用 terminate() 终止,则未定义从哪个线程发出此信号。

//[slot] void QThread::quit()
//告诉线程的事件循环以返回码 0(成功)退出。 相当于调用 QThread::exit(0)。
//如果线程没有事件循环,则此函数不执行任何操作。

//[slot] void QThread::start(QThread::Priority priority = InheritPriority)
//通过调用 run() 开始执行线程。
//操作系统会根据优先级参数调度线程。
//如果线程已经在运行,这个函数什么都不做。

//优先级参数的效果取决于操作系统的调度策略。
//特别是,在不支持线程优先级的系统上,
//优先级将被忽略(例如在 Linux 上,请参阅 sched_setscheduler 文档以获取更多详细信息)。

//[since 5.0] QAbstractEventDispatcher *QThread::eventDispatcher() const
//返回指向线程的事件调度程序对象的指针
//如果线程不存在事件调度程序,则此函数返回 nullptr。

//[protected] int QThread::exec()
//进入事件循环并等待 exit() 被调用,
//返回传递给 exit() 的值。
//如果 exit() 是通过 quit() 调用的,则返回的值为 0。
//该函数旨在从 run() 内部调用。 需要调用这个函数来启动事件处理。

//[since 5.2] bool QThread::isInterruptionRequested() const
//如果应停止在此线程上运行的任务,则返回 true。
//可以通过 requestInterruption() 请求中断。
//如果应停止在此线程上运行的任务,则返回 true。
//requestInterruption() 可以请求中断。

//此函数可用于使长时间运行的任务完全可中断。
//永远不要检查或操作此函数返回的值是安全的,
//但建议在长时间运行的函数中定期这样做。 注意不要太频繁地调用它,以保持低开销。
/*
    void long_task()
    {
        forever
        {
            if(QThread::currentThread()->isInterruptionRequested())
                return;
        }
    }
*/

//[static] void QThread::msleep(unsigned long msecs)
//强制当前线程休眠 msecs 毫秒。
//如果您需要等待给定条件更改,请避免使用此函数。
//相反,将插槽连接到指示更改的信号或使用事件处理程序(请参阅 QObject::event())。
//注意:此功能不保证准确性。
//在重负载条件下,应用程序的休眠时间可能超过毫秒。
//某些操作系统可能会将毫秒舍入到 10 毫秒或 15 毫秒。

//[since 5.0] void QThread::setEventDispatcher(QAbstractEventDispatcher *eventDispatcher)
//将线程的事件调度程序设置为 eventDispatcher。
//这只有在尚未为线程安装事件调度程序时才有可能。
//也就是说,在使用 start() 启动线程之前
//或者在主线程的情况下,在实例化 QCoreApplication 之前。
//此方法获取对象的所有权。

//void QThread::setPriority(QThread::Priority priority)
//此函数设置正在运行的线程的优先级。
//如果线程未运行,则此函数不执行任何操作并立即返回。
//使用 start() 启动具有特定优先级的线程。

//虽然线程的目的是允许代码并行运行
//但有时线程必须停止并等待其他线程。
//例如,如果两个线程尝试同时写入同一个变量,结果是未定义的。
//强制线程相互等待的原则称为互斥。
//它是保护共享资源(例如数据)的常用技术。
//Qt 提供了用于同步线程的低级原语和高级机制。

//Low-Level Synchronization Primitives
//QMutex 是强制互斥的基本类。
//线程锁定互斥体以获得对共享资源的访问。
//如果第二个线程在互斥锁已被锁定时尝试锁定它
//则第二个线程将进入休眠状态
//直到第一个线程完成其任务并解锁互斥锁。
//QReadWriteLock 与 QMutex 类似,只是它区分了“读”和“写”访问。
//当一块数据没有被写入时,多个线程同时读取它是安全的。
//QMutex 强制多个读者轮流读取共享数据,
//但 QReadWriteLock 允许同时读取,从而提高并行性。
//QSemaphore 是 QMutex 的概括,它保护一定数量的相同资源。
//相比之下,QMutex 只保护一种资源。
//信号量示例展示了信号量的典型应用
//同步访问生产者和消费者之间的循环缓冲区。
//这些同步类可用于使方法线程安全。
//然而,这样做会导致性能损失,这就是为什么大多数 Qt 方法都不是线程安全的。


//! [0]
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;

    enum { ColormapSize = 512 };
    uint colormap[ColormapSize];
};
//! [0]

#endif // RENDERTHREAD_H

renderthread.cpp

#include "renderthread.h"

#include <QImage>

#include <QElapsedTimer>
#include <QTextStream>

#include <cmath>

int RenderThread::numPasses = 8;

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

//! [1]
RenderThread::~RenderThread()
{
    mutex.lock();
    abort = true;
    condition.wakeOne();
    mutex.unlock();

    wait();
}
//! [1]

//! [2]
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();
    }
}
//! [2]

//! [3]
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();
//! [3]

//! [4]
        int halfWidth = resultSize.width() / 2;
//! [4] //! [5]
        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;
            const 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);
                }
//! [5] //! [6]
                ++pass;
            }
//! [6] //! [7]
        }
//! [7]

//! [8]
        mutex.lock();
//! [8] //! [9]
        if (!restart)
            condition.wait(&mutex);
        restart = false;
        mutex.unlock();
    }
}
//! [9]

//! [10]
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));
}
//! [10]

mandelbrotwidget.h

#ifndef MANDELBROTWIDGET_H
#define MANDELBROTWIDGET_H

#include <QPixmap>
#include <QWidget>
#include "renderthread.h"


//! [0]
class MandelbrotWidget : public QWidget
{
    Q_OBJECT

public:
    MandelbrotWidget(QWidget *parent = nullptr);

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;
    QString help;
    QString info;
    double centerX;
    double centerY;
    double pixmapScale;
    double curScale;
};
//! [0]

#endif // MANDELBROTWIDGET_H

mandelbrotwidget.cpp

#include "mandelbrotwidget.h"

#include <QPainter>
#include <QKeyEvent>

#include <math.h>

//! [0]
const double DefaultCenterX = -0.637011;
const double DefaultCenterY = -0.0395159;
const double DefaultScale = 0.00403897;

const double ZoomInFactor = 0.8;
const double ZoomOutFactor = 1 / ZoomInFactor;
const int ScrollStep = 20;
//! [0]

//! [1]
MandelbrotWidget::MandelbrotWidget(QWidget *parent) :
    QWidget(parent),
    centerX(DefaultCenterX),
    centerY(DefaultCenterY),
    pixmapScale(DefaultScale),
    curScale(DefaultScale)
{
    help = tr("Use mouse wheel or the '+' and '-' keys to zoom. "
              "Press and hold left mouse button to scroll.");
    connect(&thread, &RenderThread::renderedImage,
            this, &MandelbrotWidget::updatePixmap);

    setWindowTitle(tr("Mandelbrot"));
#if QT_CONFIG(cursor)
    setCursor(Qt::CrossCursor);
#endif
}
//! [1]

//! [2]
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..."));
//! [2] //! [3]
        return;
//! [3] //! [4]
    }
//! [4]

//! [5]
    if (qFuzzyCompare(curScale, pixmapScale)) {
//! [5] //! [6]
        painter.drawPixmap(pixmapOffset, pixmap);
//! [6] //! [7]
    } else {
//! [7] //! [8]
        auto previewPixmap = qFuzzyCompare(pixmap.devicePixelRatio(), qreal(1))
            ? pixmap
            : pixmap.scaled(pixmap.size() / pixmap.devicePixelRatio(), Qt::KeepAspectRatio,
                            Qt::SmoothTransformation);
        double scaleFactor = pixmapScale / curScale;
        int newWidth = int(previewPixmap.width() * scaleFactor);
        int newHeight = int(previewPixmap.height() * scaleFactor);
        int newX = pixmapOffset.x() + (previewPixmap.width() - newWidth) / 2;
        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()).adjusted(-1, -1, 1, 1);
        painter.drawPixmap(exposed, previewPixmap, exposed);
        painter.restore();
    }
//! [8] //! [9]

    QString text = help;
    if (!info.isEmpty())
        text += ' ' + info;
    QFontMetrics metrics = painter.fontMetrics();
    int textWidth = metrics.horizontalAdvance(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);
}
//! [9]

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

//! [11]
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);
    }
}
//! [11]

#if QT_CONFIG(wheelevent)
//! [12]
void MandelbrotWidget::wheelEvent(QWheelEvent *event)
{
    const int numDegrees = event->angleDelta().y() / 8;
    const double numSteps = numDegrees / double(15);
    zoom(pow(ZoomInFactor, numSteps));
}
//! [12]
#endif

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

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

//! [15]
void MandelbrotWidget::mouseReleaseEvent(QMouseEvent *event)
{
    if (event->button() == Qt::LeftButton) {
        pixmapOffset += event->position().toPoint() - lastDragPos;
        lastDragPos = QPoint();

        const auto pixmapSize = pixmap.size() / pixmap.devicePixelRatio();
        int deltaX = (width() - pixmapSize.width()) / 2 - pixmapOffset.x();
        int deltaY = (height() - pixmapSize.height()) / 2 - pixmapOffset.y();
        scroll(deltaX, deltaY);
    }
}
//! [15]

//! [16]
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();
}
//! [16]

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

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

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值