// ============================================================================ // mandelbrot_widget.cpp —— 交互与渲染调度实现(详见 .h) // ============================================================================ #include "mandelbrot_widget.h" #include #include #include #include #include #include #include #include #include #include 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(); }