Files
scuc-qt-course/docs/teaching/examples/mandelbrot_renderer/mandelbrot_widget.cpp
T
张宗平 993aa30a1c Mandelbrot 教学示例增强:主线程渲染循环至 ≥1.5s,让界面冻结肉眼可感
实测单帧 820×640@256 单线程仅约 70ms,远低于人眼可感阈值,直接算一帧看不出
「冻结」。故「主线程渲染」按钮改为自适应循环渲染直到累计 ≥ 1500ms(实测 ~23 帧 /
1506ms),上限 100 轮防止极慢机器卡死过久。冻结的本质是事件循环被占用,循环只是
模拟任意一个占着 GUI 线程不返回的耗时操作。

- mandelbrot_widget.cpp:onRenderOnMainThread 改 do-while 循环,标签加「· N 轮」
- README.md:补冻结时长说明与课堂演示技巧
- 设计文档同步:三段式对比章节更新实测数据与冻结本质解释
- 新增 docs/teaching/MULTITHREADING.md:多线程教学要点汇总

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 16:50:46 +08:00

173 lines
7.3 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ============================================================================
// 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 只为复用同一纯函数
// 自适应加重:单帧仅约几十毫秒(实测 820x640@256 ≈ 66ms),不足以让「冻结」肉眼可感。
// 故循环渲染直到累计 ≥ 1500ms——冻结的本质是事件循环被占用,与「算什么」无关,
// 这模拟任意一个「占着 GUI 线程不返回事件循环」的耗时操作(解析大文件、复杂运算……)。
// 上限 100 轮防止极慢机器卡死过久。
int frames = 0;
do {
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);
}
++frames;
} while (timer_.elapsed() < 1500 && frames < 100);
// 注意:本函数返回前事件循环不跑——期间按钮无响应、窗口拖不动、paintEvent 排队等待。
update();
label_->setText(QString(QStringLiteral("主线程渲染 (界面冻结) · %1 ms · %2 轮"))
.arg(int(timer_.elapsed())).arg(frames));
}
// ---- 单/多线程切换:复用同一套分块逻辑,仅切线程池大小 ----
void MandelbrotWidget::onToggleThreadMode() {
multiThread_ = !multiThread_;
QThreadPool::globalInstance()->setMaxThreadCount(
multiThread_ ? QThread::idealThreadCount() : 1);
threadCount_ = QThreadPool::globalInstance()->maxThreadCount();
scheduleRender();
}