# Day4 Mandelbrot 分形渲染器 —— 设计 / 实现规格

- 日期：2026-07-08
- 用途：川大 Qt 方向课程 Day4 综合演示示例（事件机制 + QPainter 绘图 + 多线程）
- 状态：已与用户确认设计；本文同时作为实现计划（压缩 writing-plans 环节，经用户授权「一次性完成」）

## 1. 目标与定位

把 Day4 三个知识点串成一条可视化故事线：**缩放分形 → 触发重绘 → 多线程后台分块计算 → 界面不卡**。
通过三段式对比直击 slides「主线程耗时 = 冻结」痛点并量化多核收益：

1. 主线程同步渲染 → **卡死且慢**（按钮无响应、窗口拖不动、`paintEvent` 停滞）
2. 后台单线程（线程池 = 1）→ **不卡但慢**
3. 后台多线程（线程池 = N）→ **不卡且快**

定位为 `docs/teaching/examples/` 下的手工维护教学示例（非 `tools/gen_part3.py` 生成产物），
配 README，遵循仓库构建/验证约定。同时作为**可独立共享的单 CMake 工程**。

## 2. 知识点覆盖映射

| Day4 知识点 | 本示例对应实现 |
|---|---|
| QPainter 绘图（框架驱动，只在 `paintEvent` 画） | `MandelbrotWidget::paintEvent` 把累积 QImage 一次性 `drawImage` |
| 事件 vs 信号 / 事件循环 / 事件拦截 | `wheelEvent`（缩放）、`mousePress/Move/ReleaseEvent`（平移）重写；「主线程渲染」按钮演示事件循环被占用即冻结 |
| 多线程防 UI 阻塞 | `QThreadPool` + `QRunnable` 分水平条带并行；排队信号槽回传结果 |

## 3. 架构与组件（3 个类 + main，多文件）

### 3.1 `MandelbrotWidget`（`public QWidget`，主角）
- 持有视口参数：`double centerX, centerY, scale`（scale = 单位像素对应的复平面跨度）；`int maxIter = 256`。
- 持有 `quint64 renderEpoch = 0`（渲染版本号，防过期结果覆盖）。
- 持有累积 `QImage pixmap_`（widget 尺寸，`QImage::Format_RGB32`）。
- 重写：`paintEvent`（`drawImage` 累积图）、`wheelEvent`（以鼠标为锚点缩放）、`mousePressEvent/mouseMoveEvent/mouseReleaseEvent`（拖拽平移）、`resizeEvent`（尺寸变化触发重渲染）。
- 底部 `QHBoxLayout` 工具条：「主线程渲染」按钮、「单/多线程」切换按钮、耗时 `QLabel`。
- 槽：`onStripReady(quint64 epoch, int stripIndex, QImage strip)`——校验 epoch、拼图、计数、全部到齐则 `update()` 并显示耗时。

### 3.2 `MandelbrotStripTask`（`public QObject, public QRunnable`，带 `Q_OBJECT`）
- 数据成员：`quint64 epoch`、`int stripIndex`、`QRect viewport`（像素区域）、视口参数副本、调色板指针/引用。
- `run()` override：调用自由函数 `renderStrip(...)` 算 `[y0, y1]` 行像素 → `emit stripReady(epoch, stripIndex, img)`（排队连接，自动回主线程）。
- `signals: void stripReady(quint64 epoch, int stripIndex, QImage strip);`
- `setAutoDelete(true)`（默认）、无 QObject parent，靠线程池回收。
- *教学点*：QRunnable 本身无信号，多继承 QObject 是 Qt 让 QRunnable 拥有信号的标准写法。

### 3.3 自由函数 `renderStrip`（纯计算，两路径共用）
- `QImage renderStrip(const Viewport& vp, int y0, int y1, const RenderPalette& pal);`
- 对 `[y0,y1) × [0,width)` 每个像素做标准 Mandelbrot 迭代 `z = z² + c`（逃逸半径 2），按迭代次数经调色板映射成 RGB，写入 QImage。
- 被 `MandelbrotStripTask::run()`（多线程路径）和「主线程渲染」按钮（卡死路径）**共用**，保证两路径算同一份分形，差异只在「是否阻塞事件循环 / 是否并行」。

### 3.4 调色板 `RenderPalette`
- 迭代次数 → HSV(h, s=1, v=1) → RGB 的简单预计算表（256 项 `QRgb`），构造时生成。
- 提升趣味，与知识点无关但成本低。

### 3.5 `main.cpp`
- 建 `QApplication` + `MandelbrotWidget`，`resize(800,600)`、`show()`。
- offscreen 平台（`qApp->platformName() == "offscreen"`）时：启动即触发一次后台渲染（覆盖计算路径），并 `QTimer::singleShot(200, &app, &QApplication::quit)` 自动退出码 0（仓库自动化验证约定）。

## 4. 数据流（一轮缩放）

