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,164 @@
// ============================================================================
// mandelbrot_widget.cpp —— 交互与渲染调度实现(详见 .h)
// ============================================================================
#include "mandelbrot_widget.h"
#include <QHBoxLayout>
#include <QLabel>
#include <QMouseEvent>
#include <QPainter>
#include <QPushButton>
#include <QResizeEvent>
#include <QThreadPool>
#include <QThread>
#include <QVBoxLayout>
#include <QWheelEvent>
MandelbrotWidget::MandelbrotWidget(QWidget* parent)
: QWidget(parent), pal_(256) {
// 线程池默认按核数;切到「单线程」模式时再 setMaxThreadCount(1)。
QThreadPool::globalInstance()->setMaxThreadCount(QThread::idealThreadCount());
threadCount_ = QThreadPool::globalInstance()->maxThreadCount();
auto* btnFreeze = new QPushButton(QStringLiteral("主线程渲染 (将冻结)"));
auto* btnToggle = new QPushButton(QStringLiteral("切换 单/多线程"));
label_ = new QLabel(QStringLiteral("就绪 · 滚轮缩放 · 左键拖拽平移"));
auto* bar = new QHBoxLayout;
bar->setContentsMargins(8, 4, 8, 4);
bar->addWidget(btnFreeze);
bar->addWidget(btnToggle);
bar->addSpacing(16);
bar->addWidget(label_);
bar->addStretch();
auto* root = new QVBoxLayout(this);
root->setContentsMargins(0, 0, 0, 0);
root->setSpacing(0);
root->addStretch(); // 画布占主体(paintEvent 画整个 widget 背景,工具条浮在底部)
root->addLayout(bar);
connect(btnFreeze, &QPushButton::clicked, this, &MandelbrotWidget::onRenderOnMainThread);
connect(btnToggle, &QPushButton::clicked, this, &MandelbrotWidget::onToggleThreadMode);
}
// ---- 绘图:只在 paintEvent 内对【窗口 widget】构造 QPainter(核心踩坑点 1----
void MandelbrotWidget::paintEvent(QPaintEvent*) {
QPainter p(this);
if (!pixmap_.isNull()) {
p.drawImage(0, 0, pixmap_);
} else {
p.fillRect(rect(), Qt::black);
}
}
// ---- 事件处理:滚轮缩放(以鼠标点为锚,缩放后该点复坐标不变)----
void MandelbrotWidget::wheelEvent(QWheelEvent* e) {
const int dy = e->angleDelta().y();
if (dy == 0) { QWidget::wheelEvent(e); return; }
const double factor = (dy > 0) ? 0.8 : 1.25; // 上滚放大、下滚缩小
const QPointF m = e->position();
const double mx = vp_.centerX + (m.x() - width() / 2.0) * vp_.scale;
const double my = vp_.centerY + (m.y() - height() / 2.0) * vp_.scale;
vp_.scale *= factor;
vp_.centerX = mx - (m.x() - width() / 2.0) * vp_.scale;
vp_.centerY = my - (m.y() - height() / 2.0) * vp_.scale;
scheduleRender();
}
// ---- 事件处理:左键拖拽平移 ----
void MandelbrotWidget::mousePressEvent(QMouseEvent* e) {
if (e->button() == Qt::LeftButton) { dragging_ = true; lastMouse_ = e->pos(); }
QWidget::mousePressEvent(e);
}
void MandelbrotWidget::mouseMoveEvent(QMouseEvent* e) {
if (dragging_) {
const QPoint d = e->pos() - lastMouse_;
lastMouse_ = e->pos();
vp_.centerX -= d.x() * vp_.scale;
vp_.centerY -= d.y() * vp_.scale;
scheduleRender();
}
QWidget::mouseMoveEvent(e);
}
void MandelbrotWidget::mouseReleaseEvent(QMouseEvent* e) {
if (e->button() == Qt::LeftButton) dragging_ = false;
QWidget::mouseReleaseEvent(e);
}
void MandelbrotWidget::resizeEvent(QResizeEvent* e) {
QWidget::resizeEvent(e);
scheduleRender(); // 首次 show 与窗口缩放都经此触发首帧
}
// ---- 多线程调度:切条带丢池,渐进回传 ----
void MandelbrotWidget::scheduleRender() {
++epoch_; // 令所有在途旧任务的结果作废
vp_.width = width();
vp_.height = height();
if (vp_.width <= 0 || vp_.height <= 0) return;
pixmap_ = QImage(vp_.width, vp_.height, QImage::Format_RGB32);
stripsTotal_ = multiThread_ ? QThread::idealThreadCount() * 2 : 1; // 多线程:2×核数均衡负载
stripsDone_ = 0;
timer_.start();
const int N = stripsTotal_;
for (int i = 0; i < N; ++i) {
const int y0 = vp_.height * i / N;
const int y1 = vp_.height * (i + 1) / N;
auto* task = new MandelbrotStripTask(epoch_, i, vp_, pal_, y0, y1);
// 排队连接:stripReady 自动调度回主线程(接收者 this 所在线程)执行 onStripReady。
connect(task, &MandelbrotStripTask::stripReady, this, &MandelbrotWidget::onStripReady);
QThreadPool::globalInstance()->start(task);
}
}
void MandelbrotWidget::onStripReady(quint64 epoch, int stripIndex, QImage strip) {
if (epoch != epoch_) return; // 过期结果丢弃(防竞态覆盖新画面)
if (!strip.isNull()) {
QPainter p(&pixmap_); // 对【离屏 QImage】构造 QPainter——合法(非窗口 widget
p.drawImage(0, vp_.height * stripIndex / stripsTotal_, strip);
}
++stripsDone_;
update(); // 渐进刷新:能看到条带自上而下并行填满
if (stripsDone_ >= stripsTotal_) {
label_->setText(QString(QStringLiteral("%1 · %2 ms · %3 线程"))
.arg(multiThread_ ? QStringLiteral("多线程渲染")
: QStringLiteral("单线程渲染"))
.arg(int(timer_.elapsed()))
.arg(threadCount_));
}
}
// ---- 卡死演示:在 GUI 线程同步算整帧,不分块、不丢池 → 事件循环停摆 → 界面冻结 ----
void MandelbrotWidget::onRenderOnMainThread() {
++epoch_; // 让在途后台任务的结果作废,避免覆盖本帧
vp_.width = width();
vp_.height = height();
if (vp_.width <= 0 || vp_.height <= 0) return;
timer_.start();
pixmap_ = QImage(vp_.width, vp_.height, QImage::Format_RGB32);
const int H = vp_.height;
const int chunk = 24; // 分块调 renderStrip 只为复用同一纯函数
for (int y = 0; y < H; y += chunk) {
const QImage s = renderStrip(vp_, pal_, y, qMin(y + chunk, H));
QPainter p(&pixmap_);
p.drawImage(0, y, s);
}
// 注意:本函数返回前事件循环不跑——期间按钮无响应、窗口拖不动、paintEvent 排队等待。
update();
label_->setText(QString(QStringLiteral("主线程渲染 (界面冻结) · %1 ms"))
.arg(int(timer_.elapsed())));
}
// ---- 单/多线程切换:复用同一套分块逻辑,仅切线程池大小 ----
void MandelbrotWidget::onToggleThreadMode() {
multiThread_ = !multiThread_;
QThreadPool::globalInstance()->setMaxThreadCount(
multiThread_ ? QThread::idealThreadCount() : 1);
threadCount_ = QThreadPool::globalInstance()->maxThreadCount();
scheduleRender();
}