6773f9b29e
三段式对比直击 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>
165 lines
6.7 KiB
C++
165 lines
6.7 KiB
C++
// ============================================================================
|
||
// 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();
|
||
}
|