```
wheelEvent 改 scale/center（以鼠标点为锚，缩放后该点复坐标不变）
  → ++renderEpoch
  → 切 N = hardwareConcurrency()*2 条水平带
  → 每带 new MandelbrotStripTask(epoch,i,viewport) → QThreadPool::globalInstance()->start(task)
        ↓ (子线程并行)
  task.run() → renderStrip(...) → emit stripReady(epoch,i,QImage)
        ↓ (Qt::QueuedConnection，自动回主线程)
  Widget::onStripReady: epoch != currentEpoch ? 丢弃
                       否则把 strip 画到 pixmap_ 对应行，received++ ;
                       received == N → update() + 显示 QElapsedTimer 耗时与线程数
  paintEvent: QPainter(this); p.drawImage(0,0, pixmap_);
```

## 5. 三段式对比（教学核心）

- **「主线程渲染」按钮**：槽里在 GUI 线程同步调用 `renderStrip` 算整帧（不分块、不丢池）。
  实测单帧 820×640@256 仅约 70ms，不足以肉眼可感，故**自适应循环渲染到累计 ≥ 1500ms**（实测 ~23 帧 / 1506ms）。
  期间事件循环停摆 → 按钮无响应、窗口拖不动 → 直观复现痛点。算完 `update()` 并显示「主线程渲染 Xms · N 轮」。
  冻结本质是事件循环被占用，循环只是模拟一个持续 ~1.5s 的耗时操作。
- **「单/多线程」切换**：复用同一套分块逻辑，仅切 `QThreadPool::globalInstance()->setMaxThreadCount(1 或 hardwareConcurrency())`。
  单线程不卡但慢、多线程不卡且快，耗时标签量化加速比。
- 三段对比让学员一眼看懂：耗时任务必须离开主线程；多线程进一步压榨多核。

## 6. 关键决策与取舍

- **条带数** = `hardwareConcurrency()*2`：负载均衡，避免某核分到全「集合内部」的快带。
- **过期防护（非主动取消）**：`renderEpoch` 版本号，新视口令旧结果作废丢弃。YAGNI 不做主动 cancel；
  但版本号保证画面正确性——这本身是教学点（多线程结果回收要防竞态）。
- **无锁设计**：每 task 算自己的 strip QImage，信号复制回传，主线程拼。呼应 `qthread_worker` README「优先信号槽传数据，退而求其次才用锁」。
- **maxIter 默认 256**：画质 vs 卡死演示时长的平衡点。
- **UI 纯代码**（无 `.ui`），避免分散对 painting/线程/事件的注意力。

## 7. 构建系统：双模式 CMake（与仓库 `lineedit_eye_toggle` 同模式）

仓库权威约定（见 `lineedit_eye_toggle/CMakeLists.txt` 注释）：顶层 `CMakeLists.txt` 只
`add_subdirectory(docs/teaching/examples)` 且**不递归**子目录的 CMakeLists；统一构建走
`examples/CMakeLists.txt` 的 `add_executable`/`add_teaching_example`；子目录的独立 CMakeLists.txt
仅供单独打开/共享。本示例遵循此约定。

### 7.1 子目录 `mandelbrot_renderer/CMakeLists.txt` = 完整独立工程（供单独打开/共享）
- `cmake_minimum_required(VERSION 3.16)` / `project(qt_mandelbrot_renderer LANGUAGES CXX)`
- C++17 严格（`CXX_STANDARD 17 / REQUIRED ON / CXX_EXTENSIONS OFF`）、`set(CMAKE_AUTOMOC ON)`
- `find_package(Qt5 COMPONENTS Core Gui Widgets REQUIRED)`（不重复 5.14 断言——顶层已有全局断言）
- `add_executable(qt_mandelbrot_renderer main.cpp mandelbrot_widget.cpp mandelbrot_strip_task.cpp)`
- `target_link_libraries(... PRIVATE Qt5::Core Qt5::Gui Qt5::Widgets)`、`-Wall -Wextra`
- 可脱离主仓库独立 `cmake -S . -B build` 构建、单独打包共享。

### 7.2 主工程纳入
- `docs/teaching/examples/CMakeLists.txt` 新增 `add_executable(qt_mandelbrot_renderer …)` 直接引用
  `mandelbrot_renderer/*.cpp`（多文件，不走单文件 `add_teaching_example` 宏），设 C++17/AUTOMOC/Qt5 链接；
- 子目录独立 CMakeLists.txt **不**被仓库构建递归（避免 target 重复定义）；
- 把 `qt_mandelbrot_renderer` 加入 `all_teaching_examples` 的 DEPENDS 列表。
- target 定义在父子两处各一份，源文件相同（零代码重复）——与 `lineedit_eye_toggle`/`mainwindow_showcase` 一致。

### 7.3 权衡
子独立 CMakeLists 不内置 Qt 5.14 断言（顶层 `CMakeLists.txt` 第 44–49 行已有全局断言；
独立构建时靠 `-DCMAKE_PREFIX_PATH` 指对 Qt 5.14.2 路径），与 `lineedit_eye_toggle` 风格一致。
独立共享时接收方需装 Qt 5.14.2，README 指向 `tools/install_qt_host.sh`（Linux）或官方
Qt 5.14.2 MinGW 安装（Windows）。

