Files
scuc-qt-course/docs/teaching/examples/mandelbrot_renderer/mandelbrot_widget.cpp
T
张宗平 6773f9b29e 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>
2026-07-09 09:01:29 +08:00

165 lines
6.7 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 只为复用同一纯函数
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();
}