Day4 教学示例:Mandelbrot 分形渲染器(QPainter 绘图 + 多线程 + 事件处理综合演示)

三段式对比直击 Day4 slides「主线程耗时=冻结」痛点:主线程渲染冻结 / 后台单线程不卡但慢 / 后台多线程不卡且快。
- QThreadPool + QRunnable 分水平条带并行,排队信号槽回传,epoch 版本号防过期覆盖
- wheelEvent 缩放(鼠标为锚)+ mouseEvent 拖拽平移 + paintEvent 画 QImage
- 子目录自带独立 CMakeLists.txt,可单独构建/共享(双模式约定同 lineedit_eye_toggle)
- 配套 README(概念/走读/双构建/5 踩坑点/练习)+ 设计规格
- 已验证:独立工程 offscreen 运行 exit=0;主工程构建 exit=0(w64devkit g++16 + mingw73 Qt5.14.2)

注:主工程 examples/CMakeLists.txt 的接入(mandelbrot + lineedit_eye_toggle + qdialog + themes 原子接入)与顶层 CMakeLists 本地调试改动未一并提交,待整体确认后单独提交。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
张宗平
2026-07-09 09:01:29 +08:00
parent 05a685c952
commit 6773f9b29e
8 changed files with 706 additions and 0 deletions
@@ -0,0 +1,75 @@
// ============================================================================
// mandelbrot_strip_task —— Day4 Mandelbrot 渲染器的「分块计算 + 调色板」单元
// 职责:
// * MandelViewport : 视口参数(复平面中心 / 像素跨度 / 尺寸 / 最大迭代)
// * MandelPalette : 迭代次数→RGB 的预计算调色板(提升趣味,与知识点无关)
// * renderStrip() : 纯计算函数,渲染视口内 [y0,y1) 行像素。多线程 task 与
// 「主线程卡死」演示路径共用它——保证两路径算的是同一份分形,
// 差异只在「是否阻塞事件循环 / 是否并行」。
// * MandelbrotStripTask : 一条水平带的 QRunnable 任务。多继承 QObject+QRunnable
// 以获得信号能力(QRunnable 本身无信号——这是初学者高频困惑,
// 见 README 踩坑点 2)。
// 设计要点:task 持 palette/viewport 的【值副本】,自包含、不引用主线程对象,
// 避免主对象先析构导致的悬垂访问(见 README 踩坑点 4)。
// ============================================================================
#ifndef MANDELBROT_STRIP_TASK_H
#define MANDELBROT_STRIP_TASK_H
#include <QImage>
#include <QObject>
#include <QRunnable>
#include <QRgb>
#include <QVector>
// 视口参数:复平面映射。scale = 每像素对应的复平面跨度。
struct MandelViewport {
double centerX = -0.5; // 经典 Mandelbrot 主分形居中
double centerY = 0.0;
double scale = 3.0 / 600.0;
int width = 0; // show 后由 widget 尺寸填入
int height = 0;
int maxIter = 256;
};
// 调色板:迭代次数 → RGB。集合内部(迭代未逃逸)映射为黑。
class MandelPalette {
public:
explicit MandelPalette(int maxIter = 256);
QRgb color(int i) const { return table_.at(i); }
private:
QVector<QRgb> table_;
};
// 纯计算:渲染视口内 [y0, y1) 行像素,返回 (width × (y1-y0)) 的 RGB32 QImage。
QImage renderStrip(const MandelViewport& vp, const MandelPalette& pal, int y0, int y1);
// 一条水平带的多线程任务。多继承 QObject + QRunnable 以拥有信号。
// setAutoDelete(true)(默认):run() 结束后由线程池回收;不设 QObject parent
// 避免与 QThreadPool 的自动删除发生双重所有权——见 README 踩坑点 2。
class MandelbrotStripTask : public QObject, public QRunnable {
Q_OBJECT
public:
MandelbrotStripTask(quint64 epoch, int stripIndex,
const MandelViewport& vp, const MandelPalette& pal,
int y0, int y1)
: QObject(nullptr), epoch_(epoch), stripIndex_(stripIndex),
vp_(vp), pal_(pal), y0_(y0), y1_(y1) { setAutoDelete(true); }
void run() override {
// 算完一条带 → 通过信号把结果「排队」送回主线程(Qt 自动跨线程调度)。
emit stripReady(epoch_, stripIndex_, renderStrip(vp_, pal_, y0_, y1_));
}
signals:
void stripReady(quint64 epoch, int stripIndex, QImage strip);
private:
quint64 epoch_;
int stripIndex_;
MandelViewport vp_;
MandelPalette pal_; // 值副本:任务自包含
int y0_;
int y1_;
};
#endif // MANDELBROT_STRIP_TASK_H