## 8. 显式踩坑点（README 专节，代码里复现/规避）

1. 在 `paintEvent` 之外对窗口 widget 构造 `QPainter`（UB）——严格只在 `paintEvent` 画。
2. QRunnable 无信号 → 必须多继承 `QObject` 才能 `emit`（初学者高频困惑）。
3. 主线程渲染卡死 vs 后台流畅的**本质**：事件循环是否被占用。
4. 过期渲染结果覆盖新画面（版本号防护）。
5. `wheelEvent`/`mouseEvent` 重写时基类调用与事件 `accept/ignore` 的处理。

## 9. 验证方式（CLAUDE.md 行为风格第 2 点：端到端实跑）

### 9.1 Windows 本地（本次开发已实跑通过，Qt 5.14.2 mingw73_64 + w64devkit g++ 16）
```bash
# 独立工程（Release，避开 mingw Qt 可能缺 debug 平台插件的问题）
cmake -S docs/teaching/examples/mandelbrot_renderer -B build_mandel_rel -G Ninja \
      -DCMAKE_BUILD_TYPE=Release \
      -DCMAKE_CXX_COMPILER=E:/workenv/w64devkit/bin/g++.exe \
      -DCMAKE_PREFIX_PATH=E:/Qt/Qt5.14.2/5.14.2/mingw73_64
cmake --build build_mandel_rel
export PATH=/e/Qt/Qt5.14.2/5.14.2/mingw73_64/bin:$PATH
export QT_QPA_PLATFORM_PLUGIN_PATH=/e/Qt/Qt5.14.2/5.14.2/mingw73_64/plugins/platforms
QT_QPA_PLATFORM=offscreen ./build_mandel_rel/qt_mandelbrot_renderer.exe ; echo "exit=$?"   # 实测 exit=0
# 主工程内（Debug，仅验证纳入与编译链接）
cmake -S . -B build_main -G Ninja -DBUILD_QT_PART=ON -DCMAKE_BUILD_TYPE=Debug \
      -DCMAKE_C_COMPILER=E:/workenv/w64devkit/bin/gcc.exe \
      -DCMAKE_CXX_COMPILER=E:/workenv/w64devkit/bin/g++.exe \
      -DCMAKE_PREFIX_PATH=E:/Qt/Qt5.14.2/5.14.2/mingw73_64
cmake --build build_main --target qt_mandelbrot_renderer   # 实测 build_exit=0
```
> 注：w64devkit g++16 与 mingw73（Qt 编译所用的 GCC7）ABI 在本例链接通过、运行正常；
> 若他人环境报 ABI 错误，改用与 Qt 同源的 MinGW 7.3 工具链即可。

### 9.2 主工程内（仓库标准，Linux 隔离 Qt）
```bash
cmake --build build --target qt_mandelbrot_renderer
tools/run_qt.sh qt_mandelbrot_renderer
QT_QPA_PLATFORM=offscreen tools/run_qt.sh qt_mandelbrot_renderer   # 自动退出码 0
```

## 10. 文件清单与实现步骤

```
docs/teaching/examples/mandelbrot_renderer/
├ CMakeLists.txt              # 独立工程（双模式复用）
├ main.cpp
├ mandelbrot_widget.h / .cpp
├ mandelbrot_strip_task.h / .cpp   # 含调色板 RenderPalette + renderStrip 纯函数
└ README.md                   # 概念/代码走读/两套构建/练习方向/踩坑点
docs/teaching/examples/CMakeLists.txt   # 改：add_subdirectory + 挂 all_teaching_examples
```

实现顺序：① `mandelbrot_strip_task.h/.cpp`（调色板 + renderStrip + task，可独立单测的纯计算）
→ ② `mandelbrot_widget.h/.cpp`（组装交互与渲染调度）→ ③ `main.cpp` → ④ `CMakeLists.txt`（独立）
→ ⑤ 改 `examples/CMakeLists.txt`（纳入）→ ⑥ `README.md` → ⑦ 构建 + offscreen 验证。

## 11. 成功标准（可验证）

- [x] 独立工程 `cmake -S mandelbrot_renderer -B build` 能配置 + 构建，产物 offscreen 运行（Windows 实测 exit=0）。
- [x] 主工程 `cmake --build build --target qt_mandelbrot_renderer` 全绿，并入 `all_teaching_examples`（Windows 实测 build_exit=0）。
- [x] offscreen 运行 200ms 自动退出，退出码 0；启动即触发一次后台渲染（计算路径被覆盖）。
- [ ] 有屏人工交互（滚轮缩放、拖拽平移、单/多线程切换耗时对比、「主线程渲染」冻结演示）：当前为自动化上下文，**未做有屏人工验证**；交互逻辑经代码走读 + offscreen 覆盖，待真机点验。
- [x] README 完整覆盖概念/走读/双构建/踩坑/练习方向。
- [x] 无编译警告（`-Wall -Wextra`，Debug + Release 均零警告）。
