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:
@@ -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();
|
||||
}
|
||||
Reference in New Issue
Block a user